python爬虫框架——Scrapy架构原理介绍

说起写爬虫,大多数第一时间想到的就是python了。python语法简洁明了,加上及其丰富好用的库,用它来写爬虫有天然的优势。

之前学python的时候也用requests+lxml写过几个爬虫玩,但是都就爬取一些内容就没继续下去了,都没做成一个项目,中间python也荒废了好久。最近要学kafka,就打算爬点数据来实践实践。于是就学起scrapy来,总的来说,scrapy还是很容易上手的,也比较简单,花了几天的时间学习加实践,也渐渐的掌握了这个爬虫框架。所以打算写个博客做个总结,以免之后又太久没用忘记了。

scrapy架构原理

scrapy架构图
[图片上传失败...(image-21611e-1532663400039)]

  1. scrapy引擎向spider获取起始Request集合,也就是spider中定义的start_urls。如果spider重写了start_requests()方法,那么这个方法返回的Request集合就是起始Request。
  2. scrapy引擎将拿到的Request发给调度中心开始调度。
  3. scrapy引擎向调度中心请求获取下一个要爬取的Request。
  4. scrapy引擎拿到Request后,然后将Request发给下载器。这个过程经过一系列在settings.py中配置的下载中间件,所有在settings.py中配置的下载中间件会依次对Request进行处理。——对应DownloaderMiddleware#process_request()方法
  5. 下载器根据Request拉取响应的内容,比如Request的url是http://www.baidu.com,下载器就会拉取对应的网页内容下来并封装成Response对象。
  6. 下载器将Response发送给scrapy引擎。这个过程也会经过一系列在settings.py中配置的下载中间件,这些下载中间件会依次对Response进行处理。——对应DownloaderMiddleware#process_response()方法
  7. scrapy引擎拿到Response后将Response发给spider,交给对应的spider函数处理。这里默认方法是parse(),这个回调方法构造Request的时候指定。引擎发送Response的过程会经过一系列在settings.py中配置的spider中间件,这些spider中间件会依次对Response进行一些处理。——对应SpiderMiddleware#process_spider_input()
  8. spider处理完Response后会返回一个result,这个result是一个包含 Request 或 Item 对象的可迭代对象(iterable)。然后将result发给scrapy引擎,这个过程也会经过一系列在settings.py中配置的spider中间件,这些spider中间件会依次对这个result进行一些处理。——对应SpiderMiddleware#process_spider_output()
  9. scrapy引擎拿到这个result后,会将其中的Item发送给Item Pipeline处理,这些item就会被一系列我们在settings.py中配置的pipeline处理。同时,scrapy也会将result中的Request发给调度中间准备调度。
  10. 继续重复第2步的步骤,直到所有的Request全部处理完后程序退出。

在scrapy 0.15版本后,spider中间件新增了一个方法,用于处理第一步中spider发送给引擎的Request,也就是SpiderMiddleware#process_start_requests(start_requests, spider)

scrapy的组件介绍

一、Spider

Spider组件主要用来生成要爬取的url,解析返回的内容,然后生成新的url继续交给scrapy去爬取,或者生成item交给pipeline处理。

编写scrapy爬虫应用时,我们大多数精力应该都是放在spider的编写上面。

  1. 通过定义start_urls参数或者重写start_requests()方法来给scrapy引擎提供起始的拉取url。
  2. 之后,scrapy引擎拿到url后去获取对应url网页中的内容后就会回调spider中的parse()方法,我们可以重写parse()方法来解析scrapy引擎返回回来的内容。
  3. parse()方法中,我们可以返回一个可迭代的对象,可以理解为是一个list,因为list也是一个可迭代对象,这个list里面可以放Request对象或者Item对象。
  4. scrapy拿到list后,会遍历整个容器,然后把Request再放进调度等会去获取内容,对于Item对象,scrapy引擎就把这个item对象发到pipeline去处理。在pipeline有用户自己编写的一套处理逻辑,可以选择的将item存到文件或者数据库中。
# -*- coding: utf-8 -*-  
import scrapy  
  
