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

作者: troy_ld | 来源:发表于2016-10-23 19:49 被阅读1615次

    初探

    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 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参数对比

    项目 commits branches releases contributors open closed
    scrapy 5896 26 70 219 299 766
    pyspider 851 4 11 32 91 385

    截止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')
    

    相关文章

      网友评论

      • 91e1e51dc46f:哇!!!
        我看到以这种方式@classmethod实例化的时候,同样觉得很懵逼。
        我只是知道第一种的实例化的方式。
        但是用@classmethod这种方式,还真是不懂,没见过。
        对于@classmethod的理解只是停留在,数据成员的访问上。
        老哥,有没有@classmethod使用的详细说明啊?

      本文标题:scrapy 源代码阅读笔记(0)-- 背景

      本文链接:https://www.haomeiwen.com/subject/viekuttx.html