说起写爬虫,大多数第一时间想到的就是python了。python语法简洁明了,加上及其丰富好用的库,用它来写爬虫有天然的优势。
之前学python的时候也用requests+lxml写过几个爬虫玩,但是都就爬取一些内容就没继续下去了,都没做成一个项目,中间python也荒废了好久。最近要学kafka,就打算爬点数据来实践实践。于是就学起scrapy来,总的来说,scrapy还是很容易上手的,也比较简单,花了几天的时间学习加实践,也渐渐的掌握了这个爬虫框架。所以打算写个博客做个总结,以免之后又太久没用忘记了。
scrapy架构原理
scrapy架构图
[图片上传失败...(image-21611e-1532663400039)]
scrapy引擎向spider获取起始Request集合,也就是spider中定义的start_urls。如果spider重写了start_requests()方法,那么这个方法返回的Request集合就是起始Request。
scrapy引擎将拿到的Request发给调度中心开始调度。
scrapy引擎向调度中心请求获取下一个要爬取的Request。
scrapy引擎拿到Request后,然后将Request发给下载器。这个过程经过一系列在settings.py中配置的下载中间件,所有在settings.py中配置的下载中间件会依次对Request进行处理。——对应DownloaderMiddleware#process_request()方法
下载器根据Request拉取响应的内容,比如Request的url是http://www.baidu.com,下载器就会拉取对应的网页内容下来并封装成Response对象。
下载器将Response发送给scrapy引擎。这个过程也会经过一系列在settings.py中配置的下载中间件,这些下载中间件会依次对Response进行处理。——对应DownloaderMiddleware#process_response()方法
scrapy引擎拿到Response后将Response发给spider,交给对应的spider函数处理。这里默认方法是parse(),这个回调方法构造Request的时候指定。引擎发送Response的过程会经过一系列在settings.py中配置的spider中间件,这些spider中间件会依次对Response进行一些处理。——对应SpiderMiddleware#process_spider_input()
spider处理完Response后会返回一个result,这个result是一个包含 Request 或 Item 对象的可迭代对象(iterable)。然后将result发给scrapy引擎,这个过程也会经过一系列在settings.py中配置的spider中间件,这些spider中间件会依次对这个result进行一些处理。——对应SpiderMiddleware#process_spider_output()
scrapy引擎拿到这个result后,会将其中的Item发送给Item Pipeline处理,这些item就会被一系列我们在settings.py中配置的pipeline处理。同时,scrapy也会将result中的Request发给调度中间准备调度。
继续重复第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的编写上面。
通过定义start_urls参数或者重写start_requests()方法来给scrapy引擎提供起始的拉取url。
之后,scrapy引擎拿到url后去获取对应url网页中的内容后就会回调spider中的parse()方法,我们可以重写parse()方法来解析scrapy引擎返回回来的内容。
在parse()方法中,我们可以返回一个可迭代的对象,可以理解为是一个list,因为list也是一个可迭代对象,这个list里面可以放Request对象或者Item对象。
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引擎的过程中。
scrapy引擎发送Request到下载器
scrapy引擎发送Request到下载器的过程中,会经过一个个的下载中间件,这些中间件会对Request进行处理,可以将处理后的Request再发送给下一个中间件,也可以中断Request的处理,甚至可以不需要经过下载器就直接生成Response然后返回给scrapy引擎,具体的策略由代码实现来决定。在所有的中间件都处理过后,下载器拿到Request就会开始下载内容然后返回Response了。
下载器返回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进行一些处理,然后根据返回的对象做一些操作:
返回None,通知下一个下载中间件继续对这个Request进行处理
返回Response对象,直接生成Response发给scrapy引擎,这样Request就不会交给下载器去下载内容了。注意,生成的Response还是会经过一个个下载中间件处理
返回Request对象,直接把新的Request返回给scrapy引擎重新调度,但是后面的下载中间件就不会再执行了
如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用
process_response(request, response, spider):这个方法接收下载器或者其他下载中间件发送过来的Response,同时还包括对应的Request对象,并在方法内对response进行一些处理,然后根据返回的对象类型做一些操作:
如果返回一个Request对象,下载中间件的执行就会被停止,并且会把这个Request对象交给scrapy引擎重新调度。
如果返回一个Response对象,就会通知下一个下载中间件继续对这个response进行处理
如果其抛出一个 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。根据返回类型的不同会有不同的表现行为:
如果其返回 None ,Scrapy将会继续处理该response,调用所有其他的中间件直到spider处理该response
如果其跑出一个异常(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() 抛出异常时, 该方法被调用
如果其返回 None ,Scrapy将继续处理该异常,调用中间件链中的其他中间件的 process_spider_exception() 方法,直到所有中间件都被调用,该异常到达引擎(异常将被记录并被忽略)
如果其返回一个可迭代对象,则中间件链的 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学起来还是很快的。有空写写爬虫也是挺有意思的,大家有空可以学一学。
最后,有对爬虫或者java技术感兴趣的欢迎联系我一起交流~本人邮箱在下方