Python爬虫 7.Scrapy多页抓取与数据保存实战


一、实战目标说明

前面的文章中,我们已经学习了:

  • 如何使用 requestsurllib 发送请求
  • 如何使用 XPath、BeautifulSoup 提取数据
  • 如何使用 Scrapy 创建项目、编写 Spider、运行爬虫

但是这些知识点还停留在“框架基础”的阶段。

这一篇文章,我们要真正做一个可以直接运行的 Scrapy 实战项目,把前面学过的知识串起来。

本篇的目标非常明确:

  • 抓取网站列表页中的数据
  • 实现自动翻页抓取
  • 提取页面中的结构化字段
  • 将抓取结果导出为 JSON 文件

为了保证案例稳定、代码真实可运行,本篇选择的网站是:

https://quotes.toscrape.com

这个网站本身就是专门为爬虫练习准备的测试站点,特点是:

  • 页面结构清晰
  • 没有复杂反爬
  • 分页规则明显
  • 非常适合用来学习 Scrapy

本篇最终要完成的结果是:

  • 抓取所有分页中的名言数据
  • 提取每条名言的文本、作者和标签
  • 导出到 result.json 文件中

也就是说,学完这一篇后,你不只是“知道 Scrapy 怎么写”,而是已经能够完成一个完整的 Scrapy 抓取项目。


二、分析目标网站结构

在写爬虫之前,最重要的不是马上写代码,而是先分析页面结构。

打开首页后,可以观察到几个非常关键的信息。

1. 页面中的数据结构

首页中每一条名言都在一个 div.quote 区块中。

每个区块里包含:

  • 名言文本
  • 作者名称
  • 多个标签

也就是说,我们后面可以先定位每一个 quote 区块,再从区块内部继续提取字段。

2. 分页结构

页面底部有一个 Next 按钮。

点击之后,URL 会发生变化:

第一页:

https://quotes.toscrape.com/

第二页:

https://quotes.toscrape.com/page/2/

第三页:

https://quotes.toscrape.com/page/3/

这说明分页有两种可行做法:

  • 直接提取页面中的“下一页”链接
  • 人工拼接分页 URL

在真实爬虫中,优先提取页面中的下一页链接更稳,因为它更贴近网站本身的导航结构。

3. 需要提取的数据字段

本篇准备提取这三个字段:

  • text:名言内容
  • author:作者
  • tags:标签列表

这三个字段足够完整,既能演示 XPath 提取,也能展示 JSON 导出结果。

4. 建议先用浏览器检查 XPath

在正式写 Spider 之前,建议先打开浏览器开发者工具,查看页面节点结构。

例如,单条名言的大致 HTML 结构如下:

<div class="quote">
    <span class="text">...</span>
    <span>
        <small class="author">...</small>
    </span>
    <div class="tags">
        <a class="tag">...</a>
        <a class="tag">...</a>
    </div>
</div>

根据这个结构,我们后面可以很自然地写出这些 XPath:

  • //div[@class="quote"]:获取所有名言区块
  • ./span[@class="text"]/text():提取当前区块中的名言文本
  • ./span/small[@class="author"]/text():提取作者
  • ./div[@class="tags"]/a/text():提取标签列表

只要这一步分析清楚,后面的代码就会顺很多。


三、创建项目与爬虫

下面正式开始写 Scrapy 项目。

1. 创建项目

在终端中执行:

scrapy startproject scrapy_demo

这条命令会创建一个名为 scrapy_demo 的 Scrapy 项目。

创建完成后,进入项目目录:

cd scrapy_demo

2. 创建爬虫文件

继续执行:

scrapy genspider quotes quotes.toscrape.com

这条命令的作用是:

  • 创建一个名为 quotes 的 Spider
  • 默认允许访问的域名设置为 quotes.toscrape.com

生成后的文件路径通常是:

scrapy_demo/spiders/quotes.py

3. 项目目录说明

此时项目目录大致如下:

