共计 9546 个字符,预计需要花费 24 分钟才能阅读完成。
-
目录
- 前言
- 创建项目
- 创建 Item
- 创建 Spider
- 解析付费榜
- 运行爬取初始 app 列表
- Selenium 调用 JS 脚本
- 获取 app 详情
前言
熟悉 Scrapy 之后,本篇文章带大家爬取七麦数据(https://www.qimai.cn/rank)的 ios appstore 付费应用排行榜前 100 名应用。
爬取内容包括 app 在列表中的下标,app 图标地址,app 的名称信息,app 的类型,在分类中的排行,开发者,详情等。
考虑的问题:
- Forbidden by robots.txt 的错误
- 网页返回 403
- 页面通过动态渲染,普通的请求 url,在页面渲染之前已经返回 response,解析没有数据
- 列表一页 20 个 app,想要拿到前 100 个需要翻页,但是翻页没有更改 url,而是通过 js 动态加载
- …
创建项目
在需要放置项目的目录下,
> scrapy startproject qimairank
回车即可创建默认的 Scrapy 项目架构。
创建 Item
创建 Item 来存储我们爬取的 app 在列表中的下标,app 图标地址,app 的名称信息,app 的类型,在分类中的排行,开发者,详情。
修改items.py
,在下面增加
class RankItem(scrapy.Item):
# 下标
index = scrapy.Field()
# 图标地址
src = scrapy.Field()
# app 标题信息
title = scrapy.Field()
# app 类型
type = scrapy.Field()
# 分类中的排行
type_rank = scrapy.Field()
# 开发者
company = scrapy.Field()
# 详情信息
info = scrapy.Field()
创建 Spider
在 spiders
目录下创建RankSpider.py
,并创建class RankSpider
,继承于 scrapy.Spider。
import scrapy
class RankSpider(scrapy.Spider):
name = "RankSpider"
start_urls = ["https://www.qimai.cn/rank"]
def parse(self, response):
pass
-
name
:用于区别 Spider,该名字必须是唯一的。 -
start_urls
:Spider 在启动时进行爬取的 url 列表,首先会爬取第一个。 -
def parse(self, response)
:得到 url 的 response 信息后的解析方法。
解析付费榜
解析用的 Selectors 选择器有多种方法:
- xpath(): 传入 xpath 表达式,返回该表达式所对应的所有节点的 selector list 列表。
- css(): 传入 CSS 表达式,返回该表达式所对应的所有节点的 selector list 列表.
- extract(): 序列化该节点为 unicode 字符串并返回 list。
- re(): 根据传入的正则表达式对数据进行提取,返回 unicode 字符串 list 列表。
下面我们用 xpath()选择节点,xpath 的语法可参考 w3c 的 http://www.w3school.com.cn/xp… 学习,需要熟悉语法、运算符、函数等。
def parse(self, response):
base = response.xpath("//div[@class='ivu-row rank-all-item']/div[@class='ivu-col ivu-col-span-8'][2]//ul/li[@class='child-item']/div[@class='ivu-row']")
for box in base:
# 创建实例
rankItem = RankItem()
# 下标
rankItem['index'] = \
box.xpath(".//div[@class='ivu-col ivu-col-span-3 left-item']/span/text()").extract()[0]
# 图标地址
rankItem['src'] = box.xpath(".//img/@src").extract()[0]
# app 名称信息
rankItem['title'] = box.xpath(".//div[@class='info-content']//a/text()").extract()[0]
# app 类型
rankItem['type'] = box.xpath(".//div[@class='info-content']//p[@class='small-txt']/text()").extract()[0]
# 分类中的排行
rankItem['type_rank'] = box.xpath(".//div[@class='info-content']//p[@class='small-txt']//span[@class='rank-item']/text()").extract()[0]
# 开发者
rankItem['company'] = box.xpath(".//div[@class='info-content']//p[@class='small-txt']//span[@class='company-item']/text()").extract()[0]
# 详情页地址
infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
yield rankItem
运行爬取初始 app 列表
直接运行
qimairank>scrapy crawl RankSpider -o data.json
你会发现窗口没有 item 输出,data.json 中也没有数据,是我们写错了吗?
scrapy 默认遵守 robot 协议的,在访问网址前会先访问 robot.txt 来查看自己是否有权限访问。如果网站不允许被爬,就不能访问。
怎么样不遵守协议呢?
settings.py
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
再次运行仍然失败,我们来看下具体原因:
因为七麦网站对请求的 User-Agent
做了校验,解决办法是在配置文件
settings.py
# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
# 'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
'qimairank.middlewares.RandomUserAgent': 1,
}
USER_AGENTS = ["Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
]
并在 middlewares.py
中创建RandomUserAgent
import random
class RandomUserAgent(object):
"""随机获取 settings.py 中配置的 USER_AGENTS 设置'User-Agent'"""
def __init__(self, agents):
self.agents = agents
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.getlist('USER_AGENTS'))
def process_request(self, request, spider):
request.headers.setdefault('User-Agent', random.choice(self.agents))
再次运行,没有报错,但是没有数据,是我们的 xpath 写错啦?我们在 parse 中增加输出 body 的信息
可以看到 body 为空,没有我们需要的列表数据,这是因为七麦数据是通过 js 动态渲染的,在渲染完成前,我们的 response 已经返回,那么怎么样才能等一等呀,等到渲染完成才返回呢?
爬取动态渲染的方式,我知道是通过 Splash 或者 Selenium,像我们的桌面版系统可以选择用 Selenium,操作可以设置可视化,所有界面操作都能看见,Splash 依赖于 Docker,无界面。
安装 Selenium 包:
pip install selenium
使用前需要安装驱动,配置详情点击
驱动安装完成,在 middlewares.py 中创建 SeleniumMiddleware
class SeleniumMiddleware(object):
def __init__(self):
self.timeout = 50
# 2.Firefox---------------------------------
# 实例化参数对象
options = webdriver.FirefoxOptions()
# 无界面
# options.add_argument('--headless')
# 关闭浏览器弹窗
options.set_preference('dom.webnotifications.enabled', False)
options.set_preference('dom.push.enabled', False)
# 打开浏览器
self.browser = webdriver.Firefox(firefox_options=options)
# 指定浏览器窗口大小
self.browser.set_window_size(1400, 700)
# 设置页面加载超时时间
self.browser.set_page_load_timeout(self.timeout)
self.wait = WebDriverWait(self.browser, self.timeout)
def process_request(self, request, spider):
# 当请求的页面不是当前页面时
if self.browser.current_url != request.url:
# 获取页面
self.browser.get(request.url)
time.sleep(5)
else:
pass
# 返回页面的 response
return HtmlResponse(url=self.browser.current_url, body=self.browser.page_source,
encoding="utf-8", request=request)
def spider_closed(self):
# 爬虫结束 关闭窗口
self.browser.close()
pass
@classmethod
def from_crawler(cls, crawler):
# 设置爬虫结束的回调监听
s = cls()
crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
return s
在 settins.py 中配置
# Enable or disable downloader middlewares
DOWNLOADER_MIDDLEWARES = {
# 'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
'qimairank.middlewares.RandomUserAgent': 1,
'qimairank.middlewares.SeleniumMiddleware': 10,
}
再次运行scrapy crawl RankSpider -o data.json
,啦啦啦~ 这回有数据啦。
Selenium 调用 JS 脚本
观察爬取出来的 data.json,发现怎么肥四,只有 20 条数据,而且除了前 6 个的 app 图标都是七麦的默认图标。
这是因为七麦数据的列表默认每页 20 条,而且默认渲染前 6 个的图标,其余的页需要触发滑动事件加载,而且滑动到的图标才开始渲染。这样怎么办呢?我们只需要滑动到可以加载的按钮就可以啦,检查发现在三个列表的外层标签有一个 class 为 cm-explain-bottom 的标签
我们用 Selenium 调用 js 脚本,滑动到这个标签就可以啦,在中间件 process_request 方法更改
def process_request(self, request, spider):
# 当请求的页面不是当前页面时
if self.browser.current_url != request.url:
# 获取页面
self.browser.get(request.url)
time.sleep(5)
# 请求的 url 开始为 https://www.qimai.cn/rank/ 时,调用滑动界面,每页 20 个,滑动 4 次
if request.url.startswith("https://www.qimai.cn/rank"):
try:
for i in (0, 1, 2, 3):
self.browser.execute_script("document.getElementsByClassName('cm-explain-bottom')[0].scrollIntoView(true)")
time.sleep(4)
except JavascriptException as e:
pass
except Exception as e:
pass
再次执行scrapy crawl RankSpider -o data1.json
,则可看见已经生成 data1.json 里面有 100 个 item。
获取 app 详情
详情页需要跟进 url,我们在 RankSpider#parse 方法中,不用 yield Item,而是 yield Request 就可以跟进。
# 详情页地址
infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
# yield rankItem
yield Request(infoUrl.replace("rank", "baseinfo"), self.parseInfo,
meta={'rankItem': dict(rankItem).copy()}, dont_filter=True)
解析的 infoUrl 替换 ”rank” 字符串为 ”baseinfo” 就可以访问 app 应用信息页,用 meta 传递 item 到下一个解析方法中,用软拷贝的方式,避免 Item 因为地址相同,内容覆盖。
self.parseInfo 为指定这次请求的解析方法,
def parseInfo(self, response):
print("基地址:" + response.url)
if response.status != 200:
return
rankItem = response.meta['rankItem']
info = dict()
base = response.xpath("//div[@id='app-container']")
if base.extract():
# try:
# 描述
try:
info['desc'] = base.xpath(".//div[@class='app-header']//div[@class='app-subtitle']/text()").extract()[0]
except Exception as e:
print("无描述")
# 开发商
info['auther'] = base.xpath(".//div[@class='app-header']//div[@class='auther']//div[@class='value']/text()").extract()[0]
# 分类
info['classify'] = base.xpath(".//div[@class='app-header']//div[@class='genre']//div[@class='value']/a/text()").extract()[0]
# appid
info['appid'] = base.xpath(".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/text()").extract()[0]
# appstore 地址
info['appstorelink'] = base.xpath(".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/@href").extract()[0]
# 价格
info['price'] = base.xpath(".//div[@class='app-header']//div[@class='price']//div[@class='value']/text()").extract()[0]
# 最新版本
info['version'] = base.xpath(".//div[@class='app-header']//div[@class='version']//div[@class='value']/text()").extract()[0]
# 应用截图
info['screenshot'] = base.xpath(".//div[@class='router-wrapper']//div[@class='app-screenshot']//div[@class='screenshot-box']//img/@src").extract()
# 应用描述
info['desc'] = base.xpath(".//div[@class='router-wrapper']//div[@class='app-describe']//div[@class='description']").extract()[0]
# 应用基本信息
info['baseinfo'] = []
for infoBase in base.xpath(".//div[@class='router-wrapper']//div[@class='app-baseinfo']//ul[@class='baseinfo-list']/li"):
# print(info['baseinfo'])
try:
info['baseinfo'].append(dict(type=infoBase.xpath(".//*[@class='type']/text()").extract()[0],
info=infoBase.xpath(".//*[@class='info-txt']/text()").extract()[0]))
except Exception as e:
pass
rankItem['info'] = info
# 替换图标 列表加载为默认图标
rankItem['src'] = \
response.xpath("//*[@id='app-side-bar']//div[@class='logo-wrap']/img/@src").extract()[0]
yield rankItem
再次执行scrapy crawl RankSpider -o data1.json
,则可看见已经生成 data2.json,但是生成的列表不是排行的列表,甚至是乱序的,原因是因为我们使用了 url 跟进返回,每个页面的请求返回的速度不一样,需要排序的话就写个小脚本按照 index 排个序。
项目源码
原文链接