news 2026/9/18 16:57:33

Scrapy爬虫框架实战:从异步原理到分布式部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy爬虫框架实战:从异步原理到分布式部署

简介:这是一份讲解开源Python网络爬虫框架Scrapy的PDF文档,专为Python爬虫初学者和希望系统掌握Scrapy架构的开发者准备。文档首先介绍网络爬虫的基本概念,然后围绕Scrapy引擎、调度器、下载器、蜘蛛、项目管道、下载器中间件、蜘蛛中间件等核心组件逐一解析,并通过数据流向图直观呈现从初始URL到下载网页、解析内容、调度新请求、存储数据的完整工作流程。在此基础上,还给出了Windows环境下安装Scrapy所需的Twisted等依赖、安装步骤以及常见问题处理方法,方便读者边看边练。资源为单个PDF文件,体积仅401KB,轻量便携,适合离线速查。目前已有748人学习,是一份实用性很强的Scrapy入门参考资料。

1. Scrapy是异步框架,但并发不是它最有价值的部分

Scrapy 有个反直觉的地方:新手往往是被它“异步、并发、快”吸引入门的,但真正拉开差距的是它把请求调度、页面解析、中间件拦截、数据清洗和持久化做成了固定的工程结构。用 requests + BeautifulSoup 写脚本时,这些职责散落在函数调用里,一旦页面改版或目标站点开始限流,你就得在回调里一层层补逻辑;到了 Scrapy 里,每个环节都有清晰的挂载点,改一处不影响其它模块,这也是它多年后依然是 Python 网络爬虫框架事实标准的原因。如果你写过十几个能跑的爬虫但总在维护期崩溃,或者第一次想用 Scrapy 搭一个可监控、可重试、可扩展的采集任务,下面这些内容能帮你把安装、核心组件、故障定位和分布式扩展一次串起来。

2. Scrapy安装与第一个爬虫:从startproject到跑通最小命令

2.1 环境准备:python安装与虚拟环境怎么选

动手之前先把 python 安装细节确认掉。Scrapy 对整个技术栈的依赖比较挑剔,它的异步引擎基于 Twisted,而 Twisted 对 Python 新版本的支持通常慢半拍。常见做法是生产环境固定在 Python 3.10 到 3.12 之间的某个小版本,不要在最新的 3.13+ 上直接跑,否则可能遇到 Twisted 还没适配的扩展模块报错。第一步先检查当前环境:

python --version python -m venv scrapy_env source scrapy_env/bin/activate # Windows 用 scrapy_env\Scripts\activate pip install scrapy python -c "import scrapy; print(scrapy.__version__)"

venv这步最重要也最容易被跳过。直接用全局 pip 装 Scrapy,等下一次系统 Python 升级后,很可能出现依赖冲突;把它收进scrapy_env里,重装环境的成本就变得很低。python -c "import scrapy"是跑完安装后最快的一条自检命令,正常情况下会打印出版本号,如果报ModuleNotFoundError,再回头看 pip 输出里的报错,通常是 Twisted 编译相关依赖缺失,需要在系统里补装编译工具链后重试。

2.2 用 startproject 创建项目骨架

安装完成后,第一条命令通常是scrapy startproject,它生成的项目结构已经决定了后续代码的组织方式:

scrapy startproject example_store cd example_store scrapy genspider product product.example.com

执行完目录长这样(省略与配置无关的空目录):

example_store/ ├── scrapy.cfg └── example_store/ ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders/ └── product.py
文件职责你主要在这写什么
scrapy.cfg部署配置一般不动
items.py字段结构定义商品、文章等数据模型
pipelines.py清洗、去重、入库数据落地前的所有处理
middlewares.py请求/响应拦截UA、代理、重试逻辑
settings.py全局参数并发、延迟、管道启停
spiders/抓取与解析回调函数和选择器

很多从脚本转过来的初学者会把所有逻辑塞进spiders里的回调函数,这会在爬虫变多后迅速失控,因为调度、清洗、重试全部耦合在一起。Scrapy 的设计意图是把“如何抓”和“抓到后怎么办”分开,骨架里这些文件就是给你划分边界的,按文件职责去放代码,后续做维护会省很多事。

2.3 写一个最小 Spider 并跑起来

