简介:Scrapy是基于Python的高效异步网络爬虫框架,广泛用于网页数据抓取与处理。本“爬虫scrapy框架小实例”详细演示了在DOS命令行环境下如何安装Scrapy、创建项目、生成爬虫并运行抓取任务。通过 scrapy startproject 和 genspider 命令快速搭建基础结构,利用XPath或CSS选择器从HTML中提取数据,并通过 scrapy crawl 命令启动爬虫。项目还介绍了Scrapy的核心组件如Spider类、parse方法、settings配置及高级功能如Item Pipeline、Middleware和Extensions,帮助开发者构建可扩展、高性能的爬虫系统。该实例适合初学者掌握Scrapy的基本使用流程与实战技巧。
1. Scrapy框架的核心概念与开发环境构建
Scrapy简介与架构概览
Scrapy采用基于Twisted的异步非阻塞IO模型,实现高并发网页抓取。其核心组件包括 引擎(Engine) 、 调度器(Scheduler) 、 下载器(Downloader) 、 Spider爬虫类 、 Item Pipeline 和 Downloader Middleware ,各组件通过信号协作,形成高效数据流闭环。该架构支持分布式扩展,适用于从简单页面采集到大规模数据中台建设。
开发环境搭建步骤
# 推荐使用Python 3.8~3.11版本
python -m venv scrapy_env
source scrapy_env/bin/activate # Linux/Mac
# 或 scrapy_env\Scripts\activate # Windows
pip install scrapy pyopenssl pywin32
安装后可通过 scrapy version 验证是否成功。建议在虚拟环境中隔离依赖,确保项目可移植性。
核心依赖与验证方式
| 库名 | 作用说明 |
|---|---|
pyopenssl | 支持HTTPS/TLS加密连接 |
pywin32 | Windows平台下提升I/O性能 |
lxml | 高效解析HTML/XML文档结构 |
执行 scrapy startproject testspider 可生成初始项目,确认环境可用。理解异步机制是掌握Scrapy高性能的关键前提。
2. Scrapy项目结构与爬虫模板生成机制
Scrapy 作为 Python 生态中最成熟的网络爬虫框架之一,其高度模块化的项目结构设计和自动化代码生成能力极大地提升了开发效率。理解 Scrapy 的标准项目布局及其内部组件的职责分工,是构建可维护、可扩展爬虫系统的前提。本章将深入剖析 Scrapy 的项目初始化流程,从命令行工具 scrapy startproject 到 scrapy genspider 的完整机制,逐步揭示其背后的工程化思维与自动化逻辑。同时,通过对 Spider 类核心属性与方法的深度解析,帮助开发者掌握如何高效组织请求调度、响应处理与数据流转。
2.1 使用 scrapy startproject 创建标准化项目
使用 scrapy startproject 命令是开启任何 Scrapy 开发工作的第一步。该命令不仅创建了一个符合最佳实践的目录结构,还自动生成了关键配置文件和入口模块,为后续的爬虫开发提供了统一的框架基础。这一过程看似简单,实则蕴含了 Scrapy 框架对项目工程化管理的深刻理解。通过分析其生成内容,可以清晰地看到各组件之间的依赖关系与职责边界。
2.1.1 项目目录结构解析与各文件职责划分
执行如下命令即可创建一个名为 myproject 的 Scrapy 项目:
scrapy startproject myproject
执行后,系统会生成以下标准目录结构:
myproject/
├── myproject/
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders/
│ └── __init__.py
└── scrapy.cfg
下面对每一部分进行详细说明:
| 文件/目录 | 职责描述 |
|---|---|
myproject/ (外层) | 项目根目录,包含部署配置文件 scrapy.cfg |
myproject/myproject/ | 主包目录,存放所有 Scrapy 组件模块 |
items.py | 定义数据容器类(Item),用于结构化提取的数据字段 |
pipelines.py | 实现数据处理流水线,如清洗、去重、存储等 |
settings.py | 全局配置中心,控制爬虫行为参数 |
middlewares.py | 自定义 Downloader 和 Spider 中间件 |
spiders/ | 存放所有 Spider 类文件,每个文件对应一个或多个爬虫 |
scrapy.cfg | 部署配置文件,定义项目名称、部署路径等元信息 |
该结构遵循“关注点分离”原则,确保不同功能模块独立演进。例如, items.py 仅负责数据建模,不涉及网络请求;而 pipelines.py 专注于后期处理,无需关心页面抓取细节。这种分层架构使得团队协作更加高效,也便于单元测试与持续集成。
此外,Scrapy 在设计上支持多爬虫共存于同一项目中,所有 Spider 都注册在 spiders/ 目录下,并可通过 scrapy crawl <spider_name> 精确调用。这种集中式管理方式特别适合需要采集多个目标网站的企业级应用。
为了更直观展示组件间的调用流程,以下是一个典型的 Scrapy 数据流流程图(Mermaid 格式):
graph TD
A[启动命令: scrapy crawl demo] --> B{加载 settings.py}
B --> C[实例化 Engine]
C --> D[调度器 Scheduler]
D --> E[Downloader]
E --> F[向目标站点发送 Request]
F --> G[接收 Response]
G --> H[传递给 Spider 的 parse() 方法]
H --> I[提取数据并生成 Item 或新 Request]
I --> J{判断类型}
J -->|Item| K[进入 Pipeline 处理链]
J -->|Request| D
K --> L[持久化至数据库或文件]
此图展示了从命令行启动到最终数据落地的完整生命周期。可以看出, startproject 所建立的结构正是支撑这一流程的基础骨架。
值得注意的是,尽管默认结构已足够通用,但在实际生产环境中常需扩展。例如,在大型项目中可能引入 utils/ 目录存放公共函数库,或添加 logs/ 目录用于日志归档。这些自定义调整应在不影响核心组件的前提下进行,以保证与 Scrapy 内核的兼容性。
2.1.2 settings.py 配置文件的关键参数说明
settings.py 是 Scrapy 项目的“中枢神经系统”,它决定了爬虫的行为模式、性能表现以及安全策略。正确配置该文件不仅能提升采集效率,还能有效规避反爬机制。以下是几个最关键的配置项及其作用详解。
常用核心配置参数表
| 参数名 | 默认值 | 说明 |
|---|---|---|
BOT_NAME | 项目名 | 设置爬虫机器人名称,用于 User-Agent 构造 |
SPIDER_MODULES | [‘myproject.spiders’] | 指定 Spider 模块搜索路径 |
NEWSPIDER_MODULE | ‘myproject.spiders’ | genspider 命令生成 Spider 的默认位置 |
ROBOTSTXT_OBEY | False | 是否遵守 robots.txt 协议 |
DOWNLOAD_DELAY | 0 | 下载间隔(秒),用于节流控制 |
CONCURRENT_REQUESTS | 16 | 并发请求数上限 |
CONCURRENT_REQUESTS_PER_DOMAIN | 8 | 每个域名并发请求数限制 |
USER_AGENT | Scrapy/X.X.X | 自定义 User-Agent 字符串 |
COOKIES_ENABLED | True | 是否启用 Cookies 中间件 |
ITEM_PIPELINES | {} | 注册数据管道类及优先级 |
DOWNLOADER_MIDDLEWARES | {} | 注册下载中间件及优先级 |
其中, DOWNLOAD_DELAY 和 CONCURRENT_REQUESTS 对服务器压力影响显著。若设置过低,可能导致 IP 被封禁;若过高,则降低采集速度。建议根据目标网站的响应时间和容忍度动态调整。例如:
# settings.py 示例配置
DOWNLOAD_DELAY = 1.5
RANDOMIZE_DOWNLOAD_DELAY = True
CONCURRENT_REQUESTS = 8
CONCURRENT_REQUESTS_PER_DOMAIN = 4
上述配置表示:平均延迟 1.5 秒,允许 ±50% 的随机波动,每域名最多并发 4 个请求,全局最大 8 个并发。这既避免了高频请求带来的风险,又保持了一定的吞吐量。
另一个重要参数是 ITEM_PIPELINES ,用于激活自定义的数据处理逻辑。假设我们有一个用于存储到 MongoDB 的管道类 MongoPipeline ,可在 pipelines.py 中定义后注册:
# pipelines.py
class MongoPipeline:
def open_spider(self, spider):
self.client = MongoClient('localhost', 27017)
self.db = self.client['scrapy_db']
self.collection = self.db['items']
def process_item(self, item, spider):
self.collection.insert_one(dict(item))
return item
def close_spider(self, spider):
self.client.close()
然后在 settings.py 中启用:
ITEM_PIPELINES = {
'myproject.pipelines.MongoPipeline': 300,
}
数字代表优先级,越小越早执行。多个管道按顺序串联,形成一条处理链。
此外, ROBOTSTXT_OBEY = True 是一种合规性配置,表示尊重网站的爬虫访问规则。虽然大多数商业采集场景会关闭此项以提高灵活性,但从法律和道德角度出发,建议在非必要情况下遵守该协议。
最后, LOG_LEVEL 可用于控制输出信息粒度:
LOG_LEVEL = 'INFO' # 可选 DEBUG, INFO, WARNING, ERROR, CRITICAL
合理设置日志级别有助于调试问题而不至于被冗余信息淹没。
2.1.3 scrapy.cfg 的作用与跨平台部署意义
scrapy.cfg 是位于项目根目录下的配置文件,采用 INI 格式编写,主要用于部署阶段的环境适配与服务集成。虽然日常开发中较少直接修改此文件,但它在自动化部署、远程任务调度等方面发挥着关键作用。
典型内容如下:
[settings]
default = myproject.settings
[deploy]
url = http://localhost:6800/
project = myproject
其中 [settings] 段指定了默认的配置模块路径,即 myproject/settings.py 。当运行 scrapy crawl 时,框架首先读取此处设定的模块来加载全局参数。若切换环境(如开发/生产),可通过环境变量覆盖此设置,实现灵活切换。
更重要的是 [deploy] 段,它为 Scrapy 与 Scrapyd 服务通信提供支持。Scrapyd 是一个专为 Scrapy 设计的守护进程,可用于远程部署、定时运行和监控爬虫任务。通过配置 url 指向 Scrapyd 接口地址,开发者可使用如下命令一键发布项目:
scrapyd-deploy
该命令将项目打包成 egg 文件并上传至指定服务器,极大简化了 CI/CD 流程。
此外,还可通过扩展配置支持多环境部署:
[settings]
default = myproject.settings
dev = myproject.settings_dev
prod = myproject.settings_prod
[deploy:production]
url = https://scrapyd.example.com/
project = myproject
username = admin
password = secret
[deploy:staging]
url = http://staging-scrapyd:6800/
project = myproject_staging
此时可通过 scrapyd-deploy staging 或 scrapyd-deploy production 指定部署目标。
除了 Scrapyd, scrapy.cfg 还可用于集成其他工具,如:
- Docker 构建脚本引用
- 云平台(AWS Lambda、Google Cloud Run)部署钩子
- 定时任务管理系统(如 Apache Airflow)的任务注册
因此, scrapy.cfg 不仅是一个静态配置文件,更是连接本地开发与生产环境的桥梁,具备重要的工程价值。
2.2 利用 scrapy genspider 命令快速生成爬虫
在完成项目初始化后,下一步通常是创建具体的爬虫类。Scrapy 提供了 scrapy genspider 命令来自动生成基础 Spider 模板,大幅减少了样板代码的编写负担。该命令不仅支持多种模板类型,还能智能填充域名和起始 URL,使开发者能迅速进入业务逻辑开发阶段。
2.2.1 命令语法详解与常见错误规避
genspider 的基本语法格式为:
scrapy genspider [options] <name> <domain>
-
<name>:爬虫的唯一标识符,将在scrapy crawl <name>中使用。 -
<domain>:目标网站的主域名,用于设置allowed_domains属性。
例如:
scrapy genspider example example.com
执行后会在 spiders/ 目录下生成 example.py 文件,内容如下:
import scrapy
class ExampleSpider(scrapy.Spider):
name = 'example'
allowed_domains = ['example.com']
start_urls = ['http://example.com/']
def parse(self, response):
pass
这是最简形式的 Spider 模板。需要注意的是, genspider 默认使用 basic 模板,但可通过 -t 参数指定其他类型:
scrapy genspider -t crawl news sina.com.cn
这将基于 crawl 模板生成一个支持链接自动跟踪的爬虫。
常见错误与解决方案:
-
重复命名导致覆盖
若已存在同名 Spider,genspider不会提示警告,直接覆盖原文件。建议先检查spiders/目录是否存在冲突。 -
域名格式错误
输入https://www.example.com包含协议和路径,会导致allowed_domains设置异常。应只传入主域:example.com。 -
未指定模板却期望高级功能
如需使用Rule和CrawlSpider,必须显式指定-t crawl,否则生成的是普通Spider类。 -
中文路径或空格引发编码问题
确保项目路径不含中文字符或空格,防止 Python 导入失败。
综上,正确使用 genspider 能显著提升开发效率,但也需注意输入规范与上下文环境。
2.2.2 不同模板类型(basic, crawl 等)的应用场景分析
Scrapy 内置多种 Spider 模板,可通过 scrapy genspider -l 查看可用模板列表:
$ scrapy genspider -l
Available templates:
basic
crawl
csvfeed
xmlfeed
各模板适用场景如下:
| 模板名 | 继承类 | 适用场景 |
|---|---|---|
basic | scrapy.Spider | 通用型爬虫,手动编写所有请求逻辑 |
crawl | scrapy.spiders.CrawlSpider | 多层级页面遍历,自动提取链接并过滤 |
csvfeed | scrapy.spiders.CsvFeedSpider | 定期轮询 CSV 文件中的 URL 列表 |
xmlfeed | scrapy.spiders.XmlFeedSpider | 解析 RSS/Atom 等 XML 格式的更新源 |
示例:使用 crawl 模板实现新闻站全站抓取
假设要抓取新浪新闻首页及其子栏目文章列表页,可执行:
scrapy genspider -t crawl sina sina.com.cn
生成代码片段如下:
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
class SinaSpider(CrawlSpider):
name = 'sina'
allowed_domains = ['sina.com.cn']
start_urls = ['http://news.sina.com.cn/']
rules = (
Rule(LinkExtractor(allow=r'/article/\d+\.shtml'), callback='parse_item', follow=True),
)
def parse_item(self, response):
self.logger.info('Parsing %s', response.url)
其中 Rule 定义了链接提取规则:
- LinkExtractor(allow=...) 使用正则匹配符合条件的链接;
- callback 指定解析函数;
- follow=True 表示继续跟进该链接提取新的链接。
相比 basic 模板, crawl 更适合复杂导航结构的网站,减少手动构造 Request 的工作量。
而对于定时采集 RSS 源的场景, xmlfeed 模板更为合适:
from scrapy.spiders import XmlFeedSpider
class RssSpider(XmlFeedSpider):
name = 'rss'
start_urls = ['https://example.com/feed.xml']
itertag = 'item'
def parse_node(self, response, node):
item = {}
item['title'] = node.xpath('title/text()').get()
item['link'] = node.xpath('link/text()').get()
return item
itertag 指定迭代标签, parse_node 对每个节点单独处理,非常适合结构化 Feed 数据采集。
选择合适的模板,本质上是在权衡“自动化程度”与“控制精度”。对于结构简单、URL 固定的目标, basic 已足够;而对于需要广度优先遍历的场景, crawl 提供了更高层次的抽象。
2.2.3 自动生成代码的结构优化策略
尽管 genspider 生成的代码具备基本功能,但往往需要进一步优化才能满足生产需求。以下是几种常见的重构方向:
1. 分离配置与逻辑
避免硬编码 start_urls ,改用配置注入:
# settings.py
START_URLS = {
'demo': ['https://example.com/page1', 'https://example.com/page2']
}
# spider 中读取
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_urls = self.settings.getlist('START_URLS')['demo']
2. 增强异常处理与日志记录
def parse(self, response):
if response.status != 200:
self.logger.error("Failed to fetch %s", response.url)
return
try:
title = response.css('h1::text').get().strip()
except Exception as e:
self.logger.warning("Parse error on %s: %s", response.url, e)
title = None
3. 支持命令行参数传递
scrapy crawl example -a category=tech
在 Spider 中接收:
def __init__(self, category='', *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_urls = [f'https://example.com/{category}']
4. 引入异步加载支持(配合 Selenium 或 Playwright)
对于 JavaScript 渲染页面,可在 start_requests 中替换 downloader:
def start_requests(self):
for url in self.start_urls:
yield SplashRequest(url, self.parse, args={'wait': 2})
需提前配置 Splash 服务并启用相应中间件。
通过以上优化,原始模板代码可演变为一个健壮、可配置、易维护的生产级爬虫。
2.3 Spider 类核心属性与方法剖析
Spider 是 Scrapy 中最核心的组件之一,负责定义抓取行为、解析响应和生成数据或新请求。每一个爬虫都必须继承自 scrapy.Spider 或其子类,并实现必要的属性与方法。深入理解这些元素的工作机制,是掌握 Scrapy 编程模型的关键。
2.3.1 name、allowed_domains 与 start_urls 的协同工作机制
这三个类属性构成了 Spider 的“启动三要素”,共同决定爬虫的初始行为和作用范围。
class MySpider(scrapy.Spider):
name = "myspider"
allowed_domains = ["example.com"]
start_urls = ["https://example.com/page1"]
-
name:必须唯一,用于命令行调用scrapy crawl myspider。不允许重复或特殊字符。 -
allowed_domains:限定请求域名白名单。若请求超出此列表,Scheduler 会自动过滤,防止意外爬取外部站点。 -
start_urls:起始 URL 列表,框架会自动将其封装为Request对象并提交给 Downloader。
它们的协同流程如下:
sequenceDiagram
participant Engine
participant Scheduler
participant Downloader
participant Spider
Engine->>Spider: 初始化实例
Spider->>Engine: 提供 name、domains、urls
Engine->>Scheduler: 将 start_urls 转为 Requests
Scheduler->>Downloader: 调度第一个请求
Downloader->>Target: 发送 HTTP 请求
值得注意的是, allowed_domains 仅做初步过滤,不能完全替代业务逻辑中的 URL 校验。例如,子域名 api.example.com 不在 example.com 白名单内时仍会被拦截,需手动扩展。
此外, start_urls 可动态赋值,支持从数据库或 API 获取初始链接:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_urls = get_seeds_from_api()
这种模式适用于种子 URL 频繁变动的场景。
2.3.2 parse() 方法的数据流处理逻辑与响应对象解析
parse() 是 Spider 的默认回调函数,每当一个页面成功返回时,该方法就会被调用。其签名如下:
def parse(self, response):
pass
response 是 TextResponse 或 HtmlResponse 实例,封装了 HTTP 响应体、头信息、编码等元数据。
常用属性包括:
| 属性 | 说明 |
|---|---|
response.url | 当前请求的 URL |
response.status | HTTP 状态码 |
response.body | 原始字节内容 |
response.text | 解码后的字符串 |
response.css() | CSS 选择器查询接口 |
response.xpath() | XPath 查询接口 |
response.headers | 响应头字典 |
示例:提取网页标题与链接
def parse(self, response):
yield {
'title': response.css('h1::text').get(),
'links': response.xpath('//a/@href').getall()
}
这里 css() 和 xpath() 返回 SelectorList , .get() 取第一个匹配项, .getall() 取全部。
若需跳转下一页:
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, callback=self.parse)
response.follow() 是推荐方式,会自动处理相对 URL 并继承 allowed_domains 检查。
整个数据流遵循“请求 → 响应 → 解析 → 输出(Item 或 Request)”的闭环模型,构成 Scrapy 的核心驱动力。
2.3.3 请求回调机制与多级页面抓取实现原理
Scrapy 支持深度嵌套的请求链,通过回调函数实现多级页面抓取。典型案例如“列表页 → 详情页 → 评论页”。
def parse_list(self, response):
for href in response.css('.item a::attr(href)'):
yield response.follow(href, callback=self.parse_detail)
def parse_detail(self, response):
item = {
'title': response.css('h1::text').get(),
'content': response.css('.content::text').get()
}
comment_url = response.css('.comments-link::attr(href)').get()
if comment_url:
request = response.follow(comment_url, callback=self.parse_comments)
request.meta['item'] = item # 传递上下文
yield request
def parse_comments(self, response):
item = response.meta['item']
item['comments'] = response.css('.comment::text').getall()
yield item
关键点在于 request.meta ,它允许在请求之间传递任意数据,突破了 HTTP 无状态的限制。这也是实现复杂业务逻辑的重要手段。
整个机制基于 Twisted 异步引擎,所有请求非阻塞执行,极大提升了整体吞吐能力。
综上,Spider 的设计充分体现了事件驱动与函数式编程的思想,通过简洁的接口实现了强大的扩展能力。
3. 数据提取技术与反爬应对策略实战
在现代网络爬虫开发中,数据提取的准确性和稳定性直接决定了整个采集系统的可用性。而随着目标网站安全机制的不断升级,各类反爬手段层出不穷——从简单的请求频率限制到复杂的验证码系统、行为分析模型等,都对爬虫提出了更高的技术要求。Scrapy作为一款成熟的异步爬虫框架,不仅提供了强大的数据提取能力,还通过灵活的中间件架构支持多种反爬策略的集成。本章将深入探讨如何在Scrapy中高效使用XPath与CSS选择器进行结构化数据提取,并结合真实场景讲解常见反爬机制的识别方法与应对方案,涵盖User-Agent轮换、下载延迟控制、自动重试机制以及请求头和会话管理等关键技术点。
3.1 XPath与CSS选择器在Scrapy中的高效应用
数据提取是爬虫工作的核心环节,其本质是从HTML或XML文档中精准定位并抽取所需信息。Scrapy内置了基于 parsel 库的Selector系统,支持XPath和CSS两种主流选择语言,能够高效处理复杂的DOM结构。理解这两种语法的特点及其在Scrapy环境下的最佳实践方式,对于提升爬取效率和维护代码可读性至关重要。
3.1.1 Selector对象的使用方式与响应体预处理技巧
当Scrapy的Spider接收到HTTP响应后, response 对象会自动封装一个 Selector 实例,开发者可以通过 .xpath() 或 .css() 方法直接调用选择器功能。该机制屏蔽了底层解析细节,使得开发者可以专注于路径表达式的编写。
import scrapy
class ExampleSpider(scrapy.Spider):
name = 'example'
start_urls = ['https://example.com']
def parse(self, response):
# 使用XPath提取标题
title = response.xpath('//h1/text()').get()
# 使用CSS选择器提取所有段落文本
paragraphs = response.css('p::text').getall()
yield {
'title': title,
'paragraphs': paragraphs
}
代码逻辑逐行解读:
-
response.xpath('//h1/text()'):查找文档中所有的<h1>标签,并提取其内部的纯文本内容(text()函数)。 -
.get():返回第一个匹配结果,若无匹配则返回None,避免因空列表引发异常。 -
response.css('p::text'):使用CSS语法选取所有<p>标签内的文本节点(::text伪元素)。 -
.getall():返回所有匹配项组成的列表,适用于多值提取场景。
为了提高提取精度,建议在调用选择器前先对响应内容做初步验证:
def parse(self, response):
if response.status != 200:
self.logger.error(f"Failed to fetch {response.url}, status: {response.status}")
return
if b'html' not in response.headers.get('Content-Type', b''):
self.logger.warning(f"Non-HTML content at {response.url}")
return
此外,部分网站可能返回非标准编码或压缩格式的内容,需手动解码或启用 HttpCompressionMiddleware 来确保Selector能正确解析。例如:
from scrapy.http import TextResponse
# 手动构建TextResponse用于调试
body = response.body.decode('gbk') # 如遇GBK编码页面
custom_response = TextResponse(url=response.url, body=body, encoding='utf-8')
title = custom_response.xpath('//title/text()').get()
此技巧常用于处理中文站点常见的编码混乱问题。
3.1.2 复杂DOM结构下的路径定位优化方案
面对嵌套层级深、类名动态生成的现代前端框架渲染页面(如React/Vue),静态选择器容易失效。此时需要借助更智能的XPath表达式或属性过滤策略增强鲁棒性。
常见挑战与解决方案对比表:
| 挑战类型 | 典型表现 | 解决策略 |
|---|---|---|
| 动态class名称 | <div class="item_abc123"> | 使用 contains(@class, "item") |
| 多级嵌套结构 | 列表项内含多个同级元素 | 使用轴操作符如 following-sibling:: |
| 属性缺失或变化 | id 字段不稳定 | 改用文本内容或位置索引定位 |
| JavaScript懒加载 | 关键数据不在初始HTML中 | 结合Selenium或Splash抓取完整DOM |
以电商商品列表为例,假设每个商品块结构如下:
<div class="product-card">
<h3 class="name">iPhone 15</h3>
<span class="price">¥6999</span>
<a href="/detail?id=123" class="link"></a>
</div>
传统写法易受类名变动影响,改进后的XPath应更具容错性:
products = response.xpath('//div[contains(@class, "product")]')
for product in products:
item = {}
item['name'] = product.xpath('.//h3[contains(@class, "name")]/text()').get()
item['price'] = product.xpath('.//span[contains(@class, "price")]/text()').get()
item['url'] = response.urljoin(product.xpath('.//a/@href').get())
yield item
其中 . 表示相对当前节点,避免全局搜索性能损耗; contains() 函数提升匹配灵活性。
Mermaid流程图展示选择器优化决策路径:
graph TD
A[开始提取数据] --> B{是否存在动态class?}
B -- 是 --> C[使用contains(@class, 'keyword')]
B -- 否 --> D[使用精确class选择]
C --> E{是否有多层嵌套?}
D --> E
E -- 是 --> F[采用轴操作或嵌套查询]
E -- 否 --> G[直接提取文本]
F --> H[测试表达式覆盖率]
G --> H
H --> I[输出结构化Item]
该流程帮助开发者系统化评估提取路径的健壮性,减少后期维护成本。
3.1.3 提取结果的清洗与类型转换实践
原始提取结果通常包含空白字符、换行符或单位符号,必须经过清洗才能用于后续存储或分析。Scrapy提供了简洁的链式处理方式:
def parse_price(value):
# 移除货币符号、空格并转为浮点数
return float(value.replace('¥', '').strip().replace(',', ''))
def clean_text(value):
return value.strip().replace('\n', ' ').replace('\t', ' ')
# 在parse方法中使用map处理器
price = response.xpath('//span[@class="price"]/text()') \
.get() \
.replace('¥', '') \
.strip()
# 或通过注册processor实现复用
import scrapy.loader.processors as processors
class ProductItem(scrapy.Item):
name = scrapy.Field(
input_processor=processors.MapCompose(str.strip),
output_processor=processors.TakeFirst()
)
price = scrapy.Field(
input_processor=processors.MapCompose(parse_price),
output_processor=processors.TakeFirst()
)
上述示例展示了两种清洗模式:
- 即时清洗 :在提取后立即调用字符串方法处理。
- 声明式清洗 :利用
scrapy.loader模块定义字段级别的处理流水线,适合复杂项目统一规范。
此外,对于日期、数字等特殊类型,推荐封装通用转换函数库:
import re
from datetime import datetime
def extract_number(text):
match = re.search(r'[\d,]+\.?\d*', text)
return float(match.group().replace(',', '')) if match else None
def parse_date_chinese(text):
# 示例:"发布于2024年3月15日"
match = re.search(r'(\d{4})年(\d{1,2})月(\d{1,2})日', text)
if match:
year, month, day = map(int, match.groups())
return datetime(year, month, day).isoformat()
return None
这些函数可在多个爬虫间共享,显著提升开发效率。
3.2 反爬机制识别与基础应对措施
几乎所有具有一定规模的网站都会部署某种形式的反爬机制,旨在防止自动化访问带来的服务器压力或数据泄露风险。Scrapy虽然具备高性能异步抓取能力,但若不加以节制地发起请求,极易触发IP封禁、验证码弹窗甚至法律纠纷。因此,合理识别并规避常见反爬策略,是构建可持续运行爬虫系统的前提。
3.2.1 用户代理(User-Agent)轮换配置与随机化策略
User-Agent(UA)是HTTP请求头中最基本的身份标识字段,服务器可通过它判断客户端是否为浏览器。许多网站会对默认的Python或Scrapy UA(如 Scrapy/2.8 (+https://scrapy.org) )进行拦截。
配置静态UA的方法:
在 settings.py 中设置全局UA:
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
}
但单一UA仍易被识别为机器人。更优做法是建立UA池并随机切换:
import random
USER_AGENT_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...',
'Mozilla/5.0 (X11; Linux x86_64) ...'
]
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
ua = random.choice(USER_AGENT_LIST)
request.headers['User-Agent'] = ua
然后在 settings.py 中启用中间件:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomUserAgentMiddleware': 400,
}
UA轮换效果对比表:
| 策略 | 请求成功率 | 被封概率 | 实现难度 |
|---|---|---|---|
| 固定UA | 低 | 高 | 简单 |
| 随机UA池 | 中高 | 中 | 中等 |
| 浏览器指纹模拟 | 高 | 低 | 复杂(需Puppeteer/Selenium) |
注:实际应用中建议结合 ua-parser 库动态生成合法UA,或从公开API获取最新主流UA列表。
3.2.2 下载延迟(DOWNLOAD_DELAY)设置与节流控制
Scrapy默认以最快速度发送请求,极易造成短时间内大量连接冲击目标服务器。为此,应在 settings.py 中合理配置节流参数:
# 设置基础下载间隔(秒)
DOWNLOAD_DELAY = 1.5
# 启用自动调节延迟(推荐)
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
RANDOMIZE_DOWNLOAD_DELAY = True
-
DOWNLOAD_DELAY:两次请求间的最小等待时间。 -
AUTOTHROTTLE_ENABLED:根据服务器响应速度动态调整延迟,模仿人类浏览节奏。 -
RANDOMIZE_DOWNLOAD_DELAY:在基础延迟基础上增加随机抖动(0.5~1.5倍),降低规律性。
该机制特别适用于对响应时间敏感的站点,如新闻门户、电商平台。
3.2.3 自动重试中间件与HTTP状态码过滤机制
网络波动或临时封禁可能导致部分请求失败。Scrapy内置的 RetryMiddleware 可根据状态码自动重试:
# settings.py
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = ['500', '502', '503', '504', '408', '429']
其中 429 Too Many Requests 是典型的限流响应码,表明需降低请求频率。
自定义重试逻辑示例:
class CustomRetryMiddleware(RetryMiddleware):
def process_response(self, request, response, spider):
if response.status == 429:
# 记录被限流次数,动态延长延迟
retry_times = request.meta.get('retry_times', 0)
delay = 2 ** retry_times # 指数退避
time.sleep(delay)
return self._retry(request, exception=None, spider=spider) or response
return response
此策略结合指数退避算法,有效应对短期流量限制。
3.3 请求头定制与会话保持技术
高级反爬系统往往依赖完整的请求上下文进行行为分析,仅修改UA已不足以绕过检测。通过精细化定制请求头并维持会话状态,可大幅提升爬虫的真实性。
3.3.1 headers字段精细化配置示例
除了User-Agent,还可添加其他常见浏览器头字段:
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
某些站点还会检查 Referer 来源,可在请求中显式指定:
yield scrapy.Request(
url='https://target.com/detail',
headers={'Referer': 'https://target.com/list'},
callback=self.parse_detail
)
3.3.2 Cookies管理与登录态维持方法探讨
对于需要登录的网站,Scrapy可通过 FormRequest.from_response() 自动处理表单提交并保存Cookies:
def start_requests(self):
return [scrapy.Request('https://login-site.com', callback=self.login)]
def login(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={'username': 'user', 'password': 'pass'},
callback=self.after_login
)
def after_login(self, response):
if "welcome" in response.text:
yield scrapy.Request('/dashboard', self.parse_dashboard)
else:
self.logger.error("Login failed")
此外,也可手动注入持久化Cookie:
cookies = {'session_id': 'abc123', 'token': 'xyz789'}
yield scrapy.Request(url, cookies=cookies, callback=self.parse)
配合 JOBDIR 功能,Scrapy还能在中断后恢复原有会话状态,实现长时间任务连续执行。
综上所述,数据提取与反爬对抗是一项系统工程,涉及语法技巧、网络协议理解和行为模拟等多个层面。掌握这些核心技术,不仅能提升爬虫成功率,也为后续接入代理池、验证码识别等高级模块奠定坚实基础。
4. 数据管道与中间件系统的深度集成
Scrapy之所以能够在众多爬虫框架中脱颖而出,其强大的扩展机制是关键因素之一。在实际开发中,原始的网页抓取只是第一步,真正决定系统可用性的是对采集数据的清洗、去重、验证和持久化能力,以及在整个请求生命周期中对网络行为的精细控制。这正是 Item Pipeline 和两大类中间件(Downloader Middleware 与 Spider Middleware)的核心价值所在。
本章将深入剖析 Scrapy 的数据处理链路设计哲学,从数据流的末端——Pipeline 出发,逐步回溯至请求发起阶段的 Downloader 中间层,再到爬虫逻辑层面的 Spider 中间层,揭示这些组件如何协同工作以实现高度可定制化的数据采集系统。我们将不仅讲解理论模型,还将结合真实场景构建完整的扩展模块,包括动态代理切换、结构化数据入库、自动去重机制等生产级功能,并通过代码示例、流程图和配置表格全面展示其实现细节。
更重要的是,本章内容将贯穿“可维护性”与“可扩展性”的工程思维,帮助开发者理解为何要分离关注点、如何合理设置优先级、怎样避免性能瓶颈,从而为后续构建分布式、高并发的爬虫集群打下坚实基础。
4.1 Item Pipeline实现数据清洗与持久化存储
Scrapy 的 Item Pipeline 是整个爬虫系统中负责数据后处理的关键环节。它位于数据提取之后、最终输出之前,承担着诸如数据清洗、类型转换、重复检测、格式标准化以及写入数据库或文件系统等职责。每一个进入 Pipeline 的 Item 都会依次经过注册的多个处理器,形成一条清晰的数据加工流水线。
4.1.1 定义Item容器与字段映射规范
在使用 Pipeline 前,必须首先定义一个结构化的 Item 类来承载待处理的数据。Scrapy 提供了 scrapy.Item 和 scrapy.Field() 来声明字段,这种做法类似于 ORM 模型中的表结构定义,有助于统一数据接口并提升代码可读性。
以下是一个针对电商产品信息采集的典型 ProductItem 示例:
import scrapy
class ProductItem(scrapy.Item):
title = scrapy.Field() # 商品标题
price = scrapy.Field() # 价格(字符串)
currency = scrapy.Field() # 货币单位
url = scrapy.Field() # 商品详情页链接
image_urls = scrapy.Field() # 图片URL列表
images = scrapy.Field() # 下载后的图片路径(由ImagesPipeline填充)
category = scrapy.Field() # 分类标签
brand = scrapy.Field() # 品牌名称
created_at = scrapy.Field() # 抓取时间戳
字段命名规范与语义一致性
建议遵循如下命名原则:
- 使用小写字母 + 下划线风格(snake_case)
- 字段名应具有明确业务含义,避免模糊缩写
- 对于列表型字段如图片或多规格参数,使用复数形式(e.g., image_urls )
该 Item 类的作用不仅是数据容器,更是各组件之间通信的标准契约。Spider 负责填充字段,Pipeline 根据字段进行处理,Exporter 则依据字段生成 JSON/CSV 等格式输出。
| 字段名 | 数据类型 | 是否必填 | 描述 |
|---|---|---|---|
title | str | 是 | 商品主标题 |
price | str | 是 | 价格文本(含符号) |
currency | str | 否 | 如 USD, CNY |
url | str | 是 | 绝对URL地址 |
image_urls | list[str] | 否 | 用于图片下载 |
images | list[dict] | 自动填充 | 下载结果元数据 |
category | str | 否 | 一级分类 |
brand | str | 否 | 制造商品牌 |
created_at | datetime | 是 | ISO8601 时间格式 |
此表可用于团队协作时的接口文档参考,确保前后端或其他服务能准确解析输出数据。
4.1.2 编写自定义Pipeline类处理数据去重与格式标准化
一旦定义好 Item ,就可以编写自定义的 Pipeline 类来进行数据清洗。常见的任务包括去除HTML标签、标准化数值格式、过滤无效记录、防止重复插入等。
下面实现一个综合性的 DataCleaningPipeline :
import re
from itemadapter import ItemAdapter
from urllib.parse import urljoin
class DataCleaningPipeline:
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# 清洗标题:去除多余空白与换行
if adapter.get('title'):
cleaned_title = re.sub(r'\s+', ' ', adapter['title']).strip()
adapter['title'] = cleaned_title
# 标准化价格:提取数字部分并转为浮点数
if adapter.get('price'):
price_text = adapter['price']
match = re.search(r'[\d,]+\.?\d*', price_text.replace(',', ''))
if match:
try:
adapter['price'] = float(match.group())
except ValueError:
spider.logger.warning(f"无法解析价格: {price_text}")
adapter['price'] = None
else:
adapter['price'] = None
# 补全相对URL为绝对URL
if adapter.get('url') and spider.base_url:
adapter['url'] = urljoin(spider.base_url, adapter['url'])
# 设置默认分类
if not adapter.get('category'):
adapter['category'] = 'Uncategorized'
return item
代码逻辑逐行分析
-
ItemAdapter(item):使用 Scrapy 推荐的适配器模式访问字段,兼容不同类型的 Item(如 dict、dataclass)。 -
re.sub(r'\s+', ' ', ...):将连续空白字符替换为空格,防止出现\n\t导致显示异常。 -
urljoin(spider.base_url, ...):确保即使页面返回的是相对路径/product/123,也能还原成完整 URL。 - 异常捕获机制保障了流程不会因单条数据错误而中断。
此外,还可以添加去重功能,利用 Python 集合暂存已处理的 URL:
class DuplicatesPipeline:
def __init__(self):
self.seen_urls = set()
def process_item(self, item, spider):
adapter = ItemAdapter(item)
url = adapter['url']
if url in self.seen_urls:
raise DropItem(f"发现重复项: {url}")
else:
self.seen_urls.add(url)
return item
⚠️ 注意:内存去重仅适用于小规模任务;大规模场景需结合 Redis 或布隆过滤器。
4.1.3 将数据导出至JSON、MySQL或MongoDB的完整流程
Scrapy 支持多种内置导出方式(JSON、CSV、XML),同时也允许通过 Pipeline 写入外部数据库。以下是三种典型存储方案的实现方式。
方案一:导出为 JSON 文件
无需额外编码,直接运行命令即可:
scrapy crawl myspider -o output.json
支持增量追加(append mode),但要注意编码问题。推荐配置 settings.py :
FEEDS = {
'output.json': {
'format': 'json',
'encoding': 'utf8',
'store_empty': False,
'indent': 2,
}
}
方案二:写入 MySQL 数据库
使用 pymysql 或 mysql-connector-python 实现持久化:
import pymysql
from itemadapter import ItemAdapter
class MySQLPipeline:
def __init__(self, host, database, user, password, port):
self.host = host
self.database = database
self.user = user
self.password = password
self.port = port
self.connection = None
self.cursor = None
@classmethod
def from_crawler(cls, crawler):
return cls(
host=crawler.settings.get('MYSQL_HOST'),
database=crawler.settings.get('MYSQL_DBNAME'),
user=crawler.settings.get('MYSQL_USER'),
password=crawler.settings.get('MYSQL_PASS'),
port=crawler.settings.get('MYSQL_PORT', 3306),
)
def open_spider(self, spider):
self.connection = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
passwd=self.password,
db=self.database,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
self.cursor = self.connection.cursor()
def close_spider(self, spider):
self.connection.commit()
self.cursor.close()
self.connection.close()
def process_item(self, item, spider):
adapter = ItemAdapter(item)
sql = """
INSERT INTO products (title, price, currency, url, category, brand, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
price=VALUES(price), title=VALUES(title)
"""
values = [
adapter.get('title'),
adapter.get('price'),
adapter.get('currency', 'CNY'),
adapter.get('url'),
adapter.get('category'),
adapter.get('brand'),
adapter.get('created_at'),
]
try:
self.cursor.execute(sql, tuple(values))
except Exception as e:
spider.logger.error(f"MySQL写入失败: {e}")
self.connection.rollback()
return item
参数说明:
-
from_crawler():Scrapy 专用工厂方法,用于从 settings 注入配置。 -
open_spider()/close_spider():资源初始化与释放,确保连接安全关闭。 -
ON DUPLICATE KEY UPDATE:启用主键冲突时更新而非报错。
需要在 settings.py 中注册:
ITEM_PIPELINES = {
'myproject.pipelines.DataCleaningPipeline': 300,
'myproject.pipelines.DuplicatesPipeline': 350,
'myproject.pipelines.MySQLPipeline': 400,
}
# 数据库配置
MYSQL_HOST = 'localhost'
MYSQL_DBNAME = 'scrapy_db'
MYSQL_USER = 'root'
MYSQL_PASS = 'password'
MYSQL_PORT = 3306
方案三:写入 MongoDB
对于非结构化或嵌套数据更友好的 NoSQL 方案:
import pymongo
from itemadapter import ItemAdapter
class MongoDBPipeline:
collection_name = 'scraped_items'
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
data = ItemAdapter(item).asdict()
self.db[self.collection_name].update_one(
{'url': data['url']}, # 查询条件
{'$set': data}, # 更新内容
upsert=True # 存在则更新,否则插入
)
return item
配置项:
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DATABASE = 'scrapy_data'
不同存储方式对比
| 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON/CSV | 简单易读,便于分析 | 不支持复杂查询 | 一次性导出、临时测试 |
| MySQL | 结构严谨,支持事务 | 扩展成本高 | 结构化强、需关联查询 |
| MongoDB | 灵活 schema,支持嵌套 | 一致性弱于关系型 | 多媒体、动态字段 |
数据流执行顺序可视化
graph TD
A[Spider yield Item] --> B{进入Pipeline}
B --> C[DataCleaningPipeline]
C --> D[RemoveDuplicatesPipeline]
D --> E{目标存储?}
E -->|JSON| F[Feed Exporter 输出到文件]
E -->|MySQL| G[MySQLPipeline 插入记录]
E -->|MongoDB| H[MongoDBPipeline Upsert]
该流程图展示了数据从爬虫产出到最终落盘的完整路径,体现了 Scrapy “责任分离”的设计理念:每个 Pipeline 只专注一项任务,组合起来却构成强大处理链。
4.2 Downloader Middleware工作原理与扩展开发
Downloader Middleware 是 Scrapy 请求-响应循环中的核心拦截层,位于引擎与下载器之间。它可以在请求发出前修改 Headers、添加代理、延迟发送,也可以在响应到达后预处理内容、拦截异常状态码,甚至伪造响应对象。由于其处于底层网络调用之前,因此非常适合用于反爬策略的集中管理。
4.2.1 中间件执行链路与优先级排序机制
Scrapy 使用一个有序列表来管理所有启用的中间件,按优先级升序排列(数字越小越靠前)。当请求流出时,先经过低优先级中间件的 process_request() ,再依次向高优先级传递;响应则反向流动。
执行顺序示意
sequenceDiagram
participant Engine
participant Middlewares
participant Downloader
Engine->>Middlewares: send request
Middlewares->>Middleware1: process_request()
Middleware1-->>Middlewares: 返回None → 继续
Middlewares->>Middleware2: process_request()
Middleware2-->>Downloader: 修改request后放行
Downloader->>Website: 发起HTTP请求
Website-->>Downloader: 返回Response
Downloader->>Middlewares: receive response
Middlewares->>Middleware2: process_response()
Middleware2-->>Middlewares: 返回response → 继续
Middlewares->>Middleware1: process_response()
Middleware1-->>Engine: 最终响应
若某个 process_request() 返回 Response 或 Request ,则短路后续处理;若返回 None ,则继续传递。
配置优先级
在 settings.py 中设置:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.CustomUserAgentMiddleware': 400,
'myproject.middlewares.ProxyMiddleware': 500,
'myproject.middlewares.TooManyRequestsRetryMiddleware': 550,
}
数字范围通常为 1–1000,Scrapy 内建中间件如 HttpCompressionMiddleware 默认在 100 左右。
4.2.2 实现IP代理池集成与动态切换功能
为应对 IP 封禁,常需接入代理池。以下实现一个轮询式代理中间件:
import random
from scrapy.exceptions import IgnoreRequest
class ProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.get('PROXY_POOL', [])
if not proxy_list:
raise NotConfigured("未配置PROXY_POOL")
return cls(proxy_list)
def process_request(self, request, spider):
if 'dont_proxy' in request.meta:
return
proxy = random.choice(self.proxies)
request.meta['proxy'] = f"http://{proxy}"
spider.logger.debug(f"使用代理: {proxy}")
def process_exception(self, request, exception, spider):
current_proxy = request.meta.get('proxy')
if current_proxy:
spider.logger.warning(f"代理失效: {current_proxy}, 异常: {exception}")
return None # 触发重试机制
配置代理列表:
PROXY_POOL = [
"192.168.1.100:8080",
"192.168.1.101:8080",
"192.168.1.102:8080"
]
💡 提示:生产环境建议对接 Redis + 动态检测脚本,实时剔除不可用节点。
4.2.3 日志记录增强与异常请求拦截处理
为了监控异常流量,可增加日志中间件:
import time
import logging
class LoggingMiddleware:
def process_request(self, request, spider):
spider.logger.info(f"[REQUEST] {request.method} {request.url}")
request.meta['start_time'] = time.time()
def process_response(self, request, response, spider):
duration = time.time() - request.meta['start_time']
level = logging.WARNING if response.status >= 400 else logging.INFO
spider.logger.log(
level,
f"[RESPONSE] {response.status} {response.url} ({duration:.2f}s)"
)
return response
def process_exception(self, request, exception, spider):
spider.logger.error(f"[EXCEPTION] {exception} on {request.url}")
该中间件可辅助排查超时、503 错误等问题,尤其适合长时间运行的任务监控。
4.3 Spider Middleware的应用场景与开发模式
Spider Middleware 作用于 Spider 层,主要处理 Response 到 Item 或 Request 的转换过程。它可以修改传给 parse() 方法的响应对象,也可以拦截 Spider 产生的 Request 进行预处理。
4.3.1 修改Spider输入输出的典型用例分析
例如,某些网站返回 JavaScript 渲染内容,原生 Response 为空。可通过中间件提前调用 Selenium 获取真实 HTML:
from scrapy.http import HtmlResponse
from selenium import webdriver
class SeleniumMiddleware:
def __init__(self):
options = webdriver.ChromeOptions()
options.add_argument('--headless')
self.driver = webdriver.Chrome(options=options)
def process_response(self, request, response, spider):
if request.meta.get('use_selenium'):
self.driver.get(request.url)
body = self.driver.page_source
return HtmlResponse(
url=request.url,
body=body,
encoding='utf-8',
request=request
)
return response
def __del__(self):
self.driver.quit()
启用方式:
yield Request(url, meta={'use_selenium': True})
4.3.2 结合Feed Exporters实现定时批量导出任务
利用 Scrapy 的 Feed Exporters 机制,配合调度器(如 APScheduler)实现每日自动导出:
from scrapy.extensions.feedexport import FeedExporter
from apscheduler.schedulers.twisted import TwistedScheduler
def export_daily_data(crawler):
exporter = FeedExporter()
settings = crawler.settings
uri = f"backup/products_{time.strftime('%Y%m%d')}.json"
feed_options = {
'format': 'json',
'encoding': 'utf-8',
'store_empty': False
}
slot = exporter.open_spider(crawler.spider, uri, feed_options)
for item in global_item_buffer:
exporter.export_item(slot, item)
exporter.close_spider(slot)
启动定时任务:
sched = TwistedScheduler()
sched.add_job(export_daily_data, 'cron', hour=2)
sched.start()
此方案适用于离线报表生成、归档备份等后台任务。
5. 完整Scrapy爬虫开发流程综合实战
5.1 需求分析与目标网站结构调研
在启动一个完整的Scrapy项目前,系统化的需求分析和网站结构调研是确保爬虫高效、稳定运行的前提。以某电商图书销售平台为例(如虚构站点 http://books.toscrape.com ),我们的核心目标是从该网站抓取图书信息,包括书名、价格、评分等级、库存状态、类别分类及详情页URL。
5.1.1 确定爬取范围与合法性边界
首先需确认目标网站的 robots.txt 文件内容:
GET http://books.toscrape.com/robots.txt
响应内容显示:
User-agent: *
Disallow: /docs/
Disallow: /search/
Crawl-delay: 5
根据此策略,我们应遵守以下规范:
- 避免访问 /docs/ 和 /search/ 路径
- 设置 DOWNLOAD_DELAY = 5 以尊重爬取延迟建议
- 不进行高频请求,防止对服务器造成压力
此外,数据用途必须符合非商业性研究或学习目的,并在必要时联系网站运营方获取授权。
5.1.2 页面层级关系建模与URL调度策略设计
通过浏览器开发者工具分析页面结构,可建立如下层级模型:
| 层级 | URL模式 | 内容类型 | 抓取方式 |
|---|---|---|---|
| 1 | http://books.toscrape.com/ | 分类列表页 | 初始入口 |
| 2 | http://books.toscrape.com/catalogue/category/books/{category}_\d+/ | 类别目录页 | 从首页提取链接 |
| 3 | http://books.toscrape.com/catalogue/{book-slug}/ | 图书详情页 | 从类别页翻页抓取 |
由此设计调度逻辑:
1. Spider 启动后从首页提取所有书籍分类链接;
2. 每个分类进入后自动翻页(检测“next”按钮);
3. 在每一页中提取图书详情链接并发送请求;
4. 在 parse_book() 方法中解析具体字段。
使用 Scrapy 的 LinkExtractor 和 Rule 可实现自动化的链接发现机制(适用于 CrawlSpider 模板)。但为增强控制力,本例采用基础 Spider 手动构建请求链路。
5.2 全流程编码实现与模块协同调试
5.2.1 联调Spider、Item、Pipeline组件确保数据贯通
定义 BookItem 容器(位于 items.py ):
import scrapy
class BookItem(scrapy.Item):
title = scrapy.Field()
price = scrapy.Field()
rating = scrapy.Field()
availability = scrapy.Field()
category = scrapy.Field()
url = scrapy.Field()
创建爬虫类 book_spider.py :
import scrapy
from scrapy.loader import ItemLoader
from myproject.items import BookItem
class BookSpider(scrapy.Spider):
name = 'book_spider'
allowed_domains = ['books.toscrape.com']
start_urls = ['http://books.toscrape.com/']
def parse(self, response):
# 提取分类链接
for href in response.css('ul.nav-list a::attr(href)').getall():
yield response.follow(href, self.parse_category)
def parse_category(self, response):
# 提取书籍详情链接
for book_url in response.css('h3 > a::attr(href)').getall():
yield response.follow(book_url, self.parse_book)
# 处理分页
next_page = response.css('li.next > a::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse_category)
def parse_book(self, response):
loader = ItemLoader(item=BookItem(), response=response)
loader.add_value('url', response.url)
loader.add_css('title', 'h1::text')
loader.add_css('price', 'p.price_color::text')
loader.add_css('rating', 'p.star-rating::attr(class)', re=r'Star Rating ([A-Za-z]+)')
loader.add_css('availability', 'p.instock.availability::text', re=r'In stock')
loader.add_value('category', response.css('.breadcrumb li:nth-child(3) a::text').get())
yield loader.load_item()
配置 pipelines.py 实现去重与导出 JSON:
class DuplicatesPipeline:
def __init__(self):
self.seen_titles = set()
def process_item(self, item, spider):
if item['title'] in self.seen_titles:
raise DropItem(f"Duplicate item found: {item['title']}")
self.seen_titles.add(item['title'])
return item
class JsonExportPipeline:
def __init__(self):
self.file = open('books.json', 'w', encoding='utf-8')
def process_item(self, item, spider):
line = json.dumps(dict(item), ensure_ascii=False) + "\n"
self.file.write(line)
return item
def close_spider(self, spider):
self.file.close()
启用 Pipeline( settings.py ):
ITEM_PIPELINES = {
'myproject.pipelines.DuplicatesPipeline': 300,
'myproject.pipelines.JsonExportPipeline': 400,
}
5.2.2 利用DOS命令行运行scrapy crawl并实时监控日志输出
在项目根目录执行:
scrapy crawl book_spider -L INFO --nologstats
关键参数说明:
- -L INFO :设置日志级别为 INFO,过滤 DEBUG 冗余信息
- --nologstats :关闭周期性统计日志,聚焦业务输出
- 可附加 -o output.json 直接导出结果
典型日志片段示例:
[scrapy.core.engine] DEBUG: Crawled (200) <GET http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html>
[scrapy.pipelines.files] INFO: File (downloaded): Downloading file from ...
[myproject.pipelines.DuplicatesPipeline] DEBUG: Duplicate item found: A Light in the Attic
5.2.3 断点调试与response样本保存技巧
为了便于本地调试,可在代码中临时保存响应对象:
def parse_book(self, response):
with open(f'debug_{response.css("h1::text").get()}.html', 'wb') as f:
f.write(response.body)
# 此后可用 HtmlResponse 构造测试环境
结合 PyCharm 或 VSCode 的调试器,在 parse_book() 中设置断点,逐步查看 loader 字段填充情况。
5.3 扩展机制(Extensions)与生产级部署准备
5.3.1 启用Telnet Console与Stats Collection进行性能监测
Scrapy 内建统计系统可通过以下配置开启:
# settings.py
TELNETCONSOLE_ENABLED = True
TELNETCONSOLE_PORT = [6023] # 默认端口
连接方式:
telnet localhost 6023
常用命令:
- estats :查看当前统计数据(请求数、响应数、错误等)
- prefs() :分析内存引用瓶颈
- engine.status() :查看调度器状态
统计指标样例(部分):
| 指标名称 | 示例值 | 含义 |
|---|---|---|
downloader/request_count | 247 | 总请求数 |
response_received_count | 245 | 成功响应数 |
item_scraped_count | 100 | 已提取条目数 |
log_count/ERROR | 2 | 错误日志数量 |
5.3.2 自定义Extension实现关闭通知或资源释放功能
创建 extensions.py :
from scrapy import signals
import logging
logger = logging.getLogger(__name__)
class AlertOnCloseExtension:
def __init__(self, notify_uri):
self.notify_uri = notify_uri
@classmethod
def from_crawler(cls, crawler):
ext = cls(notify_uri=crawler.settings.get("NOTIFY_URI"))
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
return ext
def spider_closed(self, spider):
logger.info(f"Spider {spider.name} finished scraping.")
if self.notify_uri:
import requests
requests.post(self.notify_uri, json={"status": "finished", "spider": spider.name})
注册扩展( settings.py ):
EXTENSIONS = {
'myproject.extensions.AlertOnCloseExtension': 500,
}
NOTIFY_URI = "https://your-webhook-endpoint.com/scrapy-alert"
5.3.3 Docker容器化打包与Linux服务器自动化运行方案
编写 Dockerfile :
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["scrapy", "crawl", "book_spider"]
构建镜像并后台运行:
docker build -t book-crawler .
docker run -d --name crawler-job book-crawler
配合 cron 实现每日定时执行(Linux):
# crontab -e
0 2 * * * docker restart crawler-job
或使用 supervisord 管理长期任务进程,保障异常重启能力。
简介:Scrapy是基于Python的高效异步网络爬虫框架,广泛用于网页数据抓取与处理。本“爬虫scrapy框架小实例”详细演示了在DOS命令行环境下如何安装Scrapy、创建项目、生成爬虫并运行抓取任务。通过 scrapy startproject 和 genspider 命令快速搭建基础结构,利用XPath或CSS选择器从HTML中提取数据,并通过 scrapy crawl 命令启动爬虫。项目还介绍了Scrapy的核心组件如Spider类、parse方法、settings配置及高级功能如Item Pipeline、Middleware和Extensions,帮助开发者构建可扩展、高性能的爬虫系统。该实例适合初学者掌握Scrapy的基本使用流程与实战技巧。
64万+

被折叠的 条评论
为什么被折叠?



