爬虫总结(二)--scrapy。用现成的框架的好处就是不用担心 cookie、retry、频率限制、多线程的事。这一篇把上一篇的实例用 scrapy 框架重新实现一遍。主要步骤就是新建项目 (Project) –> 定义目标(Items)–> 制作爬虫(Spider)–> 存储结果(Pipeline)
Scrapy 概述
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试
Scrapy 架构
Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下
绿线是数据流向,首先从初始 URL 开始,Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,Spider 分析出来的结果有两种:一种是需要进一步抓取的链接,例如之前分析的“下一页”的链接,这些东西会被传回 Scheduler ;另一种是需要保存的数据,它们则被送到 Item Pipeline 那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。另外,在数据流动的通道里还可以安装各种中间件,进行必要的处理。
组件">Scrapy 组件
引擎(Scrapy): 用来处理整个系统的数据流处理, 触发事务(框架核心)调度器(Scheduler): 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
下载器(Downloader): 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
爬虫(Spiders): 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
项目管道(Pipeline): 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
下载器中间件(Downloader Middlewares): 位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
爬虫中间件(Spider Middlewares): 介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
调度中间件(Scheduler Middewares): 介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。
Scrapy 运行流程
引擎从调度器中取出一个链接(URL)用于接下来的抓取引擎把URL封装成一个请求(Request)传给下载器,下载器把资源下载下来,并封装成应答包(Response)爬虫解析Response若是解析出实体(Item),则交给实体管道进行进一步的处理;若是解析出的是链接(URL),则把URL交给Scheduler等待抓取默认情况下,Scrapy使用 LIFO 队列来存储等待的请求。简单的说,就是深度优先顺序。如果想要广度优先顺序进行爬取,需要进行设定。
Scrapy 存在的问题
爬虫是一个很依赖于网络io的应用,单机的处理能力有限,很快就变成瓶颈。而scrapy并不是一个分布式的设计,在需要大规模爬取的情况下就很成问题。当然可以通过修改Request队列来实现分布式爬取,而且工作量也不算特别大。
scrapy的并行度不高。力图在爬虫里做一些计算性的操作就会影响抓取的速率。这主要是python里的线程机制造成的,因为Python使用了GIL(和Ruby一样),多线程并不会带来太多速度上的提升(除非用Python的C扩展实现自己的模块,这样绕过了GIL)。Summary:Use Python threads if you need to run IO operations in parallel. Do not if you need to run computations in parallel.scrapy的内存消耗很快。可能是出于性能方面的考虑,pending requests并不是序列化存储在硬盘中,而是放在内存中的(毕竟IO很费时),而且所有Request都放在内存中。你抓取到 百万网页的时候,考虑到单个网页时产生很多链接的,pending request很可能就近千万了,加上脚本语言里的对象本来就有额外成本,再考虑到GC不会立即释放内存,内存占用就相当可观了。
归根到底,这两个问题是根植于语言之中的。
Scrapy 实例
新建项目 (Project)
scrapy startproject news_scrapy
输入以上命令之后,就会看见命令行运行的目录下多了一个名为 news_scrapy 的目录,目录的结构如下:
|---- news_scrapy | |---- news_scrapy | |---- __init__.py | |---- items.py #用来存储爬下来的数据结构(字典形式) | |---- pipelines.py #用来对爬出来的item进行后续处理,如存入数据库等 | |---- settings.py #爬虫配置文件 | |---- spiders #此目录用来存放创建的新爬虫文件(爬虫主体) | |---- __init__.py | |---- scrapy.cfg #项目配置文件
定义目标(Items)
Items是装载抓取的数据的容器,工作方式像 python 里面的字典,但它提供更多的保护,比如对未定义的字段填充以防止拼写错误
通过创建scrapy.Item类, 并且定义类型为 scrapy.Field 的类属性来声明一个Item,通过将需要的item模型化,来控制站点数据。
编辑 items.py
# -*- coding: utf-8 -*- import scrapy class NewsScrapyItem(scrapy.Item): # define the fields for your item here like: category = scrapy.Field() url = scrapy.Field() secondary_title = scrapy.Field() secondary_url = scrapy.Field() #text = Field()
制作爬虫(Spider)
Spider 定义了用于下载的URL列表、跟踪链接的方案、解析网页内容的方式,以此来提取items。
要建立一个Spider,你必须用scrapy.spider.BaseSpider创建一个子类,并确定三个强制的属性:
start_urls:爬取的URL列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
parse():解析的方法,调用的时候传入从每一个URL传回的Response对象作为唯一参数,负责解析并匹配抓取的数据(解析为item),跟踪更多的URL。
在 spiders 目录下新建 Wynews.py,代码如下。利用 yield Request(url=item[‘url’],meta={‘item_1’: item},callback=self.second_parse) 来进行第二层爬取。
class WynewsSpider(BaseSpider): name = "Wynews" start_urls = ['https://news.163.com/rank/'] def parse(self,response): html = HtmlXPathSelector(response) page = html.xpath('//p[@class="subNav"]/a') for i in page: item = dict() item['category'] = i.xpath('text()').extract_first() item['url'] = i.xpath('@href').extract_first() print item['category'],item['url'] yield Request(url=item['url'],meta={'item_1': item},callback=self.second_parse) def second_parse(self,response): item_1= response.meta['item_1'] html = HtmlXPathSelector(response) #print 'response ',response page = html.xpath('//tr/td/a') #print 'page ',page items = [] for i in page: item = DidiScrapyItem() item['category'] = item_1['category'].encode('utf8') item['url'] = item_1['url'].encode('utf8') item['secondary_title'] = i.xpath('text()').extract_first().encode('utf8') item['secondary_url'] = i.xpath('@href').extract_first().encode('utf8') #print i.xpath('text()').extract(),i.xpath('@href').extract() items.append(item) return items
存储结果(Pipeline)
Item pipeline 的主要责任是负责处理 spider 抽取的 Item,主要任务是清理、验证和存储数据。当页面被 spider 解析后,将被发送到 pipeline,每个 pipeline 的组件都是由一个简单的方法组成的Python类。pipeline 获取Item,执行相应的方法,并确定是否需要在 pipeline中继续执行下一步或是直接丢弃掉不处理。
执行过程
清理HTML数据验证解析到的数据(检查Item是否包含必要的字段)
检查是否是重复数据(如果重复就删除)
将解析到的数据存储到 数据库/文件 中
主要方法
process_item(item, spider)
每一个item管道组件都会调用该方法,并且必须返回一个item对象实例或raise DropItem异常。
被丢掉的item将不会在管道组件进行执行
open_spider(spider)
当spider执行的时候将调用该方法
close_spider(spider)
当spider关闭的时候将调用该方法
编写自己的 Pipeline
编辑 pipelines.py。把抓取的 items 保存到 json 文件中。
import json class NewsScrapyPipeline(object): def __init__(self): self.file = open('items.json', 'w') def process_item(self, item, spider): line = json.dumps(dict(item),ensure_ascii=False) + "\n" self.file.write(line) return item
另外,如果不考虑编码(没有中文),可以在运行爬虫的时候直接通过下面的命令导出结果。
dump到JSON文件:
scrapy crawl myspider -o items.json
dump到CSV文件:
scrapy crawl myspider -o items.csv
dump到XML文件:
scrapy crawl myspider -o items.xml
激活Item Pipeline组件
在settings.py文件中,往ITEM_PIPELINES中添加项目管道的类名,激活项目管道组件
ITEM_PIPELINES = { 'news_scrapy.pipelines.NewsScrapyPipeline': 300, }
开启爬虫 (Crawl)
scrapy crawl Wynews
完整代码
可能出现的问题 (Problem)
打开 items.json 文件,中文可能会出现文件乱码问题
[{"category": "\u93c2\u4f34\u6908", "url": "https://news.163.com/special/0001386F/rank_news.html", "secondary_title": "\u934b\u950b\u9422\u5cf0\u30b3\u95c3\u8e6d\u7b09\u9473\u6ec8\u69fb\u951b\u5c7e\u5d0f\u6fc2\u7a3f\u5dfb\u9359\u53c9\u7c2e\u6769\u6ec4\u7966\u95c0", "secondary_url": "https://caozhi.news.163.com/16/0615/09/BPJG6SB60001544E.html"},
这一行代码就能解决。
line = json.dumps(dict(item),ensure_ascii=False) + "\n"
结果
{"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "A股闯关MSCI再度失败 索罗斯们押注对冲胜出", "secondary_url": "https://money.163.com/16/0615/06/BPJ4T69300253B0H.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "湖北副省长担心房价下跌:泡沫若破裂后果很严重", "secondary_url": "https://money.163.com/16/0615/08/BPJBM36U00252G50.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "马云:假货质量超过正品 打假很复杂", "secondary_url": "https://money.163.com/16/0615/08/BPJAIOVI00253G87.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "A股闯关未成功 纳入MSCI新兴市场指数被延迟", "secondary_url": "https://money.163.com/16/0615/07/BPJ7260D00252G50.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "马云称许多假货比真品好 网友:怪不得淘宝假货多", "secondary_url": "https://money.163.com/16/0615/08/BPJC437N002526O3.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "贪官示意家人低价买地 拆迁后获赔近亿元", "secondary_url": "https://money.163.com/16/0615/08/BPJAT58400252G50.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "又是毒胶囊:浙江查获1亿多粒毒胶囊 6人被捕", "secondary_url": "https://money.163.com/16/0615/07/BPJ8NMRG00253B0H.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "还不起了?委内瑞拉寻求宽限1年偿还中国贷款", "secondary_url": "https://money.163.com/16/0615/07/BPJ9IH3400252C1E.html"} {"category": "财经", "url": "https://money.163.com/special/002526BH/rank.html", "secondary_title": "A股频现清仓式减持 上半年十大减持王曝光", "secondary_url": "https://money.163.com/16/0615/07/BPJ7Q9BC00254IU4.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "《装X购车指南》 30-50万都能买到啥车?", "secondary_url": "https://auto.163.com/16/0615/07/BPJ6U1J900084TUP.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "看挡杆还以为是A8L 新款哈弗H9内饰曝光", "secondary_url": "https://auto.163.com/16/0615/00/BPIGTP4B00084TUO.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "前脸/尾灯有变 新款捷达搭1.5L油耗更低", "secondary_url": "https://auto.163.com/16/0615/00/BPIGMEHE00084TUO.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "主打车型不超10万良心价 远景SUV将8月上市", "secondary_url": "https://auto.163.com/16/0615/00/BPIHR2A500084TUO.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "Macan并不是我真姓 众泰SR8搭2.0T/D", "secondary_url": "https://auto.163.com/16/0613/00/BPDBPB0J00084TUO.html"} {"category": "汽车", "url": "https://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "上海福特翼搏优惠1.5万元", "secondary_url": "https://auto.163.com/16/0615/00/BPIHH8FF000857M6.html"}
添加命令行参数
第一种方法,在命令行用crawl控制spider爬取的时候,加上-a选项,如
scrapy crawl WangyiSpider -a category=打车
然后在 spider 的构造函数里加上带入的参数
12345678910 |
import scrapyclass WangyiSpider(BaseSpider): name = "Wangyi" def __init__(self, category=None, *args, **kwargs): super(WangyiSpider, self).__init__(*args, **kwargs) self.base_url = 'https://news.yodao.com/' self.start_urls = ['https://news.yodao.com/search?q=' + category] |
代码
通过关键词爬取网易新闻-代码
运行多个爬虫
默认情况当你每次执行scrapy crawl命令时会创建一个新的进程。但我们可以使用核心API在同一个进程中同时运行多个spider,如下,在 settings.py 的同级目录下编辑 run.py,导入编写的 spider 类如 JingdongSpider, SuningSpider。
12345678910111213141516171819202122232425262728 |
import scrapyfrom twisted.internet import reactorfrom scrapy.crawler import CrawlerRunnerfrom scrapy.utils.log import configure_loggingfrom scrapy.spiders import Spiderfrom scrapy.selector import HtmlXPathSelectorfrom items import FaqscrapyItemfrom scrapy.http import Requestfrom scrapy.selector import Selectorfrom scrapy.utils.project import get_project_settingsfrom spiders.FAQ_jingdong import JingdongSpiderfrom spiders.FAQ_suning import SuningSpiderimport reif __name__ == '__main__': settings = get_project_settings() configure_logging(settings) runner = CrawlerRunner(settings) runner.crawl(JingdongSpider) runner.crawl(SuningSpider) d = runner.join() d.addBoth(lambda _: reactor.stop()) # blocks process so always keep as the last statement reactor.run() |
然而不幸的是同一进程内运行多个 spider 可能会出现数据丢失问题,影响进一步的数据使用。如下:
123 |
{"url": "https://help.jd.com/user/issue/231-213.html", "text": "订单已提交成功,如何付款?付款方式分为以下几种:(注:先款订单请您在订单提交后24小时内完成支付,否则订单会自动取消)1.货到付款:选择货到付款,在订单送达时您可选择现金、POS机刷卡、支票方式支付货款或通过京东APP手机客户端【扫一扫】功能扫描包裹单上的订单条形码方式用手机来完成订单的支付(扫码支付);在订单未妥投之前您还可以进入“我的订单”在线支付货款。注意:货到付款的订单,如果一个ID帐号在一个月内有过1次以上或一年内有过3次以上,无理由不接收我司配送的商品,我司将在相应的ID帐户里按每单扣除500个京豆做为运费;时间计算方法为:成功提交订单后向前推算30天为一个月,成功提交订单后向前推算365天为一年,不以自然月和自然年计算。2.在线支付:选择在线支付,请您进入“我的订单”,点击“付款”,按提示进行操作;目前在线支付支持京东白条、余额、银行卡、网银+、微信、银联在线、网银钱包、信用卡等方式进行支付,可根据您的使用喜好进行选择。3.分期付款:目前不支持信用卡分期付款。4.公司转账:提交订单后选择线下公司转账会生成15位汇款识别码,请您按照提示到银行操作转账,然后进入“我的订单”填写付款确认;5.邮局汇款:订单提交成功后,请您按照提示到邮局操作汇款,然后进入“我的订单”填写付款确认。", "question": "订单已提交成功,如何付款?", "title": "支付流程"}?系统停机维护期间。(二) 电信设备出现故障不能进行数据传输的。(三) 由于黑客攻击、网络供应商技术调整或故障、网站升级、银行方面的问题等原因而造成的易付宝服务中断或延迟。(四) 因台风、地震、海啸、洪水、停电、战争、恐怖袭击等不可抗力之因素,造成易付宝系统障碍不能执行业务的。 第十三条 关于本协议条款和其他协议、告示或其他有关您使用本服务的通知,易付宝将以电子形式或纸张形式通知您,包括但不限于依据您向易付宝提供的电子邮件地址发送电子邮件的方式、依据投资者提供的联系地址寄送挂号信的方式、易付宝或合作伙伴网站公告、或发送手机短信、系统内通知和电话通知等方式。 第十四条 易付宝有权根据需要不时地修改本协议或制定、修改各类规则,但是,对于减少您权益或加重您义务的新增、变更或修改,易付宝将在生效日前提前至少7个日历日进行公示,如您不同意相关新增、变更或修改,您可以选择在公示期内终止本协议并停止使用本服务。如果相关新增、变更或修改生效后,您继续使用本服务则表示您接受修订后的权利义务条款。 第十五条 因本协议引起的或与本协议有关的争议,均适用中华人民共和国法律。 第十六条 因本协议引起的或与本协议有关的争议,易付宝与用户协商解决。协商不成的,任何一方均有权向被告住所地人民法院提起诉讼。 第十七条 本协议作为《易付宝余额理财服务协议》的有效补充,本协议未约定的内容,双方需按照《易付宝余额理财服务协议》相关约定。 ", "question": "零钱宝定期转出服务协议", "title": "苏宁理财"}l": "https://help.suning.com/page/id-536.htm", "text": " |
代码
Scrapy 调优
提高并发能力
增加并发
并发是指同时处理的request的数量。其有全局限制和局部(每个网站)的限制。Scrapy 默认的全局并发限制(16)对同时爬取大量网站的情况并不适用,因此需要增加这个值。 增加多少取决于爬虫能占用多少CPU。 一般开始可以设置为 100 。不过最好的方式是做一些测试,获得 Scrapy 进程占取CPU与并发数的关系。选择一个能使CPU占用率在80%-90%的并发数比较恰当。
# 增加全局并发数 CONCURRENT_REQUESTS = 100
mac 下调试,运行程序后通过 top 监控,p 按 cpu 排序。观察发现,在 CONCURRENT_REQUESTS = 32 时,cpu 占用最多到 50% 左右,调整到 CONCURRENT_REQUESTS = 100,cpu 占用 90% 上下。
查看本机 cpu 信息,用 sysctl machdep.cpu 命令,如下,可以看到我的机子是双核、4线程的。
# cpu 信息 $ sysctl machdep.cpu .......... machdep.cpu.core_count: 2 machdep.cpu.thread_count: 4 machdep.cpu.tsc_ccc.numerator: 0 machdep.cpu.tsc_ccc.denominator: 0
降低log级别
为了减少CPU使用率(及记录log存储的要求), 当调试程序完毕后,可以不使用 DEBUG log级别。
# 设置Log级别: LOG_LEVEL = 'INFO'
禁止cookies
禁止cookies能减少CPU使用率及Scrapy爬虫在内存中记录的踪迹,提高性能。
# 禁止cookies: COOKIES_ENABLED = False
禁止重试
对失败的HTTP请求进行重试会减慢爬取的效率,尤其是当站点响应很慢(甚至失败)时, 访问这样的站点会造成超时并重试多次。这是不必要的,同时也占用了爬虫爬取其他站点的能力。
# 禁止重试: RETRY_ENABLED = False
减小下载超时
对一个非常慢的连接进行爬取(一般对通用爬虫来说并不重要), 减小下载超时能让卡住的连接能被快速的放弃并解放处理其他站点的能力。
# 减小下载超时: DOWNLOAD_TIMEOUT = 15 # 可能会引发的错误 TimeoutError: User timeout caused connection failure: Getting https://homea.people.com.cn/n1/2016/0628/c69176-28504657.html took longer than 15.0 seconds..
通过如上配置,我的爬虫每分钟响应的request是之前的4倍,然而值得注意的是,这些设置并不是在所有场景都适用,需要通过具体场景试验,具体问题具体分析。
避免被禁止(ban)
有些网站实现了特定的机制,以一定规则来避免被爬虫爬取。下面是些处理这些站点的建议(tips):
使用user agent池,轮流选择之一来作为user agent。池中包含常见的浏览器的user agent(google一下一大堆)禁止cookies(参考 COOKIES_ENABLED),有些站点会使用cookies来发现爬虫的轨迹。
设置下载延迟(2或更高)。参考 DOWNLOAD_DELAY 设置。
如果可行,使用 Google cache 来爬取数据,而不是直接访问站点。
使用IP池。例如免费的 Tor项目 或付费服务(ProxyMesh)。
使用高度分布式的下载器(downloader)来绕过禁止(ban),就只需要专注分析处理页面。这样的例子有: Crawlera
如果仍然无法避免被ban,考虑商业支持.
实例
首先要有的是user agent池和IP池。user agent池如下,添加在 settings.py 中。
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", ]
IP池 获取方式有多种,这里抓取的是西刺免费代理IP的 IP,注意实时更新问题,否则很容易失败。将抓取的 IP 以https://host1:port的格式存储于 list.txt 文本中。在 settings.py 里添加 PROXY_LIST = ‘/path/to/proxy/list.txt’。
有了user agent池和IP池,接下来需要编写中间件,如下。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
import reimport randomimport base64from scrapy import logclass RandomProxy(object): def __init__(self, settings): self.proxy_list = settings.get('PROXY_LIST') fin = open(self.proxy_list) self.proxies = {} for line in fin.readlines(): parts = re.match('(\w+://)(\w+:\w+@)?(.+)', line) if not parts: continue # Cut trailing @ if parts.group(2): user_pass = parts.group(2)[:-1] else: user_pass = '' self.proxies[parts.group(1) + parts.group(3)] = user_pass fin.close() @classmethod def from_crawler(cls, crawler): return cls(crawler.settings) def process_request(self, request, spider): # Don't overwrite with a random one (server-side state for IP) if 'proxy' in request.meta: return proxy_address = random.choice(self.proxies.keys()) proxy_user_pass = self.proxies[proxy_address] request.meta['proxy'] = proxy_address if proxy_user_pass: basic_auth = 'Basic ' + base64.encodestring(proxy_user_pass) request.headers['Proxy-Authorization'] = basic_auth print "**************ProxyMiddleware have pass************" + proxy['ip_port'] def process_exception(self, request, exception, spider): proxy = request.meta['proxy'] log.msg('Removing failed proxy <%s>, %d proxies left' % ( proxy, len(self.proxies))) try: del self.proxies[proxy] except ValueError: passclass RandomUserAgent(object): """Randomly rotate user agents based on a list of predefined ones""" 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): print "**************************" + random.choice(self.agents) request.headers.setdefault('User-Agent', random.choice(self.agents)) |
改写 spider,check 某个元素,确保 proxy 能够返回 target page。
if not pageUrls: yield Request(url=response.url, dont_filter=True)
配置 settings.py
# Retry many times since proxies often fail RETRY_TIMES = 10 # Retry on most error codes since proxies fail for different reasons RETRY_HTTP_CODES = [500, 503, 504, 400, 403, 404, 408] # Configure a delay for requests for the same website (default: 0) DOWNLOAD_DELAY=3 # Disable cookies (enabled by default) COOKIES_ENABLED=False # Enable downloader middlewares DOWNLOADER_MIDDLEWARES = { 'scrapy.contrib.downloadermiddleware.retry.RetryMiddleware': 90, # Fix path to this module 'blogCrawler.middlewares.RandomProxy': 100, 'blogCrawler.middlewares.RandomUserAgent': 1, 'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 110, }
这是一份简单的测试代码
其他调优
来自使用scrapy进行大规模抓取
如果想要爬取的质量更高,尽量使用宽度优先的策略,在配置里设置 SCHEDULER_ORDER = ‘BFO’
修改单爬虫的最大并行请求数 CONCURRENT_REQUESTS_PER_SPIDER
修改twisted的线程池大小,默认值是10。参考Using Threads in Twisted,在scrapy/core/manage.py爬虫启动前加上
reactor.suggestThreadPoolSize(poolsize)
可以开启dns cache来提高性能。在配置里面加上
EXTENSIONS={’scrapy.contrib.resolver.CachingResolver’: 0,}
如果自己实现duplicate filter的话注意要保证它是一直可用的,dupfilter里的异常是不会出现在日志文件中的,好像外面做了try-expect处理
去重与增量抓取
去重
Scrapy支持通过RFPDupeFilter来完成页面的去重(防止重复抓取)。RFPDupeFilter实际是根据request_fingerprint实现过滤的,实现如下:
1234567891011121314151617 |
def request_fingerprint(request, include_headers=None):if include_headers: include_headers = tuple([h.lower() for h in sorted(include_headers)])cache = _fingerprint_cache.setdefault(request, {})if include_headers not in cache: fp = hashlib.sha1() fp.update(request.method) fp.update(canonicalize_url(request.url)) fp.update(request.body or '') if include_headers: for hdr in include_headers: if hdr in request.headers: fp.update(hdr) for v in request.headers.getlist(hdr): fp.update(v) cache[include_headers] = fp.hexdigest()return cache[include_headers] |
我们可以看到,去重指纹是sha1(method + url + body + header),所以,实际能够去掉重复的比例并不大。
如果我们需要自己提取去重的finger,需要自己实现Filter,并配置上它。
例如下面这个Filter只根据url去重:
1234567891011 |
from scrapy.dupefilter import RFPDupeFilterclass SeenURLFilter(RFPDupeFilter): """A dupe filter that considers the URL""" def __init__(self, path=None): self.urls_seen = set() RFPDupeFilter.__init__(self, path) def request_seen(self, request): if request.url in self.urls_seen: return True else: self.urls_seen.add(request.url) |
要在 settings 添加配置。
DUPEFILTER_CLASS =’scraper.custom_filters.SeenURLFilter’
增量爬取
可以看这篇汇总贴
其实如果根据 url 判断的话有很多种方案,如下面这种(比起上面汇总贴的其他方案来说算是复杂的)。
增量抓取。一个针对多个网站的爬虫很难一次性把所有网页爬取下来,并且网页也处于不断更新的状态中,爬取是一个动态的过程,爬虫支持增量的抓取是很必要的。大概的流程就是关闭爬虫时保存duplicate filter的数据,保存当前的request队列,爬虫启动时导入duplicate filter,并且用上次request队列的数据作为start url。这里还涉及scrapy一个称得上bug的问题,一旦抓取队列里url过多,关闭scrapy需要很久,有时候要花费几天的时间。我们hack了scrapy的代码,在接收到关闭命令后,保存duplicate filter数据和当前的request队列和已抓取的url列表,然后调用twisted的reactor.stop()强制退出。当前的request队列可以通过scrapy.core.scheduler的pending_requests成员得到。
然而,如果使所有网站的动态过滤,比如是不是多了一个新回复,在url上的变化并不能体现出来,搜索引擎采用的是一系列的算法,判断某一个页面的更新时机。这个时候只能尝试用网页在进入下一级页面的时候都类似于最后更新时间、最后活动时间的参数进行判断了。
有机会会去尝试。