class TestSpider(scrapy.Spider):  
    name = "kongtrio"  
    # 定义爬取的域名,如果返回的url所属的域名不在这个列表里面,scrapy就不会去爬取内容
    allowed_domains = ["bbs.hupu.com"]  
    # 定义起始的url
    start_urls = (  
        'http://bbs.hupu.com/bxj/',  
    )  
    # 如果重写了这个方法,scrapy引擎取到的起始url就是这个方法返回的内容了
    # 这样start_urls就不会生效了
    def start_requests(self):
        for i in range(1, 10):
            yield scrapy.Request('http://bbs.hupu.com/bxj-' + str(i))
                  
    def parse(self, response):  
        # scrapy拉取到url的内容后,会封装成Response对象,然后回调这个parse()方法
        # 我们可以对这个response进行解析,然后根据策略返回响应的内容
        # scrapy 自带了xpath的方式解析内容,xpath教程可以看这篇  https://blog.csdn.net/u013332124/article/details/80621638
        title_href = response.xpath(".//a[@class='title']/@href").extract_first()
        title = response.xpath(".//a[@class='title']/text()").extract_first()
        # 返回一个request对象和一个item对象,request对象放的是标题的url,后面scrapy会继续读取这个url然后交给parse继续解析
        return [scrapy.Request(content_url, self.post_content_parse, dont_filter=True),{"title":title}]

二、pipeline

这也是我们需要关心的组件。前面spider返回的item会经过scrapy引擎的调度发向pipeline。
pipeline的组件做的事情很简单,就是拿到item,然后具体的操作用户自己实现。


class HupuSpiderPipeline(object):
    def process_item(self, item, spider):
        if not item["title"]:
            # 如果这个pipeline抛出DropItem异常,那么这个item就不会传给后面的pipeline了
            raise DropItem("invalid item")
        title = item["title"]
        print(title)
        # return后 会把这个item继续传给后面的pipeline
        return item

上面这个pipeline做的事情很简单,就是从item中获取title,然后打印出来。

我们可以写多个pipeline,分别做不同业务的事情。但是要注意的是,在process_item()方法中,必须将item返回,不然后面的pipeline就不会被调起来处理item了。或者抛出DropItem异常也会中断item的传递。

编写好pipeline之后还要记得在settings.py里面配置,这样pipeline才会真正被scrapy引擎知道,并开始工作。

# 后面的数字表示pipeline的次序
ITEM_PIPELINES = {
    'hupu_spider.pipelines.HupuSpiderPipeline': 300,
    # 'hupu_spider.pipelines.HupuImgDownloadPipeline': 400,
}

编写完pipeline和spider,我们其实就基本实现了一个简单的scrapy爬虫应用了。挺大一部分场景也只要我们编写pipeline和spider就可以了。当然,其他的组件也需要了解一下,以面对丰富多样的需求变动。

三、下载中间件

下载中间件主要是用于在scrapy引擎发送Request到下载器和下载器返回Response给scrapy引擎的过程中。

  1. scrapy引擎发送Request到下载器
    scrapy引擎发送Request到下载器的过程中,会经过一个个的下载中间件,这些中间件会对Request进行处理,可以将处理后的Request再发送给下一个中间件,也可以中断Request的处理,甚至可以不需要经过下载器就直接生成Response然后返回给scrapy引擎,具体的策略由代码实现来决定。在所有的中间件都处理过后,下载器拿到Request就会开始下载内容然后返回Response了。
  2. 下载器返回Response给scrapy引擎
    下载器通过Request拉取到数据后,就会封装成Response返回给scrapy引擎,在这个过程中,也会经过这些下载中间件的处理。下载中间件可以生成Request重新交给scrapy引擎处理,也可以对Response进行一些处理后交给下一个下载中间件,最后抵达scrapy引擎。
下载中间件的定义和使用

我们需要写一个类来继承DownloaderMiddleware,并在settings.py中配置编写好的这个下载中间件。

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomDownloaderMiddleware': 543,
    'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None,
}
  • 后面的数字表示该下载中间件的顺序,数字越小越接近scrapy引擎
  • 如果要禁用内置的一些下载中间件,可以将数字设置为None

编写下载中间件需要关注的几个方法:
process_request(request, spider): 这个方法接收scrapy传过来的Request对象,我们可以在这个方法里面对这个Request进行一些处理,然后根据返回的对象做一些操作:

  1. 返回None,通知下一个下载中间件继续对这个Request进行处理
  2. 返回Response对象,直接生成Response发给scrapy引擎,这样Request就不会交给下载器去下载内容了。注意,生成的Response还是会经过一个个下载中间件处理
  3. 返回Request对象,直接把新的Request返回给scrapy引擎重新调度,但是后面的下载中间件就不会再执行了
  4. 如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用

