第六章 Scrapy框架
回顾一下写一个爬虫需要做的一些步骤,使用requests库发送网络请求、使用lxml等解析技术对数据进行解析、使用数据库等方法进行存储数据,另外还可以在请求网络的时候进行更换IP、设置请求头等。
每次爬虫都要干这么多活,如果每次都从零开始写则比较浪费时间,所以我们需要一个框架,这个框架帮我们把一些基本的爬虫前奏都准备好了,我们只需要“站在巨人的肩膀上”即可。而Scrapy
框架就是这个“巨人的肩膀”。
它的工作原理如下:
各模块功能如下:
Engine(引擎)
: scrap框架的核心部分。负责每个模块之间的通信、传递数据等。spiders(爬虫)
:将需要爬取的链接发送引擎,最后引擎把其他模块请求回来的数据再发送回爬虫,然后爬虫就可以对数据进行解析。(这部分是开发者自己写的,因为要爬取哪些连接,解析哪些数据是我们自己决定的)Scheduler(调度器)
:负责接收引擎发送过来的请求,并按照一定的方式进行排列和整理,决定了链接爬取的顺序。Downloader(下载器)
:负责接收引擎传过来的下载请求,然后去网络上下载对应的数据再交还给引擎。Item Pipelines(管道)
:负责将spider(爬虫)
传递过来的数据进行保存。具体保存的方式和位置,也是由开发者自行决定。Downloader Middlewares(下载中间件)
:处于引擎跟下载器中间,处理下载请求部分。如在此可以设置请求头、IP地址等。Spider Middlewares(爬虫中间件)
:处于爬虫跟引擎中间,处理解析部分。
各模块执行过程如下:
- 引擎打开一个网站,找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。
- 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。
- 引擎向调度器请求下一个要爬取的URL。
- 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。
- 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。
- 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。
- Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。
- 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。
- (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。
原理图及执行过程的描述参考于这篇文章:Scrapy架构图(工作原理)
如果觉得很不理解没关系,可以先往下学,之后再回过头来看这张图就会豁然开朗了。
6.1 快速入门
使用Scrapy之前需要先安装:pip install scrapy
。
如果在window
系统下,还需安装一个东西:pip install pypiwin32
创建项目:
进入你想把此项目存放的目录,使用命令 scrapy startproject 项目名称
来创建项目。如 scrapy startproject scrapy_demo
用pycharm或其他编辑器打开该项目,项目结构如下:
主要文件的作用:
items.py
:用来存放爬虫爬取下来数据的模型,即要存储的各字段。middlewares.py
:用来存放下载/爬虫中间件的文件。pipelines.py
:用来将items.py
中的模型进行持久化存储。settings.py
:爬虫的一些配置信息(比如请求头、多久发送一次请求、IP代理池等许多设置)。scrap.cfg
:项目的配置文件。spiders包
:存放所有的爬虫文件。
以上步骤只是创建了项目,下一步还需要创建爬虫(我们在爬虫文件夹下进行写代码)
创建爬虫前需要先进入到刚才创建的项目中,然后通过命令 scrapy genspider 爬虫名字 要爬取网站的域名
。如scrapy genspider demo1 baidu.com
注意:爬虫名字不能跟项目名字重复。
此时,你会发现spiders文件夹下多了个demo1
文件,内容如下:
allowed_domains
是指以后所有的爬虫的链接都是在该域名下的,并不会说你要爬取百度的网址,却给你爬了个谷歌的网址。
stats_urls
是指爬虫刚启动时是向该链接发送网络请求。
这些都创建好之后,就可以运行项目了,需要注意的是,在编辑器中是无法直接运行的,需要进入到爬虫文件夹下运行cmd命令 scrapy crwal 爬虫名字
运行项目。放心,以后在编辑器中有便捷方式来运行,现在先不介绍。
至此,一个scrapy就成功创建并运行起来了。来回顾一下刚才的操作:
- 创建项目:
scrapy startproject 项目名字
- 创建爬虫:
scrapy genspider 爬虫名字 爬取的域名
,注意爬虫的名字不能与项目名字相同。 - 运行爬虫:
scrapy crwal 爬虫名字
6.2 渐渐深入
pipelines.py文件函数:
__init__(self)
构造函数,创建pipelines时执行open_spider(self,spider)
spider(爬虫) 被打开时自动执行process_item(self, item, spider)
当爬虫有item传入时被调用close_spider(self,spider)
当spider(爬虫)被关闭的时候执行
tips: 一般需要存储文件时在__init__(self)
或者open_spider(self,spider)
中编写打开文件的操作或者链接数据库操作,在process_item(self, item, spider)
中编写写入数据的操作,最后在close_spider(self,spider)
中关闭文件或数据库
settings.py文件常用设置:
-
ROBOTSTXT_OBEY = True
是否遵循机器人协议,我们需要把它改成False
-
DEFAULT_REQUEST_HEADERS
设置请求头DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en', # 设置User-Agent 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362' }
-
DOWNLOADER_MIDDLEWARES
开启下载中间件,例如我这里写了三个下载中间件,后面的数字越小表示优先级越高DOWNLOADER_MIDDLEWARES = { 'scrapy_demo.middlewares.ScrapyDemoDownloaderMiddleware': 543, 'scrapy_demo.middlewares.UserAgentDownloadMiddleware': 500, 'scrapy_demo.middlewares.IPProxyDownloadMiddlleware': 400, }
-
SPIDER_MIDDLEWARES
爬虫中间件,用法同下载中间件。 -
ITEM_PIPELINES
开启下载项。
tips:以上的配置在settings文件中都是注释了的,我们再使用到相应功能的时候,要记得解除注释。
如何在编辑器中运行项目(怎么样才不用每次都去cmd那里运行项目):
-
在项目根目录中创建一个
py
文件,一般我喜欢命名为start.py
。 -
内容为:
# 导入命令行 from scrapy import cmdline # 使用命令行运行项目 cmdline.execute("scrapy crawl 项目名".split())
-
每次要运行项目时运行此文件即可。
结合案例介绍文件:
以爬取糗事百科网站为例,假如本次案例我们需要爬取到糗事百科的段子跟作者,并把它保存为json文件。
首先创建好项目、爬虫,在根目录下创建start.py
,接着在settings
文件夹下把协议变成False,设置请求头。
qsbk_spider.py 内容:
# -*- coding: utf-8 -*-
import scrapy
# 从本项目的items中导入模型
from qsbk.items import QsbkItem
class QsbkSpiderSpider(scrapy.Spider):
name = 'qsbk_spider'
allowed_domains = ['qiushibaike.com']
# 将默认的start_urls改成自己想要爬取的网址,这里选择第一页的段子
start_urls = ['https://www.qiushibaike.com/text/page/1/']
def parse(self, response):
# 在scrapy中response可以直接使用xpath、css选择器
# 返回的是SelectorList(继承自List)
divs = response.xpath("//div[@class='col1 old-style-col1']/div")
# SelectorList里面的每个元素都是Selector
for div in divs:
# 通过get 获取SelectorList的第一个Selector并将其变成字符串
author = div.xpath(".//h2/text()").get()
# text的类型为SelectorList,因为他返回很多的Selector
# 通过getall 将所有的Selector变成字符串再将这些字符串组成列表
text = div.xpath(".//div[@class='content']//span//text()").getall()
# 将列表变成字符串同时去掉头尾的空白符
text = ''.join(text).strip()
# 将参数传给item模型,然后自动把item传到pipelines
yield QsbkItem(author=author, text=text)
itmes.py 内容:
import scrapy
class QsbkItem(scrapy.Item):
# 在item模型中定义好要保存的数据
# 接收qsbk_spider传过来的参数
author = scrapy.Field()
text = scrapy.Field()
pipelines.py 内容:
import json
class QsbkPipeline(object):
def open_spider(self, spider):
self.fp = open('qsbk.json', 'a', encoding='utf-8')
def process_item(self, item, spider):
# 此时的item为QsbkItem类型,需要转成字典才可以变成json
text = json.dumps(dict(item), ensure_ascii=False)
self.fp.write(text + '\n')
return item
def close_spider(self, spider):
self.fp.close()
- 记得要在settings中打开pipelines。
小结:
- 爬虫第一步,对协议说“不”
response
对象可以执行xpath
、css
语法来提取数据。- 提取出来的数据类型可能是
Selector
或SelectorList
,如果想要获取其中的字符串或字符串列表,应该执行get
或getall
方法 - 如果要将数据传给pipelines处理,可以使用
yield
yield
与return
是有区别的,读者可自行百度查阅资料。
到了这一步,就对一个网页解析完了,现在来看下如何多其他页面发起“进攻”吧。
首先要爬取其他页面,就必须知道其他页面的url或者找到这些url的规律,分析糗事百科段子的url可以发现,
text/page/1/
page后面的数字就代表了页数,这时你可能想到用个变量来代表页数,实际操作后你会发现,这种操作并不那么理想。而实际我们一般也不会这么干,像这种网站一般都可以用域名+相对路径来访问到不同的页面,糗事百科中可以找到 下一页按钮 的 a标签中 的href
就是下一页的url
的相对路径
只需要改动qsbk_spider.py 的内容:
import scrapy
from qsbk.items import QsbkItem
class QsbkSpiderSpider(scrapy.Spider):
name = 'qsbk_spider'
allowed_domains = ['qiushibaike.com']
start_urls = ['https://www.qiushibaike.com/text/page/1/']
# 设置基本域名 方便连接成完整url
domains_url = 'https://www.qiushibaike.com'
def parse(self, response):
divs = response.xpath("//div[@class='col1 old-style-col1']/div")
for div in divs:
author = div.xpath(".//h2/text()").get()
text = div.xpath(".//div[@class='content']//span//text()").getall()
text = ''.join(text).strip()
yield QsbkItem(author=author, text=text)
# 对一个页面解析完了之后继续解析其他页面
# 最后一个li标签的href是下一页的相对路径
hrefs = response.xpath('//div[@class="col1 old-style-col1"]/ul/li[last()]/a/@href').get()
# 在最后一页的时候无此相对路径,此时结束爬虫
if not hrefs:
return
else:
# 将要爬取下一页的url传给调度器并告诉他,要用parse函数解析该url
# 注意parse不要加括号
yield scrapy.Request(self.domains_url+hrefs, callback=self.parse)
还记得工作原理图的第8步吗,引擎会根据 yield
返回的实例类型来执行不同的操作。
如果是 scrapy.Request
对象,引擎把该对象指向的链接发送给调度器并在请求完成后调用该对象的回调函数。该链接就是 self.domains_url+hrefs
回调函数就是self.parse
如果是 scrapy.Item
对象,引擎会将这个对象传递给 pipelines.py
做进一步处理
tips:在爬取整个网站的时候,由于爬取的速度很快爬取的数量较大,容易把别人服务器弄垮,所以身为良好市民的我们可以在settings中把DOWNLOAD_DELAY
(下载延迟) 打开,可以设置为1,表示下载完成后等待1秒钟再继续下载。
6.3 POST请求
前面的例子是发送get请求的,如果要发送post请求则需要使用FormRquest
,而发送post请求情况又分为:爬虫一开始就要发送post请求、爬虫过程中发送post请求。这两种使用post请求的方式有点不同:
爬虫一开始就要发送post请求
- 需要重写
start_requests
方法并且请求的url不要使用start_urls
里面的,要自己写过请求的url
。因为该方法默认使用的是用get方法来请求start_urls
中的链接。 - 在此方法中使用
FromRequest
来发送post请求。
爬虫过程中发送post请求:使用FormRequest
发送post请求
看例子:
import scrapy
class RenrenSpider(scrapy.Spider):
name = 'renren'
allowed_domains = ['renren.com']
start_urls = ['http://renren.com/']
# 重写start_requests
def start_requests(self):
# 重写要请求的url
url = 'http://www.renren.com/Login.do'
data = {
"email": "你的账号",
"password": "你的密码"
}
# 携带数据,发送post请求
yield scrapy.FormRequest(url, formdata=data, callback=self.htmlParser)
def htmlParser(self, response):
# 解析页面或其他对页面的操作
pass
爬虫过程中发送post请求:
# -*- coding: utf-8 -*-
import scrapy
class QsbkSpiderSpider(scrapy.Spider):
name = 'qsbk_spider'
allowed_domains = ['qiushibaike.com']
start_urls = ['https://www.qiushibaike.com/text/page/1/']
def parse(self, response):
url = ''
# 发送post请求
yield scrapy.FormRequest(url, callback=self.parse)
小结:
- 发送post请求使用
FormRequest
函数
6.4 Request跟Response对象
request常用属性:
url
:请求的urlcallback
:解析url执行的回调函数methods
:请求的方法,默认为get。headers
:请求头,随机请求头信息可以在此设置,否则在settings中设置。meta
:在不同请求或请求与响应中传递数据,如后面设置代理IP的时候会用到dont_filter
:是否使用调度器过滤功能,默认为True
response常用属性:
meta
:接收请求传递的meta属性。text
:返回unicode字符串xpath
:xpath选择器css
:css选择器
6.5 CrawelSpider
上面的案例中,爬取多个网页时,明明我们已经发现了url的规律,却还需要自己获取相对路径,比较麻烦。而CrawelSpider
就可以帮我们解决这个不必要的麻烦,直接告诉它url的规律,他就会屁颠屁颠的帮我们去爬取。
CrawelSpider
继承自Scrapy
,在scrapy基础上加了新的功能。
创建CrawelSpider
的步骤跟scrapy差不多:
- 创建工程:
scrapy startproject 项目名
- 创建爬虫:
scrapy genspider -t crawl 爬虫名 域名
(就这一步不同) - 运行爬虫:
scrapy crawl 爬虫名
了解CrawelSpider
主要了解下面两个东西:
LinkExtractors
url提取器:自动爬取满足正则表达式规则的url
主要参数:
- allow:允许爬取的url,即只有满足该条件下的url才会被爬取。
- deny:禁止爬取的url,即满足该条件下的url不会被爬取。(较少用)
- allow_domains:允许爬取的域名。
- deny_domains:禁止爬取的域名。
- restrict_xpaths:严格的xapth,和allow共同过滤链接。
Rule
规则类:对爬取网站的动作定义了特定的操作。
主要参数:
- Link_extractors:
LinkExtractors
对象,制定爬取规则。 - callback:指定满足规则的url执行的回调函数。
- follow:指定根据该规则从response中提取的链接是否跟进。
- process_links:指定该Spider中哪个的函数将会被调用,从link_extractor中获取到链接列表将会调用该函数。
tips:callback
跟follow
是“死对头”,callback为None,则follow默认设置为True。
class CrawlDemoSpider(CrawlSpider):
name = 'crawl_demo'
allowed_domains = ['qiushibaike.com']
start_urls = ['https://www.qiushibaike.com/text/page/1/']
# 规则类的元祖,包含多个规则
rules = (
# 规则1:一直爬取满足.+/text/page/\d/的url
Rule(LinkExtractor(allow=r'.+/text/page/\d/'), follow=True),
# 规则2:对满足.+article-.+\.html的url使用detail_parse函数进行解析,解析完就结束
Rule(LinkExtractor(allow=r'.+article/\d'), callback="detail_parse", follow=False)
)
def parse_item(self, response):
# 解析操作
pass
规则1是爬取每一页的段子详情的url,而规则二则是对段子详情的url进行爬取。所以前者需要使用follow一直跟进到最后一页,后者这不需要follow,反而需要callback来对段子详情进行解析。
可以看到,crawlSpider为我们省去了爬取每页的url这一个步骤,我们只需要指定规则让spider去爬,就不用了手动写yield scrapy.Request函数了。
6.6 下载图片或文件
使用scrapy框架也可以很快速便捷的下载大量的图片或文件,只需要根据它的规定来做就可以了。
如下载图片的ImagesPipeline
(下载文件就把Images改为Files)
当使用ImagesPipeline
下载文件的时候,按以下步骤:
-
在items中定义两个属性,
image_urls
以及images
# 存储下载图片的链接,为一个列表 image_urls = scrapy.Field() # 当下载完成后,会把下载的相关信息存储到images中。比如下载路径等 images = scrapy.Field()
-
在settings中配置
IMAGE_STROE = '存放路径'
,设置图片下载存放的路径 -
在pipelines中将原有的设置改为
scrapy.pipelines.images.ImagesPipeline:100
-
如果觉得scrapy内置的ImagesPipeline类满足不了自己,可以自己写一个方法,然后此方法继承ImagesPipeline。(具体可以根据源码来改)
6.7 下载中间件(设置随机请求头和代理IP)
下载中间件可以在引擎将请求发给下载器之前进行一些操作,比如设置随机请求头和代理IP等。要实现下载中间件必须在middlewares.py
中自己编写下载中间件的类,该类至少实现两个函数的其中一个:process_request
、process_response
这两个函数都是下载中间件自动执行的函数:
process_request(self,request,spider) :在下载器发送请求之前执行,一般在这里设置随机请求头跟IP
参数:
- request:发送请求的request对象
- spider:发送请求的spider对象
返回值:
- 为None时:爬虫会继续处理request,执行其他中间件中的相应方法,直到合适的下载器处理函数被调用(返回response)
- 为response对象时:爬虫不会调用其他的process_request方法。
- 为request对象时:不在使用之前的request对象,而是用此request对象返回数据。
process_response(self,request,response,spider) :下载器将下载的数据传送到引擎中执行。
参数:
- request:发送请求的request对象
- spider:发送请求的spider对象
- response:被处理的response对象
返回值:
-
为response对象时:会将这个新的response对象传给其他中间件,最终传给引擎。
-
为request对象时:下载器被切断,返回的request会重新被下载器调度。
设置随机请求头和IP代理:
middlewares.py
代码如下:
import random
class UserAgentDownloadMiddleware(object):
user_agent = [] # 写你的请求头列表
def process_request(self,request,spider):
# 随机选择一个请求头
ug = random.choice(self.user_agent)
# 将该请求头作为本次请求的请求头
request.headers['User-Agent'] = ug
class IPProxyDownloadMiddlleware(object):
PROXIES = [] # 写你的代理IP列表
def process_request(self,request,spider):
# 随机选择一个代理IP
proxy = random.choice(self.PROXIES)
# 将该IP作为本次请求的IP
request.meta['proxy'] = proxy
小结:
- 设置随机请求头:
request.headers['User-Agent'] = ug
- 设置随机IP代理:
request.meta['proxy'] = proxy
整个爬虫教程就到此结束啦~~,希望对下伙伴们有所帮助!
加油,学习永不停歇.