Scrapy 框架
-
Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛。
-
框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便。
-
Scrapy 使用了 Twisted['twɪstɪd] 异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。
5c17862075585.png
-
Scrapy Engine(引擎):负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。
-
Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
-
Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
-
Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
-
Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.
-
Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。
-
Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
5c17863914b96.png
注意!只有当调度器中不存在任何request了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy也会重新下载。)
一、项目流程
1. 新建scrapy项目(scrapy startproject)
scrapy startproject 爬虫项目名称
例:scrapy startproject jobboleproject
![](https://img.haomeiwen.com/i12865014/e488e00bc4624d0a.png)
2.新建爬虫文件
scrapy genspider 爬虫名称 域名(.com前一个字符串)
例:scrapy genspider jobbole jobbole.com
3.明确目标:在items.py中添加字段
根据目标网站分析需要提取的数据,在item.py文件中添加字段
- 打开jobboleproject文件下的item.py文件
- Item 定义结构化数据字段,用来保存爬取到的数据,有点像Python中的dict,但是提供了一些额外的保护减少错误。
- 可以通过创建一个 scrapy.Item 类, 并且定义类型为 scrapy.Field的类属性来定义一个Item(可以理解成类似于ORM的映射关系)。
- 接下来,创建一个JobboleprojectItem 类,和构建item模型(model)。
class JobboleItem(scrapy.Item):
# define the fields for your item here like:
#标题
title = scrapy.Field()
#创建时间
create_date = scrapy.Field()
#文章地址
url = scrapy.Field()
#id
url_object_id = scrapy.Field()
#文章图片
front_image_url = scrapy.Field()
#文章图片地址
front_image_path = scrapy.Field()
# 点赞数
praise_nums = scrapy.Field()
#收藏数
bookmark_nums = scrapy.Field()
# 评论数
comment_nums = scrapy.Field()
#文章内容
content = scrapy.Field()
#标签
tags = scrapy.Field()
4.制作爬虫 (spiders/jobbole.py)
打开 jobboleproject/spider目录里的 jobbole.py,默认增加了下列代码:
import scrapy
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
def parse(self, response):
pass
- name = "" :这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。
- allow_domains = [] 是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。
- start_urls = () :爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
- parse(self, response) :解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,
1.负责解析返回的网页数据(response.body),提取结构化数据(生成item)
2.生成需要下一页的URL请求。
将start_urls(设置起始url)的值修改为需要爬取的第一个url
5.分析网站结构取数据
6.在parse方法中做数据的提取
from jobboleproject.items import JobboleprojectItem
1.获取图片和文章详情的链接
def parse(self, response):
# css选择器获取当前列表页面的所有的节点
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
# 如果不是完整的域名 需要拼接完整 response.url + post_url
# 获取到的URL可能不是一个域名,也可能是具体的文章需要使用parse函数from urllib import parse
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first("")
post_url = post_node.css("::attr(href)").extract_first("")
full_url = response.urljoin(post_url)
#meta参数对应的是一个字典,用来传递数据
yield scrapy.Request(url=full_url, meta={"front_image_url": image_url},
callback=self.parse_detail)
2.然后将我们得到的数据封装到一个 JobboleItem 对象中,可以保存每个文章的属性:
def parse_detail(self,response):
# print(response)
# 使用xpath语法或者css语法提取网页的相关信息
# extract() 串行化并将匹配到的节点返回一个unicode字符串列表
item = JobboleprojectItem()
#标题
item['title'] = response.xpath('//div[@class="entry-header"]/h1/text()').extract_frist("")
#发布日期
item['create_date'] = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract_first("").strip().replace("·","").strip()
#文章地址详情地址
item['url'] = response.url
# http://blog.jobbole.com/113949/
#文章的id
item['url_object_id'] = re.match(".*?(\d+).*", url).group(1)
# 文章图片
item['front_image_url'] = response.meta.get("front_image_url","")
# 点赞数
item['praise_nums'] = response.xpath("//span[contains(@class,'vote-post-up')]/h10/text()").extract_first("")
# 收藏数
bookmark_nums = response.xpath("//span[contains(@class,'bookmark-btn')]/text()").extract_first("")
match_bookmark_nums = re.match(".*?(\d+).*",bookmark_nums)
if match_bookmark_nums:
item['bookmark_nums'] = int(match_bookmark_nums.group(1))
else:
item['bookmark_nums'] = 0
# 评论数
comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract_first("")
match_comment_nums = re.match(".*?(\d+).*",comment_nums)
if match_comment_nums:
item['comment_nums'] = int(match_comment_nums.group(1))
else:
item['comment_nums'] = 0
# 文章内容
item['content'] = response.xpath("//div[@class='entry']").extract_first("")
# 过滤评论标签
tag_list = response.xpath("//p[@class='entry-meta-hide-on-mobile']//a/text()").extract()
tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
# 标签
item['tags'] = ",".join(tag_list)
print(item)
# return返回数据,不经过pipelines
# return item
# yield将获取的数据交给pipelines
# yield item
7.关于yeild函数介绍:
简单地讲,yield 的作用就是把一个函数变成一个 generator(生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,带有yeild的函数遇到yeild的时候就返回一个迭代值,下次迭代时, 代码从 yield 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行, 直到再次遇到 yield。
通俗的讲就是:在一个函数中,程序执行到yield语句的时候,程序暂停,返回yield后面表达式的值,在下一次调用的时候,从yield语句暂停的地方继续执行,如此循环,直到函数执行完。
8.Item Pipeline
当Item在Spider中被收集之后,它将会被传递到Item Pipeline,这些Item Pipeline组件按定义的顺序处理Item。
每个Item Pipeline都是实现了简单方法的Python类,比如决定此Item是丢弃而存储。以下是item pipeline的一些典型应用:
- 验证爬取的数据(检查item包含某些字段,比如说name字段)
- 查重(并丢弃)
- 将爬取结果保存到文件或者数据库中
编写item pipeline
import something
class SomethingPipeline(object):
def __init__(self):
# 可选实现,做参数初始化等
# doing something
def process_item(self, item, spider):
# item (Item 对象) – 被爬取的item
# spider (Spider 对象) – 爬取该item的spider
# 这个方法必须实现,每个item pipeline组件都需要调用该方法,
# 这个方法必须返回一个 Item 对象,被丢弃的item将不会被之后的pipeline组件所处理。
return item
def open_spider(self, spider):
# spider (Spider 对象) – 被开启的spider
# 可选实现,当spider被开启时,这个方法被调用。
def close_spider(self, spider):
# spider (Spider 对象) – 被关闭的spider
# 可选实现,当spider被关闭时,这个方法被调用
item写入JSON文件 以下pipeline将所有(从所有'spider'中)爬取到的item,存储到一个独立地items.json 文件,每行包含一个序列化为'JSON'格式的'item':
import json
class JobboleprojectPipeline(object):
def __init__(self):
self.file = open(‘jobbole.json', 'w')
def process_item(self, item, spider):
content = json.dumps(dict(item), ensure_ascii=False) + "\n"
self.file.write(content)
return item
def close_spider(self, spider):
self.file.close()
启用一个Item Pipeline组件 为了启用Item Pipeline组件,必须将它的类添加到 settings.py文件ITEM_PIPELINES 配置,就像下面这个例子:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
#'mySpider.pipelines.SomePipeline': 300,
"mySpider.pipelines.JobboleprojectPipeline":300
}
分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,
通过pipeline,通常将这些数字定义在0-1000范围内(0-1000随意设置,数值越低,组件的优先级越高)
9.重新启动爬虫
scrapy crawl jobbole
二、定制图片下载管道
Scrapy提供了一个 item pipeline ,来下载属于某个特定项目的图片,比如,当你抓取产品时,也想把它们的图片下载到本地。
这条管道,被称作图片管道,在 ImagesPipeline 类中实现,提供了一个方便并具有额外特性的方法,来下载并本地存储图片:
Pillow 是用来生成缩略图,并将图片归一化为JPEG/RGB格式,因此为了使用图片管道,你需要安装这个库。 Python Imaging Library (PIL) 在大多数情况下是有效的,但众所周知,在一些设置里会出现问题,因此我们推荐使用 Pillow 而不是 PIL.
使用图片管道 当使用 ImagesPipeline ,典型的工作流程如下所示:
- 在一个爬虫里,你抓取一个项目,把其中图片的URL放入 image_urls 组内。
- 项目从爬虫内返回,进入项目管道。
- 当项目进入 ImagesPipeline,image_urls 组内的URLs将被Scrapy的调度器和下载器(这意味着调度器和下载器的中间件可以复用)安排下载,当优先级更高,会在其他页面被抓取前处理。项目会在这个特定的管道阶段保持“locker”的状态,直到完成图片的下载(或者由于某些原因未完成下载)。
- 当图片下载完,另一个组(images)将被更新到结构中。这个组将包含一个字典列表,其中包括下载图片的信息,比如下载路径、源抓取地址(从 image_urls 组获得)和图片的校验码。images 列表中的图片顺序将和源 image_urls 组保持一致。如果某个图片下载失败,将会记录下错误信息,图片也不会出现在 images 组中。 实现定制图片管道
在settings.py中设置 IMAGES_STORE 设置为一个有效的文件夹,用来存储下载的图片
IMAGES_STORE = '/xxx/xxx/xxx'
from scrapy.contrib.pipeline.images import ImagesPipeline
import scrapy
from scrapy.exceptions import DropItem
import os
from scrapy.utils.project import get_project_settings
#下载图片
image_store = get_project_settings().get('IMAGES_STORE')
class jobboleImagePipline(ImagesPipeline):
def get_media_requests(self, item, info):
#根据图片地址,构建请求
yield scrapy.Request(item['coverImage'])
def item_completed(self, results, item, info):
print(results)
#获取图片下载成功的请求路径
image_paths = [x['path'] for ok, x in results if ok]
#判断图片是否下载成功
if not image_paths:
#出现错误,例如没有图片,可以丢弃item
raise DropItem("Item contains no images")
else:
#替换图片的名称,自定义图片名称
os.rename(image_store+'/'+image_paths[0],
image_store+'/'+item['title']+'.jpg')
#将图片地址赋值给item
item['localImagePath'] = image_store+'/'+item['title']+'.jpg'
return item
解释
item_completed() 接收的元组列表需要保证与 get_media_requests() 方法返回请求的顺序相一致。下面是 results 参数的一个典型值:
[(True,
{'checksum': '2b00042f7481c7b056c4b410d28f33cf',
'path': 'full/7d97e98f8af710c7e7fe703abc8f639e0ee507c4.jpg',
'url': 'http://www.example.com/images/product1.jpg'}),
(True,
{'checksum': 'b9628c4ab9b595f72f280b90c4fd093d',
'path': 'full/1ca5879492b8fd606df1964ea3c1e2f4520f076f.jpg',
'url': 'http://www.example.com/images/product2.jpg'}),
(False,
Failure(...))]
默认 get_media_requests() 方法返回 None ,这意味着项目中没有图片可下载。
三、Scrapy Shell
Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。
如果安装了 IPython ,Scrapy终端将使用 IPython (替代标准Python终端)。 IPython 终端与其他相比更为强大,提供智能的自动补全,高亮输出,及其他特性
启动Scrapy Shell
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
Scrapy Shell根据下载的页面会自动创建一些方便使用的对象,例如 Response 对象,以及 Selector 对象 (对HTML及XML内容)
- 当shell载入后,将得到一个包含response数据的本地 response 变量,输入 response.body将输出response的包体,输出 response.headers 可以看到response的包头。
- 输入 response.selector 时, 将获取到一个response 初始化的类 Selector 的对象,此时可以通过使用 response.selector.xpath()或response.selector.css() 来对 response 进行查询。
- Scrapy也提供了一些快捷方式, 例如 response.xpath()或response.css()同样可以生效(如之前的案例)。
Selectors选择器 Scrapy Selectors 内置 XPath 和 CSS Selector 表达式机制
- xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表
- extract(): 序列化该节点为字符串并返回list
- css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4
- re(): 根据传入的正则表达式对数据进行提取,返回字符串list列表
尝试使用Selector:
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
返回 xpath选择器对象列表
response.xpath("//title")
[<Selector xpath='//title' data='<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title'>]
使用 extract()方法返回 Unicode字符串列表
response.xpath('//title').extract()
['<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>']
打印列表第一个元素,终端编码格式显示
response.xpath('//title').extract()[0]
'<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>'
返回 xpath选择器对象列表
response.xpath('//title/text()')
[<Selector xpath='//title/text()' data='职位搜索 | 社会招聘 | Tencent 腾讯招聘'>]
返回列表第一个元素的Unicode字符串
response.xpath('//title/text()')[0].extract()
'职位搜索 | 社会招聘 | Tencent 腾讯招聘'
职位名称:
response.xpath("//tr[@class='even']/td/a/text()").extract()[0]
'15605-火影手游服务器开发工程师(深圳)'
职位名称详情页:
response.xpath("//tr[@class='even']/td/a/@href").extract()[0]
'position_detail.php?id=40217&keywords=&tid=0&lid=0'
职位类别:
response.xpath("//tr[@class='even']/td[2]/text()").extract()[0]
'技术类'
其他:
- scrapy -h 查看所有可用的命令:
- scrapy view -h 查看view命令的详细内容:
- scrapy list列出当前项目中所有可用的spider
- scrapy runspider xxxx.py在未创建项目的情况下,运行一个编写在Python文件中的spider。
- scrapy version输出Scrapy版本
四、Scrapy Spider文件介绍
Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。
scrapy.Spider是最基本的类,所有编写的爬虫必须继承这个类。
主要用到的函数及调用顺序为:
init() : 初始化爬虫名字和start_urls列表
start_requests() 调用make_requests_from url():生成Requests对象交给Scrapy下载并返回response
parse():
- 解析response,并返回Item或Requests(需指定回调函数)。
- Item传给Item pipline持久化 , 而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse()),一直进行循环,直到处理完所有的数据为止。
源码
class Spider(object_ref):
#定义spider名字的字符串(string)。spider的名字定义了Scrapy如何定位(并初始化)spider,所以其必须是唯一的。
#name是spider最重要的属性,而且是必须的。
#一般做法是以该网站(domain)(加或不加 后缀 )来命名spider。 例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite
name = None
#初始化,提取爬虫名字,start_ruls
def __init__(self, name=None, **kwargs):
if name is not None:
self.name = name
# 如果爬虫没有名字,中断后续操作则报错
elif not getattr(self, 'name', None):
raise ValueError("%s must have a name" % type(self).__name__)
# python 对象或类型通过内置成员__dict__来存储成员信息
self.__dict__.update(kwargs)
#URL列表。当没有指定的URL时,spider将从该列表中开始
进行爬取。 因此,第一个被获取到的页面的URL将是该列表
之一。 后续的URL将会从获取到的数据中提取。
if not hasattr(self, 'start_urls'):
self.start_urls = []
# 打印Scrapy执行后的log信息
def log(self, message, level=log.DEBUG, **kw):
log.msg(message, spider=self, level=level, **kw)
# 判断对象object的属性是否存在
def set_crawler(self, crawler):
assert not hasattr(self, '_crawler'), "Spider already bounded to %s" % crawler
self._crawler = crawler
@property
def crawler(self):
assert hasattr(self, '_crawler'), "Spider not bounded to any crawler"
return self._crawler
@property
def settings(self):
return self.crawler.settings
#该方法将读取start_urls内的地址,并为每一个地址
生成一个Request对象,交给Scrapy下载并返回Response
#该方法仅调用一次
def start_requests(self):
for url in self.start_urls:
yield self.make_requests_from_url(url)
#start_requests()中调用,实际生成Request的函数。
#Request对象默认的回调函数为parse(),提交的方式为get
def make_requests_from_url(self, url):
return Request(url, dont_filter=True)
#默认的Request对象回调函数,处理返回的response。
#生成Item或者Request对象。用户必须实现这个类
def parse(self, response):
raise NotImplementedError
@classmethod
def handles_request(cls, request):
return url_is_from_spider(request.url, cls)
def __str__(self):
return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))
__repr__ = __str__
主要属性和方法
- name
定义spider名字的字符串。
例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite
- allowed_domains
包含了spider允许爬取的域名(domain)的列表,可选。
- start_urls
初始URL元组/列表。当没有制定特定的URL时,spider将从该列表中开始进行爬取。
- start_requests(self)
该方法必须返回一个可迭代对象(iterable)。该对象包含了spider用于爬取(默认实现是使用 start_urls 的url)的第一个Request。
当spider启动爬取并且未指定start_urls时,该方法被调用。
- parse(self, response)
当请求url返回网页没有指定回调函数时,默认的Request对象回调函数。用来处理网页返回的response,以及生成Item或者Request对象。
- log(self, message[, level, component])
使用 scrapy.log.msg() 方法记录(log)message。
更多数据请参见 logging
parse()方法的工作机制:
1.因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型;
2.如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息。
3.scrapy取到第一部分的request不会立马就去发送这个request,只是把这个request放到队列里,然后接着从生成器里获取;
4.取尽第一部分的request,然后再获取第二部分的item,取到item了,就会放到对应的pipeline里处理;
5.parse()方法作为回调函数(callback)赋值给了Request,指定parse()方法来处理这些请求 scrapy.Request(url, callback=self.parse)
6.Request对象经过调度,执行生成 scrapy.http.response()的响应对象,并送回给parse()方法,直到调度器中没有Request(递归的思路)
7.取尽之后,parse()工作结束,引擎再根据队列和pipelines中的内容去执行相应的操作;
8.程序在取得各个页面的items前,会先处理完之前所有的request队列里的请求,然后再提取items。
9.这一切的一切,Scrapy引擎和调度器将负责到底。
五、Scrapy CrawlSpiders
scrapy通用爬虫
CrawlSpider它是Spider的派生类,Spider类的设计原则是只爬取start_url列表中的网页,而CrawlSpider类定义了一些规则Rule来提供跟进链接的方便的机制,从爬取的网页结果中获取链接并继续爬取的工作.
源码参考
class CrawlSpider(Spider):
rules = ()
def __init__(self, *a, **kw):
super(CrawlSpider, self).__init__(*a, **kw)
self._compile_rules()
#首先调用parse()来处理start_urls中返回的response对象
#parse()则将这些response对象传递给了_parse_response()函数处理,并设置回调函数为parse_start_url()
#设置了跟进标志位True
#parse将返回item和跟进了的Request对象
def parse(self, response):
return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)
#处理start_url中返回的response,需要重写
def parse_start_url(self, response):
return []
def process_results(self, response, results):
return results
#从response中抽取符合任一用户定义'规则'的链接,并构造成Resquest对象返回
def _requests_to_follow(self, response):
if not isinstance(response, HtmlResponse):
return
seen = set()
#抽取之内的所有链接,只要通过任意一个'规则',即表示合法
for n, rule in enumerate(self._rules):
links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
#使用用户指定的process_links处理每个连接
if links and rule.process_links:
links = rule.process_links(links)
#将链接加入seen集合,为每个链接生成Request对象,并设置回调函数为_repsonse_downloaded()
for link in links:
seen.add(link)
#构造Request对象,并将Rule规则中定义的回调函数作为这个Request对象的回调函数
r = Request(url=link.url, callback=self._response_downloaded)
r.meta.update(rule=n, link_text=link.text)
#对每个Request调用process_request()函数。该函数默认为indentify,即不做任何处理,直接返回该Request.
yield rule.process_request(r)
#处理通过rule提取出的连接,并返回item以及request
def _response_downloaded(self, response):
rule = self._rules[response.meta['rule']]
return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)
#解析response对象,会用callback解析处理他,并返回request或Item对象
def _parse_response(self, response, callback, cb_kwargs, follow=True):
#首先判断是否设置了回调函数。(该回调函数可能是rule中的解析函数,也可能是 parse_start_url函数)
#如果设置了回调函数(parse_start_url()),那么首先用parse_start_url()处理response对象,
#然后再交给process_results处理。返回cb_res的一个列表
if callback:
#如果是parse调用的,则会解析成Request对象
#如果是rule callback,则会解析成Item
cb_res = callback(response, **cb_kwargs) or ()
cb_res = self.process_results(response, cb_res)
for requests_or_item in iterate_spider_output(cb_res):
yield requests_or_item
#如果需要跟进,那么使用定义的Rule规则提取并返回这些Request对象
if follow and self._follow_links:
#返回每个Request对象
for request_or_item in self._requests_to_follow(response):
yield request_or_item
def _compile_rules(self):
def get_method(method):
if callable(method):
return method
elif isinstance(method, basestring):
return getattr(self, method, None)
self._rules = [copy.copy(r) for r in self.rules]
for rule in self._rules:
rule.callback = get_method(rule.callback)
rule.process_links = get_method(rule.process_links)
rule.process_request = get_method(rule.process_request)
def set_crawler(self, crawler):
super(CrawlSpider, self).set_crawler(crawler)
self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
创建 CrawlSpider模板 的代码
scrapy genspider -t crawl 爬虫文件 域名
CrawlSpider继承于Spider类,除了继承过来的属性外(name、allow_domains),还提供了新的属性和方法:
class XcfcrawlspiderSpider(CrawlSpider):
#爬虫名称
name = 'xcfCrawlSpider'
#设置允许爬取的域
allowed_domains = ['xiachufang.com']
#设置起始的url
start_urls = ['http://www.xiachufang.com/category/']
rules = (
Rule(
LinkExtractor(allow=r'.*?/category/\d+/'),
callback='parse_item',
follow=True,
process_links='check_category_url'
),
)
rules
CrawlSpider使用rules属性来决定爬虫的爬取规则,并将匹配后的url请求提交给引擎,完成后续的爬取工作。
在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了某种特定操作,比如提取当前相应内容里的特定链接,是否对提取的链接跟进爬取,对提交的请求设置回调函数等。
注意:如果多个rule匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用。
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)
link_extractor:是一个Link Extractor对象,用于定义需要提取的链接。
callback: 从link_extractor中每获取到链接得到Responses时,会调用参数所指定的值作为回调函数,该回调函数接收一个response作为其一个参数。
follow:是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。如果callback为None,follow 默认设置为True ,否则默认为False。
process_links:指定spider中哪个的函数将会被调用,从link_extractor中获取到链接列表时将会调用该函数。该方法主要用来过滤。
process_request:指定处理函数,根据该Rule提取到的每个Request时,该函数将会被调用,可以对Request进行处理,该函数必须返回Request或者None
注意:当编写爬虫规则时,避免使用parse作为回调函数。由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了 parse方法,crawl spider将会运行失败。
LinkExtractors
class scrapy.linkextractors.LinkExtractor
目的是提取链接
每个LinkExtractor有唯一的公共方法是 extract_links(),它接收一个 Response 对象,并返回一个 scrapy.link.Link 对象
class scrapy.linkextractors.LinkExtractor(
allow = (),
deny = (),
allow_domains = (),
deny_domains = (),
deny_extensions = None,
restrict_xpaths = (),
tags = ('a','area'),
attrs = ('href'),
canonicalize = True,
unique = True,
process_value = None
)
allow:满足括号中“正则表达式”的URL会被提取,如果为空,则全部匹配。
deny:满足括号中“正则表达式”的URL一定不提取(优先级高于allow)。
allow_domains:会提取的链接的domains。
deny_domains:一定不会被提取链接的domains。
restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接。
Scrapy Request和Response
# 部分代码
class Request(object_ref):
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None):
self._encoding = encoding # this one has to be set first
self.method = str(method).upper()
self._set_url(url)
self._set_body(body)
assert isinstance(priority, int), "Request priority not an integer: %r" % priority
self.priority = priority
assert callback or not errback, "Cannot use errback without a callback"
self.callback = callback
self.errback = errback
self.cookies = cookies or {}
self.headers = Headers(headers or {}, encoding=encoding)
self.dont_filter = dont_filter
self._meta = dict(meta) if meta else None
@property
def meta(self):
if self._meta is None:
self._meta = {}
return self._meta
- url: 就是需要请求,并进行下一步处理的url
- callback: 指定该请求返回的Response,由那个函数来处理。
- method: 请求一般不需要指定,默认GET方法,可设置为"GET", "POST", "PUT"等,且保证字符串大写
- headers: 请求头
- cookies: cookies,模拟用户登录需要指定用户的cookies,字典dict型
- meta: 比较常用,在不同的请求之间传递数据使用的。字典dict型
request_with_cookies = Request(
url="http://www.example.com",
cookies={'currency': 'USD', 'country': 'UY'},
meta={'dont_merge_cookies': True}
) - encoding: 编码类型,使用默认的 'utf-8' 就行。
- dont_filter: 表明该请求不由调度器过滤。这是当你想使用多次执行相同的请求,忽略重复的过滤器。默认为False。
- errback: 指定错误处理函数
发送POST请求(补充)
yield scrapy.FormRequest(url, formdata, callback)
如果希望程序执行一开始就发送POST请求,可以重写Spider类的start_requests(self) 方法,并且不再调用start_urls里的url。
class mySpider(scrapy.Spider):
name = 'myspider'
allow_domeas = ['renren.com']
start_urls = ["http://www.renren.com/PLogin.do"]
def start_requests(self):
# FormRequest 是Scrapy发送POST请求的方法
for url in self.start_urls:
yield scrapy.FormRequest(
url = url,
formdata = {"email" : "mr_mao_hacker@163.com", "password" : "axxxxxxxe"},
callback = self.parse_page
)
def parse_page(self, response):
"""
请求成的回调函数
"""
pass
Response相关参数介绍
class Response(object_ref):
def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
self.headers = Headers(headers or {})
self.status = int(status)
self._set_body(body)
self._set_url(url)
self.request = request
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
try:
return self.request.meta
except AttributeError:
raise AttributeError("Response.meta not available, this response " \
"is not tied to any request")# 部分代码
class Response(object_ref):
def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
self.headers = Headers(headers or {})
self.status = int(status)
self._set_body(body)
self._set_url(url)
self.request = request
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
try:
return self.request.meta
except AttributeError:
raise AttributeError("Response.meta not available, this response " \
"is not tied to any request")
- status: 响应码
- body: 响应体
- url:响应url
- self.request (request对象)
- self.headers (响应头)
Scrapy DOWNLOADER_MIDDLEWARE 的使用
反反爬虫相关机制
通常防止爬虫被反主要有以下几个策略:
- 动态设置User-Agent(随机切换User-Agent,模拟不同用户的浏览器信息)
- 禁用Cookies(前提是爬取的网站不需要cookies参数)(也就是不启用cookies middleware,不向Server发送cookies,有些网站通过cookie的使用发现爬虫行为)
使用cookies池,自定义中间件
除非特殊需要,否则禁用cookies,防止某些网站根据Cookie来封锁爬虫。
COOKIES_ENABLED = False
- 设置延迟下载(降低访问网站的频率)(设置为2秒或更高)
DOWNLOAD_DELAY = 2
- 使用IP代理地址池:VPN和代理IP,现在大部分网站都是根据IP来反爬的。
针对于反爬手段
1.使用 Crawlera(专用于爬虫的代理组件),正确配置和设置下载中间件后,项目所有的request都是通过crawlera发出。
DOWNLOADER_MIDDLEWARES = {
'scrapy_crawlera.CrawleraMiddleware': 600
}
CRAWLERA_ENABLED = True
CRAWLERA_USER = '注册/购买的UserKey'
CRAWLERA_PASS = '注册/购买的Password'
2.自定义下载中间件(Downloader Middlewares)
下载中间件是处于引擎(crawler.engine)和下载器(crawler.engine.download())之间的一层组件,可以用来修改Request和Response。
- 当引擎传递请求(Request)给下载器的过程中,下载中间件可以对请求进行处理 (例如增加http header信息(User-Agent),增加proxy代理等);
- 在下载器完成http请求,传递响应给引擎的过程中, 下载中间件可以对响应进行处理
要激活下载器中间件组件,将其加入到 DOWNLOADER_MIDDLEWARES 设置中。 该设置是一个字典(dict),键为中间件类的路径,值为其中间件的优先级。值越低,代表优先级越高。
每个中间件组件是一个定义了以下一个或多个方法
def process_request(self, request, spider)
当每个request对象通过下载中间件时该方法被调用。
process_request() 必须返回以下其中之一:
- None
如果返回 None: Scrapy将继续处理request,执 行其他的中间件的相应方法。 - Response 对象
如果返回 Response 对象: Scrapy不会再调用任 何其他的中间件的 process_request() 或相应地下 载函数; 直接返回这个response对象。 已激活的中间件的 process_response()方法则会在 每个 response 返回时被调用。 - Request 对象
如果返回 Request 对象,Scrapy则停止调用 其他中间件的process_request方法,并重新将返回的 request对象放置到调度器等待下载。 - IgnoreRequest异常
如果返回raise IgnoreRequest 异常: 下载中间件的 process_exception() 方法会被用。 如果没有捕获该异常, 则request发情请求时设置的 errback(Request.errback)方法会被调用。如果也 没有设置异常回调,则该异常被忽略且不记录。
process_request()有两个参数:
- request (Request 对象) – 处理的request
- spider (Spider 对象) – 该request对应的spider
process_response(self, request, response, spider)
当下载器完成http请求,传递Response给引擎的时调用
process_response() 必须返回以下其中之一:
- Response 对象
如果返回 Request: 更低优先级的下载中间件的 process_response方法不会继续调用,该Request会被 重新放到调度器任务队列中等待调度,相当于一个新的 Request。 - Request 对象
如果返回 Response 对象,更低优先级的下载中间 件的process_response方法会被继续调用对Response对象进行处理 - IgnoreRequest异常
如果抛出 IgnoreRequest 异常,则调用request 设置的errback(Request.errback)函数。 如果异常没有 被处理,则该异常被忽略且不记录。
process_response()有三个参数:
- request (Request 对象) – response所对应的request
- response (Response 对象) – 被处理的response
- spider (Spider 对象) – response所对应的spider
常见的中间件代码实例
1.设置User-Agent中间件
Scrapy代理IP、Uesr-Agent的切换都是通过DOWNLOADER_MIDDLEWARES进行控制,我们在settings.py同级目录下创建middlewares.py文件,包装所有请求。
在settings.py文件中添加USER_AGENTS:
USER_AGENTS = [
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
]
方式一
import random
class RandomUserAgent(object):
def process_request(self, request, spider):
#获取到代理的代理池
useragents = spider.settings['USERAGENTS']
#随机获取一个User-Agent
user_agent = random.choice(useragents)
print('执行下载中间件'+user_agent)
if user_agent:
#赋值的两种方式
request.headers.setdefault(b'User-Agent', user_agent)
# request.headers['User-Agent'] = user_agent
方式二
# 使用第三方插件来随机获取User-Agent
from fake_useragent import UserAgent
class RandomUserAgentMiddlewareTwo(object):
def process_request(self, request, spider):
user_agent = UserAgent().random
print('执行下载中间件'+user_agent)
if user_agent:
#赋值的两种方式
request.headers.setdefault(b'User-Agent', user_agent)
# request.headers['User-Agent'] = user_agent
最后设置setting.py里的DOWNLOADER_MIDDLEWARES,激活自己编写的下载中间件类。
最后设置setting.py里的DOWNLOADER_MIDDLEWARES,激活自己编写的下载中间件类。
2.设置代理中间件。
- 添加代理IP设置PROXIES: 代理一般分为公开代理(不需要账号密码)和私密代理(需要账号密码) 代理获取方式:
- 免费代理IP可以网上搜索,
- 也可以付费购买一批可用的私密代理IP
# 在settings.py文件中模拟一个代理池(对于少量代理可以这么做)
PROXIES = [
{'ip_port': '111.8.60.9:8123', 'user_pwd': 'user1:pass1'},
{'ip_port': '101.71.27.120:80', 'user_pwd': 'user2:pass2'},
{'ip_port': '122.96.59.104:80', 'user_pwd': None},
{'ip_port': '122.224.249.122:8088', 'user_pwd': None},
]
# middlewares.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import random
import base64
#定义一个代理的中间件
import base64
class RandomProxyMiddleware(object):
def process_request(self, request, spider):
proxies = spider.settings['PROXIES']
proxy = random.choice(self.proxies)
if proxy['user_pwd'] is None:
# 没有代理账户验证的代理使用方式
request.meta['proxy'] = proxy['ip_port']
else:
#对账户密码进行base64编码
user_pwd = base64.b64encode(proxy['user_pwd'].encode('utf-8')).decode('utf-8')
#对应到代理服务器的信令格式里
request.headers['Proxy-Authorization'] = 'Basic ' + user_pwd
request.meta['proxy'] = proxy['ip_port']
3. 设置cookies中间件。
import random
class RandomCookiesMiddleware(object):
def process_request(self,request,spider):
cookies = spider.settings['COOKIES']
#随机获取一个cookies
cookie = random.choice(cookies)
if cookie:
request.cookies = cookie
**4. 设置selenium中间件。**
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from scrapy.http import HtmlResponse
class SeleniumMiddleware(object):
def __init__(self):
self.drive = webdriver.Chrome(executable_path='')
self.drive.set_page_load_timeout(10)
def process_request(self,request,spider):
try:
url = request.url
self.drive.get(url)
if self.drive.page_source:
return HtmlResponse(url=url,body=self.drive.page_source,status=200,encoding='utf-8',request=request)
except TimeoutException:
print('请求超时')
return HtmlResponse(url=url,body=None,status=500)
网友评论