写在前面: 不少朋友还在用
requests
+BeautifulSoup
手写爬虫,虽然灵活,但遇到复杂网站、需要异步或者数据持久化时,代码就容易变得臃肿难维护。Scrapy 作为一个为爬虫设计的框架,提供了一套完整的解决方案,能显著提升开发效率和项目健壮性。这篇文章就带大家从基础开始,一步步了解 Scrapy 的核心组件。掌握 Scrapy,对于需要高效获取网络数据的场景(比如数据分析、自动化任务等)会非常有帮助。
在日常开发或数据收集中,我们经常需要从网页上获取信息。如果只是简单几个页面,手动处理或者用基础库可能还行。但当面临以下情况时,传统方法的局限性就显现出来了:
requests
+ BeautifulSoup
的瓶颈:对于简单页面尚可,但面对大规模、多页面、需要登录、需要处理反爬、需要异步加速、需要结构化存储的复杂场景,代码量会急剧膨胀,逻辑混乱,难以维护和扩展。如果需要自己处理并发、队列、重试等机制,工作量会非常大。这时候,引入 Scrapy 框架就显得很有必要了。它提供了一整套的工具和机制,让我们能更专注于核心的爬取逻辑,把常见的工程问题交给框架处理。
Scrapy 的优势在于其清晰的架构设计和基于 Twisted 的异步处理能力。理解它的核心组件和数据流动方式,是掌握 Scrapy 的基础。
核心组件介绍:
数据流简述:
整个流程大致是:引擎从 Spider 获取初始请求,交给调度器;调度器取出请求给下载器;下载器下载网页后将响应通过中间件交给引擎,引擎再发送给 Spider 处理;Spider 解析响应,生成 Item 或新的 Request;Item 被发送到 Item Pipeline 处理,Request 被发送到调度器。这个过程利用 Twisted 异步执行,提高了爬取效率。
这种模块化的设计使得 Scrapy 非常灵活,可以方便地扩展或替换组件以适应特定需求。
理论部分了解后,我们来动手实践一下。以爬取 books.toscrape.com
(一个用于练习爬虫的网站)的书籍信息为例,看看如何用 Scrapy 完成这个任务。
环境准备:
首先,确保你安装了 Python (推荐 3.8 及以上版本)。然后通过 pip 安装 Scrapy:
# 如果你的网络访问 PyPI 较慢,可以考虑使用国内镜像源,例如:
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple scrapy
pip install scrapy
步骤1:创建Scrapy项目
打开终端或命令行界面,cd
到你打算存放项目的目录下,然后运行 Scrapy 命令:
scrapy startproject bookscraper
这个命令会自动生成一个名为 bookscraper
的项目骨架,目录结构大致如下:
bookscraper/
├── scrapy.cfg # 项目配置文件
└── bookscraper/ # 项目的Python模块
├── __init__.py
├── items.py # 定义数据结构 (Item)
├── middlewares.py # 中间件
├── pipelines.py # 数据处理管道 (Pipeline)
├── settings.py # 项目设置
└── spiders/ # 存放爬虫 (Spider)
└── __init__.py
步骤2:定义数据结构 (Item)
我们需要先定义好要从网页中提取哪些数据。打开项目内的 bookscraper/items.py
文件,创建一个继承自 scrapy.Item
的类,并为每个需要的数据项定义一个 scrapy.Field
:
# bookscraper/items.py
import scrapy
class BookscraperItem(scrapy.Item):
# 定义你想要抓取的字段
title = scrapy.Field() # 书名
price = scrapy.Field() # 价格 (我们只取数字)
rating = scrapy.Field() # 评分 (1-5星)
availability = scrapy.Field() # 库存状态
url = scrapy.Field() # 书籍详情页URL
# Field 对象本身比较简单,主要起声明作用,也可以在后续用于元数据定义等高级场景
明确地定义 Item 有助于代码的组织和后续数据处理流程。虽然 Scrapy 也允许直接传递字典 (dict) 作为数据项,但对于结构化的项目,推荐使用 Item。
步骤3:编写爬虫 (Spider)
爬虫是 Scrapy 中执行具体爬取和解析任务的核心组件。在项目 spiders
目录下创建一个名为 books_spider.py
的 Python 文件(或者使用命令 scrapy genspider books books.toscrape.com
来生成一个基础模板)。
# bookscraper/spiders/books_spider.py
import scrapy
from bookscraper.items import BookscraperItem # 导入我们定义的Item
class BooksSpider(scrapy.Spider):
# 爬虫的唯一标识,不能重复
name = "books"
# 允许爬取的域名范围,防止爬虫跑飞
allowed_domains = ["books.toscrape.com"]
# 爬虫启动时请求的URL列表
start_urls = ["http://books.toscrape.com/"]
# Scrapy为start_urls中的每个URL自动调用这个方法
# response 参数是下载器返回的网页内容
def parse(self, response):
self.logger.info(f"正在爬取页面: {response.url}") # 使用 Scrapy logger 记录日志
# === 核心:提取当前页面的书籍信息 ===
# 使用CSS选择器定位到包含每本书信息的 标签
books_on_page = response.css('article.product_pod')
if not books_on_page:
self.logger.warning(f"警告:在页面 {response.url} 未找到书籍信息!")
return # 如果没找到,提前退出
for book in books_on_page:
# 实例化一个Item对象,像字典一样操作
item = BookscraperItem()
# 提取书名 (获取下标签的title属性)
item['title'] = book.css('h3 a::attr(title)').get()
# 提取价格 (获取价格标签的文本,并用正则提取数字部分)
# .re_first() 对于只匹配第一个结果很有用
item['price'] = book.css('div.product_price p.price_color::text').re_first(r"£([\d.]+)")
# 提取评分 (评分存储在class名中,如"star-rating Three")
rating_map = {'One': 1, 'Two': 2, 'Three': 3, 'Four': 4, 'Five': 5}
rating_text = book.css('p.star-rating::attr(class)').re_first(r'star-rating (\w+)')
item['rating'] = rating_map.get(rating_text, 0) # 使用get避免KeyError
# 提取库存状态 (获取文本并去除首尾空白)
# .strip() 去除空白更简洁
item['availability'] = book.css('div.product_price p.instock.availability::text').get().strip()
# 提取书籍详情页URL (需要拼接成完整URL)
# response.urljoin() 能自动处理相对路径和绝对路径
item['url'] = response.urljoin(book.css('h3 a::attr(href)').get())
# === 关键一步:将提取到的Item交给Scrapy引擎 ===
# yield关键字很重要,它会将Item发送给后续的Pipeline处理
yield item
# --- 实用技巧:交互式调试 ---
# 在开发时,可以使用 Scrapy Shell:
# 在项目根目录运行: scrapy shell "http://books.toscrape.com/"
# 然后就可以在交互环境中测试选择器了:
# >>> response.css('h3 a::attr(title)').getall()
# >>> book = response.css('article.product_pod')[0]
# >>> book.css('p.price_color::text').get()
# 这能极大提高开发效率!
# === 核心:处理分页 ===
# 查找"下一页"按钮的链接
next_page_selector = response.css('li.next a::attr(href)')
if next_page_selector:
next_page_url = next_page_selector.get()
# 使用 response.follow() 可以更方便地处理相对URL,并自动创建Request
# 它会自动继承当前请求的某些属性
# 第一个参数是URL或选择器,第二个是回调函数(处理下一页响应的方法,这里还是用parse)
self.logger.info(f"发现下一页,准备爬取: {next_page_url}")
yield response.follow(next_page_url, callback=self.parse)
# --- 另一种写法(更明确):---
# next_page_full_url = response.urljoin(next_page_url)
# yield scrapy.Request(next_page_full_url, callback=self.parse)
else:
self.logger.info("已到达最后一页或未找到下一页链接。")
# --- 实用技巧:处理错误 ---
# 你可以为 Request 指定 errback 参数来处理下载过程中的错误
# def start_requests(self):
# for url in self.start_urls:
# yield scrapy.Request(url, callback=self.parse, errback=self.handle_error)
#
# def handle_error(self, failure):
# self.logger.error(f"请求失败: {failure.request.url}, 错误: {failure.value}") # 使用 Scrapy logger 记录错误
# # 在这里可以记录错误、重试等
代码说明:
name
, allowed_domains
, start_urls
: Spider 的基本配置属性。parse(self, response)
: 这是处理响应的核心方法。response
对象包含了下载器返回的网页内容及相关信息 (如 response.text
, response.body
, response.url
, response.status
等)。response.css()
和 response.xpath()
: Scrapy 内置的选择器方法,用于方便地从 HTML/XML 中提取数据。CSS 选择器通常更简洁。::text
获取元素的文本内容,::attr(attribute_name)
获取元素的属性值。.get()
返回第一个匹配结果的字符串,.getall()
返回所有匹配结果的字符串列表,.re_first()
使用正则表达式提取第一个匹配的子串。yield item
: 将提取并填充好的 BookscraperItem
对象发送给 Scrapy 引擎,引擎会将其传递给后续的 Item Pipeline 处理。yield response.follow()
或 yield scrapy.Request()
: 生成一个新的请求,用于爬取下一个页面或链接。callback
参数指定处理这个新请求响应的方法 (这里仍然是 self.parse
,用于递归爬取所有分页)。response.urljoin()
可以方便地将相对 URL 转换为绝对 URL。步骤4:实现数据管道 (Pipeline)
Spider 提取出 Item 后,通常需要进行进一步的处理或存储。Item Pipeline 就是负责这个任务的组件。这里我们实现一个简单的 Pipeline,将爬取到的书籍信息保存到 CSV 文件中。打开项目中的 bookscraper/pipelines.py
文件:
# bookscraper/pipelines.py
import csv
from itemadapter import ItemAdapter # 推荐使用ItemAdapter来访问Item字段,兼容Item和dict
import os # 用于创建目录
class CSVPipeline:
# Spider启动时调用一次
def open_spider(self, spider):
# 确保保存文件的目录存在
save_dir = 'output_data'
os.makedirs(save_dir, exist_ok=True) # exist_ok=True 避免目录已存在时报错
file_path = os.path.join(save_dir, f'{spider.name}_data.csv')
self.file = open(file_path, 'w', newline='', encoding='utf-8-sig') # utf-8-sig 避免Excel打开CSV乱码
# 创建CSV写入器
self.writer = csv.writer(self.file)
# 写入表头 (确保顺序与写入时一致)
# 从Item的fields属性动态获取表头,更健壮
if spider.name == 'books' and hasattr(spider, 'crawler'): # 确保是我们的books爬虫
# 需要通过crawler访问Item类定义
item_cls = spider.crawler.settings.get('ITEM_CLASSES', {}).get(spider.name)
if item_cls:
# 从 ItemAdapter 获取字段名列表
# 需要导入Item类才能访问fields
try:
# 动态导入Item类
from importlib import import_module
module_path, class_name = item_cls.rsplit('.', 1)
item_module = import_module(module_path)
actual_item_cls = getattr(item_module, class_name)
self.headers = list(actual_item_cls.fields.keys())
except (ImportError, AttributeError, ValueError):
spider.log("警告:无法动态加载Item类以获取表头,将使用硬编码表头。")
self.headers = ['title', 'price', 'rating', 'availability', 'url'] # 硬编码备用
else:
spider.log("警告:未在settings中找到ITEM_CLASSES配置,将使用硬编码表头。")
self.headers = ['title', 'price', 'rating', 'availability', 'url'] # 硬编码备用
self.writer.writerow(self.headers)
else:
# 如果不是books爬虫或没有crawler属性,也使用备用方案
spider.log("警告:非目标爬虫或缺少crawler属性,将使用硬编码表头。")
self.headers = ['title', 'price', 'rating', 'availability', 'url'] # 硬编码备用
self.writer.writerow(self.headers)
spider.log(f"CSV文件已打开: {file_path}")
# Spider关闭时调用一次
def close_spider(self, spider):
if hasattr(self, 'file') and self.file:
self.file.close()
spider.log("CSV文件已关闭。")
else:
spider.log("警告:尝试关闭未成功打开的CSV文件。")
# 每个Item被yield时都会调用这个方法
def process_item(self, item, spider):
# 确保writer已初始化
if not hasattr(self, 'writer') or not self.writer:
spider.log("错误:CSV writer未初始化,无法处理Item。")
from scrapy.exceptions import DropItem
raise DropItem(f"CSV writer未初始化: {item}")
# 使用ItemAdapter方便地访问Item数据
adapter = ItemAdapter(item)
# 按照表头顺序准备要写入的数据行
# 使用 adapter.get(header, None) 避免KeyError
row = [adapter.get(header) for header in self.headers]
try:
self.writer.writerow(row)
except Exception as e:
spider.log(f"写入CSV时出错: {e}, Item: {adapter.asdict()}")
# 可以选择在这里抛出 DropItem 异常来丢弃这个Item
# from scrapy.exceptions import DropItem
# raise DropItem(f"写入CSV失败: {item}")
# 必须返回Item,否则后续的Pipeline(如果有的话)将收不到这个Item
return item
# --- 实用技巧:多个Pipeline ---
# 你可以定义多个Pipeline,比如一个用于清洗数据,一个用于存数据库,一个用于存文件
# class DataCleaningPipeline:
# def process_item(self, item, spider):
# adapter = ItemAdapter(item)
# # 示例:清洗价格数据,去除货币符号等
# if 'price' in adapter:
# raw_price = adapter['price']
# # 做一些清洗逻辑...
# adapter['price'] = cleaned_price
# return item # 交给下一个Pipeline
# class MongoDBPipeline:
# # ... 连接数据库、写入数据等 ...
# def process_item(self, item, spider):
# # ... 写入MongoDB ...
# return item
代码解读:
open_spider(self, spider)
: 在爬虫开始时执行,通常用于初始化操作,如打开文件、连接数据库。这里我们打开CSV文件并写入表头。动态获取表头更灵活。增加了目录创建和更健壮的表头获取逻辑。close_spider(self, spider)
: 在爬虫结束时执行,用于清理操作,如关闭文件、断开数据库连接。增加了文件是否成功打开的检查。process_item(self, item, spider)
: 核心处理方法,每个从Spider yield
出来的Item都会经过这里。我们将Item数据写入CSV文件。注意:必须 return item
,否则这个Item会被丢弃,无法传递给后续的Pipeline(如果有)。如果你想丢弃某个Item(比如数据不符合要求),可以 raise DropItem("原因")
。增加了对writer是否存在的检查,并使用adapter.get()
防止KeyError。ItemAdapter
: Scrapy推荐使用它来处理Item,因为它同时兼容scrapy.Item
对象和字典,让你的Pipeline更通用。步骤5:启用Pipeline
最后一步,告诉Scrapy我们要使用刚才编写的Pipeline。打开 bookscraper/settings.py
文件,找到 ITEM_PIPELINES
设置项,取消注释并修改:
# bookscraper/settings.py
# Scrapy 项目 bookscraper 的设置
#
# 为简单起见,此文件仅包含被认为重要或
# 常用的设置。您可以通过查阅文档找到更多设置:
#
# https://docs.scrapy.org/en/latest/topics/settings.html
# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = "bookscraper"
SPIDER_MODULES = ["bookscraper.spiders"]
NEWSPIDER_MODULE = "bookscraper.spiders"
# 通过在 user-agent 中标识自己(和您的网站)来负责任地爬取
# 设置User-Agent,模拟浏览器访问
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 (+http://www.yourdomain.com)' # 建议加上你的网站或联系方式
# 遵守 robots.txt 规则
# 是否遵守网站的robots.txt规则 (建议遵守)
ROBOTSTXT_OBEY = True
# 配置 Scrapy 执行的最大并发请求数(默认为 16)
# 同一时间最大并发请求数
#CONCURRENT_REQUESTS = 16
# 为同一网站的请求配置延迟(默认为 0)
# 请参阅 https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# 另请参阅 autothrottle 设置和文档
# 配置请求之间的下载延迟(秒),避免给目标网站造成太大压力
DOWNLOAD_DELAY = 1
# 下载延迟设置将仅遵循以下其中一项:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16 # 每个域名最大并发请求数
#CONCURRENT_REQUESTS_PER_IP = 16 # 每个IP最大并发请求数
# 禁用 Cookie(默认启用)
#COOKIES_ENABLED = False
# 禁用 Telnet 控制台(默认启用)
#TELNETCONSOLE_ENABLED = False
# 覆盖默认请求头:
#DEFAULT_REQUEST_HEADERS = {
# "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
# "Accept-Language": "en",
#}
# 启用或禁用爬虫中间件
# 请参阅 https://docs.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# "bookscraper.middlewares.BookscraperSpiderMiddleware": 543,
#}
# 启用或禁用下载器中间件
# 请参阅 https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# "bookscraper.middlewares.BookscraperDownloaderMiddleware": 543,
#}
# 启用或禁用扩展
# 请参阅 https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# "scrapy.extensions.telnet.TelnetConsole": None,
#}
# 配置项目管道
# 请参阅 https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
# '模块名.Pipeline类名': 优先级 (数字越小,越先执行)
'bookscraper.pipelines.CSVPipeline': 300,
# 如果有多个Pipeline,像这样配置:
# 'bookscraper.pipelines.DataCleaningPipeline': 200, # 先执行清洗
# 'bookscraper.pipelines.MongoDBPipeline': 400, # 后存入数据库
}
# --- 增强:让Pipeline知道Item类型 ---
# 这样CSVPipeline中的动态表头逻辑才能工作
# 注意:这里的键名 'books' 必须与你的Spider的 'name' 属性完全一致
ITEM_CLASSES = {
'books': 'bookscraper.items.BookscraperItem',
}
# 启用并配置 AutoThrottle 扩展(默认禁用)
# 请参阅 https://docs.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# 初始下载延迟
#AUTOTHROTTLE_START_DELAY = 5
# 在高延迟情况下设置的最大下载延迟
#AUTOTHROTTLE_MAX_DELAY = 60
# Scrapy 应并行发送到
# 每个远程服务器的平均请求数
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# 为收到的每个响应启用显示限制统计信息:
#AUTOTHROTTLE_DEBUG = False
# 启用并配置 HTTP 缓存(默认禁用)
# 请参阅 https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = "httpcache"
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"
# 将默认值已弃用的设置设为面向未来的值
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8" # 确保内置导出功能也使用utf-8
# 配置日志级别和输出文件 (可选)
# LOG_LEVEL = 'INFO' # 设置日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
# LOG_FILE = 'scrapy_log.txt' # 将日志输出到文件
步骤6:运行爬虫!
一切就绪!回到你的终端,确保你在 bookscraper
项目的根目录下(即包含 scrapy.cfg
的目录),然后运行:
scrapy crawl books
你将看到Scrapy启动,开始请求页面,解析数据,并将结果通过Pipeline写入到 output_data/books_data.csv
文件中。
案例总结与关键点:
这个简单的案例涵盖了Scrapy的核心流程:
scrapy startproject
items.py
,明确数据结构。spiders/xxx.py
,核心解析逻辑,使用选择器提取数据,yield item
和 yield request/response.follow()
。pipelines.py
,处理和存储Item,实现open_spider
, close_spider
, process_item
。settings.py
,配置ITEM_PIPELINES
。scrapy crawl
。掌握这些,你就已经推开了Scrapy的大门!
requests+BS4
特性 | Scrapy | requests + BeautifulSoup4 (手动实现) |
---|---|---|
开发效率 | 高 (框架结构清晰,内置功能丰富) | 中低 (需自行处理并发、队列、重试等) |
运行性能 | 非常高 (基于Twisted异步非阻塞IO) | 低 (同步阻塞,需手动实现异步/多线程) |
并发处理 | 内置支持,简单配置即可 | 需借助asyncio , threading , multiprocessing |
请求调度 | 内置队列和去重机制 | 需手动实现 (set , 数据库等) |
数据处理 | Item Pipeline机制,解耦清晰 | 通常耦合在爬虫逻辑中,或需自行设计 |
可扩展性 | 非常高 (中间件、信号机制等) | 较低,代码复杂度高 |
内存占用 | 相对较高 (框架本身开销) | 较低 (轻量级库) |
学习曲线 | 稍陡峭 (需要理解框架概念) | 平缓 (库API简单) |
适用场景 | 大规模、复杂、需要持久化的爬虫项目 | 简单、一次性、小规模的爬取任务 |
结论:对于需要长期维护、追求效率和稳定性的数据采集任务,特别是为大模型应用或自动化办公流水线提供数据源时,Scrapy是明显更优的选择。
Scrapy爬取到的数据仅仅是开始,真正的价值在于如何利用这些数据。结合2025技术趋势,我们可以:
Scrapy进阶之路推荐:
Scrapy的世界广阔而精彩,入门只是第一步。
欢迎在评论区分享你的想法、经验和问题!需要本文完整 bookscraper
示例代码和 requirements.txt
的同学,请评论区留言交流
点赞、收藏、转发⤴️、关注,是对博主最大的支持!持续关注,获取更多Python爬虫、数据分析、AI应用和自动化办公的硬核干货!让我们一起在技术的道路上不断进步!