scrapy_demo/
├── scrapy_demo/
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders/
│       ├── __init__.py
│       └── quotes.py
└── scrapy.cfg

这里面和本篇最相关的文件有三个:

  • spiders/quotes.py:写爬虫逻辑
  • items.py:定义数据结构
  • settings.py:项目配置

本篇先聚焦在 Spider 和 Item,数据导出先使用 Scrapy 自带命令完成。


四、编写基础抓取逻辑

这一节先不急着做分页,先把“抓取一页数据”跑通。

打开 scrapy_demo/spiders/quotes.py,把代码改成下面这样:

# 导入 scrapy
import scrapy


# 定义 Spider 类
class QuotesSpider(scrapy.Spider):
    # 爬虫名称
    name = 'quotes'

    # 限制允许访问的域名
    allowed_domains = ['quotes.toscrape.com']

    # 爬虫启动时首先访问的地址
    start_urls = ['https://quotes.toscrape.com/']

    # 默认解析函数
    def parse(self, response):
        # 获取当前页面中所有名言区块
        quotes = response.xpath('//div[@class="quote"]')

        # 遍历每一个名言区块
        for q in quotes:
            # 提取名言文本
            text = q.xpath('./span[@class="text"]/text()').extract_first()

            # 提取作者名称
            author = q.xpath('./span/small[@class="author"]/text()').extract_first()

            # 提取标签列表
            tags = q.xpath('./div[@class="tags"]/a/text()').extract()

            # 打印结果,方便先测试是否提取成功
            print(text)
            print(author)
            print(tags)
            print('-' * 50)

代码说明

这段代码的执行逻辑如下:

  1. Scrapy 启动后访问 start_urls 中的首页
  2. 页面下载完成后,自动进入 parse()
  3. response.xpath('//div[@class="quote"]') 先取到当前页所有名言区块
  4. 然后逐条遍历每个区块
  5. 在当前区块内部继续提取 textauthortags
  6. 把提取结果打印出来

为什么 XPath 前面要写 ./

这里有一个细节很重要:

q.xpath('./span[@class="text"]/text()')

前面的 ./ 表示:

  • 从当前 q 这个区块内部继续查找
  • 而不是从整个页面重新开始查找

这样可以保证每一条数据都对应当前区块,避免取错内容。

先运行测试

在项目根目录下运行:

scrapy crawl quotes

如果终端能正常打印出名言、作者和标签,说明单页抓取已经成功。

这一阶段的目标只有一个:

先确认 XPath 正确,先把一页抓通。


五、实现分页抓取(核心)

单页抓通之后,下一步就是本篇最关键的部分:分页抓取。

如果只抓首页,那这个爬虫只能拿到第一页的数据。真正的 Scrapy 实战,核心就在于:

当前页抓完后,自动进入下一页,直到没有下一页为止。

quotes.py 修改为下面的版本:

# 导入 scrapy
import scrapy


# 定义 Spider 类
class QuotesSpider(scrapy.Spider):
    # 爬虫名称
    name = 'quotes'

    # 限制允许访问的域名
    allowed_domains = ['quotes.toscrape.com']

    # 起始页面
    start_urls = ['https://quotes.toscrape.com/']

    # 解析函数
    def parse(self, response):
        # 获取当前页所有名言区块
        quotes = response.xpath('//div[@class="quote"]')

        # 遍历当前页的每一条数据
        for q in quotes:
            # 提取名言内容
            text = q.xpath('./span[@class="text"]/text()').extract_first()

            # 提取作者名称
            author = q.xpath('./span/small[@class="author"]/text()').extract_first()

            # 提取标签列表
            tags = q.xpath('./div[@class="tags"]/a/text()').extract()

            # 返回结构化数据
            yield {
                'text': text,
                'author': author,
                'tags': tags,
            }

        # 提取下一页相对链接
        next_page = response.xpath('//li[@class="next"]/a/@href').extract_first()

        # 如果下一页存在,则继续发请求
        if next_page:
            # 把相对路径拼接成完整地址
            next_url = response.urljoin(next_page)

            # 继续请求下一页,并仍然交给 parse() 处理
            yield scrapy.Request(url=next_url, callback=self.parse)

