Scrapy

Scrapy

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其可以应用在数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。

Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下

Scrapy主要包括了以下组件:

  • 引擎(Scrapy)
    用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares)
    介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares)
    介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

Scrapy运行流程大概如下:

  1. 引擎从调度器中取出一个链接(URL)用于接下来的抓取
  2. 引擎把URL封装成一个请求(Request)传给下载器
  3. 下载器把资源下载下来,并封装成应答包(Response)
  4. 爬虫解析Response
  5. 解析出实体(Item),则交给实体管道进行进一步的处理
  6. 解析出的是链接(URL),则把URL交给调度器等待抓取

一、安装

Linux

1
pip3 install scrapy

Windows

1
2
3
4
5
pip3 install wheel
下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
进入下载目录,执行 pip3 install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
pip3 install pywin32
pip3 install scrapy

二、基本使用

基本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. scrapy startproject 项目名称
- 在当前目录中创建中创建一个项目文件(类似于Django)

2. cd 项目目录
scrapy genspider <name> <domain>
- 创建爬虫应用
如:
scrapy gensipider oldboy oldboy.com
scrapy gensipider autohome autohome.com.cn
PS:
查看所有命令:scrapy gensipider -l
查看模板命令:scrapy gensipider -d 模板名称

3. scrapy list
- 展示爬虫应用列表

4. scrapy crawl 爬虫应用名称
- 运行单独爬虫应用
如果不想看日志信息则在爬虫应用名称后面加上--nolog

项目结构以及爬虫应用简介

1
2
3
4
5
6
7
8
9
10
11
12
13
project_name/
scrapy.cfg # 配置文件(部署)项目的主配置信息
project_name/
__init__.py
items.py # 设置数据存储模板,用于结构化数据,如:Django的Model
pipelines.py # 数据处理行为,如:一般结构化的数据持久化
settings.py # 配置文件(爬虫),如:递归的层数、并发数,延迟下载等
middlewares.py # 中间件
spiders/ # 爬虫目录,如:创建文件,编写爬虫规则
__init__.py
爬虫1.py
爬虫2.py
爬虫3.py

注意:一般创建爬虫文件时,以网站域名命名

pipelines.py

专门用来保存数据的,其中三个方法会经常使用。

open_spider(self, spider):当爬虫被打开的时候执行

process_item(self, item, spider):当爬虫中有item传过来的时候被调用

close_spider(self,spider):当爬虫关闭的时候会被调用

要激活并使用pipelines的话,记得在settings.py中将ITEM_PIPELINES设置

1
2
3
ITEM_PIPELINES = {
'test1_scrapy.pipelines.Test1ScrapyPipeline': 300,
}

items.py

专门用来定义模型。建议使用,以后就不要在spider文件中使用字典

1
name = scrapy.Field()

spiders文件夹下创建的爬虫.py

1
2
3
4
5
response是一个 scrapy.http.response.html.HtmlResponse 对象,可以执行 xpath 和css 语法来提取数据
提取出来的数据是一个 Selector 或者是一个 SelectorList对象,如果想要获取其中的字符串,那么应该执行getall()或者get()方法
getall():获取的是Selector中所有文本。返回的是一个列表
get():获取的是Selector中的第一个文本。返回的是一个str类型
如果数据解析回来,要传给pipelines处理,那么可以使用yield来返回

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import scrapy
from test1_scrapy.items import Test1ScrapyItem


class MessiSpider(scrapy.Spider):
name = 'messi'
allowed_domains = ['messilessblog.com']
start_urls = ['http://messilessblog.com/']

def parse(self, response):
articles = response.xpath("//article[@class='post post-type-normal']")
for article in articles:
title = article.xpath(".//a[@class='post-title-link']//text()").get()
# 使用items模块,不推荐直接使用字典
item = Test1ScrapyItem(title=title)
yield item

保存json数据

JsonItemExporter和JsonLinesItemExporter

保存json数据类型的时候。可以使用这两个类,让操作变得简单

1.JsonItemExporter:这个是每次把数据添加到内存,最后统一写入磁盘中。好处是,储存的数据是一个满足json规则的数据,坏处是如果数据量比较大,那么比较耗内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from scrapy.exporters import sonItemExporter

class Test1ScrapyPipeline(object):

def __init__(self):
self.fp = open("messiblog.json", 'wb')
self.exporter = JsonItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')
self.exporter.start_exporting()

def open_spider(self, spider):
print("爬虫开始")

