一、实战目标说明
前面的文章中,我们已经学习了:
- 如何使用
requests和urllib发送请求 - 如何使用 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)
代码说明
这段代码的执行逻辑如下:
- Scrapy 启动后访问
start_urls中的首页 - 页面下载完成后,自动进入
parse() response.xpath('//div[@class="quote"]')先取到当前页所有名言区块- 然后逐条遍历每个区块
- 在当前区块内部继续提取
text、author、tags - 把提取结果打印出来
为什么 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
这条命令做了什么
它会同时完成两件事:
- 运行
quotes这个爬虫 - 把
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 跑起来”。