这一版和上一版的区别

最核心的变化有两个:

1. 不再只 print(),而是 yield 数据

yield {
    'text': text,
    'author': author,
    'tags': tags,
}

这表示:

  • 当前提取到的数据不只是打印到控制台
  • 而是作为 Scrapy 的输出结果返回
  • 后面可以直接导出到 JSON 文件

2. 加入了分页逻辑

next_page = response.xpath('//li[@class="next"]/a/@href').extract_first()

这行代码用于获取“下一页”链接。

在这个网站里,返回的通常是相对路径,例如:

/page/2/

所以还需要:

next_url = response.urljoin(next_page)

把它拼接成完整地址,例如:

https://quotes.toscrape.com/page/2/

最后:

yield scrapy.Request(url=next_url, callback=self.parse)

这行代码表示:

  • 继续请求下一页
  • 请求成功后,仍然交给当前的 parse() 函数处理

这样就形成了一个递归流程:

  • 当前页抓取数据
  • 找下一页
  • 请求下一页
  • 下一页再重复同样逻辑

只要页面里还存在下一页链接,Scrapy 就会继续往后抓。

为什么这就是分页抓取的核心

很多初学者学 Scrapy 时,最难理解的就是:

为什么一个函数里既能产出数据,又能继续翻页?

原因就在于 yield

在 Scrapy 里,yield 可以做两件事:

  • yield item:把数据交出去
  • yield Request(...):把新的请求交出去

所以同一个 parse() 里,既可以:

  • 提取当前页的数据
  • 又可以继续生成下一页请求

这就是 Scrapy 特别适合做分页抓取的地方。


六、使用 Item(可选但推荐)

在上面的分页代码里,我们直接使用了字典:

yield {
    'text': text,
    'author': author,
    'tags': tags,
}

这种写法完全可以运行,入门也很方便。

但是当项目逐渐复杂后,更推荐使用 Item 来定义数据结构。

1. 在 items.py 中定义字段

打开 scrapy_demo/items.py,修改为:

# 导入 scrapy
import scrapy


# 定义名言数据结构
class QuoteItem(scrapy.Item):
    # 名言文本
    text = scrapy.Field()

    # 作者名称
    author = scrapy.Field()

    # 标签列表
    tags = scrapy.Field()

2. 在 Spider 中使用 Item

然后修改 quotes.py

# 导入 scrapy
import scrapy

# 导入自定义 Item
from scrapy_demo.items import QuoteItem


# 定义 Spider 类
class QuotesSpider(scrapy.Spider):
    # 爬虫名称
    name = 'quotes'

    # 限制允许访问的域名
    allowed_domains = ['quotes.toscrape.com']

    # 起始地址
    start_urls = ['https://quotes.toscrape.com/']

    # 解析函数
    def parse(self, response):
        # 获取当前页所有名言区块
        quotes = response.xpath('//div[@class="quote"]')

        # 遍历每一条数据
        for q in quotes:
            # 创建 Item 对象
            item = QuoteItem()

            # 提取名言内容
            item['text'] = q.xpath('./span[@class="text"]/text()').extract_first()

            # 提取作者名称
            item['author'] = q.xpath('./span/small[@class="author"]/text()').extract_first()

            # 提取标签列表
            item['tags'] = q.xpath('./div[@class="tags"]/a/text()').extract()

            # 返回 Item
            yield item

        # 提取下一页链接
        next_page = response.xpath('//li[@class="next"]/a/@href').extract_first()

        # 如果有下一页,则继续抓取
        if next_page:
            # 拼接完整地址
            next_url = response.urljoin(next_page)

            # 继续发送请求
            yield scrapy.Request(url=next_url, callback=self.parse)