def process_item(self, item, spider):
self.exporter.export_item(item)
return item

def close_spider(self,spider):
self.exporter.finish_exporting()
self.fp.close()
print("爬虫结束")

2.JsonLinesItemExporter这个是每次调用export_item()的时候就把这个item储存到硬盘中。坏处是每一个字典是一行,整个文件不是一个满足json格式的文件,好处是每次处理数据的时候就直接存储到了硬盘,这样不会耗内存,数据也比较安全,而且不需要写start和finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.exporters import JsonLinesItemExporter

class Test1ScrapyPipeline(object):

def __init__(self):
self.fp = open("messiblog.json", 'wb')
self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')

def open_spider(self, spider):
print("爬虫开始")

def process_item(self, item, spider):
self.exporter.exporter._item(item)
return item

def close_spider(self,spider):
self.fp.close()
print("爬虫结束")

两种方式,都是byte写入,所以,文件打开要用byte类型打开

CrawlSpider

在之前我们解析完整个页面后获取下一页的url,然后重新发送一个请求。

有时候我们想要这么做,只要满足某个条件的URL,都给我爬取,那么这个时候我们就可以通过CrawlSpider 来帮我们完成了。CrawlSpider继承自Spider只不过是在之前的基础上增加了新的功能,可以定义爬取url的规则,以后scrapy碰到满足条件的url都进行爬取,而不需要自己手动yield scrapy.Request

创建CrawlSpider爬虫

之前创建爬虫的方式是通过scrapy genspider [爬虫名] [域名]的方式创建的。如果想要创建CrawlSpider爬虫,那么应该通过以下的命令创建

1
scrapy genspider -t crawl [爬虫名] [域名]

-t crawl表示以crawl为模板来创建爬虫

scrapy genspider -l可以查看所有的模板

LinkExtractors链接提取器

使用LinkExtractors可以不要程序员自己提取想要的url,然后发送请求,这些工作都可以交给LinkExtractors,他会在所有爬的页面中找到满足规则的url,实现自动的爬取,以下对LinkExtractors类做一个简单的介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
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。所有满足这个正则表达式的url都会被提取。
  • deny:禁止的url。所有满足这个正则表达式的url都不会被提取。
  • allow_domains:允许的域名。只有在这个里面指定的域名的url才会被提取。
  • deny_domains:禁止的域名。所有在这个里面指定的域名的url都不会被提取。
  • restrict_xpaths:严格的xpath。和allow共同过滤链接。

Rule规则类

定义爬虫的规则类。以下对这个类做一个简单的介绍:

1
2
3
4
5
6
7
8
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)

主要参数讲解:

  • link_extractor:一个LinkExtractor对象,用于定义爬取规则。
  • callback:满足这个规则的url,应该要执行哪个回调函数。因为CrawlSpider使用了parse作为回调函数,因此不要覆盖parse作为回调函数自己的回调函数。什么情况下使用:如果这个url对应的页面,只是为了获取更多的url,并不需要里面的数据,那么可以不指定callback。
  • follow:指定根据该规则从response中提取的链接是否需要跟进。什么情况下使用:如果在爬取页面的时候,需要将满足当前条件的url在进行跟进,那么就设置为True,否则为False
  • process_links:从link_extractor中获取到链接后会传递给这个函数,用来过滤不需要爬取的链接。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from test1_scrapy.items import Test1ScrapyItem

class Meesi2Spider(CrawlSpider):
name = 'meesi2'
allowed_domains = ['robotno42.github.io']
start_urls = ['https://robotno42.github.io/']

rules = (
Rule(LinkExtractor(allow=r'https://robotno42.github.io/page/\d/'), follow=True),
Rule(LinkExtractor(allow=r'.+/\d{4}/\d{2}/\d{2}/.+'), callback="parse_item", follow=False)

)

def parse_item(self, response):
title = response.xpath("//*[@id='posts']/article/div/header/h1/text()").get()
create_time = response.xpath("//*[@id='posts']/article/div/header/div/span[1]/time/text()").get().strip()
item = Test1ScrapyItem(title=title, create_time=create_time)
yield item

Scrapy Shell

我们想要在爬虫中使用xpath、beautifulsoup、正则表达式、css选择器等来提取想要的数据,但是引文scrapy是一个比较中的框架,每次运行起来都要等待一段时间,因此要去验证我们写的提取规则是否正确,是一个比较麻烦的事情,因此scrapy提供了一个shell,用来方便的测试规则,当然也不仅仅局限于这个一个功能。