新建的product.py已经有一个最基础的模板,把它改成下面这样,就能跑通一个最小的 scrapy 爬虫案例:

import scrapy class ProductSpider(scrapy.Spider): name = "product" def start_requests(self): # 入口:按列表逐个制造请求 urls = [ "https://quotes.toscrape.com/page/1/", "https://quotes.toscrape.com/page/2/", ] for url in urls: # 注意用 yield 而不是 return,让 Scrapy 异步调度 yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): # 每个 div.quote 对应一条完整引用数据 for quote in response.css("div.quote"): yield { "text": quote.css("span.text::text").get(), "author": quote.css("small.author::text").get(), }

跑这个爬虫只用一个子命令:

scrapy crawl product -o quotes.jsonl

callback=self.parse决定这个请求成功后由哪个方法处理响应,parse里每yield一个 dict,Scrapy 会把它交给 Item Pipeline 走后续流程,写入quotes.jsonl只是-o参数带来的输出动作。这里用::text提取文本节点,用.get()只取第一条结果,因为每个div.quote里对应的只有一个span.text;如果这里误用getall(),就要在 Pipeline 再做一次展平,节奏会乱。这是一个标准的网络爬虫新手入门教程式写法,跑通它之后,再去看 Pipeline 和 Middleware 的挂载方式,会更容易理解数据是怎么一步步流动的。

3. Scrapy核心组件调参:Selector、Pipeline、Middleware 的必调参数

3.1 Selector:CSS 与 XPath 的选用边界和 shell 验证

Scrapy 自带 Selector,底层是 parsel 库,支持 CSS 和 XPath 两套语法。常见误区是问“哪个更快“,实际上绝大多数页面差异不在解析速度,而在表达式是否容易写、是否稳定,以及页面改版后哪个更好维护。我的选择习惯是先看结构,再选语法,而不是抱着一种写法走到底。

场景推荐写法理由
类名稳定的平面列表response.css("div.product-item")简短,可读性好
按文本内容定位节点//div[contains(text(), '缺货')]CSS 做不到文本匹配
找父节点再取兄弟节点//h3/following-sibling::div[1]结构复杂时 XPath 更直接
从文本里提数字片段.re(r'价格[::]?\s*([\d.]+)')直接在元素上做正则

写表达式之前用scrapy shell验证是最省时间的做法,它能直接拉回 URL 并进入交互式控制台,再逐步执行选择器:

scrapy shell https://quotes.toscrape.com
>>> response.css("div.quote").getall() >>> response.xpath("//div[@class='quote']//span[@class='text']/text()").get() >>> response.xpath("//div[contains(@class, 'quote')]//a/@href").getall()

在 shell 里连续试三到四次再回写代码,比在爬虫里反复scrapy crawl要快得多。还要注意一个边界:::text/text()都只取直接文本节点,遇到子标签夹在中间的混合文本,要用//text()在父级上做拼接,否则会丢失部分内容。

3.2 Item Pipeline:清洗、去重、入库的先后顺序

Pipeline 的触发顺序由settings.pyITEM_PIPELINES字典的数值决定,数值小的先进,大的后进。常见做法是把它排成“清洗 → 去重 → 入库”三个管道,每一段只干一件事:

# settings.py ITEM_PIPELINES = { "example_store.pipelines.CleanPricePipeline": 100, "example_store.pipelines.DuplicateCheckPipeline": 200, "example_store.pipelines.DatabasePipeline": 300, }

清洗管道的典型实现如下,它的职责是把页面里的价格字符串转成可计算的数字,再塞回同一个 item:

import re class CleanPricePipeline: def process_item(self, item, spider): # get() 拿不到键时返回 None,所以统一转成空串再正则 raw = item.get("price", "") or "" cleaned = re.sub(r"[^\d.]", "", raw) # 去掉货币符号和千分位逗号 if cleaned: item["price"] = float(cleaned) return item

process_item返回的 item 会继续往下传,返回None则是丢弃这个 item 的通用做法。去重管道里如果判断字段已经出现过,直接返回None就能让后续的入库管道不再执行。字段完全相同的 item 可以放开到入库阶段让数据库唯一索引兜底,跑定时任务时这个兜底位置特别重要,不要只依赖爬虫进程内的内存去重。