3. 为什么推荐 Item

使用 Item 的好处主要有三个:

  • 字段更明确
  • 数据结构更统一
  • 后续对接 Pipeline、数据库时更方便

所以对于简单案例,字典和 Item 都可以;但从工程化角度看,Item 更推荐。


七、导出 JSON 文件

现在 Spider 已经可以正常抓取多页数据了,下一步就是把结果保存下来。

Scrapy 最方便的一种方式,就是直接在命令行中导出 JSON。

运行命令:

scrapy crawl quotes -o result.json

这条命令做了什么

它会同时完成两件事:

  1. 运行 quotes 这个爬虫
  2. yield 返回的数据写入 result.json

也就是说,只要你的 Spider 中确实有:

yield item

或者:

yield {
    ...
}

那么 Scrapy 就可以自动把这些结果导出为 JSON 文件。

这种方式为什么很适合入门

因为它有几个明显优点:

  • 不需要先写 Pipeline
  • 不需要先连接数据库
  • 可以先把“抓到数据”这件事跑通

对于初学者来说,这种方式非常友好。

关于文件覆盖的提醒

如果 result.json 已经存在,重新运行时建议先检查原文件内容。

在实际使用中,最好明确知道当前导出是:

  • 新生成文件
  • 还是继续写入已有文件

这样可以避免测试多次后把旧数据和新数据混在一起。


八、运行爬虫与结果查看

完整运行命令如下:

scrapy crawl quotes -o result.json

执行之后,终端中会输出 Scrapy 的运行日志。

1. 运行日志里通常能看到什么

一般会看到这些信息:

  • Scrapy 版本和环境信息
  • Spider 启动信息
  • 请求发送日志
  • 响应状态码
  • 爬虫结束统计信息

这些日志非常重要,因为当爬虫出问题时,第一步通常就是先看日志。

2. 查看结果文件

运行完成后,项目目录下会生成一个 result.json 文件。

打开后,内容大致如下:

[
  {
    "text": "The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.",
    "author": "Albert Einstein",
    "tags": [
      "change",
      "deep-thoughts",
      "thinking",
      "world"
    ]
  },
  {
    "text": "It is our choices, Harry, that show what we truly are, far more than our abilities.",
    "author": "J.K. Rowling",
    "tags": [
      "abilities",
      "choices"
    ]
  }
]

3. 如何判断分页是否真的成功

你可以通过这几个方式判断:

  • result.json 中是否有很多条数据,而不是只几条
  • 运行日志里是否访问了 /page/2//page/3/ 等链接
  • 数据量是否明显超过一页的数量

如果这些都满足,说明分页抓取已经成功。


九、常见问题

Scrapy 实战中,最常见的问题通常不是“代码不能写”,而是“代码运行后结果不对”。

这一节整理几个本篇最可能遇到的问题。

1. 只抓取到一页数据

原因

通常是因为分页逻辑没有写完整,例如:

  • 没有提取到 next_page
  • 没有写 yield scrapy.Request(...)
  • 下一页 URL 拼接错误

检查方法

可以先打印:

print(next_page)

看看当前页是否真的拿到了下一页链接。

如果拿到了相对路径,再检查:

next_url = response.urljoin(next_page)

是否拼接出了正确的完整地址。

2. XPath 提取为空

原因

通常是 XPath 路径写错,或者没有从当前节点内部提取。

解决方式

建议使用:

scrapy shell https://quotes.toscrape.com/

进入调试环境后测试:

response.xpath('//div[@class="quote"]')
response.xpath('//div[@class="quote"]/span[@class="text"]/text()').extract()

先确认表达式能不能提取出结果,再写回 Spider。

3. JSON 文件为空

原因

最常见的原因有两个:

  • Spider 中没有 yield 数据
  • 提取逻辑失败,结果根本没有生成

例如,如果你只写了:

print(text)

而没有:

yield item

那么 Scrapy 是不会把结果写进 JSON 文件的。

