Scrapy 框架学习笔记

Scrapy 是一个基于 Python 的快速的、高级别的 web 爬虫和 web 抓取框架,旨在用于抓取 web 站点并从页面中提取结构化的数据。Scrapy 可以应用于一系列的网路数据抓取任务,从信息提取、数据挖掘到监控和自动化测试。

以下是笔者根据官方文档和 B 站上的教程写的一份学习笔记。


Scrapy 核心组件

  • 爬虫(Spiders):这些是遍历网络收集数据的网络爬虫。它们向网站发送请求并解析响应。
  • 调度器(Scheduler):此组件管理爬虫爬行网页的顺序。它安排爬虫发出的请求,通常使用算法来优先决定访问哪些页面。
  • 下载器(Downloader):请求被调度后,发送给下载器,下载器从互联网上获取网页。下载器处理对网服务器的实际 HTTP 请求。
  • 引擎(Engine):引擎是系统的核心部分,协调整个过程。它控制系统组件之间的数据流动,并触发事件。
  • 中间件(Middleware):中间件组件用于处理系统中传递的请求和响应。它们可以修改、丢弃或丰富请求和响应。例如,它们可以处理用户代理轮换、代理管理或内容转换等事宜。
  • 项目管道(Item Pipelines):数据被爬取后,需要进行处理、清洗和存储。项目管道负责在爬虫提取数据后处理数据项。

Scrapy 框架工作流:

  1. 爬虫向引擎发送请求(1)。

  2. 引擎将请求发送给调度器(2)。

  3. 调度器将请求排队,然后发送回引擎(3)。

  4. 引擎再将请求发送至下载器(4)。

  5. 下载器从互联网获取数据,并将响应发送回引擎(5)。

  6. 引擎将响应发送给爬虫(6)。

  7. 爬虫处理响应并提取项目,这些项目被发送到项目管道(7)。

  8. 处理过的项目然后从项目管道发送到引擎(8),可能用于进一步处理或存储。

安装库并新建一个 Scrapy 项目

安装 scrapy

1
pip install scrapy

新建 Scrapy 项目

1
scrapy startproject project_name

使用 scrapy startproject 后就会在当前工作目录下新建一个 Scrapy 项目,项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
project_name/
├── scrapy.cfg
└── project_name
├── __init__.py
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
├── __init__.py
├── spider1.py
└── spider2.py

这个结构图展示了一个典型的Scrapy项目的文件和目录层次,其中:

  • scrapy.cfg 是项目的部署配置文件。
  • myproject 文件夹是项目的Python模块,您的代码会在这里编写和组织。
  • items.py 中定义了项目中的数据结构。
  • middlewares.py 中定义了项目的中间件。
  • pipelines.py 中定义了项目的管道,用于处理抓取的数据。
  • settings.py 包含了项目的设置。
  • spiders 目录包含了所有爬虫相关的代码。

spiders目录下,每个文件代表了一个爬虫。您可以根据需要添加更多的爬虫文件。每个爬虫文件定义了一个Spider类,用于指定如何抓取网页和解析页面内容以提取数据。

scrapy 命令行工具

除了上文提到的创建项目指令,Scrapy 还提供了一系列其他命令:

  • 在当前项目下创建一个新的 Spider

    1
    scrapy genspider spider_name 'example.com'

  • 运行当前项目下的某个 Spider

    1
    scrapy crawl spider_name

  • 列出当前所有的 Spider

    1
    scrapy list

  • 启动一个交互式的 Scrapy Shell,可以在这里尝试抓取数据

    1
    scrapy shell 'http://example.com'

  • 下载一个网页并且显示在控制台上:

    1
    scrapy fetch 'http://example.com'

  • 在浏览器中打开一个网页,方便进行可视化调试

    1
    scrapy view 'http://example.com'

  • 使用 Spider 解析一个网页,这对于测试和调试爬虫非常有用

    1
    scrapy parse 'http://example.com/page' --callback parse_item

    这将使用 parse_item 方法来解析指定的 URL

Spider

在 Scrapy 框架中,Spider 类是定义如何抓取某个或某些网站的关键组成部分。这包括抓取的动作,例如:访问网站的哪些页面,如何进行跟进链接,以及如何从页面内容中提取结构化数据(即抓取什么数据)。每个 Spider 都能处理一个或多个特定的网站或页面。