打开Scrapy Shell

打开cmd终端,进入到Scrapy项目所在的目录,然后进入到scrapy框架所在的虚拟环境中,输入命令scrapy shell [链接]。就会进入到scrapy的shell环境中。在这个环境中,你可以跟在爬虫的parse方法中一样使用了。

Scrapy Shell笔记

  1. 可以方便我们做一些数据提取的测试代码。
  2. 如果想要执行scrapy命令,那么毫无疑问,肯定是要先进入到 scrapy所在的环境中。
  3. 如果想要读取某个项目的配置信息,那么应该先进入到这个项目中,再执行scrapy shell命令。

Request和Response对象

Request对象

Request对象在我们写爬虫,爬取一面的数据需要重新发送一个请求的时候调用。这个类需要传递一些参数,其中比较常用的参数有:

  1. url:这个request对象发送请求的url。
  2. callback:在下载器下载完相应的数据后执行的回调函数。
  3. method:请求的方法。默认为GET方法,可以设置为其他方法。
  4. headers:请求头,对于一些固定的设置,放在settings.py中指定就可以了。对于那些非固定的,可以在发送请求的时候指定。
  5. meta:比较常用。用于在不同的请求之间传递数据用的。
  6. encoding:编码。默认的为utf-8,使用默认的就可以了。
  7. dont_filter:表示不由调度器过滤。在执行多次重复的请求的时候用得比较多。
  8. errback:在发生错误的时候执行的函数。

Response对象

Response对象一般是由scrapy给你自动构建的。因此开发者不需要关心如何创建Response对象,而是如何使用它。Response对象有很多属性,可以用来提取数据的。主要有以下属性:

  1. meta:从其他请求传过来的meta属性,可以用来保持多个请求之间的数据连接。
  2. encoding:返回当前字符串编码和解码的格式。
  3. text:将返回来的数据作为unicode字符串返回。
  4. body:将返回的数据作为bytes字符串返回。
  5. xpath:xpath选择器。
  6. css:css选择器。

发送POST请求

有时候我们想要在请求数据的时候发送post请求,那么这时候需要使用 Request的子类和FormRequest 来实现。如果想要在爬虫一开始的时候就发送 POST 请求,那么需要在爬虫类中重写 start_requests(self) 方法,并且不再调用 start_urls 里的url。

模拟登录

案例一:模拟登录豆瓣网,更改个性签名

  1. 想要发送post请求,那么推荐使用scrapy.FormRequest方法。可以方便的指定表单数据。
  2. 如果想在爬虫一开始的时候就发送post请求,那么应该重写start_requests方法。在这个方法中,发送post请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding: utf-8 -*-
import scrapy

class DbSpider(scrapy.Spider):
name = 'db'
allowed_domains = ['douban.com']
start_urls = ['http://douban.com/']

def start_requests(self):
url = 'https://accounts.douban.com/j/mobile/login/basic'
data = {
'name': '18368723168',
'password': '946971',
'remember': 'true',
'ticket': 'qxmECW2q8vqDRM0jtZTOyuL1UfJrx3s6tFaeGstOZy4IvFC4nLWo7MXtWwiVni7o7gNEi3mnOKQ*'
}
request = scrapy.FormRequest(url, formdata=data, callback=self.parse_page)
yield request

def parse_page(self, response):
url = 'https://www.douban.com/people/157823813/'
request = scrapy.Request(url=url, callback=self.page,)
yield request

def page(self, response):
ck = response.xpath("//*[@id='edit_signature']/form/div/input[@name='ck']/@value").get()
formdata = {
'ck': ck,
'signature': '我就是颜色不一样的烟火'
}
yield scrapy.FormRequest(url='https://www.douban.com/j/people/157823813/edit_signature', formdata=formdata)

下载文件和图片

Scrapy为下载item中包含的文件(比如在爬取到产品时,同时也想保存对应的图片)提供了一个可重用的item pipelines。这些 pipeline有些共同的方法和结构(我们称之为 media pipeline)。一般来说会使用 Files pipeline或者 Image Pipeline

为什么要选择使用 scrapy 内置的下载文件的方法

  1. 避免重新下载最近已经下载过的数据。
  2. 可以方便的指定文件存储的路径。
  3. 可以将下载的图片转换成通用的格式。比如png或jpg。
  4. 可以方便的生成缩略图。
  5. 可以方便的检测图片的宽和高,确保他们满足最小限制。
  6. 异步下载,效率非常高。