3.3 下载中间件:UA、重试与代理的接入方式

默认的User-AgentScrapy/版本号,对于需要伪装成真实浏览器的站点,这个请求头一上来就会被特征识别。最常见做法是在中间件里随机切换 UA,优先在DOWNLOADER_MIDDLEWARES里挂一个自定义组件:

# middlewares.py import random class UARandomMiddleware: UA_LIST = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", ] def process_request(self, request, spider): # 每次请求随机取一个 UA;不返回对象,请求继续走默认流程 request.headers["User-Agent"] = random.choice(self.UA_LIST) return None

重试不用自己写,Scrapy 内置RetryMiddleware,常用参数在settings.py里调节:

RETRY_TIMES = 3 RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]

RETRY_TIMES的默认值是 2,对不稳定站点可以调到 3;RETRY_HTTP_CODES里 429 表示触发限流,碰上它更合理的做法是配合后面的AUTOTHROTTLE_ENABLED降低请求速率,而不是单纯增加重试次数。代理接入一般也是在下载中间件里改request.meta["proxy"],具体取值从配置中心或接口拿,不要在代码里写死 IP,否则爬虫一上线就要改代码再发版。

3.4 去重策略与请求指纹

Scrapy 默认的去重是内存里的指纹集合,指纹由 URL、请求方法、请求正文等信息计算出来。单机单进程时这个默认行为很够用,但爬虫一旦重启,内存里去重状态就全丢了,已抓过的 URL 会被重复请求。常见做法是给settings.pyJOBDIR,让去重结果落到本地文件后再复用:

scrapy crawl product -s JOBDIR=job_state

再启动时它会读取job_state里保存的指纹集合,实现单机断点续爬。这个方案适合单机上的一次性长任务,分布式环境则需要把去重状态放到 Redis 里,这部分的接入会在第 5 章展开。

提示:JOBDIR只解决重启丢去重的问题,不解决多进程并发调度问题。多 worker 同时跑同一个爬虫任务时,请直接看分布式方案,不要在单机方案上硬拓。

4. Scrapy爬虫的性能开关与故障定位:日志、shell、动态iframe

4.1 用 AUTOTHROTTLE 而不是手写延时控制爬取速率

新手常犯的第一个性能错误是不管目标站点负载,直接把CONCURRENT_REQUESTS拉到 32,然后被大量超时日志淹没。Scrapy 提供了自动限速模块,合理用法是先开启它,再根据日志调参数,而不是在代码里time.sleep()一把梭。

AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 1.0 AUTOTHROTTLE_MAX_DELAY = 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
参数作用建议初始值
DOWNLOAD_DELAY固定请求间隔不推荐优先调
AUTOTHROTTLE_TARGET_CONCURRENCY目标并发请求数2.0~4.0
AUTOTHROTTLE_START_DELAY初始下载延迟1.0
AUTOTHROTTLE_MAX_DELAY延迟上限10.0 左右

自动限速的逻辑是根据服务器响应时间和收到的延迟请求数量动态调节每次请求的间隔,优先级上高于手工设置的固定延时。它的一个副作用是在响应慢的站点上,整体爬取时间会被拉长,这时先观察INFO日志里的下载耗时,确认是目标站点普遍慢,再决定要不要提高目标并发。

4.2 用 scrapy parse 和日志定位问题

写完一个爬虫后不要急着全量跑,scrapy parse能单独跑一个 URL 并执行指定回调,是最快的验证手段:

scrapy parse https://quotes.toscrape.com/page/1/ -c parse

-c指定回调方法名,结果会打印到控制台。它和scrapy crawl的关键区别是不经过 Pipeline,输出的是原始 item 列表,这样可以把解析逻辑和入库逻辑分开排查:解析出错时,不会因为数据库写入失败而误判成选择器问题。

如果大量请求返回 403/503,第一反应不是加代理,而是先看日志级别和错误码:

LOG_LEVEL = "INFO" HTTPERROR_ALLOWED_CODES = [404, 410]

LOG_LEVEL调成INFO能保留请求级日志,排查“某一页明明有数据却抓不到”的问题;默认的DEBUG会输出大量框架内部日志,反而把真实错误淹没。HTTPERROR_ALLOWED_CODES是为特殊页面准备的,比如商品列表里已经下架的商品链接也会返回 404,你希望把它也送进回调统一处理,而不是让框架直接丢弃。