process_response(request, response, spider):这个方法接收下载器或者其他下载中间件发送过来的Response,同时还包括对应的Request对象,并在方法内对response进行一些处理,然后根据返回的对象类型做一些操作:

  1. 如果返回一个Request对象,下载中间件的执行就会被停止,并且会把这个Request对象交给scrapy引擎重新调度。
  2. 如果返回一个Response对象,就会通知下一个下载中间件继续对这个response进行处理
  3. 如果其抛出一个 IgnoreRequest 异常,则调用request的errback(Request.errback)

总结
理解了scrapy的整个结构后,下载中间件的功能还是比较好理解的。官方目前也内置了很多实用的下载中间件,所以在大多数场景下,也不用我们手动去编写下载中间件。不过复杂的场景还是用的上的,多学一点也没有坏处。

四、Spider中间件

Spider中间件主要用于scrapy引擎和spider之间的数据处理。包括spider发送Request和Item给scrapy引擎以及scrapy引擎发送Response给spider。
1. spider发送Request和Item给scrapy引擎
spider返回Request和Item给scrapy引擎过程中,会经过一个个spider中间件对result进行处理。spider中间件可以拿到spider返回的result和请求返回的response,这个result是Request和Item的迭代对象。spider中间件进行一些处理之后返回一个result。然后下一个spider中间件继续拿到result处理。
2.scrapy引擎发送Response给spider
scrapy引擎发送Response给spider的过程中,会经过一个个spider中间件对Response进行处理。处理之后spider中间件可以返回None或者抛出一个异常。返回None的话就会继续调用下一个spider中间件继续处理response,抛出异常的话就不会往下执行了。

spider中间件的定义和使用

我们需要写一个类来继承SpiderMiddleware,并在settings.py中配置编写好的这个下载中间件。

SPIDER_MIDDLEWARES = {
    'myproject.middlewares.CustomSpiderMiddleware': 543,
    'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': None,
}
  • 后面的数字表示该下载中间件的顺序,数字越小越接近scrapy引擎
  • 如果要禁用内置的一些下载中间件,可以将数字设置为None

编写spider中间件需要关注的几个方法:
process_spider_input(response, spider):
接收scrapy引擎传过来的response,用户可以在方法内处理该response。根据返回类型的不同会有不同的表现行为:

  1. 如果其返回 None ,Scrapy将会继续处理该response,调用所有其他的中间件直到spider处理该response
  2. 如果其跑出一个异常(exception),Scrapy将不会调用任何其他中间件的 process_spider_input() 方法,并调用request的errback。errback的输出将会以另一个方向被重新输入到中间件链中,使用 process_spider_output() 方法来处理,当其抛出异常时则带调用 process_spider_exception() 。
    process_spider_output(response, result, spider):
    当Spider处理response返回result时,该方法被调用。
    必须返回返回包含 Request 或 Item 对象的可迭代对象。
    process_spider_exception(response, exception, spider):
    当spider或(其他spider中间件的) process_spider_input() 抛出异常时, 该方法被调用
  3. 如果其返回 None ,Scrapy将继续处理该异常,调用中间件链中的其他中间件的 process_spider_exception() 方法,直到所有中间件都被调用,该异常到达引擎(异常将被记录并被忽略)
  4. 如果其返回一个可迭代对象,则中间件链的 process_spider_output() 方法被调用, 其他的 process_spider_exception() 将不会被调用
    process_start_requests(start_requests, spider):
    0.15 新版功能
    该方法以spider 启动的request为参数被调用,执行的过程类似于 process_spider_output() ,只不过其没有相关联的response并且必须返回request(不是item)。
    其接受一个可迭代的对象(start_requests 参数)且必须返回另一个包含 Request 对象的可迭代对象

总结
目前scrapy也内置了很多spider中间件,可以满足大多数场景。虽然平常时候我们可能不会有写spider中间件的时候,但是还是有必要了解的。

五、总结

scrapy的架构原理以及相关组件介绍差不多到这里就结束了。熟悉Python的话,scrapy学起来还是很快的。有空写写爬虫也是挺有意思的,大家有空可以学一学。

想深入学习还可以多去官方的scrapy中文文档看一看:
http://docs.pythontab.com/scrapy/scrapy0.24/intro/overview.html

最后,有对爬虫或者java技术感兴趣的欢迎联系我一起交流~本人邮箱在下方

我的CSDN博客地址:
https://blog.csdn.net/u013332124/article/details/80645690

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容