scrapy 源代码阅读笔记(0)-- 背景

初探

scrapy可以服务与中小型爬虫项目,异步下载性能很出色,(50M电信,scrapy单进程,半小时,最高纪录12w页)。不过更令人惊讶的是scrapy的代码风格以及官方文档和社区,笔者受益良多。仅以此文记录阅读scrapy代码的经验与想法。

基本对象

scrapy.http.Request

  1. 基本的Request对象,描述url, proxy, User-Agent, method之类的本地发送http请求之前基本信息,
  2. 注意这只是scrapy封装的描述对象,并不显含下载功能

scrapy.http.Response

  1. 基本的Response对象,与Request对应,描述对方网站服务器的回复内容,包含url, text之类的
  2. Reponse还实现了xpath方法(似乎是基于elementtree),便于实时解析网页。这部分性能损耗可基本忽略(python下elementtree的实例化耗时,thinkpad i5-6200u,单进程,大概130/s,包含读文件)

scrapy.Spider
如果了解基本的网络爬虫,可以看将Spider作网络爬虫中的“虫”,主要负责:

发送Request
解读Response(发现新的Request,抽取目标信息)

二者循环,直到没有 新的 Request为止。
到此为止,咬文嚼字就应该发现 scrapy中的Spider只是一个独立的模块,但无法独立完成“爬虫”的工作。完成爬虫至少还需要以下模块

  • 高效下载 –> 非阻塞 or 多线程
  • 提取新的url –> 去重
  • 避免并发访问统一网站 –> 合理的调度
  • 存储目标信息 –> 数据管道(指向文件系统/数据库)
  • 功能模块之间的衔接 –> 核心引擎

数据流向

《scrapy 源代码阅读笔记(0)-- 背景》

这张图来自scrapy 1.2,图虽然丑,但是标注了与代码逻辑相对应数据流的顺序,可以在不阅读核心代码的情况下理清
逻辑

推荐

值得一提的是,如果需要分布式的支持,只需要对各个模块分别实现分布式,实际上就是对模块单独开一个进程,或者说挂成服务对外开放。
分布式的支持,对于scrapy来说可以通过插件的形式引入。

  1. Frontera 官方维护的分布式框架api,提供更强大的引擎和更智能的队列控制,理论上可以接入任何爬虫系统;消息队列(Kafka or ZeroMQ),数据后端(Hbase)。
  2. Scrapy Cluster 与frontera很类似(其文档中有提到), 封装程度更高,提供了后端的monitor, 其推荐的log factory可以方便和ELK结合,实现实时的log分析(但目前并没提供相关api,需要自己定制)
  3. Scrapy Redis 轻量级插件,将scrapy默认的memory queue转到redis中, 实现了消息队列的分布式
  4. 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 一起作出对比

  1. 简书的用户 scrapy 和 pyspider 介绍
  2. 知乎上网友发问 pyspider 和 scrapy 比较起来有什么优缺点吗?
  3. scrapy 王婆 Quora 回答 How does pyspider compare to scrapy?
  4. pyspider 王婆 stackoverflow 怒顶 Can scrapy be replaced by pyspider

以上几篇是google搜索中英文“scrapy和pyspider比较” 排名较高的链接(赞叹google确实强大,两篇来自网友,两篇来自开发者)。先别去在意评论者的情感色彩,挖掘下我们能从中了解到什么干货。

  1. 初探 两种框架的架构(数据流)和主要组成(功能模块)
  2. 开发效率,网友的体会,萝卜青菜,各有所爱。
  3. 功能模块比较,主要从功能以及实现方式介绍(广告)。
  4. 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参数对比

项目commitsbranchesreleasescontributorsopenclosed
scrapy58962670219299766
pyspider8514113291385

截止2016/10/23

搜索经验

关于如何搜索scrapy相关话题,一些个人经验

  1. 知乎,quora,或者博客的文章可以解决背景问题,或者说提出一个技术问题,或者指向文档/轮子的具体位置
  2. stackoverflow至少可以找到解决问题的线索,或者轮子的线索,不过通常会引入更多的问题,比如twisted, celery, message bus之类在scrapy文档中没有详细描述的。另外,很多技术宅喜欢针对问题的贴自己的代码解决方案(逻辑)
  3. 在其他网站找到的解决方案,要去文档核实(小问题一般都能对应),代码可能已经更新了,注意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)

  1. 统一定义@classmethod from_crawler,作为实例化的接口(返回一个注入了settings的实例)
  2. 可以调用异步方法,以实现针对各个spider的处理的方法
    spider_opened, spider_closed(optional)
  3. 不用怀疑,几乎每个插件都包含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')
    原文作者:troy_ld
    原文地址: https://www.jianshu.com/p/9c61325046d5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