烟台北京网站建设,WordPress图片分类代码,余姚网站建设设计服务,合肥比较好的设计公司文章目录 一、Scrapy 框架介绍1.1 数据流1.2 项目结构1.3 Scrapy 入门 二、Selector 解析器2.1 XPath 和 CSS 选择器2.2 信息提取2.3 正则提取 三、Spider 的使用3.1 Spider 运行流程3.2 Spider 类分析3.3 Request3.4 Response 四、Download Middleware 的使用4.1 process_requ… 文章目录 一、Scrapy 框架介绍1.1 数据流1.2 项目结构1.3 Scrapy 入门 二、Selector 解析器2.1 XPath 和 CSS 选择器2.2 信息提取2.3 正则提取 三、Spider 的使用3.1 Spider 运行流程3.2 Spider 类分析3.3 Request3.4 Response 四、Download Middleware 的使用4.1 process_request(request, spider)4.2 process_response(request, response, spider)4.3 process_exception(request, exception, spider) 五、Spider Middleware 的使用5.1 process_spider_input(response, spider)5.2 process_spider_output(response, result, spider)5.3 process_spider_exception(response, exception, spider)5.4 process_start_requests(start_requests, spider)5.5 内置Spider Middleware 简介 六、Item Pipeline 的使用6.1 process_item(item, spider)6.2 open_spider(self, spider)6.3 close_spider(spider)6.4 from_crawler(cls, crawler) 七、Extension 的使用7.1 部署本地 flask 服务器7.2 extensions.py 八、Scrapy 自动化配置8.1 Scrapy 对接 Splash8.2 Splash 对接 Selenium8.3 关于其他 九、Scrapy 规则化爬虫9.1 CrawlSpider9.2 Item Loader 十、Scrapyrt 的使用9.1 GET 请求9.2 POST 请求 十一、Scrapy 对接 Docker十二、Scrapy 爬取新浪微博 Scrapy 是一个基于 Python 开发的爬虫框架是当前 Python 爬虫生态中最流行的爬虫框架该框架提供了非常多爬虫相关的基础组件架构清晰可拓展性极强。
之前大多是基于 requests 或 aiohttp 来实现爬虫的整个逻辑的可以发现在整个过程中我们需要实现爬虫相关的所有操作例如爬取逻辑异常处理数据解析数据存储等但其实这些步骤很多都是通用或者重复的。既然如此我们可以将这些步骤的逻辑分离出来把其中通用的功能做成一个个基础的组件。
在抽离处基础组件之后每次爬虫只需要在这些组件基础上加上特定的逻辑就可以实现爬取的流程了而不用再把爬虫中每个细小的流程都实现一遍。
Scrapy 框架几乎是 Python 爬虫学习和工作过程中必须掌握的框架
一、Scrapy 框架介绍 Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯信号、数据传递等。Item: 是一个抽象的数据结构定义了爬取结果的数据结构爬去的数据会被赋值成 Item 对象每个 Item 就是一个类类里面定义了爬取结果的数据字段可以理解为它用来规定爬取数据的存储格式。Scheduler(调度器): 它负责接受引擎发送过来的Request请求并按照一定的方式进行整理排列入队当引擎需要时交还给引擎。Downloader下载器负责下载Scrapy Engine(引擎)发送的所有Requests请求并将其获取到的Responses交还给Scrapy Engine(引擎)由引擎交给Spider来处理Spider爬虫它负责处理所有Responses,从中分析提取数据获取Item字段需要的数据并将需要跟进的URL提交给引擎再次进入Scheduler(调度器).Item Pipeline(管道)它负责处理Spider中获取到的Item并进行进行后期处理详细分析、过滤、存储等的地方。Downloader Middlewares下载中间件你可以当作是一个可以自定义扩展下载功能的组件。Spider MiddlewaresSpider中间件你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件比如进入Spider的Responses;和从Spider出去的Requests
1.1 数据流
详细可以看这篇文章Scrapy 入门教程 | 菜鸟教程 (runoob.com)这里十分生动的表示了 Scrapy 个组件的交流
启动爬虫项目时Engine 根据要爬取的目标站点找到处理该站点的 SpiderSpider 会生成最初需要爬取的页面对应的一个或多个 Request然后发给 Engine。Engine 从 Spider 中获取这些 Request然后把它们交给 Scheduler 等待被调度Engine 向 Scheduler 索取下一个要处理的 Request这时候 Scheduler 根据其调度逻辑选择合适的 Request 发送给 EngineEngine 将 Scheduler 发来的 Request 转发给 Downloader 进行下载执行将 Request 发送给 Downloader 的过程会经由许多定义好的 Downloader Middlewares 的处理Downloader 将 Request 发送给目标服务器得到对应的 Response然后将其返回给 Engine。将 Response 返回 Engine 的过程同样会经由许多定义好的 Downloader Middlewares 的处理。Engine 从 Downloader 处接收到的 Response 里包含了爬取的目标站点的内容Engine 会将此 Response 发送给对应的 Spider 进行处理将 Response 发送给 Spider 的过程中会经由定义好的 Spider Middlewares 的处理Spider 处理 Response解析 Response 的内容这时候 Spider 会产生一个或多个爬取结果 Item 或者后续要爬取的目标页面对应的一个或多个 Request然后再将这些 Item 或 Request 发送给 Engine 进行处理将 Item 或 Request 发送给 Engine 的过程会经由定义好的 Spider Middlewares 的处理Engine 将 Spider 发回的一个或多个 Item 转发给定义好的 Item Pipelines 进行数据处理或存储的一系列操作将 Spider 发回的一个或多个 Request 转发给 Scheduler 等待下一次被调度。
重复第2步到第8步直到 Scheduler 中没有更多的 Request这时候 Engine 会关闭 Spider整个爬取过程结束。 从整体上来看各个组件都只专注于一个功能组件和组件之间的耦合度非常低也非常容易扩展。再由 Engine 将各个组件组合起来使得各个组件各司其职互相配合共同完成爬取工作。另外加上 Scrapy 对异步处理的支持Scrapy 还可以最大限度地利用网络带宽提高数据爬取和处理的效率。
1.2 项目结构
需要先安装 Scrapy 框架可以直接使用 pip 安装
pip install scrapy安装完毕后可以使用命令行来创建一个爬虫项目这里创建一个名为 news 的项目
scrapy startproject news执行完毕后当前目录下就会出现一个名为 news 的文件夹该文件夹就对应一个 Scrapy 爬虫项目接着进入 news 文件夹然后创建一个名称为 sina 的 Spider
# 进入news 文件夹
cd .\news
# 创建 Spider 名称为 sina 域名为 news.sina.com.cn
scrapy genspider sina news.sina.com.cn最终会得到如下的一个文件结构 各个文件的功能描述如下
scrapy.cfg: Scrapy项目的配置文件其中定义了项目的配置文件路径、部署信息等items.py: 定义了Item数据结构所有Item的定义都可以放这里pipelines.py: 定义了Item Pipeline的实现所有的Item Pipeline的实现都可以放在这里settings.py: 定义了项目的全局配置middlewares.py: 定义了Downloader Middlewares和Spider Middlewares的实现spiders: 里面包含了一个个 Spider 的实现每个 Spider 都对应一个 Python 文件
1.3 Scrapy 入门
这里以 Scrapy 推荐的官方练习项目为例子进行爬取抓取的目标站点为 https://quotes.toscrape.com/
创建一个项目名为 demo 的项目spider 命名为 example得到spider文件 example.py 如下
# example.py
import scrapyclass ExampleSpider(scrapy.Spider):name exampleallowed_domains [quotes.toscrape.com]start_urls [https://quotes.toscrape.com/]def parse(self, response):passname 是每个项目唯一的名字用于区分不同的 Spider allowed_domains 是允许爬取的域名如果初始或者后续的请求链接不是这个域名下的则会被过滤掉 start_urls 包含了 spider 在启动时爬取的 URL 列表初始请求是由它来定义的 parse 是 Spider 的一个方法在默认情况下start_urls 里面的链接构成请求完成下载后得到一个 responseparse 方法就会调用response 作为参数 进入到 Items.py 文件如下
import scrapyclass DemoItem(scrapy.Item):# define the fields for your item here like:text scrapy.Field()author scrapy.Field()tags scrapy.Field()这里 Item 类似于一个字典但是必须使用 scrapy.Field() 来定义对于 Response 的解析其接口如下所示
urlRequest URLstatusResponse 状态码一般情况下请求成功状态码为200headersResponse Headers是一个字典字段是一一对应的bodyResponse Body这个通常就是访问页面之后得到的源代码结果了比如里面包含的是HTML或者JSON字符串但注意其结果是 bytes 类型。与requests模块请求后得到的响应属性content类似requestResponse 对应的 Request 对象certificate是twisted.internet.ssl.Certifucate类型的对象通常代表一个SSL证书对象ip_address是一个ipaddress.IPv4Address或IPv6Address类型的对象代表服务器的IP地址urljoin是对URL的一个处理方法可以传入当前页面的相对URL该方法处理后返回的就是绝对URLurljoin 其实使用的就是: from urllib.parse import urljoin 可以去看源码follow/follow_all是一个根据URL来生成后续Request的方法和直接构造Request不同的是该方法接收的url可以是相对URL不必一定是绝对URL因为follow方法中有做url拼接的操作text: 同body属性但结果是str类型encoding: Response的编码默认是utf-8selector: 根据Response的内容构造而成的Selector对象利用它我们可以进一步调用xpath、css等方法进行结果的提取xpath()方法: 传入XPath进行内容提取等同于调用selector的xpath方法css()方法: 传入CSS选择器进行内容提取等同于调用selector的css方法json()方法: 可以直接将text属性转换为JSON对象
与 requests 的 Response 主要的不同在于其不需要再导入 lxml 或者 bs4 来进行解析里面自带有解析的工具在了解如何解析 Response 之后我们可以将 example.py 修正如下
import scrapy
from ..items import DemoItemclass ExampleSpider(scrapy.Spider):name exampleallowed_domains [quotes.toscrape.com]start_urls [https://quotes.toscrape.com/]def parse(self, response, **kwargs):quotes response.css(.quote)for quote in quotes:item DemoItem()item[text] quote.css(.text::text).extract_first()item[author] quote.css(.author::text).extract_first()item[tags] quote.css(.tags .tag::text).extract()yield item目前只获取到首页的内容我们需要获取到下一页的内容可以在当前页面中寻找信息构建下一个 RequestRequest 的构造参数梳理如下
url: Request 的页面链接即 Request URL。callbackRequest 的回调方法通常这个方法需要定义在 Spider 类里面并且需要对应一个 response 参数代表 Request 执行请求后得到的 Response 对象。如果这个 callback 参数不指定默认会使用 Spider 类里面的 parse 方法。methodRequest 的方法默认是 GET还可以设置为 POST、PUT、DELETE 等。metaRequest 请求携带的额外参数利用 meta我们可以指定任意处理参数特定的参数经由 Scrapy 各个组件的处理可以得到不同的效果。另外meta 还可以用来向回调方法传递信息。bodyRequest 的内容即 Request Body往往 Request Body 对应的是 POST 请求我们可以使用 FormRequest 或 JsonRequest 更方便地实现 POST 请求。headersRequest Headers是字典形式。cookiesRequest 携带的 Cookies可以是字典或列表形式。encodingRequest 的编码默认是 utf-8。prorityRequest 优先级默认是0这个优先级是给 Scheduler 做 Request 调度使用的数值越大就越被优先调度并执行。dont_filterRequest 不去重Scrapy 默认会根据 Request 的信息进行去重使得在爬取过程中不会出现重复的请求设置为 True 代表这个 Request 会被忽略去重操作默认是 False。errback错误处理方法如果在请求过程中出现了错误这个方法就会被调用。flags请求的标志可以用于记录类似的处理。cb_kwargs回调方法的额外参数可以作为字典传递。
Scrapy 还专门为 POST 请求提供了两个类 —— FormRequest 和 JsonRequest它们都是 Request 类的子类我们可以利用 FormRequest 的 formdata 参数传递表单内容利用 JsonRequest 的 json 参数传递 JSON 内容其他的参数和 Request 基本是一致的。
第一个 JsonRequest我们可以观察到页面返回结果的 json 字段就是我们所请求时添加的 data 内容这说明实际上是发送了 Content-Type 为 application/json 的 POST 请求这种对应的就是发送 JSON 数据。第二个 FormRequest我们可以观察到页面返回结果的 form 字段就是我们请求时添加的 data 内容这说明实际上是发送了 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求这种对应的就是表单提交。这两种 POST 请求的发送方式我们需要区分清楚并根据服务器的实际需要进行选择。
example.py 修正如下
import scrapy
from ..items import DemoItemclass ExampleSpider(scrapy.Spider):name exampleallowed_domains [quotes.toscrape.com]start_urls [https://quotes.toscrape.com/]def parse(self, response, **kwargs):quotes response.css(.quote)for quote in quotes:item DemoItem()item[text] quote.css(.text::text).extract_first()item[author] quote.css(.author::text).extract_first()item[tags] quote.css(.tags .tag::text).extract()yield item# 获取下一页然后构造请求next response.css(.pager .next a::attr(href)).extract_first()url response.urljoin(next)# 构造请求yield scrapy.Request(urlurl, callbackself.parse)运行项目
scrapy crawl example在运行完 Scrapy 后只能在控制台上看到结果需要保存数据有两种方式
其一是使用命令行直接输出格式文件例如 json, csv, xmlk, pickle, marshal 等等完成这一任务不需要任何额外的代码Scrapy 提供的 Feed Exports 可以轻松将抓取到的结果输出
# 保存为json
scrapy crawl example -o example.json
# 保存为一行json
scrapy crawl example -o example.jl # 或
scrapy crawl example -o example.jsonlines
# 保存为 csv
scrapy crawl example -o example.csv
# 保存为 xml
scrapy crawl example -o example.xml
# 保存为 pickle
scrapy crawl example -o example.pickle
# 保存为 marshal
scrapy crawl example -o example.marshal其二是使用 Item Pipeline如果要进行更复杂的操作如将结果保存到数据库之中或者 对 Item 进行筛选操作Item Pipeline 为项目管道当 Item 生成后它会自动被送到 Item Pipeline 处进行处理可以使用 Item Pipeline 来做如下操作
清洗 HTML 数据验证爬取数据检测爬取字段查重并丢弃重复内容将爬取结果保存到数据库
Pipeline 管道的基本类模版如下
class XXXXPipeline(object):def __init__(self, a, b):self.a aself.b bdef process_item(self, item, spider):必须有为每个项管道组件调用此方法passclassmethoddef from_crawler(cls, crawler):如果存在则调用此类方法以从Crawler创建管道实例。它必须返回管道的新实例。Crawler对象提供对所有Scrapy核心组件如setting和signal的访问;它是管道访问它们并将其功能挂钩到Scrapy的一种方式。类似于初始化a和breturn cls(acrawler.settings.get(a),bcrawler.settings.get(b),)def open_spider(self, spider):如果存在这个方法是在spider打开时调用的。passdef close_spider(self, spider):如果存在这个方法是在spider关闭时调用的。pass在这里我们可以添加两个 Pipeline首先是文本处理的 Pipeline 还有存储数据库的 Pipeline
from scrapy.exceptions import DropItemclass TextPipeline(object):def __init__(self):self.limit 50def process_item(self, item, spider):if item[text]:if len(item[text]) self.limit:item[text] item[text][:self.limit].rstrip() ...return itemelse:return DropItem(Missing Text)class MongoPipeline(object):def __init__(self, connection_string, database):self.connection_string connection_stringself.database databaseclassmethoddef from_crawler(cls, crawler):return cls(connection_stringcrawler.settings.get(MONGODB_CONNECTION_STRING),databasecrawler.settings.get(MONGODB_DATABASE))def open_spider(self, spider):self.client pymongo.MongoClient(self.connection_string)self.db self.client[self.database]def process_item(self, item, spider):name item.__class__.__name__self.db[name].insert_one(dict(item))return itemdef close_spider(self, spider):self.client.close()处理完毕后我们还需要进入到 settings.py 中配置文件第一个是 Mongo数据库的配置由于 MongoPipeline 是使用 from_crawler 来进行初始化的所以 settings.py 中需要有 MONGODB_CONNECTION_STRINGMONGODB_DATABASE 这两个字段其次 Pipeline 有一个先后顺序键值越小越优先执行修改 settings.py 内容如下
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# USER_AGENT demo (http://www.yourdomain.com)# Obey robots.txt rules
ROBOTSTXT_OBEY TrueITEM_PIPELINES {爬虫项目名.pipelines.TextPipeline: 200,爬虫项目名.pipelines.MongoPipeline: 300,
}
MONGODB_CONNECTION_STRING localhost
MONGODB_DATABASE 数据库名到这里就处理完毕了开启爬虫如下
scrapy crawl example二、Selector 解析器
在Python3网络爬虫开发实战3网页数据的解析提取_etree beautifulsoup parsel-CSDN博客介绍过 Parsel 解析器parsel 是 Python 最流行的爬虫框架 Scrapy 的底层支持
而 Selector 在使用上和 Parsel 有一点点区别那就是原来的 get() 和 getall() 变成了 extract_first() 和 extract()同时Selector 是可以单独使用的
2.1 XPath 和 CSS 选择器
from scrapy import Selectorhtml
selector Selector(texthtml)# css
items selector.css(css选择器)
# xpath
items selector.xpath(xpath选择器)2.2 信息提取
extract_first从 selectorlist 对象中提取第一个 Selector 对象然后输出其中的结果extract从 selectorlist 对象中提取所有的 Selector 对象然后以列表的形式输出其中的结果
# 提取文本
selector.css(css选择器::text()).extract_first(默认值)
selector.css(css选择器::text()).extract(默认值)
selector.xpath(xpath//text()).extract_first(默认值)
selector.xpath(xpath//text()).extract(默认值)# 提取属性
selector.css(css选择器::attr(name)).extract_first(默认值)
selector.css(css选择器::attr(href)).extract(默认值)
selector.xpath(xpath/name()).extract_first(默认值)
selector.xpath(xpath/href()).extract(默认值)2.3 正则提取
如果选择器中是属性或者文本那么 re 对属性或者文本进行匹配如果选择器中不是属性和文本那么 re 对该节点的 html 字符进行匹配
from parsel import Selectorhtml
selector Selector(texthtml)
result selector.css(css选择器).re(a.*)
result selector.xpath(xpath).re(a.*)result selector.css(css选择器).re_first(a.*)
result selector.xpath(xpath).re_first(a.*)三、Spider 的使用
在 Scrapy 中网站的链接配置抓取逻辑解析逻辑其实都是在 Spider 中配置的在前一节的实例中我们发现抓取逻辑也是在 Spider 中完成的。
3.1 Spider 运行流程
Spider 定义了如何爬取某个网站的流程和解析方式就是做了以下两件事
定义爬取网站的动作分析爬取下来的网页
对于 Spider 类来说整个爬取循环如下
以初始的 URL 初始化 Request 并设置回调方法当该 Request 成功请求并返回时将生成 Response 并将其作为参数传给该回调方法在回调方法内分析返回的网页内容。返回结果可以有两种形式一种是将解析到的有效结果返回字典或 Item 对象下一步可直接保存或者经过处理后保存另一种解析的下一个如下下一页链接可以利用此链接构造 Request 并设置新的回调方法返回 Request如果返回的是字典或者 Item 对象可通过 Feed Exports 等形式存入文件如果设置了 Pipeline可以经由 Pipeline 处理如过滤修正等并保存如果返回的是 Request那么 Request 执行成功得到 Response 之后会再次传递给 Request 中定义的回调方法可以再次使用选择器来分析新得到的网页内容并根据分析的数据生成 Item
循环进行以上几步便完成了站点的爬取
3.2 Spider 类分析
参考文档Spiders - Scrapy 2.11.2文档 — Spiders — Scrapy 2.11.2 documentation
我们定义的 Spider 继承自 scrapy.Spider 类这个类是最基本的 Spider 类其他的 Spider 必须继承这个类
这个类有一些基础的属性如下
name爬虫名称是定义 Spider 名字的字符串Spider 的名字定义了 Scrapy 如何定位并初始化 Spider所以它必须是唯一的。 name 是 Spider 最重要的属性而且是必须的allowed_domains允许爬取的域名是一个可选的配置不在此范围的链接不会被跟进爬取start_urls起始 URL 列表当我们没有实现 start_requests 方法的默认会从这个列表开始抓取custom_settings一个字典是专属于本 Spider 的配置此设置会覆盖项目全局的设置而且此设置必须在初始化前被更新所以它必须定义成类变量Settings — Scrapy 2.11.2 documentationcrawler此属性是由 from_crawler 方法设置的代表的是本 Spider 类对应的 Crawler 对象Crawler 对象中包含了很多的项目组件利用它可以获取一些项目的基本配置信息常见的就是获取项目的设置信息即 SettingsCore API — Scrapy 2.11.2 documentationsettings一个 Settings 对象利用它我们可以直接获取项目的全局设置变量Settings — Scrapy 2.11.2 documentation
还有一些基础的主要的方法如下
start_requests此方法用于生成初始请求它必须返回一个可迭代对象此方法会默认使用 start_urls 里面的每个 URL 来构造 Request而且 Request 是 GET 请求方式。如果我们想在启动的时候以 POST 方式访问某个站点可以直接重写这个方法parse当 Response 没有指定回调方法时该方法会默认被调用它负责处理 Response并从中提取想要的数据和下一步的请求然后返回该方法需要返回一个包含 Request 或 Item 的可迭代对象closed当 Spider 关闭时该方法被调用这里一般会定义释放资源的一些操作
3.3 Request
Requests and Responses — Scrapy 2.11.2 documentation
在 Request 中Request 对象实质上指的就是 scrapy.http.Request 的一个实例它包含了 HTTP 请求的基本信息用这个 Request 类可以构造 Request 对象发送 HTTP 请求它会被 Engine 交给 Downloader 进行处理执行返回一个 Response 对象
scrapy.Requset(**kwargs)
scrapy.http.Requset(**kwargs)# Content-Type 为 application/json
scrapy.JsonRequest(**kwargs)
scrapy.http.JsonRequest(**kwargs)# Content-Type 为 application/x-www-form-urlencoded
scrapy.FormRequest(**kwargs)
scrapy.http.FormRequest(**kwargs)Request 类的构造参数如下
urlRequest 的页面链接即 Request URLcallbackRequest 的回调方法通常这个方法需要定义在 Spider 类里面并且需要对应一个 response 参数代表 Request 执行请求后得到的 Response 对象如果这个 callback 参数不指定默认会使用 Spider 类里面的 parse 方法methodRequest 的方法默认是 GET还可以设置为 POSTPUTDELETE 等metaRequest 请求携带的额外参数利用 meta 我们可以指定任意处理参数特定的参数经由 Scrapy 各个组件的处理可以得到不同的效果另外meta 还可以用来向回调方法传递信息bodyRequest 的内容即 Request Body往往 Request Body 对应的是 POST 请求我们可以使用 FormRequest 或 JsonRequest 更方便地实现 POST 请求headersRequest Header是字典形式cookiesRequest 携带的 Cookie可以是字典或者列表形式encodingRequest 的编码默认是 UTF-8prorityRequest 优先级默认是 0 这个优先级是给 Scheduler 做 Request 调度使用的数值越大就粤北优先调用执行dont_filter Request 不去重Scrapy 默认会根据 Request 的信息进行去重使得在爬取过程中不会出现重复请求设置为 True 代表这个 Request 会被忽略去重操作默认为 Falseerrback错误处理方法如果在请求过程中出现了错误这个方法就会被调用flags请求的标志可以用于记录类似的处理cb_kwargs回调方法的额外参数可以作为字典传递
值得注意的是meta 参数是一个十分有用而且易扩展的参数它可以以字典的形式传递包含的信息不受限制所以很多 Scrapy 的插件会基于 meta 参数做一些特殊处理在默认情况下Scrapy 就预留了一些特殊的 key 作为特殊处理 Scrapy 还专门为 POST 请求提供了两个类 —— FormRequest 和 JsonRequest它们都是 Request 类的子类我们可以利用 FormRequest 的 formdata 参数传递表单内容利用 JsonRequest 的 json 参数传递 JSON 内容其他的参数和 Request 基本是一致的。
第一个 JsonRequest我们可以观察到页面返回结果的 json 字段就是我们所请求时添加的 data 内容这说明实际上是发送了 Content-Type 为 application/json 的 POST 请求这种对应的就是发送 JSON 数据。第二个 FormRequest我们可以观察到页面返回结果的 form 字段就是我们请求时添加的 data 内容这说明实际上是发送了 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求这种对应的就是表单提交。这两种 POST 请求的发送方式我们需要区分清楚并根据服务器的实际需要进行选择。
3.4 Response
Request 由 Downloader 执行之后得到的就是 Response 结果了它代表的是 HTTP 请求得到的响应结果同样地我们可以梳理一下其可用的属性和方法以便做解析处理使用
urlRequest URLrequestResponse 对应的 Request 对象statusResponse 状态码headersResponse Header响应头是一个字典字段是一一对应的bodyResponse Body这个通常就是访问页面之后得到的源码结果了比如里面包含的是 HTML 或者 JSON 字符串但注意其结果是 bytes 类型certificate是 twisted.internet.ssl.Certificate 类型的对象通常代表一个 SSL 证书对象ip_address是一个 ipaddress.IPv4Address 或 ipaddress.IPv6Address 类型的对象代表服务器的 IP 地址urljoin是对 URL 的一个处理方法可以传入当前页面的相对 URL该方法处理后返回的就是绝对 URLfollow/follow_all是一个根据 URL 来生成后续 Request 的方法和直接构造 Request 不同的是该方法接受的 url 可以是相对 URL不必一定是绝对 URL
另外Response 还有几个常用的子类如 TextResponse 和 HtmlResponseHtmlResponse 又是 TextResponse 的子类实际上回调方法接收的 response 参数就是一个 HtmlResponse 对象它还有几个常用的方法或属性。
text同 body 属性但结果是 str 类型encodingResponse 的编码默认是 utf-8selector根据 Response 的内容构造而成的 Selector 对象xpath/css 等同于调用 selector.xpath/css 方法json可以将 text 属性转化为 JSON 对象
四、Download Middleware 的使用
Downloader Middleware 是处于 Scrapy 的 Engine 和 Downloader 之间的处理模块。Engine 把 Scheduler 获取的 Request 发送给 Downloader 的过程中以及 Downloader 把 Response 发送回 Engine 的过程中Request 和 Response 都会经过 Downloader Middleware 的处理也就是说 Downloader Middleware 在整个架构中起作用的位置是以下两个
Engine 从 Scheduler 获取 Request 发送给 Downloader Middleware在 Request 被 Engine 发送给 Downloader Middleware 执行下载之前Downloader Middleware 可以对 Request 进行修改Downloader 执行 Request 后生成 Response在 Response 被 Engine 发送给 Spider 之前Downloader Middleware 可以对 Response 进行修改
Downloader Middleware 在整个爬虫执行过程中能起到非常重要的作用功能十分强大修改 User-Agent处理重定向设置代理失败重试设置 Cookie 等功能都需要借助它来实现
需要说明的是Scrapy 其实已经提供了许多 Downloader Middleware比如负责失败重试、自动重定向等功能的 Middleware它们被 DOWNLOADER_MIDDLEWARES_BASE 变量所定义。 DOWNLOADER_MIDDLEWARES_BASE 变量的内容如下所示
{scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware: 100,scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware: 300,scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware: 350,scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware: 400,scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: 500,scrapy.downloadermiddlewares.retry.RetryMiddleware: 550,scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware: 560,scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware: 580,scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware: 590,scrapy.downloadermiddlewares.redirect.RedirectMiddleware: 600,scrapy.downloadermiddlewares.cookies.CookiesMiddleware: 700,scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware: 750,scrapy.downloadermiddlewares.stats.DownloaderStats: 850,scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware: 900,
}字典的键名是 Scrapy 内置的 Downloader Middleware 的名称键值代表了调用的优先级优先级是一个数字数字越小代表越靠近Engine数字越大代表越靠近 Downloader 。默认情况下Scrapy 已经为我们开启了 DOWNLOADER_MIDDLEWARES_BASE 所定义的 Downloader Middleware比如 RetryMiddleware 带有自动重试功能RedirectMiddleware 带有自动处理重定向功能这些功能默认都是开启的。
Downloader Middleware 固定内部代码如下
class ScrapyDemoDownloaderMiddleware:# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the downloader middleware does not modify the# passed objects.classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s cls()crawler.signals.connect(s.spider_opened, signalsignals.spider_opened)return sdef process_request(self, request, spider):# Called for each request that goes through the downloader# middleware.# Must either:# - return None: continue processing this request# - or return a Response object# - or return a Request object# - or raise IgnoreRequest: process_exception() methods of# installed downloader middleware will be calledreturn Nonedef process_response(self, request, response, spider):# Called with the response returned from the downloader.# Must either;# - return a Response object# - return a Request object# - or raise IgnoreRequestreturn responsedef process_exception(self, request, exception, spider):# Called when a download handler or a process_request()# (from other downloader middleware) raises an exception.# Must either:# - return None: continue processing this exception# - return a Response object: stops process_exception() chain# - return a Request object: stops process_exception() chainpassdef spider_opened(self, spider):spider.logger.info(Spider opened: %s % spider.name)每个 Downloader Middleware 都可以通过定义 process_request 和 process_reponse 方法来分别处理 Request 和 Response 被开启的 Downloader Middleware 的 process_request 方法和 process_response 方法会根据优先级顺序调用。
process_request由于Request是从Engine发送给Downloader的并且优先级数字越小的Downloader Middleware越靠近Engine所以优先级数字越小的Downloader Middleware的process_request方法越先被调用。
process_responseprocess_response方法则相反由于Response是由Downloader发送给Engine的优先级数字越大的Downloader Middleware越靠近Downloader所以优先级数字越大的Downloader Middleware的process_response越先被调用。
如果我们想将自定义的Downloader Middleware添加到项目中不要直接修改DOWNLOADER_MIDDLEWARES_BASE变量Scrapy提供了另外一个设置变量DOWNLOADER_MIDDLEWARES我们直接修改这个变量就可以添加自己定义的Downloader Middleware以及禁用DOWNLOADER_MIDDLEWARES_BASE里面定义的Downloader Middleware了。
4.1 process_request(request, spider)
Request被Engine发送给Downloader之前process_request方法就会被调用也就是在Request从Scheduler里被调度出来发送到Downloader下载执行之前我们都可以用process_request方法对Request进行处理。
参数
process_request方法的参数有两个。
requestRequest对象即被处理的Request。spiderSpider对象即此Request对应的Spider对象。
返回值
这个方法的返回值必须为None、Response对象、Request对象三者之一或者抛出IgnoreRequest异常。返回类型不同产生的效果也不同下面归纳一下不同的返回情况。
None当返回的是None时Scrapy将继续处理该Request接着执行其他Downloader Middleware的process_request方法一直到Downloader把Request执行得到Response才结束。这个过程其实就是修改Request的过程不同的Downloader Middleware按照设置的优先级顺序依次对Request进行修改最后送至Downloader执行。Response当返回为Response对象时更低优先级的Downloader Middleware的process_request和process_exception方法就不会被继续调用每个Downloader Middleware的process_response方法转而被依次调用调用完毕后直接将Response对象发送给Spider处理。Request当返回为Request对象时更低优先级的Downloader Middleware的process_request方法会停止执行。这个Request会重新放到调度队列里其实它就是一个全新的Request等待被调度。如果Scheduler调度了那么所有的Downloader Middleware的process_request方法会被重新按照顺序执行。IgnoreRequest如果抛出IgnoreRequest异常则所有的Downloader Middleware的process_exception方法会依次执行。如果没有一个方法处理这个异常那么Request的errorback方法就会回调。如果该异常还没有被处理那么它便会被忽略。
4.2 process_response(request, response, spider)
Downloader执行Request下载之后会得到对应的Response。Engine便会将Response发送给Spider进行解析在发送给Spider之前我们都可以用process_response方法来对Response进行处理。
参数
process_response方法的参数有3个
requestRequest对象即此Response对应的Request。responseResponse对象即被处理的Response。spiderSpider对象即此Response对应的Spider对象。
返回值
process_response方法的返回值必须为Request对象和Response对象两者之一。或者抛出IgnoreRequest异常。那么对不同的返回情况在下面做一下归纳。
Request当返回为Request对象时更低优先级的Downloader Middleware的process_response方法不会继续调用该Request对象会重新放到调度队列里等待被调度相当于一个全新的Request。然后该Request会被process_request方法顺次处理。Response当返回为Response对象时更低优先级的Downloader Middleware的process_response方法会继续被调用对该Response对象进行处理。IgnoreRequest当抛出IgnoreRequest异常时Request的errorback方法会回调。如果该异常还没有被处理那么它会被忽略。
4.3 process_exception(request, exception, spider)
当Downloader或process_request方法抛出异常时例如抛出IgnoreRequest异常process_exception方法就会被调用。
参数
process_exception方法的参数有3个。
requestRequest对象即产生异常的Request。exceptionException对象即抛出的异常。spiderSpider对象即Request对应的Spider。
返回值
方法的返回值必须为None、Response对象、Request对象三者之一。
None当返回值为None时更低优先级的Downloader Middleware的process_exception会被继续顺次调用直到所有的方法都被调用完毕。Response当返回值为Response时更低优先级的Downloader Middleware的process_exception不再被继续调用每个Downloader Middleware的process_response方法转而被依次调用。Request当返回为Request对象时更低优先级的Downloader Middleware的process_exception也不再被继续调用该Request对象会重新放到调度队列里面等待被调度相当于一个全新的Request。然后该Request又会被process_request方法顺次处理。
关于设置 header设置代理返回值等操作可以看scrapy爬虫框架四Downloader Middleware的使用 - 乐之之 - 博客园 (cnblogs.com)
五、Spider Middleware 的使用
Spider Middleware 的作用
Downloader生成Reponse之后Engine会将其发送给Spider进行解析在Response发送给Spider之前可以借助Spider Middleware对Response进行处理。Spider生成Request之后会被发送至Engine然后Request会转发到Scheduler在Request被发送给Engine之前可以借助Spider Middleware对Request进行处理。Spider生成Item之后会被发送至Engine然后Item会被转发到Item Pipeline在Item被发送给Engine之前可以借助Spider Middleware对Item进行处理。
Scrapy框架中其实已经提供了许多Spider Middleware与Downloader Middleware类似它们被SPIDER_MIDDLEWARES_BASE变量所定义SPIDER_MIDDLEWARES_BASE变量的内容如下
{scrapy.spidermiddlewares.httperror.HttpErrorMiddleware:50,scrapy.spidermiddlewares.offsite.OffsiteMiddleware:500,scrapy.spidermiddlewares.referer.RefererMiddleware:700,scrapy.spidermiddlewares.urllength.UrlLengthMiddleware: 800, scrapy.spidermiddlewares.depth.DepthMiddleware:900,
}SPIDER_MIDDLEWARES_BASE里定义的Spider Middleware是默认生效的如果我们要自定义Spider Middleware可以和Downloader Middleware一样创建Spider Middleware并将其加入SPIDER_MIDDLEWARES。直接修改这个变量就可以添加自己定义的Spider Middleware以及禁用SPIDER_MIDDLEWARES_BASE里面定义的Spider Middleware。
这些Spider Middleware的调用优先级和Downloader Middleware也是类似的数字越小的Spider Middleware是越靠近Engine的数字越大的Spider Middleware是越靠近Spider的。
5.1 process_spider_input(response, spider)
当Response通过Spider Middleware时process_spider_input方法被调用处理该Response。它有两个参数。
参数
responseResponse对象即被处理的Response。spiderSpider对象即该Response对应的Spider对象。
返回值
process_spider_input应该返回None或者抛出一个异常。
None如果它返回NoneScrapy会继续处理该Response调用所有其他的Spider Middleware直到Spider处理该Response。异常如果它抛出一个异常Scarapy不会调用任何其他Spider Middleware的process_spider_input方法并调用Request的errback方法。errback的输出将会以另一个方向被重新输入中间件使用process_spider_output处理当其抛出异常时则调用process_spider_exception来处理。
5.2 process_spider_output(response, result, spider)
当Spider处理Response返回结果时process_spider_output方法被调用。它有3个参数。
参数
responseResponse对象即生成该输出的Response。result包含Request或Item对象的可迭代对象即Spider返回的结果。spiderSpider对象即结果对应的Spider对象。
返回值
process_spider_output必须返回包含Request或Item对象的可迭代对象。
5.3 process_spider_exception(response, exception, spider)
当Spider或Spider Middleware的process_spider_input方法抛出异常时process_spider_exception方法被调用。它有3个参数。
参数
responseResponse对象即异常被抛出时被处理的Response。exceptionException对象被抛出的异常。spiderSpider对象即抛出该异常的Spider对象。
返回值
process_spider_exception必须必须返回None或者一个包含Response或Item对象的可迭代对象。
None如果它返回None那么Scrapy将继续处理该异常调用其他Spider Middleware中process_spider_exception方法直到所有Spider Middleware都被调用。可迭代对象Response或Item如果它返回的是一个可迭代对象则其他Spider Middleware的process_spider_output方法被调用其他的process_spider_exception不会被调用。
5.4 process_start_requests(start_requests, spider)
process_start_requests方法以Spider启动的Request为参数被调用执行的过程类似于process_spider_output只不过它没有相关联的Response并且必须返回Request。它有两个参数。
参数
process_start_requests包含Request的可迭代对象即Start Requests。spiderSpider对象即Start_Reqeusts所属的Spider。
返回值
process_start_requests方法必须返回另一个包含Request对象的可迭代对象。
5.5 内置Spider Middleware 简介
在这里我们再介绍一些scrapy框架中内置的Spider Middleware。
HttpErrorMiddlewareHttpErrorMiddleware的主要作用是过滤我们需要忽略的Response比如状态码为200~299的会处理500以上的不会处理。
另外,如果想要针对一些错误类型的状态码进行处理,可以修改Spider的 handle_httpstatus_list属性也可以修改 Request meta 的 handle_httpstatus_list 属性还可以修改全局 setttings中的HTTPERROR_ALLOWED_CODES。
OffsiteMiddlewareOffsiteMiddleware 的主要作用是过滤不符合 allowed_domains 的 RequestSpider 里面定义的allowed_domains其实就是在这个Spider Middleware 里生效的。
OffsiteMiddleware 首先遍历了 result然后根据 dont_filter、url 和 Spider 的 allowed_domains 进行了过滤如果不符合 allowed domains就直接输出日志并不再返回 Request只有符合要求的Request才会被返回并继续调用。
UrlLengthMiddlewareUrlLengthMiddleware 的主要作用是根据 Request 的URL长度对 Request 进行过滤如果URL的长度过长此Request就会被忽略。
UrlLengthMiddleware 利用了 process_spider_output 对 result 里面的 Request 进行过滤,如果是Request 类型并且 URL 长度超过最大限制就会被过滤。我们可以从中了解到如果想要根据URL的长度进行过滤可以设置URLLENGTH LIMIT。
其详细介绍和使用可以看scrapy爬虫框架五Spider Middleware - 乐之之 - 博客园 (cnblogs.com)
六、Item Pipeline 的使用
Item Pipeline即项目管道它的调用发生在Spider产生Item之后。当Spider解析完ResponseItem就会被Engine传递到Item Pipeline被定义的Item Pipeline组件会顺次被调用完成一连串的处理过程比如数据清洗、存储等。
Item Pipeline的主要功能如下
清洗HTML数据。验证爬取数据检查爬取字段。查重并丢弃重复内容。将爬取结果存储到数据库中。
6.1 process_item(item, spider)
process_item是必须实现的方法被定义的Item Pipeline会默认调用这个方法对Item进行处理比如进行数据处理或者将数据写入数据库等操作。
参数
process_item方法的参数有两个。
itemItem对象即被处理的Item。spiderSpider对象即生成该Item的Spider。
返回值
process_item方法必须返回Item类型的值或者抛出一个DropItem异常。该方法的返回类型如下
Item如果返回的是Item对象那么此Item会接着被低优先级的Item Pipeline的process_item方法处理直到所有的方法被调用完毕。DropItem异常如果抛出DropItem异常那么此Item就会被丢弃不再进行处理。
6.2 open_spider(self, spider)
open_spider方法是在Spider开启的时候被自动调用的在这里我们可以做一些收尾工作如关闭数据库连接等。其中参数spider就是被开启的Spider对象。
6.3 close_spider(spider)
close_spider方法是在Spider关闭的时候自动调用在这里我们可以做一些收尾工作如关闭数据库连接等其中参数spider就是被关闭的Spider对象。
6.4 from_crawler(cls, crawler)
from_crawler方法是一个类方法用classmethod标识它接受一个参数crawler。通过crawler对象我们可以拿到Scrapy的所有核心组件如全局配置的每个信息。然后可以在这个方法里面创建一个Pipeline实例。参数cls就是Class最后返回一个Class实例。
其对数据库详细的使用可以看这篇scrapy爬虫框架六Item Pipeline的使用 - 乐之之 - 博客园 (cnblogs.com)
七、Extension 的使用
Scrapy提供了一个Extension机制可以让我们添加和扩展一些自定义的功能。利用Extension我们可以注册一些处理方法并监听Scrapy运行过程中的各个信号做到发生某个事件时执行我们自定义的方法。
Scrapy已经内置了一些Extension如 LogStats 这个 Extension 用于记录一些基本的爬取信息比如爬取的页面数量、提取的Item数量等。 CoreStats 这个 Extension 用于统计爬取过程中的核心统计信息如开始爬取时间、爬取结束时间等。
和 Downloader Middleware、Spider Middleware 以及 Item Pipeline 一样Extension 也是通过settings.py 中的配置来控制是否被启用的是通过 EXTESION 这个配置项来实现的例如
EXTENSIONS{scrapy.extensions.corestats.Corestats: 500,scrapy.extensions.telnet.TelnetConsole: 501,
}通过如上配置我们就开启了 CoreStats 和 TelnetConsole 这两个 Extension。另外我们也可以实现自定义的Extension实现过程其实很简单主要分为两步
实现一个 Python 类然后实现对应的处理方法如实现一个 spider_opened 方法用于处理 Spider 开始爬取时执行的操作可以接收一个spider参数并对其进行操作。定义 from_crawler 类方法其第一个参数是cls类对象第二个参数是 crawler。利用 crawler 的 signals 对象将 Scrapy 的各个信号和已经定义的处理方法关联起来。
我们来尝试利用Extension实现爬取事件的消息通知。在爬取开始时、爬取到数据时、爬取结束时通知指定的服务器将这些事件和对应的数据通过HTTP请求发送给服务器。
本节通过上节Item Pipeline的代码进行演示主要内容如下
import scrapy
from testItemPipeline.items import TestitempipelineItemclass MovieSpiderSpider(scrapy.Spider):name movie_spiderallowed_domains [ssr1.scrape.center]start_url http://ssr1.scrape.centerdef start_requests(self):for i in range(1,11):urlself.start_urlf/page/{i}yield scrapy.Request(urlurl,callbackself.parse_index)def parse_index(self,response):data_list response.xpath(//div[classel-col el-col-18 el-col-offset-3]//div[classel-card item m-t is-hover-shadow])for item in data_list:href item.xpath(./div/div/div[1]/a/href).extract_first()url response.urljoin(href)yield scrapy.Request(urlurl,callbackself.parse_detail)def parse_detail(self, response):item TestitempipelineItem()item[name] response.xpath(//div[classel-card__body]/div[classitem el-row]/div[classp-h el-col el-col-24 el-col-xs-16 el-col-sm-12]/a/h2/text()).extract_first()item[categories] ,.join(response.xpath(//div[classel-card__body]/div[classitem el-row]/div[classp-h el-col el-col-24 el-col-xs-16 el-col-sm-12]/div[classcategories]/button/span/text()).extract())item[score] .join(response.xpath(//div[classel-card__body]/div[classitem el-row]/div[classel-col el-col-24 el-col-xs-8 el-col-sm-4]/p/text()).extract_first()).replace(\n,).replace( ,)item[drama] .join(response.xpath(//div[classel-card__body]/div[classitem el-row]/div[classp-h el-col el-col-24 el-col-xs-16 el-col-sm-12]/div[classdrama]/p/text()).extract_first()).replace(\n,)item[directors] []dd response.xpath(//div[classel-col el-col-18 el-col-offset-3]//div[classdirectors el-row])for data in dd:directors_name data.xpath(./div[classdirector el-col el-col-4]/div[classel-card is-hover-shadow]/div[classel-card__body]/p/text()).extract_first()directors_image data.xpath(./div[classdirector el-col el-col-4]/div[classel-card is-hover-shadow]/div[classel-card__body]/img/src).extract_first()item[directors].append({name: directors_name,image: directors_image})item[actors] []ss response.xpath(//div[classactors el-row]//div[classactor el-col el-col-4])for data in ss:actors_image .join(data.xpath(./div/div/img/src).extract_first())actors_name .join(data.xpath(./div/div/p/text()).extract_first())item[actors].append({name: actors_name,image: actors_image})yield item另外本节我们需要用到Flask来搭建一个简易的测试服务器也需要利用requests来实现HTTP请求的发送因此需要安装好Flask、requests和loguru这3个库使用pip安装即可
pip install flask requests loguru7.1 部署本地 flask 服务器
为了方便验证我们用Flask定义一个轻量级的服务器用于接收POST请求并输出接收到的事件和数据server.py的代码如下 7.2 extensions.py
在testItemPipeline文件夹下新建一个extensions.py文件。 注意在新建的文件夹一定要和其他组件是同一级别目录如Spider、Item等。 接下来我们先实现几个对应的事件处理方法: 这里我们定义了一个NotificationExtension类然后实现了3个方法spider_opened、spider_closed和item_scraped分别对应爬取开始、爬取结束和爬取到Item 的处理。接着调用了 requests 向刚才我们搭建的 HTTP 服务器发送了对应的事件其中包含两个字段一个是 event代表事件的名称;另一个是 data代表一些附加数据如 Spider的名称、Item的具体内容等。
但仅仅这么定义其实还不够现在启用这个Extension其实没有任何效果的我们还需要将这些方法和对应的Scrapy信号关联起来再在NotificationExtension类中添加如下类方法
添加方法前可以先导入一下Scrapy中的signals对象
from scrapy import signals其中from crawler 是一个类方法第一个参数就是 cls 类对象第二个参数 crawler 代表了Scrapy运行过程中全局的Crawler对象。
Crawler对象里有一个子对象叫作signals通过调用signals对象的connect方法我们可以将Scrapy运行过程中的某个信号和我们自定义的处理方法关联起来。这样在某个事件发生的时候被关联的处理方法就会被调用。比如这里connect方法第一个参数我们传入ext.spider_opened这个对象而ext是由cls类对象初始化的所以ext.spider_opened就代表我们在NotificationExtension类中定义的spider_opened方法。connect方法的第二个参数我们传入了signals.spider_opened这个对象这就指定了spider_opened 方法可以被spider_opened信号触发。这样在Spider 开始运行的时候会产生signals.spider_opened信号NotificationExtension类中定义的spider_opened方法就会被调用了。
完成如上定义之后我们还需要开启这个Extension在settings.py中添加如下内容即可。 我们成功启用了NotificationExtension这个Extension。下面我们来运行一下movie_spider:
scrapy crawl movie_spider这时候爬取结果和Item Pipeline的使用这节的内容大致一样不同的是日志中多了类似如下的几行: 有了这样的日志说明成功调用了requests的post方法完成了对服务器的请求。
这时候我们回到Flask服务器看一下控制台的输出结果 可以看到Flask服务器成功接收到了各个事件(SPIDER OPENED、ITEM SCRAPED、SPIDEROPENED)并输出了对应的数据这说明在 Scrapy 爬取过程中成功调用了 Extension 并在适当的时机将数据发送到服务器了验证成功!
我们通过一个自定义的 Extension成功实现了 Scrapy 爬取过程中和远程服务器的通信远程服务器收到这些事件之后就可以对事件和数据做进一步的处理了。
本节通过一个Extension的样例体会到了Extension强大又灵活的功能以后我们想实现一些自定义的功能可以借助于Extension来实现了。而对于整个scrapy框架基础到这里也就结束了后面对于一些不理解的地方一定要仔细琢磨认真观察多练多思考。
八、Scrapy 自动化配置
8.1 Scrapy 对接 Splash
要实现 Scrapy 和 Splash 的对接我们需要借助于 Scrapy-Splash 库另外还需要一个可以正常使用的 Splash 服务
Splash 本身就是一个 JavaScript 页面渲染服务我们只需要将需要渲染页面的 URL 发送给 Splash 就能得到对应的 JavaScript 渲染结果而 Scrapy-Splash 则是提供了这个过程基本功能的封装比如 Cookie 的处理URL 的转换等
首先新建一个项目名为 scrapysplashdemo命令如下
scrapy startproject scrapysplashdemo进入项目新建一个 Spider命令如下
scrapy genspider book spa5.scrape.center这样便创建了初始的 Spider然后创建一个同样的 BookItem代码如下
# items.py
from scrapy.item import Item, Fieldclass BookItem(Item):name Field()tags Field()score Field()cover Field()price Field()接下来就需要进行 Scrapy-Splash 相关的配置可以参考 Scrapy-Splash 的配置说明scrapy-plugins/scrapy-splash: ScrapySplash for JavaScript integration (github.com) 配置完毕后可以利用 Splash 来抓取页面可以直接生成一个 SplashRequest 对象并传递相应的参数Scrapy 会将此请求转发给 SplashSplash 对页面进行渲染加载再将渲染结果传递回来。此时 Response 的内容就是渲染完成的结果了最后交给 Spider 解析即可
yield SplashRequest(url, self.parse_result,args {wait: 0.5, # 等待时间},endpoint render.json, # 可选参数Splash 渲染终端splash_url url, # 可选参数覆盖 SPLASH_URL)这里构造了一个 SplashRequest 对象前两个参数依然是请求的URL和回调函数。另外我们还可以通过 args 传递一些渲染参数例如等待时间 wait 等还可以根据 endpoint 参数指定渲染接口。更多参数可以参考文档说明scrapy-plugins/scrapy-splash: ScrapySplash for JavaScript integration (github.com)
另外我们也可以生成 Request 对象Splash 的配置通过 meta 属性配置即可代码如下
yield scrapy.Request(url, self.parse_result, meta{splash: {args: {# set rendering arguments herehtml: 1,png: 1,# url is prefilled from request url# http_method is set to POST for POST requests# body is set to request body for POST requests},# optional parametersendpoint: render.json, # optional; default is render.jsonsplash_url: url, # optional; overrides SPLASH_URLslot_policy: scrapy_splash.SlotPolicy.PER_DOMAIN,splash_headers: {}, # optional; a dict with headers sent to Splashdont_process_response: True, # optional, default is Falsedont_send_headers: True, # optional, default is Falsemagic_response: False, # optional, default is True}
})SplashRequest 对象通过 args 来配置和 Request 对象通过 meta 来配置两种方式达到的效果是相同的。
可以定义一个 Lua 脚本来实现页面加载代码如下所示
function main(splash, args)assert(splash:go(args.url))assert(splash:wait(5))return {html splash:html()png splash.png()har splash.har()}这里实现的逻辑很简单就是获取参数中的 url 属性并访问然后等待 5 秒最后把截图html 代码har 信息返回接下来我们只需要在 Spider 中使用 SplashRequset 对接 Lua 脚本就好了代码如下
from scrapy import Spider
from scrapy_splash import SplashRequestscript
function main(splash, args)assert(splash:go(args.url))assert(splash:wait(5))return splash:html()
class BookSpider(Spider):name bookallowed_domains [spa5.scrape.center]base_url https://spa5.scrape.centerdef start_requests(self):start_url f{self.base_url}/page/1yield SplashRequest(start_url, callbackself.parse_index, args{lua_source: script}, endpointexecute)def parse_index(self, response):items response.css(.item)for item in items:href item.css(.top a::attr(href)).extract_first()detail_url response.urljoin(href)yield SplashRequest(detail_url, callbackself.parse_detail, priority2, args{lua_source: script}, endpointexecute)match re.search(rpage/(\d), response.url)if not math: returnpage int(match.group(1)) 1next_url f{self.base_url}/page/{page}yield SplashRequest(detail_url, callbackself.parse_detail, priority2, args{lua_source: script}, endpointexecute)def parse_detail(self, response):name response.css(.name::text).extract_first()tags response.css(.tags button span::text).extractscore response.css(.score::text).extract_first()price response.css(.price span::text).extract_first()cover response.css(.cover::attr(src)).extract_first()tags [tag.strip() if score else None] if tags else []score score.strip() if score else Noneitem BookItem(namename, tagstags, scorescore, priceprice, covercover)yield item接下来通过下列命令运行爬取
scrapy crawl book8.2 Splash 对接 Selenium
Scrapy 抓取页面的方式和 requests 库类似都是直接模拟 HTTP 请求而 Scrapy 也不能抓取 JavaScript 动态渲染的页面。在前文中抓取 JavaScript 渲染的页面有两种方式。一种是分析 Ajax 请求找到其对应的接口抓取Scrapy 同样可以用此种方式抓取。另一种是直接用 Selenium 或 Splash 模拟浏览器进行抓取我们不需要关心页面后台发生的请求也不需要分析渲染过程只需要关心页面最终结果即可可见即可爬。那么如果 Scrapy 可以对接 Selenium那 Scrapy 就可以处理任何网站的抓取了。
本节我们来看看 Scrapy 框架如何对接 Selenium以 PhantomJS 进行演示。我们依然抓取淘宝商品信息抓取逻辑和前文中用 Selenium 抓取淘宝商品完全相同。
请确保 PhantomJS 和 MongoDB 已经安装好并可以正常运行安装好 Scrapy、Selenium、PyMongo 库安装方式可以参考第 1 章的安装说明。
首先新建项目名为 scrapyseleniumtest命令如下所示
scrapy startproject scrapyseleniumtest新建一个 Spider命令如下所示
scrapy genspider taobao www.taobao.com修改 ROBOTSTXT_OBEY 为 False如下所示
ROBOTSTXT_OBEY False首先定义 Item 对象名为 ProductItem代码如下所示
from scrapy import Item, Fieldclass ProductItem(Item):collection productsimage Field()price Field()deal Field()title Field()shop Field()location Field()这里我们定义了 6 个 Field也就是 6 个字段跟之前的案例完全相同。然后定义了一个 collection 属性即此 Item 保存到 MongoDB 的 Collection 名称。
初步实现 Spider 的 start_requests() 方法如下所示
from scrapy import Request, Spider
from urllib.parse import quote
from scrapyseleniumtest.items import ProductItemclass TaobaoSpider(Spider):name taobaoallowed_domains [www.taobao.com]base_url https://s.taobao.com/search?qdef start_requests(self):for keyword in self.settings.get(KEYWORDS):for page in range(1, self.settings.get(MAX_PAGE) 1):url self.base_url quote(keyword)yield Request(urlurl, callbackself.parse, meta{page: page}, dont_filterTrue)首先定义了一个 base_url即商品列表的 URL其后拼接一个搜索关键字就是该关键字在淘宝的搜索结果商品列表页面。
关键字用 KEYWORDS 标识定义为一个列表。最大翻页页码用 MAX_PAGE 表示。它们统一定义在 setttings.py 里面如下所示
KEYWORDS [iPad]
MAX_PAGE 100在 start_requests() 方法里我们首先遍历了关键字遍历了分页页码构造并生成 Request。由于每次搜索的 URL 是相同的所以分页页码用 meta 参数来传递同时设置 dont_filter 不去重。这样爬虫启动的时候就会生成每个关键字对应的商品列表的每一页的请求了。
接下来我们需要处理这些请求的抓取。这次我们对接 Selenium 进行抓取采用 Downloader Middleware 来实现。在 Middleware 里面的 process_request() 方法里对每个抓取请求进行处理启动浏览器并进行页面渲染再将渲染后的结果构造一个 HtmlResponse 对象返回。代码实现如下所示
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from scrapy.http import HtmlResponse
from logging import getLoggerclass SeleniumMiddleware():def __init__(self, timeoutNone, service_args[]):self.logger getLogger(__name__)self.timeout timeoutself.browser webdriver.PhantomJS(service_argsservice_args)self.browser.set_window_size(1400, 700)self.browser.set_page_load_timeout(self.timeout)self.wait WebDriverWait(self.browser, self.timeout)def __del__(self):self.browser.close()def process_request(self, request, spider):用 PhantomJS 抓取页面:param request: Request 对象:param spider: Spider 对象:return: HtmlResponseself.logger.debug(PhantomJS is Starting)page request.meta.get(page, 1)try:self.browser.get(request.url)if page 1:input self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, #mainsrp-pager div.form input)))submit self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, #mainsrp-pager div.form span.btn.J_Submit)))input.clear()input.send_keys(page)submit.click()self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, #mainsrp-pager li.item.active span), str(page)))self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .m-itemlist .items .item)))return HtmlResponse(urlrequest.url, bodyself.browser.page_source, requestrequest, encodingutf-8, status200)except TimeoutException:return HtmlResponse(urlrequest.url, status500, requestrequest)classmethoddef from_crawler(cls, crawler):return cls(timeoutcrawler.settings.get(SELENIUM_TIMEOUT),service_argscrawler.settings.get(PHANTOMJS_SERVICE_ARGS))首先我们在 __init__() 里对一些对象进行初始化包括 PhantomJS、WebDriverWait 等对象同时设置页面大小和页面加载超时时间。在 process_request() 方法中我们通过 Request 的 meta 属性获取当前需要爬取的页码调用 PhantomJS 对象的 get() 方法访问 Request 的对应的 URL。这就相当于从 Request 对象里获取请求链接然后再用 PhantomJS 加载而不再使用 Scrapy 里的 Downloader。
随后的处理等待和翻页的方法在此不再赘述和前文的原理完全相同。最后页面加载完成之后我们调用 PhantomJS 的 page_source 属性即可获取当前页面的源代码然后用它来直接构造并返回一个 HtmlResponse 对象。构造这个对象的时候需要传入多个参数如 url、body 等这些参数实际上就是它的基础属性。可以在官方文档查看 HtmlResponse 对象的结构https://doc.scrapy.org/en/latest/topics/request-response.html这样我们就成功利用 PhantomJS 来代替 Scrapy 完成了页面的加载最后将 Response 返回即可。
有人可能会纳闷为什么实现这么一个 Downloader Middleware 就可以了之前的 Request 对象怎么办Scrapy 不再处理了吗Response 返回后又传递给了谁
是的Request 对象到这里就不会再处理了也不会再像以前一样交给 Downloader 下载。Response 会直接传给 Spider 进行解析。
我们需要回顾一下 Downloader Middleware 的 process_request() 方法的处理逻辑内容如下所示
当 process_request() 方法返回 Response 对象的时候更低优先级的 Downloader Middleware 的 process_request() 和 process_exception() 方法就不会被继续调用了转而开始执行每个 Downloader Middleware 的 process_response() 方法调用完毕之后直接将 Response 对象发送给 Spider 来处理。
这里直接返回了一个 HtmlResponse 对象它是 Response 的子类返回之后便顺次调用每个 Downloader Middleware 的 process_response() 方法。而在 process_response() 中我们没有对其做特殊处理它会被发送给 Spider传给 Request 的回调函数进行解析。
到现在我们应该能了解 Downloader Middleware 实现 Selenium 对接的原理了。
在 settings.py 里我们设置调用刚才定义的 SeleniumMiddleware、设置等待超时变量 SELENIUM_TIMEOUT、设置 PhantomJS 配置参数 PHANTOMJS_SERVICE_ARGS如下所示
DOWNLOADER_MIDDLEWARES {scrapyseleniumtest.middlewares.SeleniumMiddleware: 543,}Response 对象就会回传给 Spider 内的回调函数进行解析。所以下一步我们就实现其回调函数对网页来进行解析代码如下所示
def parse(self, response):products response.xpath(//div[idmainsrp-itemlist]//div[classitems][1]//div[contains(class, item)])for product in products:item ProductItem()item[price] .join(product.xpath(.//div[contains(class, price)]//text()).extract()).strip()item[title] .join(product.xpath(.//div[contains(class, title)]//text()).extract()).strip()item[shop] .join(product.xpath(.//div[contains(class, shop)]//text()).extract()).strip()item[image] .join(product.xpath(.//div[classpic]//img[contains(class, img)]/data-src).extract()).strip()item[deal] product.xpath(.//div[contains(class, deal-cnt)]//text()).extract_first()item[location] product.xpath(.//div[contains(class, location)]//text()).extract_first()yield item在这里我们使用 XPath 进行解析调用 response 变量的 xpath() 方法即可。首先我们传递选取所有商品对应的 XPath可以匹配所有商品随后对结果进行遍历依次选取每个商品的名称、价格、图片等内容构造并返回一个 ProductItem 对象。
最后我们实现一个 Item Pipeline将结果保存到 MongoDB如下所示
import pymongoclass MongoPipeline(object):def __init__(self, mongo_uri, mongo_db):self.mongo_uri mongo_uriself.mongo_db mongo_dbclassmethoddef from_crawler(cls, crawler):return cls(mongo_uricrawler.settings.get(MONGO_URI), mongo_dbcrawler.settings.get(MONGO_DB))def open_spider(self, spider):self.client pymongo.MongoClient(self.mongo_uri)self.db self.client[self.mongo_db]def process_item(self, item, spider):self.db[item.collection].insert(dict(item))return itemdef close_spider(self, spider):self.client.close()此实现和前文中存储到 MongoDB 的方法完全一致原理不再赘述。记得在 settings.py 中开启它的调用如下所示
ITEM_PIPELINES {scrapyseleniumtest.pipelines.MongoPipeline: 300,}其中MONGO_URI 和 MONGO_DB 的定义如下所示
MONGO_URI localhost
MONGO_DB taobao整个项目就完成了执行如下命令启动抓取即可
scrapy crawl taobao运行结果如图所示 再查看一下 MongoDB结果如图所示 这样我们便成功在 Scrapy 中对接 Selenium 并实现了淘宝商品的抓取。
本节代码地址为https://github.com/Python3WebSpider/ScrapySeleniumTest。
我们通过改写 Downloader Middleware 的方式实现了 Selenium 的对接。但这种方法其实是阻塞式的也就是说这样就破坏了 Scrapy 异步处理的逻辑速度会受到影响。为了不破坏其异步加载逻辑我们可以使用 Splash 实现。
8.3 关于其他
作者在这章还开发了两个包第一个是 selenium 的包介绍如下 第二个是 pyppeteer 的包介绍如下 但是目前 pyppeteer 已停止维护官方推荐使用 playwright 来继续
九、Scrapy 规则化爬虫
通过 Scrapy我们可以轻松地完成一个站点爬虫的编写。但如果抓取的站点量非常大比如爬取各大媒体的新闻信息多个 Spider 则可能包含很多重复代码。
如果我们将各个站点的 Spider 的公共部分保留下来不同的部分提取出来作为单独的配置如爬取规则、页面解析方式等抽离出来做成一个配置文件那么我们在新增一个爬虫的时候只需要实现这些网站的爬取规则和提取规则即可。
9.1 CrawlSpider
在实现通用爬虫之前我们需要先了解一下 CrawlSpider其官方文档链接为http://scrapy.readthedocs.io/en/latest/topics/spiders.html#crawlspider。
CrawlSpider 是 Scrapy 提供的一个通用 Spider。在 Spider 里我们可以指定一些爬取规则来实现页面的提取这些爬取规则由一个专门的数据结构 Rule 表示。Rule 里包含提取和跟进页面的配置Spider 会根据 Rule 来确定当前页面中的哪些链接需要继续爬取、哪些页面的爬取结果需要用哪个方法解析等。
CrawlSpider 继承自 Spider 类。除了 Spider 类的所有方法和属性它还提供了一个非常重要的属性和方法。
rules它是爬取规则属性是包含一个或多个 Rule 对象的列表。每个 Rule 对爬取网站的动作都做了定义CrawlSpider 会读取 rules 的每一个 Rule 并进行解析。parse_start_url()它是一个可重写的方法。当 start_urls 里对应的 Request 得到 Response 时该方法被调用它会分析 Response 并必须返回 Item 对象或者 Request 对象。
这里最重要的内容莫过于 Rule 的定义了它的定义和参数如下所示
class scrapy.contrib.spiders.Rule(link_extractor, callbackNone, cb_kwargsNone, followNone, process_linksNone, process_requestNone)下面对其参数依次说明
link_extractor是一个 Link Extractor 对象。通过它Spider 可以知道从爬取的页面中提取哪些链接。提取出的链接会自动生成 Request。它又是一个数据结构一般常用 LxmlLinkExtractor 对象作为参数其定义和参数如下所示
class scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow(), deny(), allow_domains(), deny_domains(), deny_extensionsNone, restrict_xpaths(), restrict_css(), tags(a, area), attrs(href,), canonicalizeFalse, uniqueTrue, process_valueNone, stripTrue)allow 是一个正则表达式或正则表达式列表它定义了从当前页面提取出的链接哪些是符合要求的只有符合要求的链接才会被跟进。deny 则相反。allow_domains 定义了符合要求的域名只有此域名的链接才会被跟进生成新的 Request它相当于域名白名单。deny_domains 则相反相当于域名黑名单。restrict_xpaths 定义了从当前页面中 XPath 匹配的区域提取链接其值是 XPath 表达式或 XPath 表达式列表。restrict_css 定义了从当前页面中 CSS 选择器匹配的区域提取链接其值是 CSS 选择器或 CSS 选择器列表。还有一些其他参数代表了提取链接的标签、是否去重、链接的处理等内容使用的频率不高。可以参考文档的参数说明http://scrapy.readthedocs.io/en/latest/topics/link-extractors.html#module-scrapy.linkextractors.lxmlhtml。
callback即回调函数和之前定义 Request 的 callback 有相同的意义。每次从 link_extractor 中获取到链接时该函数将会调用。该回调函数接收一个 response 作为其第一个参数并返回一个包含 Item 或 Request 对象的列表。注意避免使用 parse() 作为回调函数。由于 CrawlSpider 使用 parse() 方法来实现其逻辑如果 parse() 方法覆盖了CrawlSpider 将会运行失败。cb_kwargs字典它包含传递给回调函数的参数。follow布尔值即 True 或 False它指定根据该规则从 response 提取的链接是否需要跟进。如果 callback 参数为 Nonefollow 默认设置为 True否则默认为 False。process_links指定处理函数从 link_extractor 中获取到链接列表时该函数将会调用它主要用于过滤。process_request同样是指定处理函数根据该 Rule 提取到每个 Request 时该函数都会调用对 Request 进行处理。该函数必须返回 Request 或者 None。
以上内容便是 CrawlSpider 中的核心 Rule 的基本用法。但这些内容可能还不足以完成一个 CrawlSpider 爬虫。下面我们利用 CrawlSpider 实现新闻网站的爬取实例来更好地理解 Rule 的用法。
9.2 Item Loader
我们了解了利用 CrawlSpider 的 Rule 来定义页面的爬取逻辑这是可配置化的一部分内容。但是Rule 并没有对 Item 的提取方式做规则定义。对于 Item 的提取我们需要借助另一个模块 Item Loader 来实现。
Item Loader 提供一种便捷的机制来帮助我们方便地提取 Item。它提供的一系列 API 可以分析原始数据对 Item 进行赋值。Item 提供的是保存抓取数据的容器而 Item Loader 提供的是填充容器的机制。有了它数据的提取会变得更加规则化。
Item Loader 的 API 如下所示
class scrapy.loader.ItemLoader([item, selector, response,] **kwargs)Item Loader 的 API 返回一个新的 Item Loader 来填充给定的 Item。如果没有给出 Item则使用 default_item_class 中的类自动实例化。另外它传入 selector 和 response 参数来使用选择器或响应参数实例化。
下面将依次说明 Item Loader 的 API 参数。
itemItem 对象可以调用 add_xpath()、add_css() 或 add_value() 等方法来填充 Item 对象。selectorSelector 对象用来提取填充数据的选择器。responseResponse 对象用于使用构造选择器的 Response。
一个比较典型的 Item Loader 实例如下
from scrapy.loader import ItemLoader
from project.items import Productdef parse(self, response):loader ItemLoader(itemProduct(), responseresponse)loader.add_xpath(name, //div[classproduct_name])loader.add_xpath(name, //div[classproduct_title])loader.add_xpath(price, //p[idprice])loader.add_css(stock, p#stock])loader.add_value(last_updated, today)return loader.load_item()这里首先声明一个 Product Item用该 Item 和 Response 对象实例化 ItemLoader调用 add_xpath() 方法把来自两个不同位置的数据提取出来分配给 name 属性再用 add_xpath()、add_css()、add_value() 等方法对不同属性依次赋值最后调用 load_item() 方法实现 Item 的解析。这种方式比较规则化我们可以把一些参数和规则单独提取出来做成配置文件或存到数据库即可实现可配置化。
另外Item Loader 每个字段中都包含了一个 Input Processor输入处理器和一个 Output Processor输出处理器。Input Processor 收到数据时立刻提取数据Input Processor 的结果被收集起来并且保存在 ItemLoader 内但是不分配给 Item。收集到所有的数据后load_item() 方法被调用来填充再生成 Item 对象。在调用时会先调用 Output Processor 来处理之前收集到的数据然后再存入 Item 中这样就生成了 Item。
下面将介绍一些内置的 Processor。
Identity
Identity 是最简单的 Processor不进行任何处理直接返回原来的数据。
TakeFirst
TakeFirst 返回列表的第一个非空值类似 extract_first() 的功能常用作 Output Processor如下所示
from scrapy.loader.processors import TakeFirst
processor TakeFirst()
print(processor([, 1, 2, 3]))输出结果如下所示
1经过此 Processor 处理后的结果返回了第一个不为空的值。
Join
Join 方法相当于字符串的 join() 方法可以把列表拼合成字符串字符串默认使用空格分隔如下所示
from scrapy.loader.processors import Join
processor Join()
print(processor([one, two, three]))输出结果如下所示
one two three它也可以通过参数更改默认的分隔符例如改成逗号
from scrapy.loader.processors import Join
processor Join(,)
print(processor([one, two, three]))运行结果如下所示
one,two,threeCompose
Compose 是用给定的多个函数的组合而构造的 Processor每个输入值被传递到第一个函数其输出再传递到第二个函数依次类推直到最后一个函数返回整个处理器的输出如下所示
from scrapy.loader.processors import Compose
processor Compose(str.upper, lambda s: s.strip())
print(processor( hello world))运行结果如下所示
HELLO WORLD在这里我们构造了一个 Compose Processor传入一个开头带有空格的字符串。Compose Processor 的参数有两个第一个是 str.upper它可以将字母全部转为大写第二个是一个匿名函数它调用 strip() 方法去除头尾空白字符。Compose 会顺次调用两个参数最后返回结果的字符串全部转化为大写并且去除了开头的空格。
MapCompose
与 Compose 类似MapCompose 可以迭代处理一个列表输入值如下所示
from scrapy.loader.processors import MapCompose
processor MapCompose(str.upper, lambda s: s.strip())
print(processor([Hello, World, Python]))运行结果如下所示
[HELLO, WORLD, PYTHON]被处理的内容是一个可迭代对象MapCompose 会将该对象遍历然后依次处理。
SelectJmes
SelectJmes 可以查询 JSON传入 Key返回查询所得的 Value。不过需要先安装 jmespath 库才可以使用它命令如下所示
pip3 install jmespath安装好 jmespath 之后便可以使用这个 Processor 了如下所示
from scrapy.loader.processors import SelectJmes
proc SelectJmes(foo)
processor SelectJmes(foo)
print(processor({foo: bar}))运行结果
bar以上内容便是一些常用的 Processor在本节的实例中我们会使用 Processor 来进行数据的处理。
接下来我们用一个实例来了解 Item Loader 的用法。
我们以中华网科技类新闻为例来了解 CrawlSpider 和 Item Loader 的用法再提取其可配置信息实现可配置化。官网链接为http://tech.china.com/。我们需要爬取它的科技类新闻内容链接为http://tech.china.com/articles/。
我们要抓取新闻列表中的所有分页的新闻详情包括标题、正文、时间、来源等信息。 首先新建一个 Scrapy 项目名为 scrapyuniversal如下所示
scrapy startproject scrapyuniversal创建一个 CrawlSpider需要先制定一个模板。我们可以先看看有哪些可用模板命令如下所示
scrapy genspider -l运行结果如下所示
Available templates:basiccrawlcsvfeedxmlfeed之前创建 Spider 的时候我们默认使用了第一个模板 basic。这次要创建 CrawlSpider就需要使用第二个模板 crawl创建命令如下所示
scrapy genspider -t crawl china tech.china.com运行之后便会生成一个 CrawlSpider其内容如下所示
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Ruleclass ChinaSpider(CrawlSpider):name chinaallowed_domains [tech.china.com]start_urls [http://tech.china.com/]rules (Rule(LinkExtractor(allowrItems/), callbackparse_item, followTrue),)def parse_item(self, response):i {}#i[domain_id] response.xpath(//input[idsid]/value).extract()#i[name] response.xpath(//div[idname]).extract()#i[description] response.xpath(//div[iddescription]).extract()return i这次生成的 Spider 内容多了一个 rules 属性的定义。Rule 的第一个参数是 LinkExtractor就是上文所说的 LxmlLinkExtractor只是名称不同。同时默认的回调函数也不再是 parse而是 parse_item。
定义 Rule
要实现新闻的爬取我们需要做的就是定义好 Rule然后实现解析函数。下面我们就来一步步实现这个过程。
首先将 start_urls 修改为起始链接代码如下所示
start_urls [http://tech.china.com/articles/]之后Spider 爬取 start_urls 里面的每一个链接。所以这里第一个爬取的页面就是我们刚才所定义的链接。得到 Response 之后Spider 就会根据每一个 Rule 来提取这个页面内的超链接去生成进一步的 Request。接下来我们就需要定义 Rule 来指定提取哪些链接。
当前页面如图所示 这是新闻的列表页下一步自然就是将列表中的每条新闻详情的链接提取出来。这里直接指定这些链接所在区域即可。查看源代码所有链接都在 ID 为 left_side 的节点内具体来说是它内部的 class 为 con_item 的节点 此处我们可以用 LinkExtractor 的 restrict_xpaths 属性来指定之后 Spider 就会从这个区域提取所有的超链接并生成 Request。但是每篇文章的导航中可能还有一些其他的超链接标签我们只想把需要的新闻链接提取出来。真正的新闻链接路径都是以 article 开头的我们用一个正则表达式将其匹配出来再赋值给 allow 参数即可。另外这些链接对应的页面其实就是对应的新闻详情页而我们需要解析的就是新闻的详情信息所以此处还需要指定一个回调函数 callback。
到现在我们就可以构造出一个 Rule 了代码如下所示
Rule(LinkExtractor(allowarticle\/.*\.html, restrict_xpaths//div[idleft_side]//div[classcon_item]), callbackparse_item)接下来我们还要让当前页面实现分页功能所以还需要提取下一页的链接。分析网页源码之后可以发现下一页链接是在 ID 为 pageStyle 的节点内如图 13-22 所示。 但是下一页节点和其他分页链接区分度不高要取出此链接我们可以直接用 XPath 的文本匹配方式所以这里我们直接用 LinkExtractor 的 restrict_xpaths 属性来指定提取的链接即可。另外我们不需要像新闻详情页一样去提取此分页链接对应的页面详情信息也就是不需要生成 Item所以不需要加 callback 参数。另外这下一页的页面如果请求成功了就需要继续像上述情况一样分析所以它还需要加一个 follow 参数为 True代表继续跟进匹配分析。其实follow 参数也可以不加因为当 callback 为空的时候follow 默认为 True。此处 Rule 定义为如下所示
Rule(LinkExtractor(restrict_xpaths//div[idpageStyle]//a[contains(., 下一页)]))所以现在 rules 就变成了
rules (Rule(LinkExtractor(allowarticle\/.*\.html, restrict_xpaths//div[idleft_side]//div[classcon_item]), callbackparse_item),Rule(LinkExtractor(restrict_xpaths//div[idpageStyle]//a[contains(., 下一页)]))
)接着我们运行一下代码命令如下
scrapy crawl china现在已经实现页面的翻页和详情页的抓取了我们仅仅通过定义了两个 Rule 即实现了这样的功能运行效果如图。 解析页面
接下来我们需要做的就是解析页面内容了将标题、发布时间、正文、来源提取出来即可。首先定义一个 Item如下所示
from scrapy import Field, Itemclass NewsItem(Item):title Field()url Field()text Field()datetime Field()source Field()website Field()这里的字段分别指新闻标题、链接、正文、发布时间、来源、站点名称其中站点名称直接赋值为中华网。因为既然是通用爬虫肯定还有很多爬虫也来爬取同样结构的其他站点的新闻内容所以需要一个字段来区分一下站点名称。
详情页的预览图如图所示 如果像之前一样提取内容就直接调用 response 变量的 xpath()、css() 等方法即可。这里 parse_item() 方法的实现如下所示
def parse_item(self, response):item NewsItem()item[title] response.xpath(//h1[idchan_newsTitle]/text()).extract_first()item[url] response.urlitem[text] .join(response.xpath(//div[idchan_newsDetail]//text()).extract()).strip()item[datetime] response.xpath(//div[idchan_newsInfo]/text()).re_first((\d-\d-\d\s\d:\d:\d))item[source] response.xpath(//div[idchan_newsInfo]/text()).re_first( 来源(.*)).strip()item[website] 中华网 yield item这样我们就把每条新闻的信息提取形成了一个 NewsItem 对象。
这时实际上我们就已经完成了 Item 的提取。再运行一下 Spider如下所示
scrapy crawl china输出内容如图所示 现在我们就可以成功将每条新闻的信息提取出来。
不过我们发现这种提取方式非常不规整。下面我们再用 Item Loader通过 add_xpath()、add_css()、add_value() 等方式实现配置化提取。我们可以改写 parse_item()如下所示
def parse_item(self, response):loader ChinaLoader(itemNewsItem(), responseresponse)loader.add_xpath(title, //h1[idchan_newsTitle]/text())loader.add_value(url, response.url)loader.add_xpath(text, //div[idchan_newsDetail]//text())loader.add_xpath(datetime, //div[idchan_newsInfo]/text(), re(\d-\d-\d\s\d:\d:\d))loader.add_xpath(source, //div[idchan_newsInfo]/text(), re 来源(.*))loader.add_value(website, 中华网 )yield loader.load_item()这里我们定义了一个 ItemLoader 的子类名为 ChinaLoader其实现如下所示
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, Join, Composeclass NewsLoader(ItemLoader):default_output_processor TakeFirst()class ChinaLoader(NewsLoader):text_out Compose(Join(), lambda s: s.strip())source_out Compose(Join(), lambda s: s.strip())ChinaLoader 继承了 NewsLoader 类其内定义了一个通用的 Out Processor 为 TakeFirst这相当于之前所定义的 extract_first() 方法的功能。我们在 ChinaLoader 中定义了 text_out 和 source_out 字段。这里使用了一个 Compose Processor它有两个参数第一个参数 Join 也是一个 Processor它可以把列表拼合成一个字符串第二个参数是一个匿名函数可以将字符串的头尾空白字符去掉。经过这一系列处理之后我们就将列表形式的提取结果转化为去除头尾空白字符的字符串。
代码重新运行提取效果是完全一样的。
至此我们已经实现了爬虫的半通用化配置。
通用配置抽取
为什么现在只做到了半通用化如果我们需要扩展其他站点仍然需要创建一个新的 CrawlSpider定义这个站点的 Rule单独实现 parse_item() 方法。还有很多代码是重复的如 CrawlSpider 的变量、方法名几乎都是一样的。那么我们可不可以把多个类似的几个爬虫的代码共用把完全不相同的地方抽离出来做成可配置文件呢
当然可以。那我们可以抽离出哪些部分所有的变量都可以抽取如 name、allowed_domains、start_urls、rules 等。这些变量在 CrawlSpider 初始化的时候赋值即可。我们就可以新建一个通用的 Spider 来实现这个功能命令如下所示
scrapy genspider -t crawl universal universal这个全新的 Spider 名为 universal。接下来我们将刚才所写的 Spider 内的属性抽离出来配置成一个 JSON命名为 china.json放到 configs 文件夹内和 spiders 文件夹并列代码如下所示
{spider: universal,website: 中华网科技,type: 新闻,index: http://tech.china.com/,settings: {USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36},start_urls: [http://tech.china.com/articles/],allowed_domains: [tech.china.com],rules: china
}第一个字段 spider 即 Spider 的名称在这里是 universal。后面是站点的描述比如站点名称、类型、首页等。随后的 settings 是该 Spider 特有的 settings 配置如果要覆盖全局项目settings.py 内的配置可以单独为其配置。随后是 Spider 的一些属性如 start_urls、allowed_domains、rules 等。rules 也可以单独定义成一个 rules.py 文件做成配置文件实现 Rule 的分离如下所示
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rulerules {china: (Rule(LinkExtractor(allowarticle\/.*\.html, restrict_xpaths//div[idleft_side]//div[classcon_item]),callbackparse_item),Rule(LinkExtractor(restrict_xpaths//div[idpageStyle]//a[contains(., 下一页)])))
}这样我们将基本的配置抽取出来。如果要启动爬虫只需要从该配置文件中读取然后动态加载到 Spider 中即可。所以我们需要定义一个读取该 JSON 文件的方法如下所示
from os.path import realpath, dirname
import json
def get_config(name):path dirname(realpath(__file__)) /configs/ name .jsonwith open(path, r, encodingutf-8) as f:return json.loads(f.read())定义了 get_config() 方法之后我们只需要向其传入 JSON 配置文件的名称即可获取此 JSON 配置信息。随后我们定义入口文件 run.py把它放在项目根目录下它的作用是启动 Spider如下所示
import sys
from scrapy.utils.project import get_project_settings
from scrapyuniversal.spiders.universal import UniversalSpider
from scrapyuniversal.utils import get_config
from scrapy.crawler import CrawlerProcessdef run():name sys.argv[1]custom_settings get_config(name)# 爬取使用的 Spider 名称spider custom_settings.get(spider, universal)project_settings get_project_settings()settings dict(project_settings.copy())# 合并配置settings.update(custom_settings.get(settings))process CrawlerProcess(settings)# 启动爬虫process.crawl(spider, **{name: name})process.start()if __name__ __main__:run()运行入口为 run()。首先获取命令行的参数并赋值为 namename 就是 JSON 文件的名称其实就是要爬取的目标网站的名称。我们首先利用 get_config() 方法传入该名称读取刚才定义的配置文件。获取爬取使用的 spider 的名称、配置文件中的 settings 配置然后将获取到的 settings 配置和项目全局的 settings 配置做了合并。新建一个 CrawlerProcess传入爬取使用的配置。调用 crawl() 和 start() 方法即可启动爬取。
在 universal 中我们新建一个__init__() 方法进行初始化配置实现如下所示
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyuniversal.utils import get_config
from scrapyuniversal.rules import rulesclass UniversalSpider(CrawlSpider):name universaldef __init__(self, name, *args, **kwargs):config get_config(name)self.config configself.rules rules.get(config.get(rules))self.start_urls config.get(start_urls)self.allowed_domains config.get(allowed_domains)super(UniversalSpider, self).__init__(*args, **kwargs)def parse_item(self, response):i {}return i在 __init__() 方法中start_urls、allowed_domains、rules 等属性被赋值。其中rules 属性另外读取了 rules.py 的配置这样就成功实现爬虫的基础配置。
接下来执行如下命令运行爬虫
python3 run.py china程序会首先读取 JSON 配置文件将配置中的一些属性赋值给 Spider然后启动爬取。运行效果完全相同运行结果如图所示。 现在我们已经对 Spider 的基础属性实现了可配置化。剩下的解析部分同样需要实现可配置化原来的解析函数如下所示
def parse_item(self, response):loader ChinaLoader(itemNewsItem(), responseresponse)loader.add_xpath(title, //h1[idchan_newsTitle]/text())loader.add_value(url, response.url)loader.add_xpath(text, //div[idchan_newsDetail]//text())loader.add_xpath(datetime, //div[idchan_newsInfo]/text(), re(\d-\d-\d\s\d:\d:\d))loader.add_xpath(source, //div[idchan_newsInfo]/text(), re 来源(.*))loader.add_value(website, 中华网 )yield loader.load_item()我们需要将这些配置也抽离出来。这里的变量主要有 Item Loader 类的选用、Item 类的选用、Item Loader 方法参数的定义我们可以在 JSON 文件中添加如下 item 的配置
item: {class: NewsItem,loader: ChinaLoader,attrs: {title: [{method: xpath,args: [//h1[idchan_newsTitle]/text()]}],url: [{method: attr,args: [url]}],text: [{method: xpath,args: [//div[idchan_newsDetail]//text()]}],datetime: [{method: xpath,args: [//div[idchan_newsInfo]/text()],re: (\\d-\\d-\\d\\s\\d:\\d:\\d)}],source: [{method: xpath,args: [//div[idchan_newsInfo]/text()],re: 来源(.*)}],website: [{method: value,args: [中华网]}]}
}这里定义了 class 和 loader 属性它们分别代表 Item 和 Item Loader 所使用的类。定义了 attrs 属性来定义每个字段的提取规则例如title 定义的每一项都包含一个 method 属性它代表使用的提取方法如 xpath 即代表调用 Item Loader 的 add_xpath() 方法。args 即参数就是 add_xpath() 的第二个参数即 XPath 表达式。针对 datetime 字段我们还用了一次正则提取所以这里还可以定义一个 re 参数来传递提取时所使用的正则表达式。
我们还要将这些配置之后动态加载到 parse_item() 方法里。最后最重要的就是实现 parse_item() 方法如下所示 def parse_item(self, response):item self.config.get(item)if item:cls eval(item.get(class))()loader eval(item.get(loader))(cls, responseresponse)# 动态获取属性配置for key, value in item.get(attrs).items():for extractor in value:if extractor.get(method) xpath:loader.add_xpath(key, *extractor.get(args), **{re: extractor.get(re)})if extractor.get(method) css:loader.add_css(key, *extractor.get(args), **{re: extractor.get(re)})if extractor.get(method) value:loader.add_value(key, *extractor.get(args), **{re: extractor.get(re)})if extractor.get(method) attr:loader.add_value(key, getattr(response, *extractor.get(args)))yield loader.load_item()这里首先获取 Item 的配置信息然后获取 class 的配置将其初始化初始化 Item Loader遍历 Item 的各个属性依次进行提取。判断 method 字段调用对应的处理方法进行处理。如 method 为 css就调用 Item Loader 的 add_css() 方法进行提取。所有配置动态加载完毕之后调用 load_item() 方法将 Item 提取出来。
重新运行程序结果如图所示。 运行结果是完全相同的。
我们再回过头看一下 start_urls 的配置。这里 start_urls 只可以配置具体的链接。如果这些链接有 100 个、1000 个我们总不能将所有的链接全部列出来吧在某些情况下start_urls 也需要动态配置。我们将 start_urls 分成两种一种是直接配置 URL 列表一种是调用方法生成它们分别定义为 static 和 dynamic 类型。
本例中的 start_urls 很明显是 static 类型的所以 start_urls 配置改写如下所示
start_urls: {type:static,value: [http://tech.china.com/articles/]}如果 start_urls 是动态生成的我们可以调用方法传参数如下所示
start_urls: {type: dynamic,method: china,args: [5, 10]
}这里 start_urls 定义为 dynamic 类型指定方法为 urls_china()然后传入参数 5 和 10来生成第 5 到 10 页的链接。这样我们只需要实现该方法即可统一新建一个 urls.py 文件如下所示
def china(start, end):for page in range(start, end 1):yield http://tech.china.com/articles/index_ str(page) .html其他站点可以自行配置。如某些链接需要用到时间戳加密参数等均可通过自定义方法实现。
接下来在 Spider 的 __init__() 方法中start_urls 的配置改写如下所示
from scrapyuniversal import urlsstart_urls config.get(start_urls)
if start_urls:if start_urls.get(type) static:self.start_urls start_urls.get(value)elif start_urls.get(type) dynamic:self.start_urls list(eval(urls. start_urls.get(method))(*start_urls.get(args, [])))这里通过判定 start_urls 的类型分别进行不同的处理这样我们就可以实现 start_urls 的配置了。
至此Spider 的设置、起始链接、属性、提取方法都已经实现了全部的可配置化。
综上所述整个项目的配置包括如下内容。
spider指定所使用的 Spider 的名称。settings可以专门为 Spider 定制配置信息会覆盖项目级别的配置。start_urls指定爬虫爬取的起始链接。allowed_domains允许爬取的站点。rules站点的爬取规则。item数据的提取规则。
我们实现了 Scrapy 的通用爬虫每个站点只需要修改 JSON 文件即可实现自由配置。
本节代码地址为https://github.com/Python3WebSpider/ScrapyUniversal。
Scrapy 通用爬虫的实现我们将所有配置抽离出来每增加一个爬虫就只需要增加一个 JSON 文件配置。之后我们只需要维护这些配置文件即可。如果要更加方便的管理可以将规则存入数据库再对接可视化管理页面即可。
十、Scrapyrt 的使用
Scrapyrt 为 Scrapy 提供了一个调度的 HTTP 接口。有了它我们不需要再执行 Scrapy 命令而是通过请求一个 HTTP 接口即可调度 Scrapy 任务我们就不需要借助于命令行来启动项目了。如果项目是在远程服务器运行利用它来启动项目是个不错的选择。
我们以本章 Scrapy 入门项目为例来说明 Scrapyrt 的使用方法项目源代码地址为https://github.com/Python3WebSpider/ScrapyTutorial。
请确保 Scrapyrt 已经正确安装并正常运行具体安装可以参考第 1 章的说明。
首先将项目下载下来在项目目录下运行 Scrapyrt假设当前服务运行在 9080 端口上。下面将简单介绍 Scrapyrt 的使用方法。
9.1 GET 请求
目前GET 请求方式支持如下的参数。
spider_nameSpider 名称字符串类型必传参数如果传递的 Spider 名称不存在则会返回 404 错误。url爬取链接字符串类型如果起始链接没有定义的话就必须要传递如果传递了该参数Scrapy 会直接用该 URL 生成 Request而直接忽略 start_requests() 方法和 start_urls 属性的定义。callback回调函数名称字符串类型可选参数如果传递了就会使用此回调函数处理否则会默认使用 Spider 内定义的回调函数。max_requests最大请求数量数值类型可选参数它定义了 Scrapy 执行请求的 Request 的最大限制如定义为 5则最多只执行 5 次 Request 请求其余的则会被忽略。start_requests是否要执行 start_request() 函数布尔类型可选参数在 Scrapy 项目中如果定义了 start_requests() 方法那么在项目启动时会默认调用该方法但是在 Scrapyrt 就不一样了它默认不执行 start_requests() 方法如果要执行需要将它设置为 true。
例如我们执行如下命令
curl http://localhost:9080/crawl.json?spider_namequotesurlhttp://quotes.toscrape.com/得到类似如下结果如图所示 返回的是一个 JSON 格式的字符串我们解析它的结构如下所示
{status: ok,items: [{text: “The world as we have created it is a process of o...,author: Albert Einstein,tags: [change,deep-thoughts,thinking,world]},...{text: “... a mind needs books as a sword needs a whetsto...,author: George R.R. Martin,tags: [books,mind]}],items_dropped: [],stats: {downloader/request_bytes: 2892,downloader/request_count: 11,downloader/request_method_count/GET: 11,downloader/response_bytes: 24812,downloader/response_count: 11,downloader/response_status_count/200: 10,downloader/response_status_count/404: 1,dupefilter/filtered: 1,finish_reason: finished,finish_time: 2017-07-12 15:09:02,item_scraped_count: 100,log_count/DEBUG: 112,log_count/INFO: 8,memusage/max: 52510720,memusage/startup: 52510720,request_depth_max: 10,response_received_count: 11,scheduler/dequeued: 10,scheduler/dequeued/memory: 10,scheduler/enqueued: 10,scheduler/enqueued/memory: 10,start_time: 2017-07-12 15:08:56},spider_name: quotes
}这里省略了 items 绝大部分。status 显示了爬取的状态items 部分是 Scrapy 项目的爬取结果items_dropped 是被忽略的 Item 列表stats 是爬取结果的统计情况。此结果和直接运行 Scrapy 项目得到的统计是相同的。
这样一来我们就通过 HTTP 接口调度 Scrapy 项目并获取爬取结果如果 Scrapy 项目部署在服务器上我们可以通过开启一个 Scrapyrt 服务实现任务的调度并直接取到爬取结果这很方便。
9.2 POST 请求
除了 GET 请求我们还可以通过 POST 请求来请求 Scrapyrt。但是此处 Request Body 必须是一个合法的 JSON 配置在 JSON 里面可以配置相应的参数支持的配置参数更多。
目前JSON 配置支持如下参数。 spider_nameSpider 名称字符串类型必传参数。如果传递的 Spider 名称不存在则返回 404 错误。 max_requests最大请求数量数值类型可选参数。它定义了 Scrapy 执行请求的 Request 的最大限制如定义为 5则表示最多只执行 5 次 Request 请求其余的则会被忽略。 requestRequest 配置JSON 对象必传参数。通过该参数可以定义 Request 的各个参数必须指定 url 字段来指定爬取链接其他字段可选。
我们看一个 JSON 配置实例如下所示
{request: {url: http://quotes.toscrape.com/,callback: parse,dont_filter: True,cookies: {foo: bar}},max_requests: 2,spider_name: quotes
}我们执行如下命令传递该 Json 配置并发起 POST 请求
curl http://localhost:9080/crawl.json -d {request: {url: http://quotes.toscrape.com/, dont_filter: True, callback: parse, cookies: {foo: bar}}, max_requests: 2, spider_name: quotes}运行结果和上文类似同样是输出了爬取状态、结果、统计信息等内容。
以上内容便是 Scrapyrt 的相关用法介绍。通过它我们方便地调度 Scrapy 项目的运行并获取爬取结果。更多的使用方法可以参考官方文档http://scrapyrt.readthedocs.io。
十一、Scrapy 对接 Docker
环境配置问题可能一直是我们头疼的我们可能遇到过如下的情况
我们在本地写好了一个 Scrapy 爬虫项目想要把它放到服务器上运行但是服务器上没有安装 Python 环境。别人给了我们一个 Scrapy 爬虫项目项目中使用包的版本和我们本地环境版本不一致无法直接运行。我们需要同时管理不同版本的 Scrapy 项目如早期的项目依赖于 Scrapy 0.25现在的项目依赖于 Scrapy 1.4.0。
在这些情况下我们需要解决的就是环境的安装配置、环境的版本冲突解决等问题。
对于 Python 来说VirtualEnv 的确可以解决版本冲突的问题。但是VirtualEnv 不太方便做项目部署我们还是需要安装 Python 环境
如何解决上述问题呢答案是用 Docker。Docker 可以提供操作系统级别的虚拟环境一个 Docker 镜像一般都包含一个完整的操作系统而这些系统内也有已经配置好的开发环境如 Python 3.6 环境等。
我们可以直接使用此 Docker 的 Python 3 镜像运行一个容器将项目直接放到容器里运行就不用再额外配置 Python 3 环境。这样就解决了环境配置的问题。
我们也可以进一步将 Scrapy 项目制作成一个新的 Docker 镜像镜像里只包含适用于本项目的 Python 环境。如果要部署到其他平台只需要下载该镜像并运行就好了因为 Docker 运行时采用虚拟环境和宿主机是完全隔离的所以也不需要担心环境冲突问题。
如果我们能够把 Scrapy 项目制作成一个 Docker 镜像只要其他主机安装了 Docker那么只要将镜像下载并运行即可而不必再担心环境配置问题或版本冲突问题。
接下来我们尝试把一个 Scrapy 项目制作成一个 Docker 镜像。
我们要实现把前文 Scrapy 的入门项目打包成一个 Docker 镜像的过程。项目爬取的网址为http://quotes.toscrape.com/本章 Scrapy 入门一节已经实现了 Scrapy 对此站点的爬取过程项目代码为https://github.com/Python3WebSpider/ScrapyTutorial如果本地不存在的话可以 Clone 下来。
准备工作
请确保已经安装好 Docker 和 MongoDB 并可以正常运行如果没有安装可以参考第 1 章的安装说明。
创建 Dockerfile
首先在项目的根目录下新建一个 requirements.txt 文件将整个项目依赖的 Python 环境包都列出来如下所示
scrapy
pymongo如果库需要特定的版本我们还可以指定版本号如下所示
scrapy1.4.0
pymongo3.4.0在项目根目录下新建一个 Dockerfile 文件文件不加任何后缀名修改内容如下所示
FROM python:3.6
ENV PATH /usr/local/bin:$PATH
ADD . /code
WORKDIR /code
RUN pip3 install -r requirements.txt
CMD scrapy crawl quotes第一行的 FROM 代表使用的 Docker 基础镜像在这里我们直接使用 python:3.6 的镜像在此基础上运行 Scrapy 项目。
第二行 ENV 是环境变量设置将 /usr/local/bin:$PATH 赋值给 PATH即增加 /usr/local/bin 这个环境变量路径。
第三行 ADD 是将本地的代码放置到虚拟容器中。它有两个参数第一个参数是.代表本地当前路径第二个参数是 /code代表虚拟容器中的路径也就是将本地项目所有内容放置到虚拟容器的 /code 目录下以便于在虚拟容器中运行代码。
第四行 WORKDIR 是指定工作目录这里将刚才添加的代码路径设成工作路径。这个路径下的目录结构和当前本地目录结构是相同的所以我们可以直接执行库安装命令、爬虫运行命令等。
第五行 RUN 是执行某些命令来做一些环境准备工作。由于 Docker 虚拟容器内只有 Python 3 环境而没有所需要的 Python 库所以我们运行此命令来在虚拟容器中安装相应的 Python 库如 Scrapy这样就可以在虚拟容器中执行 Scrapy 命令了。
第六行 CMD 是容器启动命令。在容器运行时此命令会被执行。在这里我们直接用 scrapy crawl quotes 来启动爬虫。
修改 MongoDB 连接
接下来我们需要修改 MongoDB 的连接信息。如果我们继续用 localhost 是无法找到 MongoDB 的因为在 Docker 虚拟容器里 localhost 实际指向容器本身的运行 IP而容器内部并没有安装 MongoDB所以爬虫无法连接 MongoDB。
这里的 MongoDB 地址可以有如下两种选择。
如果只想在本机测试我们可以将地址修改为宿主机的 IP也就是容器外部的本机 IP一般是一个局域网 IP使用 ifconfig 命令即可查看。如果要部署到远程主机运行一般 MongoDB 都是可公网访问的地址修改为此地址即可。
在本节中我们的目标是将项目打包成一个镜像让其他远程主机也可运行这个项目。所以我们直接将此处 MongoDB 地址修改为某个公网可访问的远程数据库地址修改 MONGO_URI 如下所示
MONGO_URI mongodb://admin:admin123120.27.34.25:27017此处地址可以修改为自己的远程 MongoDB 数据库地址。
这样项目的配置就完成了。
构建镜像
接下来我们便可以构建镜像了执行如下命令
docker build -t quotes:latest .这样的输出就说明镜像构建成功。这时我们查看一下构建的镜像如下所示
Sending build context to Docker daemon 191.5 kB
Step 1/6 : FROM python:3.6--- 968120d8cbe8
Step 2/6 : ENV PATH /usr/local/bin:$PATH--- Using cache--- 387abbba1189
Step 3/6 : ADD . /code--- a844ee0db9c6
Removing intermediate container 4dc41779c573
Step 4/6 : WORKDIR /code--- 619b2c064ae9
Removing intermediate container bcd7cd7f7337
Step 5/6 : RUN pip3 install -r requirements.txt--- Running in 9452c83a12c5
...
Removing intermediate container 9452c83a12c5
Step 6/6 : CMD scrapy crawl quotes--- Running in c092b5557ab8--- c8101aca6e2a
Removing intermediate container c092b5557ab8
Successfully built c8101aca6e2a出现类似输出就证明镜像构建成功了这时执行如我们查看一下构建的镜像
docker images返回结果中其中有一行就是
quotes latest 41c8499ce210 2 minutes ago 769 MB这就是我们新构建的镜像。
运行
我们可以先在本地测试运行执行如下命令
docker run quotes这样我们就利用此镜像新建并运行了一个 Docker 容器运行效果完全一致如图所示。 如果出现类似行结果这就证明构建的镜像没有问题。
推送至 Docker Hub
构建完成之后我们可以将镜像 Push 到 Docker 镜像托管平台如 Docker Hub 或者私有的 Docker Registry 等这样我们就可以从远程服务器下拉镜像并运行了。
以 Docker Hub 为例如果项目包含一些私有的连接信息如数据库我们最好将 Repository 设为私有或者直接放到私有的 Docker Registry。
首先在 https://hub.docker.com 注册一个账号新建一个 Repository名为 quotes。比如我的用户名为 germey新建的 Repository 名为 quotes那么此 Repository 的地址就可以用 germey/quotes 来表示。
为新建的镜像打一个标签命令如下所示
docker tag quotes:latest germey/quotes:latest推送镜像到 Docker Hub 即可命令如下所示
docker push germey/quotesDocker Hub 便会出现新推送的 Docker 镜像了如图所示。 如果我们想在其他的主机上运行这个镜像主机上装好 Docker 后可以直接执行如下命令
docker run germey/quotes这样就会自动下载镜像然后启动容器运行不需要配置 Python 环境不需要关心版本冲突问题。
运行效果如图所示 整个项目爬取完成后数据就可以存储到指定的数据库中。
我们讲解了将 Scrapy 项目制作成 Docker 镜像并部署到远程服务器运行的过程。使用此种方式我们在本节开头所列出的问题都迎刃而解。
十二、Scrapy 爬取新浪微博
前面讲解了 Scrapy 中各个模块基本使用方法以及代理池、Cookies 池。接下来我们以一个反爬比较强的网站新浪微博为例来实现一下 Scrapy 的大规模爬取。
本次爬取的目标是新浪微博用户的公开基本信息如用户昵称、头像、用户的关注、粉丝列表以及发布的微博等这些信息抓取之后保存至 MongoDB。
请确保前文所讲的代理池、Cookies 池已经实现并可以正常运行安装 Scrapy、PyMongo 库如没有安装可以参考前文内容。
首先我们要实现用户的大规模爬取。这里采用的爬取方式是以微博的几个大 V 为起始点爬取他们各自的粉丝和关注列表然后获取粉丝和关注列表的粉丝和关注列表以此类推这样下去就可以实现递归爬取。如果一个用户与其他用户有社交网络上的关联那他们的信息就会被爬虫抓取到这样我们就可以做到对所有用户的爬取。通过这种方式我们可以得到用户的唯一 ID再根据 ID 获取每个用户发布的微博即可。
爬取分析
这里我们选取的爬取站点是https://m.weibo.cn此站点是微博移动端的站点。打开该站点会跳转到登录页面这是因为主页做了登录限制。不过我们可以直接打开某个用户详情页面如图所示。 我们在页面最上方可以看到她的关注和粉丝数量。我们点击关注进入到她的关注列表如图所示。 我们打开开发者工具切换到 XHR 过滤器一直下拉关注列表即可看到下方会出现很多 Ajax 请求这些请求就是获取关注列表的 Ajax 请求。
我们打开第一个 Ajax 请求看一下发现它的链接为https://m.weibo.cn/api/container/getIndex?containerid231051_-followers-_1916655407luicode10000011lfid1005051916655407featurecode20000320typeuidvalue1916655407page2详情如图所示。 请求类型是 GET 类型返回结果是 JSON 格式我们将其展开之后即可看到其关注的用户的基本信息。接下来我们只需要构造这个请求的参数。此链接一共有 7 个参数如图 13-37 所示。 其中最主要的参数就是 containerid 和 page。有了这两个参数我们同样可以获取请求结果。我们可以将接口精简为https://m.weibo.cn/api/container/getIndex?containerid231051_-followers-_1916655407page2这里的 containerid 的前半部分是固定的后半部分是用户的 id。所以这里参数就可以构造出来了只需要修改 containerid 最后的 id 和 page 参数即可获取分页形式的关注列表信息。
利用同样的方法我们也可以分析用户详情的 Ajax 链接、用户微博列表的 Ajax 链接如下所示
# 用户详情 API
user_url https://m.weibo.cn/api/container/getIndex?uid{uid}typeuidvalue{uid}containerid100505{uid}
# 关注列表 API
follow_url https://m.weibo.cn/api/container/getIndex?containerid231051_-_followers_-_{uid}page{page}
# 粉丝列表 API
fan_url https://m.weibo.cn/api/container/getIndex?containerid231051_-_fans_-_{uid}page{page}
# 微博列表 API
weibo_url https://m.weibo.cn/api/container/getIndex?uid{uid}typeuidpage{page}containerid107603{uid}此处的 uid 和 page 分别代表用户 ID 和分页页码。
注意这个 API 可能随着时间的变化或者微博的改版而变化以实测为准。
我们从几个大 V 开始抓取抓取他们的粉丝、关注列表、微博信息然后递归抓取他们的粉丝和关注列表的粉丝、关注列表、微博信息递归抓取最后保存微博用户的基本信息、关注和粉丝列表、发布的微博。
我们选择 MongoDB 作为存储的数据库可以更方便地存储用户的粉丝和关注列表。
新建项目
接下来我们用 Scrapy 来实现这个抓取过程。首先创建一个项目命令如下所示
scrapy startproject weibo进入项目中新建一个 Spider名为 weibocn命令如下所示
scrapy genspider weibocn m.weibo.cn我们首先修改 Spider配置各个 Ajax 的 URL选取几个大 V将他们的 ID 赋值成一个列表实现 start_requests() 方法也就是依次抓取各个大 V 的个人详情然后用 parse_user() 进行解析如下所示
from scrapy import Request, Spiderclass WeiboSpider(Spider):name weibocnallowed_domains [m.weibo.cn]user_url https://m.weibo.cn/api/container/getIndex?uid{uid}typeuidvalue{uid}containerid100505{uid}follow_url https://m.weibo.cn/api/container/getIndex?containerid231051_-_followers_-_{uid}page{page}fan_url https://m.weibo.cn/api/container/getIndex?containerid231051_-_fans_-_{uid}page{page}weibo_url https://m.weibo.cn/api/container/getIndex?uid{uid}typeuidpage{page}containerid107603{uid}start_users [3217179555, 1742566624, 2282991915, 1288739185, 3952070245, 5878659096]def start_requests(self):for uid in self.start_users:yield Request(self.user_url.format(uiduid), callbackself.parse_user)def parse_user(self, response):self.logger.debug(response)创建 Item
接下来我们解析用户的基本信息并生成 Item。这里我们先定义几个 Item如用户、用户关系、微博的 Item如下所示
from scrapy import Item, Fieldclass UserItem(Item):collection usersid Field()name Field()avatar Field()cover Field()gender Field()description Field()fans_count Field()follows_count Field()weibos_count Field()verified Field()verified_reason Field()verified_type Field()follows Field()fans Field()crawled_at Field()class UserRelationItem(Item):collection usersid Field()follows Field()fans Field()class WeiboItem(Item):collection weibosid Field()attitudes_count Field()comments_count Field()reposts_count Field()picture Field()pictures Field()source Field()text Field()raw_text Field()thumbnail Field()user Field()created_at Field()crawled_at Field()这里定义了 collection 字段指明保存的 Collection 的名称。用户的关注和粉丝列表直接定义为一个单独的 UserRelationItem其中 id 就是用户的 IDfollows 就是用户关注列表fans 是粉丝列表但这并不意味着我们会将关注和粉丝列表存到一个单独的 Collection 里。后面我们会用 Pipeline 对各个 Item 进行处理、合并存储到用户的 Collection 里因此 Item 和 Collection 并不一定是完全对应的。
提取数据
我们开始解析用户的基本信息实现 parse_user() 方法如下所示
def parse_user(self, response):解析用户信息:param response: Response 对象result json.loads(response.text)if result.get(userInfo):user_info result.get(userInfo)user_item UserItem()field_map {id: id, name: screen_name, avatar: profile_image_url, cover: cover_image_phone,gender: gender, description: description, fans_count: followers_count,follows_count: follow_count, weibos_count: statuses_count, verified: verified,verified_reason: verified_reason, verified_type: verified_type}for field, attr in field_map.items():user_item[field] user_info.get(attr)yield user_item# 关注uid user_info.get(id)yield Request(self.follow_url.format(uiduid, page1), callbackself.parse_follows,meta{page: 1, uid: uid})# 粉丝yield Request(self.fan_url.format(uiduid, page1), callbackself.parse_fans,meta{page: 1, uid: uid})# 微博yield Request(self.weibo_url.format(uiduid, page1), callbackself.parse_weibos,meta{page: 1, uid: uid})在这里我们一共完成了两个操作。
解析 JSON 提取用户信息并生成 UserItem 返回。我们并没有采用常规的逐个赋值的方法而是定义了一个字段映射关系。我们定义的字段名称可能和 JSON 中用户的字段名称不同所以在这里定义成一个字典然后遍历字典的每个字段实现逐个字段的赋值。构造用户的关注、粉丝、微博的第一页的链接并生成 Request这里需要的参数只有用户的 ID。另外初始分页页码直接设置为 1 即可。
接下来我们还需要保存用户的关注和粉丝列表。以关注列表为例其解析方法为 parse_follows()实现如下所示
def parse_follows(self, response):解析用户关注:param response: Response 对象result json.loads(response.text)if result.get(ok) and result.get(cards) and len(result.get(cards)) and result.get(cards)[-1].get(card_group):# 解析用户follows result.get(cards)[-1].get(card_group)for follow in follows:if follow.get(user):uid follow.get(user).get(id)yield Request(self.user_url.format(uiduid), callbackself.parse_user)# 关注列表uid response.meta.get(uid)user_relation_item UserRelationItem()follows [{id: follow.get(user).get(id), name: follow.get(user).get(screen_name)} for follow infollows]user_relation_item[id] uiduser_relation_item[follows] followsuser_relation_item[fans] []yield user_relation_item# 下一页关注page response.meta.get(page) 1yield Request(self.follow_url.format(uiduid, pagepage),callbackself.parse_follows, meta{page: page, uid: uid})那么在这个方法里面我们做了如下三件事。
解析关注列表中的每个用户信息并发起新的解析请求。我们首先解析关注列表的信息得到用户的 ID然后再利用 user_url 构造访问用户详情的 Request回调就是刚才所定义的 parse_user() 方法。提取用户关注列表内的关键信息并生成 UserRelationItem。id 字段直接设置成用户的 IDJSON 返回数据中的用户信息有很多冗余字段。在这里我们只提取了关注用户的 ID 和用户名然后把它们赋值给 follows 字段fans 字段设置成空列表。这样我们就建立了一个存有用户 ID 和用户部分关注列表的 UserRelationItem之后合并且保存具有同一个 ID 的 UserRelationItem 的关注和粉丝列表。提取下一页关注。只需要将此请求的分页页码加 1 即可。分页页码通过 Request 的 meta 属性进行传递Response 的 meta 来接收。这样我们构造并返回下一页的关注列表的 Request。
抓取粉丝列表的原理和抓取关注列表原理相同在此不再赘述。
接下来我们还差一个方法的实现即 parse_weibos()它用来抓取用户的微博信息实现如下所示
def parse_weibos(self, response):解析微博列表:param response: Response 对象result json.loads(response.text)if result.get(ok) and result.get(cards):weibos result.get(cards)for weibo in weibos:mblog weibo.get(mblog)if mblog:weibo_item WeiboItem()field_map {id: id, attitudes_count: attitudes_count, comments_count: comments_count, created_at: created_at,reposts_count: reposts_count, picture: original_pic, pictures: pics,source: source, text: text, raw_text: raw_text, thumbnail: thumbnail_pic}for field, attr in field_map.items():weibo_item[field] mblog.get(attr)weibo_item[user] response.meta.get(uid)yield weibo_item# 下一页微博uid response.meta.get(uid)page response.meta.get(page) 1yield Request(self.weibo_url.format(uiduid, pagepage), callbackself.parse_weibos,meta{uid: uid, page: page})这里 parse_weibos() 方法完成了两件事。
提取用户的微博信息并生成 WeiboItem。这里同样建立了一个字段映射表实现批量字段赋值。提取下一页的微博列表。这里同样需要传入用户 ID 和分页页码。
到目前为止微博的 Spider 已经完成。后面还需要对数据进行数据清洗存储以及对接代理池、Cookies 池来防止反爬虫。
数据清洗
有些微博的时间可能不是标准的时间比如它可能显示为刚刚、几分钟前、几小时前、昨天等。这里我们需要统一转化这些时间实现一个 parse_time() 方法如下所示
def parse_time(self, date):if re.match( 刚刚 , date):date time.strftime(% Y-% m-% d % H:% M, time.localtime(time.time()))if re.match(\d 分钟前 , date):minute re.match((\d), date).group(1)date time.strftime(% Y-% m-% d % H:% M, time.localtime(time.time() - float(minute) * 60))if re.match(\d 小时前 , date):hour re.match((\d), date).group(1)date time.strftime(% Y-% m-% d % H:% M, time.localtime(time.time() - float(hour) * 60 * 60))if re.match( 昨天.*, date):date re.match( 昨天 (.*), date).group(1).strip()date time.strftime(% Y-% m-% d, time.localtime() - 24 * 60 * 60) dateif re.match(\d{2}-\d{2}, date):date time.strftime(% Y-, time.localtime()) date 00:00return date我们用正则来提取一些关键数字用 time 库来实现标准时间的转换。
以 X 分钟前的处理为例爬取的时间会赋值为 created_at 字段。我们首先用正则匹配这个时间表达式写作 \d 分钟前如果提取到的时间符合这个表达式那么就提取出其中的数字这样就可以获取分钟数了。接下来使用 time 模块的 strftime() 方法第一个参数传入要转换的时间格式第二个参数就是时间戳。这里我们用当前的时间戳减去此分钟数乘以 60 就是当时的时间戳这样我们就可以得到格式化后的正确时间了。
然后 Pipeline 可以实现如下处理
class WeiboPipeline():def process_item(self, item, spider):if isinstance(item, WeiboItem):if item.get(created_at):item[created_at] item[created_at].strip()item[created_at] self.parse_time(item.get(created_at))我们在 Spider 里没有对 crawled_at 字段赋值它代表爬取时间我们可以统一将其赋值为当前时间实现如下所示
class TimePipeline():def process_item(self, item, spider):if isinstance(item, UserItem) or isinstance(item, WeiboItem):now time.strftime(% Y-% m-% d % H:% M, time.localtime())item[crawled_at] nowreturn item这里我们判断了 item 如果是 UserItem 或 WeiboItem 类型那么就给它的 crawled_at 字段赋值为当前时间。
通过上面的两个 Pipeline我们便完成了数据清洗工作这里主要是时间的转换。
数据存储
数据清洗完毕之后我们就要将数据保存到 MongoDB 数据库。我们在这里实现 MongoPipeline 类如下所示
import pymongoclass MongoPipeline(object):def __init__(self, mongo_uri, mongo_db):self.mongo_uri mongo_uriself.mongo_db mongo_dbclassmethoddef from_crawler(cls, crawler):return cls(mongo_uricrawler.settings.get(MONGO_URI), mongo_dbcrawler.settings.get(MONGO_DATABASE))def open_spider(self, spider):self.client pymongo.MongoClient(self.mongo_uri)self.db self.client[self.mongo_db]self.db[UserItem.collection].create_index([(id, pymongo.ASCENDING)])self.db[WeiboItem.collection].create_index([(id, pymongo.ASCENDING)])def close_spider(self, spider):self.client.close()def process_item(self, item, spider):if isinstance(item, UserItem) or isinstance(item, WeiboItem):self.db[item.collection].update({id: item.get(id)}, {$set: item}, True)if isinstance(item, UserRelationItem):self.db[item.collection].update({id: item.get(id)},{$addToSet:{follows: {$each: item[follows]},fans: {$each: item[fans]}}}, True)return item当前的 MongoPipeline 和前面我们所写的有所不同主要有以下几点。
在 open_spider() 方法里面添加了 Collection 的索引在这里为两个 Item 都做了索引索引的字段是 id由于我们这次是大规模爬取同时在爬取过程中涉及到数据的更新问题所以我们为每个 Collection 建立了索引建立了索引之后可以大大提高检索效率。在 process_item() 方法里存储使用的是 update() 方法第一个参数是查询条件第二个参数是爬取的 Item这里我们使用了 $set 操作符这样我们如果爬取到了重复的数据即可对数据进行更新同时不会删除已存在的字段如果这里不加 $set 操作符那么会直接进行 item 替换这样可能会导致已存在的字段如关注和粉丝列表清空所以这里必须要加上 $set 操作符。第三个参数我们设置为了 True这个参数起到的作用是如果数据不存在则插入数据。这样我们就可以做到数据存在即更新、数据不存在即插入这样就达到了去重的效果。对于用户的关注和粉丝列表我们在这里使用了一个新的操作符叫做 $addToSet这个操作符可以向列表类型的字段插入数据同时去重接下来它的值就是需要操作的字段名称我们在这里又利用了 $each 操作符对需要插入的列表数据进行了遍历这样就可以逐条插入用户的关注或粉丝数据到指定的字段了关于该操作更多的解释可以参考 MongoDB 的官方文档链接为https://docs.mongodb.com/manual/reference/operator/update/addToSet/。
Cookies 池对接
新浪微博的反爬能力非常强我们需要做一些防范反爬虫的措施才可以顺利完成数据爬取。
如果没有登录而直接请求微博的 API 接口这非常容易导致 403 状态码。这个情况我们在 10.2 节也提过。所以在这里我们实现一个 Middleware为每个 Request 添加随机的 Cookies。
我们先开启 Cookies 池使 API 模块正常运行。例如在本地运行 5000 端口访问http://localhost:5000/weibo/random 即可获取随机的 Cookies当然也可以将 Cookies 池部署到远程的服务器这样只需要更改一下访问的链接就好了。
那么在这里我们将 Cookies 池在本地启动起来再实现一个 Middleware 如下 class CookiesMiddleware():def __init__(self, cookies_url):self.logger logging.getLogger(__name__)self.cookies_url cookies_urldef get_random_cookies(self):try:response requests.get(self.cookies_url)if response.status_code 200:cookies json.loads(response.text)return cookiesexcept requests.ConnectionError:return Falsedef process_request(self, request, spider):self.logger.debug( 正在获取 Cookies)cookies self.get_random_cookies()if cookies:request.cookies cookiesself.logger.debug( 使用 Cookies json.dumps(cookies))classmethoddef from_crawler(cls, crawler):settings crawler.settingsreturn cls(cookies_urlsettings.get(COOKIES_URL))我们首先利用 from_crawler() 方法获取了 COOKIES_URL 变量它定义在 settings.py 里这就是刚才我们所说的接口。接下来实现 get_random_cookies() 方法这个方法主要就是请求此 Cookies 池接口并获取接口返回的随机 Cookies。如果成功获取则返回 Cookies否则返回 False。
接下来在 process_request() 方法里我们给 request 对象的 cookies 属性赋值其值就是获取的随机 Cookies这样我们就成功地为每一次请求赋值 Cookies 了。
如果启用了该 Middleware每个请求都会被赋值随机的 Cookies。这样我们就可以模拟登录之后的请求403 状态码基本就不会出现。
代理池对接
微博还有一个反爬措施就是检测到同一 IP 请求量过大时就会出现 414 状态码。如果遇到这样的情况可以切换代理。例如在本地 5555 端口运行获取随机可用代理的地址为http://localhost:5555/random访问这个接口即可获取一个随机可用代理。接下来我们再实现一个 Middleware代码如下所示
class ProxyMiddleware():def __init__(self, proxy_url):self.logger logging.getLogger(__name__)self.proxy_url proxy_urldef get_random_proxy(self):try:response requests.get(self.proxy_url)if response.status_code 200:proxy response.textreturn proxyexcept requests.ConnectionError:return Falsedef process_request(self, request, spider):if request.meta.get(retry_times):proxy self.get_random_proxy()if proxy:uri https://{proxy}.format(proxyproxy)self.logger.debug( 使用代理 proxy)request.meta[proxy] uriclassmethoddef from_crawler(cls, crawler):settings crawler.settingsreturn cls(proxy_urlsettings.get(PROXY_URL))同样的原理我们实现了一个 get_random_proxy() 方法用于请求代理池的接口获取随机代理。如果获取成功则返回改代理否则返回 False。在 process_request() 方法中我们给 request 对象的 meta 属性赋值一个 proxy 字段该字段的值就是代理。
另外赋值代理的判断条件是当前 retry_times 不为空也就是说第一次请求失败之后才启用代理因为使用代理后访问速度会慢一些。所以我们在这里设置了只有重试的时候才启用代理否则直接请求。这样就可以保证在没有被封禁的情况下直接爬取保证了爬取速度。
启用 Middleware
接下来我们在配置文件中启用这两个 Middleware修改 settings.py 如下所示
DOWNLOADER_MIDDLEWARES {weibo.middlewares.CookiesMiddleware: 554,weibo.middlewares.ProxyMiddleware: 555,
}注意这里的优先级设置前文提到了 Scrapy 的默认 Downloader Middleware 的设置如下
{scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware: 100,scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware: 300,scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware: 350,scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware: 400,scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: 500,scrapy.downloadermiddlewares.retry.RetryMiddleware: 550,scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware: 560,scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware: 580,scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware: 590,scrapy.downloadermiddlewares.redirect.RedirectMiddleware: 600,scrapy.downloadermiddlewares.cookies.CookiesMiddleware: 700,scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware: 750,scrapy.downloadermiddlewares.stats.DownloaderStats: 850,scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware: 900,
}要使得我们自定义的 CookiesMiddleware 生效它在内置的 CookiesMiddleware 之前调用。内置的 CookiesMiddleware 的优先级为 700所以这里我们设置一个比 700 小的数字即可。
要使得我们自定义的 ProxyMiddleware 生效它在内置的 HttpProxyMiddleware 之前调用。内置的 HttpProxyMiddleware 的优先级为 750所以这里我们设置一个比 750 小的数字即可。
运行
到此为止整个微博爬虫就实现完毕了我们运行如下命令启动一下爬虫
scrapy crawl weibocn类似的输出结果如下
2017-07-11 17:27:34 [urllib3.connectionpool] DEBUG: http://localhost:5000 GET /weibo/random HTTP/1.1 200 339
2017-07-11 17:27:34 [weibo.middlewares] DEBUG: 使用 Cookies {SCF: AhzwTr_DxIGjgri_dt46_DoPzUqq-PSupu545JdozdHYJ7HyEb4pD3pe05VpbIpVyY1ciKRRWwUgojiO3jYwlBE., _T_WM: 8fe0bc1dad068d09b888d8177f1c1218, SSOLoginState: 1501496388, M_WEIBOCN_PARAMS: uicode%3D20000174, SUHB: 0tKqV4asxqYl4J, SUB: _2A250e3QUDeRhGeBM6VYX8y7NwjiIHXVXhBxcrDV6PUJbkdBeLXjckW2fUT8MWloekO4FCWVlIYJGJdGLnA..}
2017-07-11 17:27:34 [weibocn] DEBUG: 200 https://m.weibo.cn/api/container/getIndex?uid1742566624typeuidvalue1742566624containerid1005051742566624
2017-07-11 17:27:34 [scrapy.core.scraper] DEBUG: Scraped from 200 https://m.weibo.cn/api/container/getIndex?uid1742566624typeuidvalue1742566624containerid1005051742566624
{avatar: https://tva4.sinaimg.cn/crop.0.0.180.180.180/67dd74e0jw1e8qgp5bmzyj2050050aa8.jpg,cover: https://tva3.sinaimg.cn/crop.0.0.640.640.640/6ce2240djw1e9oaqhwllzj20hs0hsdir.jpg,crawled_at: 2017-07-11 17:27,description: 成长就是一个不断觉得以前的自己是个傻逼的过程 ,fans_count: 19202906,follows_count: 1599,gender: m,id: 1742566624,name: 思想聚焦 ,verified: True,verified_reason: 微博知名博主校导网编辑 ,verified_type: 0,weibos_count: 58393}运行一段时间后我们便可以到 MongoDB 数据库查看数据爬取下来的数据如图所示。 针对用户信息我们不仅爬取了其基本信息还把关注和粉丝列表加到了 follows 和 fans 字段并做了去重操作。针对微博信息我们成功进行了时间转换处理同时还保存了微博的图片列表信息。
本节代码地址https://github.com/Python3WebSpider/Weibo。
本节实现了新浪微博的用户及其粉丝关注列表和微博信息的爬取还对接了 Cookies 池和代理池来处理反爬虫。不过现在是针对单机的爬取后面我们会将此项目修改为分布式爬虫以进一步提高抓取效率。