下载文件的 Files Pipeline

当使用Files Pipeline 下载文件的时候,按照以下步骤来完成:

  1. 定义好一个Items ,然后在这个 item 中定义两个属性,分别为 file_urls 以及 filesfile_urls是用来存储需要下载的图片的url链接,需要给一个列表。
  2. 当文件下载完成后,会把文件下载的相关信息存储到itemfiles属性中。比如下载路径、下载的url和文件的校验码等。
  3. 在配置文件 settings.py中配置 FILES_STORE,这个配置是用来设置文件下载下来的路径。设置为一个有效的文件夹,用来存储下载的图片。 否则管道将保持禁用状态,即使你在 ITEM_PIPELINES 设置中添加了它。
  4. 启动 pipeline:在 ITEM_PIPELINES中设置 scrapy.pipelines.files.FilesPipeline:1

下载图片的 Image Pipeline

当使用Image Pipeline下载文件的时候,按照以下步骤来完成:

  1. 定义好一个Item,然后在这个 Item 中定义两个属性,分别为 image_urls以及 imagesimage_urls是用来存储需要下载的图片的url链接,需要给一个列表。
  2. 当文件下载完成后,会把文件下载的相关信息存储到itemimages属性中。比如下载路径、下载的url和文件的校验码等。
  3. 在配置文件 settings.py中配置 IMAGES_STORE,这个配置是用来设置文件下载下来的路径。设置为一个有效的文件夹,用来存储下载的图片。 否则管道将保持禁用状态,即使你在 ITEM_PIPELINES 设置中添加了它。
  4. 启动 pipeline:在 ITEM_PIPELINES中设置 scrapy.pipelines.images.ImagesPipeline:1

Downloader Middlewares(下载中间件)

下载器中间件是引擎和下载器之间通信的中间件,在这个中间件中我们可以设置代理,更换请求头等来达到反反爬虫的目的。要写下载器中间件,可以再下载器中实现两个方法,一个是process_request(self,request,spider),这个方法是在请求发送之前会执行,还有一个是process_response(self,request,response,spider),这个方法是数据下载到引擎之前执行。

process_request(self,request,spider)

这个方法是下载器在发送之前会执行的。一般可以在这个里面设置随机代理ip等

  1. 参数
  • request:发送请求的request对象

  • spider:发送请求的spider对象

  1. 返回值
  • 返回None:如果返回None,Scrapy将继续处理该request,执行其他中间件中的相应方法,直到合适的下载器处理函数被调用。

  • 返回Response:Scrapy将不会调用其他任何的process_request将直接返回这个response对象,已经激活的中间件的process_response()方法则会在每个response返回时被调用。

  • 返回Request:不再使用之前的request对象去下载数据,而是根据现在返回的request对象返回数据。

    如果这个方法中抛出了异常,则会调用process_exception方法

process_response(self,request,response,spider)

这个是下载器下载的数据到引擎中间会执行的方法

  1. 参数:
  • request:request对象
  • response:被处理的response对象
  • spider:spider对象
  1. 返回值:
  • 返回Response对象:会将这个新的response对象传给其他中间件,最终传给爬虫。
  • 返回Request对象:下载器链被切断,返回的request会重新被下载器调度下载。
  • 如果抛出一个异常,那么调用request的errback方法,如果没有指定这个方法,那么会抛出一个异常。

随机请求头中间件

爬虫在频繁访问一个页面的时候,这个请求头如果一直保持一致,那么很容易被服务器发现,从而禁止掉这个请求头的访问。因此我们要在访问这个页面之前随机的更改请求头,这样才可以减小爬虫被抓的几率。随机更改请求头,可以在下载中间件中实现,在请求发送给服务器之前,随机的选择一个请求头,这样就可以避免总是使用一个请求头了,代码如下:

1
2
3
4
5
6
7
8
9
10
class UserAgentDownloadMiddleware(object):
User_Agents =[
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
' Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'
]
def process_request(self,request,spider):
user_agent = random.choice(self.User_Agents)
request.headers['User-Agent'] = user_agent

user-agent列表网站 http://useragentstring.com/pages/useragentstring.php?name=All

ip代理池中间件

购买代理:

在以下代理商中购买代理:

  1. 芝麻代理
  2. 太阳代理
  3. 快代理
  4. 讯代理
  5. 蚂蚁代理

使用ip代理池