4. 爬虫没有进入下一页

原因

可能是:

  • next_page 没取到
  • 条件判断没有成立
  • callback=self.parse 没写对

建议

可以在分页部分临时加打印:

print('next_page =', next_page)

这样可以非常直观地看到当前页有没有拿到下一页链接。

5. 结果字段不完整

例如:

  • author 为空
  • tags 少了

这通常意味着字段 XPath 写得不够准确。

解决方式依然是:

  • 先在浏览器里看结构
  • 再用 scrapy shell 验证
  • 最后再写进代码

十、多项目练习中的共同套路

不同网站的页面结构不一样,但 Scrapy 项目的处理流程基本是相同的。把多个练习项目放在一起看,可以总结出一套通用套路。

1. 列表页抓详情页

很多网站都是列表页展示多条数据,详情页展示完整内容。

处理方式通常是:

def parse(self, response):
    detail_urls = response.xpath('//a[@class="detail"]/@href').getall()

    for detail_url in detail_urls:
        yield response.follow(detail_url, callback=self.parse_detail)


def parse_detail(self, response):
    yield {
        'title': response.xpath('//h1/text()').get(),
        'url': response.url,
    }

这种写法里,parse() 负责从列表页提取详情页链接,parse_detail() 负责提取具体字段。

2. 列表页直接提取数据

有些页面的字段在列表页已经足够完整,就可以直接在 parse() 中提取。

def parse(self, response):
    rows = response.xpath('//div[@class="item"]')

    for row in rows:
        yield {
            'name': row.xpath('./h2/text()').get(),
            'price': row.xpath('./span/text()').get(),
        }

这里要注意 ./ 的使用,它表示从当前节点继续往下找,而不是从整个页面重新查找。

3. 分页的两种写法

分页通常有两种方式。

第一种是提取页面上的下一页链接:

next_page = response.xpath('//a[text()="下一页"]/@href').get()

if next_page:
    yield response.follow(next_page, callback=self.parse)

第二种是根据页码规律构造 URL:

for page in range(1, 11):
    url = f'https://example.com/list/{page}.html'
    yield scrapy.Request(url=url, callback=self.parse)

如果页面上有稳定的下一页按钮,优先用第一种;如果页面 URL 规律很清晰,可以使用第二种。

4. 输出文件的选择

练习阶段可以直接导出 JSON:

scrapy crawl demo -O result.json

如果数据量大,或者后续需要给其他系统处理,可以考虑:

输出方式 适用场景
JSON 学习、调试、结构化数据保存
CSV 表格类数据、后续用 Excel 查看
Pipeline 需要清洗、去重、写数据库

多项目练习时,不要每个项目都重新设计一套结构。可以先固定这几个步骤:

分析页面 -> 写 XPath -> 运行 scrapy shell -> 编写 Spider -> 导出 JSON -> 检查字段

流程稳定后,再根据具体网站做调整。

十一、本篇小结

这一篇我们已经完成了一个完整、可运行的 Scrapy 实战项目。

从流程上看,我们完成了:

  • 目标网站分析
  • 单页数据提取
  • 自动分页抓取
  • 使用 Item 规范字段
  • 导出 JSON 文件
  • 结果检查与问题排查

更重要的是,这一篇把前面学到的几个知识点真正串起来了:

  • XPath 提取
  • Scrapy Spider
  • yield 返回数据
  • yield scrapy.Request(...) 发起下一页请求
  • 命令行导出 JSON

学完这篇之后,你已经不只是“会写一个 Spider”,而是已经能够完成一个最基础但完整的 Scrapy 项目。

接下来更适合继续深入的方向包括:

  • 使用 Pipeline 保存数据
  • 把数据写入数据库
  • 使用 CrawlSpider 自动跟踪链接
  • 对接更复杂的网站结构

如果前面这篇基础篇是“学会 Scrapy 怎么用”,那么这一篇就是“真正把 Scrapy 跑起来”。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录