基本属性
  • name :识别 Spider 的唯一名称。这个名称在项目中必须是唯一的,即不能为不同的 Spider 设置相同的名字。
  • start_url :包含一个或多个 URL 的列表,Spider 将从这些 URL 开始抓取数据。当没有指定特定的 URL 时,Spider 将默认从这个列表中的 URL 开始进行抓取。
  • allowed_domains :可选属性,包含一个域名的列表,用于指定 Spider 可以抓取的域名。如果设置了这个属性,Spider 将不会抓取这个列表以外的域名下的页面。
基本方法
  • start_requests() :此方法用于生成初始的请求。您可以重写这个方法来实现自定义的请求初始化。默认情况下,它生成 start_urls 中定义的 URL 的请求。
  • parse()当响应返回时,Scrapy 下载器将响应作为参数传递给此方法。这是处理响应并提取数据的主要方法,或者进一步生成要跟踪的 URL 的请求。
工作方式
  1. Spiderstart_urls 中指定的 URL 开始抓取。
  2. 对每个起始 URL,Scrapy 生成 Request 对象,并将 parse 方法作为回调函数指定。
  3. 当页面被下载后,Scrapy 引擎调用 parse 方法处理响应。
  4. parse 方法内部,您可以使用响应对象来提取信息,或者查找新的 URL 来创建新的请求(Scrapy 会处理这些请求,调用相应的回调)。
  5. 通常情况下,parse 方法将解析响应数据,提取信息并用 yield 语句生成 Item 对象,或者生成新的 Request 对象。
基本示例

爬取豆瓣电影排名前 250 的电影的名字和评分:

  1. 创建项目:

    1
    scrapy startproject Douban && cd Douban

  2. 编写 item.py

    爬虫获取到的数据需要组装成 Item 对象,所以我们需要预先定义封装数据的 Item 对象:

    1
    2
    3
    4
    5
    import scrapy

    class MovieItem(scrapy.Item):
    title = scrapy.Field()
    rating = scrapy.Field()

  3. 创建并编写一个 Spider 来爬取电影:

    1
    scrapy genspider Movie 'Movie.douban.com'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import scrapy
    from scrapy import Selector, Request
    from Douban.items import MovieItem

    class MovieSpider(scrapy.Spider):
    name = "Movie"
    allowed_domains = ["movie.douban.com"]
    start_urls = ["https://movie.douban.com/top250"]

    def parse(self, response):
    sel = Selector(response)
    list_items = sel.css('#content > div > div.article > ol > li') # 根据 Selector 进行捕获
    for item in list_items:
    movieItem = MovieItem()
    movieItem['title'] = item.css('span.title::text').extract_first()
    movieItem['rating'] = item.css('span.rating_num::text').extract_first()

    yield movieItem

    hrefs_list = sel.css('#content > div > div.article > div.paginator > a::attr(href)').extract() # 获取底部其他页的链接
    next_pages = [response.urljoin(href) for href in hrefs_list if href != '?start=0&filter=']
    for url in next_pages:
    yield Request(url=url)

    这里 Selector 是一个非常重要的组件,我们传递响应作为参数可以初始化一个 Selector 对象,其可以使用 XPath 或 CSS 表达式来选择 HTML 或 XML 中的特定部分。

  4. 将抓取的数据保存:

    1
    2
    3
    scrapy crawl Movie -o top250_movie.csv # 保存为 csv 格式
    scrapy crawl Movie -o top250_movie.json # 保存为 json 格式
    scrapy crawl Movie -o top250_movie.xml # 保存为 xml 格式

另外一种 Spider 的定义方式,通过定义 start_requests() 函数来获取到更多页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import scrapy
from scrapy import Selector, Request
from Douban.items import MovieItem

class MovieSpider(scrapy.Spider):
name = "Movie"
allowed_domains = ["movie.douban.com"]

def start_requests(self):
for page in range(10):
yield Request(url=f'https://movie.douban.com/top250?start={25 * page}&filter=')

def parse(self, response):
sel = Selector(response)
list_items = sel.css('#content > div > div.article > ol > li') # 根据 Selector 进行捕获
for item in list_items:
movieItem = MovieItem()
movieItem['title'] = item.css('span.title::text').extract_first()
movieItem['rating'] = item.css('span.rating_num::text').extract_first()

yield movieItem

这样定义可以避免同一个网页 URL 相同而导致重复请求的情况,而且保证了请求的次序不被打乱。

定义多个回调函数

假设我们现在要请求 HTML 网页列表的子链接的内容,并对子链接的网页进行爬取,那我们就需要定义多个回调函数。

