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

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(urlurl, callbackself.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.jsonlcallbackself.parse决定这个请求成功后由哪个方法处理响应parse里每yield一个 dictScrapy 会把它交给 Item Pipeline 走后续流程写入quotes.jsonl只是-o参数带来的输出动作。这里用::text提取文本节点用.get()只取第一条结果因为每个div.quote里对应的只有一个span.text如果这里误用getall()就要在 Pipeline 再做一次展平节奏会乱。这是一个标准的网络爬虫新手入门教程式写法跑通它之后再去看 Pipeline 和 Middleware 的挂载方式会更容易理解数据是怎么一步步流动的。3. Scrapy核心组件调参Selector、Pipeline、Middleware 的必调参数3.1 SelectorCSS 与 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[classquote]//span[classtext]/text()).get() response.xpath(//div[contains(class, quote)]//a/href).getall()在 shell 里连续试三到四次再回写代码比在爬虫里反复scrapy crawl要快得多。还要注意一个边界::text和/text()都只取直接文本节点遇到子标签夹在中间的混合文本要用//text()在父级上做拼接否则会丢失部分内容。3.2 Item Pipeline清洗、去重、入库的先后顺序Pipeline 的触发顺序由settings.py中ITEM_PIPELINES字典的数值决定数值小的先进大的后进。常见做法是把它排成“清洗 → 去重 → 入库”三个管道每一段只干一件事# settings.py ITEM_PIPELINES { example_store.pipelines.CleanPricePipeline: 100, example_store.pipelines.DuplicateCheckPipeline: 200, example_store.pipelines.DatabasePipeline: 300, }清洗管道的典型实现如下它的职责是把页面里的价格字符串转成可计算的数字再塞回同一个 itemimport 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 itemprocess_item返回的 item 会继续往下传返回None则是丢弃这个 item 的通用做法。去重管道里如果判断字段已经出现过直接返回None就能让后续的入库管道不再执行。字段完全相同的 item 可以放开到入库阶段让数据库唯一索引兜底跑定时任务时这个兜底位置特别重要不要只依赖爬虫进程内的内存去重。3.3 下载中间件UA、重试与代理的接入方式默认的User-Agent是Scrapy/版本号对于需要伪装成真实浏览器的站点这个请求头一上来就会被特征识别。最常见做法是在中间件里随机切换 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对不稳定站点可以调到 3RETRY_HTTP_CODES里 429 表示触发限流碰上它更合理的做法是配合后面的AUTOTHROTTLE_ENABLED降低请求速率而不是单纯增加重试次数。代理接入一般也是在下载中间件里改request.meta[proxy]具体取值从配置中心或接口拿不要在代码里写死 IP否则爬虫一上线就要改代码再发版。3.4 去重策略与请求指纹Scrapy 默认的去重是内存里的指纹集合指纹由 URL、请求方法、请求正文等信息计算出来。单机单进程时这个默认行为很够用但爬虫一旦重启内存里去重状态就全丢了已抓过的 URL 会被重复请求。常见做法是给settings.py加JOBDIR让去重结果落到本地文件后再复用scrapy crawl product -s JOBDIRjob_state再启动时它会读取job_state里保存的指纹集合实现单机断点续爬。这个方案适合单机上的一次性长任务分布式环境则需要把去重状态放到 Redis 里这部分的接入会在第 5 章展开。提示JOBDIR只解决重启丢去重的问题不解决多进程并发调度问题。多 worker 同时跑同一个爬虫任务时请直接看分布式方案不要在单机方案上硬拓。4. Scrapy爬虫的性能开关与故障定位日志、shell、动态iframe4.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.04.0AUTOTHROTTLE_START_DELAY初始下载延迟1.0AUTOTHROTTLE_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 后的 DOMpip 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}请求时带上 metayield scrapy.Request( urlurl, callbackself.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启动前先往队列里推入起始 URLredis-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 也就从“能跑的脚本”变成了“多机可运维的采集系统”。本文还有配套的精品资源点击获取