scrapy 源代码阅读笔记(0)-- 背景
初探
scrapy可以服务与中小型爬虫项目,异步下载性能很出色,(50M电信,scrapy单进程,半小时,最高纪录12w页)。不过更令人惊讶的是scrapy的代码风格以及官方文档和社区,笔者受益良多。仅以此文记录阅读scrapy代码的经验与想法。
基本对象
scrapy.http.Request
- 基本的Request对象,描述url, proxy, User-Agent, method之类的本地发送http请求之前基本信息,
- 注意这只是scrapy封装的描述对象,并不显含下载功能
scrapy.http.Response
- 基本的Response对象,与Request对应,描述对方网站服务器的回复内容,包含url, text之类的
- Reponse还实现了xpath方法(似乎是基于elementtree),便于实时解析网页。这部分性能损耗可基本忽略(python下elementtree的实例化耗时,thinkpad i5-6200u,单进程,大概130/s,包含读文件)
scrapy.Spider
如果了解基本的网络爬虫,可以看将Spider作网络爬虫中的“虫”,主要负责:
发送Request
解读Response(发现新的Request,抽取目标信息)
二者循环,直到没有 新的 Request为止。
到此为止,咬文嚼字就应该发现 scrapy中的Spider只是一个独立的模块,但无法独立完成“爬虫”的工作。完成爬虫至少还需要以下模块
- 高效下载 --> 非阻塞 or 多线程
- 提取新的url --> 去重
- 避免并发访问统一网站 --> 合理的调度
- 存储目标信息 --> 数据管道(指向文件系统/数据库)
- 功能模块之间的衔接 --> 核心引擎
数据流向
这张图来自scrapy 1.2,图虽然丑,但是标注了与代码逻辑相对应数据流的顺序,可以在不阅读核心代码的情况下理清逻辑。
推荐
值得一提的是,如果需要分布式的支持,只需要对各个模块分别实现分布式,实际上就是对模块单独开一个进程,或者说挂成服务对外开放。
分布式的支持,对于scrapy来说可以通过插件的形式引入。
- Frontera 官方维护的分布式框架api,提供更强大的引擎和更智能的队列控制,理论上可以接入任何爬虫系统;消息队列(Kafka or ZeroMQ),数据后端(Hbase)。
- Scrapy Cluster 与frontera很类似(其文档中有提到), 封装程度更高,提供了后端的monitor, 其推荐的log factory可以方便和ELK结合,实现实时的log分析(但目前并没提供相关api,需要自己定制)
- Scrapy Redis 轻量级插件,将scrapy默认的memory queue转到redis中, 实现了消息队列的分布式
- Scrapyd scrapy代码部署工具,提供http api操控爬虫的进度,支持多个scrapy project。官方文档提到支持python2.6+,osx下实测其变种scrapyrt(只支持单个project)默认对于python3.5支持不够好(可能是twisted的问题),2.7.12可以正常跑
阅读文档与代码
对于初级开发者(没写过爬虫框架)在scrapy开发中遇到的问题,坚持一切以官方文档为准。刚开始你能遇到想到甚至梦到的问题,初步估计70%都能通过官方文档解决,20%通过插件可以解决,5%或许通过阅读源代码你会有更深的理解,另外5%估计scrapy就很难满足需求了。比如想通过scrapy复制一个google, 可能首先需要的是钞票而不是技术。但比如说你很讨厌scrapy 默认terminal command运行,在scrapy文档中advanced topic描述了如何在脚本中讲spider封装到crawler中(有暗坑),以及crawler的启动方式,另外也可以借助scrapyd/scrapyrt部署成service,通过http api的方式提交爬虫任务。
搜索话题
Scrapy XXX module complains XXX
How can I XXX with scrapy in order to XXX
在google,github,stackoverflow上可以搜到很多类似的话题,虽然这可能并不是最佳的答案,但一定是最快的。根据笔者印象,很多scrapy初探者甚至开发者都没有耐心在精读官方文档或者阅读scrapy源代码之后再开始开发工作(看看提的问题是什么就能猜出来),唯一能做的就是相信这个问题别人多半遇到过,即使还没有被解决,也一定有线索。通常笔者就是这样解决的,最开始遇到从脚本运行爬虫(CrawlerProcess重复运行)twisted报错到最近用splash渲染js(scrapy_splash插件)的同时实现增量爬取 (deltafetch插件) 不兼容都是这样处理。
- 很多时候我们只是缺少线索 -- 弄明白解决问题需要参数级的处理,还是代码级的处理,然后再思考要不要造轮子,或者借别人的轮子
Powerful
关于 scrapy 究竟如何 powerful,同样的 python 环境下,scrapy 常与pyspider 一起作出对比
- 简书的用户 scrapy 和 pyspider 介绍
- 知乎上网友发问 pyspider 和 scrapy 比较起来有什么优缺点吗?
- scrapy 王婆 Quora 回答 How does pyspider compare to scrapy?
- pyspider 王婆 stackoverflow 怒顶 Can scrapy be replaced by pyspider
以上几篇是google搜索中英文“scrapy和pyspider比较” 排名较高的链接(赞叹google确实强大,两篇来自网友,两篇来自开发者)。先别去在意评论者的情感色彩,挖掘下我们能从中了解到什么干货。
- 初探 两种框架的架构(数据流)和主要组成(功能模块)
- 开发效率,网友的体会,萝卜青菜,各有所爱。
- 功能模块比较,主要从功能以及实现方式介绍(广告)。
- pyspider的开发者逻辑
虽然我目前用的是scrapy,但以上文章令我映象最深的来自于4,pyspider的开发者binux的回答
spider should never stop till WWW dead
仔细想想,web crawler确实应该服务化,爬虫的巨头谷歌、百度不都是这样做的么,你能想象几十万台的机器每天一台一台去开启和关闭? binux将自己的逻辑实现在代码中,开发了webui, 一开始就实现了在线编辑、调试、控制爬虫,令人不得不爱。不过,我对pyspider的尝试也就到此为止了,原因很简单,正如scrapy开发者在Quora里回答的那样
- Scrapy is a mature framework
- Scrapy has an active community
- Documentation is one area where Scrapy really shines.
开发者躲不过 效率优先,pyspider可以很容易的实现基本功能,但更复杂的需求,如果没有完善的文档以及社区支持,单枪匹马大概很快就挂了吧。然而,如果有足够的实力单独写完所有插件,可能也不需要最开始的轮子了。
github参数对比
项目 | commits | branches | releases | contributors | open | closed |
---|---|---|---|---|---|---|
scrapy | 5896 | 26 | 70 | 219 | 299 | 766 |
pyspider | 851 | 4 | 11 | 32 | 91 | 385 |
截止2016/10/23
搜索经验
关于如何搜索scrapy相关话题,一些个人经验
- 知乎,quora,或者博客的文章可以解决背景问题,或者说提出一个技术问题,或者指向文档/轮子的具体位置
- stackoverflow至少可以找到解决问题的线索,或者轮子的线索,不过通常会引入更多的问题,比如twisted, celery, message bus之类在scrapy文档中没有详细描述的。另外,很多技术宅喜欢针对问题的贴自己的代码解决方案(逻辑)
- 在其他网站找到的解决方案,要去文档核实(小问题一般都能对应),代码可能已经更新了,注意scrapy/python版本
代码风格提示
终于到了正题了,scrapy的的代码模块化程度很高(对于本渣来说是这样),刚开始阅读的时候,经常读不懂;后来渐渐发现是由于代码风格的差异。以功能模块实例化为例,经常我们设计类
class A:
def __init__(self,x,y,z):
pass
def method1(self):
pass
a = A(x,y,z) 做实例化,然后调用方法。然而,scrapy的做法
class A:
def __init__(self,x,y,z):
pass
@classmethod
def from_crawler(cls, cralwer):
# do something with crawler
x, y, z = foo(crawler)
return cls(x,y,z)
def method1(self):
pass
scrapy喜欢将参数配置在settings.py内,然后将settings参数倒入crawler中,然后通过引擎的初始化,spider与crawler绑定(姑且认为是“爬虫”中的“爬”的开关, 可以启动多个“虫”),然后通过插件中的from_crawler方法实例化插件,并导入参数。(完整代码参考文章末尾scrapy.extensions.spiderstate)
- 统一定义@classmethod from_crawler,作为实例化的接口(返回一个注入了settings的实例)
- 可以调用异步方法,以实现针对各个spider的处理的方法
spider_opened, spider_closed(optional) - 不用怀疑,几乎每个插件都包含1,2的特征;实际上,如果不包含 1 所推荐的实例化方法,这个插件不能通过官方推荐的方案集成到scrapy爬虫中去。
笔者猜测,插件的实例化方案是 MWs = SpiderState.from_crawler(crawler), 这样在生成实例的同时,注入了在spider(已经喝crawler绑定)开始/结束时的处理方案。这对于异常退出保存数据很重要。
To be continued
class SpiderState(object):
"""Store and load spider state during a scraping job"""
def __init__(self, jobdir=None):
self.jobdir = jobdir
@classmethod
def from_crawler(cls, crawler):
jobdir = job_dir(crawler.settings)
if not jobdir:
raise NotConfigured
obj = cls(jobdir)
crawler.signals.connect(obj.spider_closed, signal=signals.spider_closed)
crawler.signals.connect(obj.spider_opened, signal=signals.spider_opened)
return obj
def spider_closed(self, spider):
if self.jobdir:
with open(self.statefn, 'wb') as f:
pickle.dump(spider.state, f, protocol=2)
def spider_opened(self, spider):
if self.jobdir and os.path.exists(self.statefn):
with open(self.statefn, 'rb') as f:
spider.state = pickle.load(f)
else:
spider.state = {}
@property
def statefn(self):
return os.path.join(self.jobdir, 'spider.state')