示例

  1. 分析chinaz.com网站行业网站页面结构(链接:https://top.chinaz.com/hangye/)

  2. 爬取每个行业分类下面的网站信息,包括网站标题、网站关键词、网站描述信息。

  3. 将爬取结果存取成 csv 格式,每行的数据格式为:网站域名,网站行业分类、网站标题、网站关键词、网站描述信息

1
2
3
4
5
6
7
8
9
10
# Item 对象定义

import scrapy

class HangyeItem(scrapy.Item):
domain = scrapy.Field()
web_class = scrapy.Field()
title = scrapy.Field()
key_words = scrapy.Field()
description = scrapy.Field()
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
32
33
34
35
36
37
38
# Spider 定义

import scrapy
from scrapy import Request, Selector
from ChinaZ.items import HangyeItem

class HangyeSpider(scrapy.Spider):
name = "Hangye"
allowed_domains = ["top.chinaz.com"]

def start_requests(self):
yield Request(url='https://top.chinaz.com/hangye/index.html')
for page in range(2, 1873):
yield Request(url=f'https://top.chinaz.com/hangye/index_{page}.html')

def parse(self, response):
sel = Selector(response)
items_list = sel.css('#content > div.Wrapper.TopIndexCentWrap.pt10 > div.TopListCent > div.TopListCent-listWrap > ul > li.clearfix > div.CentTxt')
for item in items_list:
title = item.css('h3 > a::text').extract_first()
domain = item.css('h3 > span::text').extract_first()
href = item.css('h3 > a::attr(href)').extract_first()

yield Request(url=response.urljoin(href), callback=self.parse_sublink, meta={
'title': title,
'domain': domain
})

def parse_sublink(self, response):
hangyeItem = HangyeItem()
hangyeItem['title'] = response.meta['title']
hangyeItem['domain'] = response.meta['domain']
sel = Selector(response)
hangyeItem['web_class'] = ', '.join(sel.css('#content > div.Wrapper.TopIndexCentWrap.pt10 > div > div.TopPageCent.clearfix > div.TPageCent-TopMain.mt10.clearfix > div > div > p:nth-child(1) > a::text').getall())
hangyeItem['description'] = sel.css('#content > div.Wrapper.TopIndexCentWrap.pt10 > div > div.TopPageCent.clearfix > div.TPageCent-TMain01.mb40 > div:nth-child(2) > div.Centright.fr.SimSun > p::text').extract_first()
hangyeItem['key_words'] = ', '.join(sel.css('#content > div.Wrapper.TopIndexCentWrap.pt10 > div > div.TopPageCent.clearfix > div.clearfix.mb40 > div.TPageCent-TMainmob.fl > ul > li:not(.ListHead) > span.Lnone::text').getall())

yield hangyeItem

运行:

1
scrapy crawl Hangye -o cn_web_info.csv

数据管道

Scrapy 中的数据管道(Data Pipeline)是用于处理从网页中提取出的数据的组件。它们提供了一个清晰的方式来处理、过滤和存储爬虫抓取的数据。

要在 Scrapy 项目中使用数据管道,你需要在 settings.py 文件中启用并配置它:

1
2
3
ITEM_PIPELINES = {
"MyProject.pipelines.MyProjectPipeline": 300
}

每个数据管道都是一个 Python 类,定义在 pipelines.py 中,它实现了一个或多个方法,这个方法会被 Scrapy 自动调用来处理每一个提取的项(Item)。

例如,将爬取到的数据保存到数据库:

1
2
3
4
5
6
# setting.py
# ...

ITEM_PIPELINES = {
"Douban.pipelines.DoubanPipeline": 300
}
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
32
33
34
# piplines.py

from itemadapter import ItemAdapter
import mysql.connector

class DoubanPipeline:
def __init__(self):
try:
self.my_db = mysql.connector.connect(
host='localhost',
user='root',
passwd='',
database='movie'
)
self.cs = self.my_db.cursor()
self.cs.execute('truncate table top250_mv')
except mysql.connector.Error as err:
print(err)

def process_item(self, item, spider):
mv_name, rating = item['title'], item['rating']
try:
self.cs.execute('insert into top250_mv(name, rating) values(%s, %s)', (mv_name, rating))
self.my_db.commit()
return item
except mysql.connector.Error as err:
print(err)

def close_spider(self, spider):
try:
self.cs.close()
self.my_db.close()
except mysql.connector.Error as err:
print(err)

效果:

设置多个管道

在 Scrapy 的 ITEM_PIPELINES 设置中,数字(如上文例子中的 300)表示该管道的优先级。Scrapy 支持同时定义多个管道,并且通过这个优先级数字来决定各个管道的执行顺序。优先级的范围是从 0 到 1000。

  • 低数字表示高优先级:数字越小,管道的优先级越高。这意味着优先级数字较低的管道会先于优先级数字较高的管道处理 Item。

  • 执行顺序:当一个 Item 被爬虫抓取并传递给 Item Pipeline 时,它会按照由低到高的优先级顺序经过每个激活的管道。

例如,假设你有以下的设置:

1
2
3
4
5
ITEM_PIPELINES = {
'MyProject.pipelines.MyFirstPipeline': 300,
'MyProject.pipelines.MySecondPipeline': 400,
'MyProject.pipelines.MyThirdPipeline': 100
}

在这个例子中,MyThirdPipeline(优先级为 100)将首先处理 Item,其次是 MyFirstPipeline(优先级为 300),最后是 MySecondPipeline(优先级为 400)。

这种机制允许开发者灵活地控制不同管道处理 Item 的顺序,这在进行复杂的数据处理时非常有用,例如,一个管道可能负责清洗数据,另一个管道可能负责将数据存储到数据库中。通过调整它们的优先级,可以确保数据在存储之前被正确地清洗。

中间件的使用

Scrapy 中的中间件是一种可插拔的组件,用于在请求和响应的处理过程中执行自定义的操作。在 Scrapy 中有两种主要类型的中间件:下载器中间件(Downloader Middleware)和蜘蛛中间件(Spider Middleware)

  • 下载中间件

    下载器中间件用于处理 Scrapy 发出的 HTTP 请求和收到的 HTTP 响应。它在请求从蜘蛛发送到服务器和响应从服务器返回到蜘蛛的过程中起作用。下载器中间件的常见用途包括:

    • 修改发出的请求:比如设置代理、添加或修改请求头、重定向请求等。
    • 处理返回的响应:比如处理重定向或错误的响应、修改响应内容等。
    • 处理请求异常:比如在连接错误或超时时重试请求。

    主要方法

    • process_request(request, spider) :在请求被发送到下载器之前调用。
    • process_response(request, response, spider) :在下载器完成 HTTP 请求,返回响应之后调用。
    • process_exception(request, exception, spider) :当下载处理过程中出现异常时调用。

    例如,写一个中间件修改所有的请求头:

    1
    2
    3
    4
    5
    6
    7
    8
    class MyDownloaderMiddleware:
    def process_request(self, request, spider):
    request.headers['User-Agent'] = 'My Custom User Agent'
    return None # 返回 None 继续正常处理

    def process_response(self, request, response, spider):
    # 可以在这里处理响应,例如检查状态码
    return response # 必须返回 Response 对象

    通常,我们可以在请求中间件内给请求加上 Cookie,从而获得某些权限。

  • 蜘蛛中间件

    蜘蛛中间件用于处理蜘蛛的输入(响应)和输出(提取的项目和新的请求)。它主要在解析响应和生成提取项或进一步请求的过程中起作用。蜘蛛中间件的常见用途包括:

    • 修改或丢弃蜘蛛接收到的响应。
    • 修改蜘蛛产生的提取项。
    • 修改或丢弃蜘蛛产生的新的请求。

    主要方法

    • process_spider_input(response, spider) :在蜘蛛解析响应之前调用。
    • process_spider_output(response, result, spider) :在蜘蛛解析响应并返回结果之后调用。
    • process_spider_exception(response, exception, spider) :当解析过程中出现异常时调用。
    • process_start_requests(start_requests, spider) :在蜘蛛开始发出初始请求时调用。

    例如,写一个中间件过滤掉某些响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MySpiderMiddleware:
    def process_spider_input(self, response, spider):
    if '不需要处理的条件' in response.text:
    raise IgnoreRequest()
    return None

    def process_spider_output(self, response, result, spider):
    for i in result:
    yield i

要启用中间,需要在 settings.py 中进行配置:

1
2
3
4
5
6
7
DOWNLOADER_MIDDLEWARES = {
'MyProject.middlewares.MyCustomDownloaderMiddleware': 543,
}

SPIDER_MIDDLEWARES = {
'MyProject.middlewares.MyCustomSpiderMiddleware': 543,
}

这里的数字表示中间件的优先级,数字越小优先级越高。


Scrapy 框架学习笔记
https://goer17.github.io/2023/11/20/Scrapy 框架学习笔记/
作者
Captain_Lee
发布于
2023年11月20日
许可协议