4.3 动态页面:scrapy-playwright 与 iframe 跨文档解析

React 或 Vue 渲染的页面里,静态请求的 HTML 可能只包含空壳节点,选择器能匹配到但getall()长度为 0。常见的兜底方案是给 Scrapy 配上 scrapy-playwright 这个异步渲染桥接层,它继承自 Playwright,能拿到页面执行完 JS 后的 DOM:

pip install scrapy-playwright playwright install chromium

settings.py里做三处配置:

DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } PLAYWRIGHT_BROWSER_TYPE = "chromium" PLAYWRIGHT_LAUNCH_OPTIONS = {"headless": True}

请求时带上 meta:

yield scrapy.Request( url=url, callback=self.parse_detail, meta={ "playwright": True, "playwright_page_methods": [ {"method": "wait_for_selector", "args": ["div.product-detail"]} ], }, )

playwright_page_methods是页面加载后要额外执行的动作列表,常见用法是等一个关键节点出现再渲染,避免在骨架节点上取数据。这个方案还能顺带处理动态 iframe:对frame类型的元素要先去拿src,再单独发起一次请求解析子页面,不能指望 iframe 里的内容出现在主响应中。用上浏览器渲染必然会带来内存和耗时上升,建议只在关键页面上开启,列表页仍然走普通请求。

5. 把调度器换到 Redis:分布式去重与断点续爬的验证

单机爬虫在 URL 量过百万级,或者要多个服务器共享爬取进度时,内存调度器和内存去重就成了明显的瓶颈。社区里最常用的做法是引入 scrapy-redis,用它替换默认的调度器和去重器。注意它不是 Scrapy 官方模块,把它当插件用,只接调度、去重两个能力,其余 Pipeline 仍按自己项目来写。

安装与配置:

pip install scrapy-redis
# settings.py 关键配置 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 调度器换成 Redis 队列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 去重指纹存到 Redis SCHEDULER_PERSIST = True # 爬虫退出后保留队列数据 REDIS_URL = "redis://127.0.0.1:6379/0"

改造 Spider,继承RedisSpider,把起始 URL 从代码里移到 Redis 队列,你会发现所有 worker 其实都在消费同一个队列:

from scrapy_redis.spiders import RedisSpider class ProductRedisSpider(RedisSpider): name = "product_redis" # 从这个 key 里读起始 URL,不再写 start_urls redis_key = "product:start_urls"

启动前先往队列里推入起始 URL:

redis-cli lpush product:start_urls "https://quotes.toscrape.com/page/1/" scrapy crawl product_redis

分布式改造的验证并不复杂,也不依赖压测工具:起两个爬虫进程,从一个进程手动推入若干 URL,看两个进程的INFO日志是否各抓了一部分、且没有重复抓取。如果出现重复,先确认两个进程连的是同一个REDIS_URL,再查 Redis 里product_redis:dupefilter这个集合的大小变化,它记录着所有被去重过的请求指纹。如果要断点续爬,SCHEDULER_PERSIST = True会让队列在爬虫退出后保留;但 Redis 里去重集合不会自动清理,重跑同一批 URL 前要删掉对应的product_redis:dupefilter键,否则新任务会认为这些 URL 已经抓过。处理完这一步,分布式去重与断点续爬才真正闭环,Scrapy 也就从“能跑的脚本”变成了“多机可运维的采集系统”。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 16:57:10

招聘信息可视化分析:从爬虫到Word报告的一站式Python实践

简介:docx文档《基于Python语言的招聘信息可视化分析》面向数据分析初学者、互联网行业求职者及人力资源从业者,利用招聘平台数据讲解从采集到决策的完整链路。文档从Python基础工具链入手,介绍Pandas、NumPy、Matplotlib、Seaborn、Scrapy、…

作者头像 李华
网站建设 2026/9/18 16:56:09

查重报告里 AI 率飘红?TaoToken 这样改 Codex 的模型通道再复检

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 16:47:40

LL(1)预测分析表从零构造:FIRST/FOLLOW集计算与冲突排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华