
Scrapy这个框架说它是Python爬虫界最“重”的装备一点也不夸张。很多新手一开始用requests写个循环抓点数据觉得挺爽但一旦目标网站页面多了、结构复杂了、需要反爬对抗了代码就会迅速变成一堆难以维护的散装函数。我这两年帮团队搭数据采集管道凡是上了规模的项目最后几乎都收敛到了Scrapy上。这篇就从一个实际工程的角度把这个10.6章节该有的东西——框架设计、实操流程、动态页面处理和常见坑——一次性讲透。1. 为什么全网教程都绕不开Scrapy一个框架的定位与核心逻辑1.1 它不是“库”是一套完整的采集流水线先说一个很多教程没点透的事Scrapy不是像requests那样“调一下就能用”的库而是一个完整的应用框架。什么意思就好比requests是给你一套锅碗瓢盆你想吃什么菜得自己从洗菜切菜开始Scrapy则是一条已经组装好的中央厨房流水线你把菜谱爬虫规则丢进去从抓取、解析、清洗、存储到去重调度它全给你自动串起来了。这条流水线的灵魂是它的事件驱动架构。Scrapy基于Twisted异步网络框架底层用Reactor模式维护一个事件循环所有网络请求都是非阻塞的。这意味着在一个线程里可以同时挂起几百个请求哪个响应回来了就处理哪个而不是傻等着某个请求超时。我做过一个对比测试同样抓5000个详情页用requests写多线程大概需要20分钟Scrapy单进程默认并发16差不多5分钟出头而且代码量还少了一半。差距就是这么来的。1.2 五大核心组件和一次完整请求的旅程理解Scrapy记住它的五大组件就够了引擎Engine、调度器Scheduler、下载器Downloader、爬虫Spiders、管道Item Pipeline。它们之间的协作关系我画了张简化的流程引擎是整个框架的大脑负责在组件之间传递数据调度器维护一个待抓取的请求队列负责去重和排序下载器真正执行HTTP请求并把响应交给引擎爬虫解析响应、提取数据、生成新的请求管道逐级处理提取出来的结构化数据。一次完整的采集流程是这样的引擎从爬虫拿到初始请求交给调度器入队调度器按优先级吐出下一个请求引擎再转给下载器下载器执行请求拿到响应回抛给引擎引擎把响应发给爬虫的parse方法爬虫解析出Item和新的RequestItem走管道清洗入库新Request又回到调度器排队。整个循环直到队列清空才结束。理解这个循环特别重要因为很多诡异问题——比如请求顺序乱了、重复请求变多——本质上都是没搞懂这个流转过程。2. Scrapy实操从安装建项目到跑通第一个爬虫2.1 环境准备别在Python版本上栽跟头我见过太多人在环境这步卡住所以先把版本问题说清楚。Scrapy要求Python 3.8我在3.10和3.11上都跑得很稳。安装用pip一行命令pip install scrapy但有个坑必须提醒如果你用的是Windows最好用WSL2或者装Visual Studio Build Tools C生成工具因为scrapy会编译一些C扩展缺了编译环境会报error: Microsoft Visual C 14.0 is required。macOS用户则建议先装好Xcode Command Line Tools。另外强烈建议在虚拟环境里装不要直接打进系统Python。我用的是python -m venv venv激活后再装依赖这样项目迁移、依赖管理都干净不会出现把系统环境搅浑了导致其他项目跑不起来的情况。2.2 创建项目和第一个爬虫装好之后命令行执行scrapy startproject tutorial cd tutorial scrapy genspider quotes quotes.comstartproject会生成一个标准的项目骨架结构是这样tutorial/ scrapy.cfg # 项目配置文件 tutorial/ __init__.py items.py # 定义你要抓取的数据结构 middlewares.py # 自定义中间件下载器/爬虫中间件 pipelines.py # Item管道做数据清洗和存储 settings.py # 全局配置 spiders/ # 爬虫代码目录很多人不理解为什么Scrapy要把文件拆得这么碎直接一个脚本搞定不香吗但工程化的意义就在这里爬虫、数据结构、存储逻辑、配置完全解耦。一个项目里抓十几个不同站点每个站点一个spider文件公共的反爬逻辑放middleware存储逻辑统一放pipeline改一处不牵全身这才是能维护的代码。拿经典的quotes网站为例写第一个爬虫import scrapy class QuotesSpider(scrapy.Spider): name quotes start_urls [ https://quotes.toscrape.com/page/1/, ] def parse(self, response): for quote in response.css(div.quote): yield { text: quote.css(span.text::text).get(), author: quote.css(small.author::text).get(), tags: quote.css(div.tags a.tag::text).getall(), } next_page response.css(li.next a::attr(href)).get() if next_page is not None: yield response.follow(next_page, callbackself.parse)运行命令scrapy crawl quotes -O quotes.json数据就落盘了。注意-O是大写会直接覆盖输出文件小写-o是追加模式实跑的时候要看清楚。这段代码里有几个细节值得说道。response.css方法返回的是SelectorList对象你可以用.css()接.xpath()链式调用我一般混着用CSS选不出来的复杂结构就上XPath比如//*[contains(class, tag)]/text()这种。.get()只返回匹配的第一个结果.getall()返回所有匹配结果这个区别特别容易记混用错了数据悄无声息就残缺了。对于翻页操作response.follow会自动处理相对URL的拼接不用手动做字符串拼接省掉了编码和路径的坑。2.3 选择器语法速查与解析策略做爬虫一半的时间都耗在写选择器上。Scrapy同时支持CSS和XPath我总结了一个速查表目标CSS写法XPath写法所有a标签a//aclassauthor的节点.author//*[classauthor]idmain的节点#main//*[idmain]a标签的href属性a::attr(href)//a/href节点的文本a::text//a/text()包含特定文本的节点不支持//a[contains(text(),关键词)]选择器的核心技巧是“先定位容器再提取字段”。也就是先用一个大容器把包含所有目标数据的模块圈出来再在这个容器内做字段提取。这比写一个巨大的全局选择器要稳妥得多因为页面局部改版时只需要调整容器选择器字段选择器基本不用动。2.4 关键的settings配置直接跑第一个爬虫没问题但上了真实网站settings.py里这些配置几乎必改# 请求延迟秒对目标站点最基本的礼貌 DOWNLOAD_DELAY 1.5 # 并发请求数调太大容易被封IP太小效率低 CONCURRENT_REQUESTS 16 # 默认请求头很多站点没有UA直接拒连 DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, } # robots协议默认True会遵守robots.txt ROBOTSTXT_OBEY True # 开启pipeline的配置 ITEM_PIPELINES { tutorial.pipelines.TutorialPipeline: 300, }DOWNLOAD_DELAY这个参数很多人不重视但我觉得它是最能体现实战经验的一个配置。爬虫和网站之间是一场长期博弈你把对方服务器抓挂了自己的IP第二天也就进了黑名单。我之前做电商竞品监测刚开始没设延迟5000个请求下去账号就收到了警告邮件把延迟调到1.5秒之后稳定跑了一个月没出事。这个参数需要根据目标网站的规模和反爬力度去权衡建议从1秒起步再慢慢往下试。ITEM_PIPELINES的数字表示执行优先级数值越小越先执行。如果配置了多个管道数据会像流水线一样依次经过每个管道的process_item方法。3. 动态内容与iframe场景用scrapy-playwright补齐短板3.1 为什么传统Scrapy拿动态页面没辙Scrapy原生的下载器用的是Twisted底层那套HTTP客户端本质上只能拿到服务器直接返回的HTML。但现在越来越多网站用Vue、React这类前端框架渲染数据不是写在HTML里的而是页面加载后用JavaScript异步请求接口再动态渲染到DOM上。更麻烦的是有些页面还套了iframe嵌套内容在子文档里。这种情况下你用Scrapy直接抓拿到的只是一副空壳里面根本没有目标数据。这个困境是当前爬虫领域最常见也最头疼的问题。解决思路无非两条一是想方设法去找页面背后的XHR接口直接请求JSON这条路要是走通了效率和稳定性都远胜渲染方案二是干脆用无头浏览器把页面完整渲染出来再抓。第二条虽然慢一点但胜在通用——页面长什么样我就抓什么不用费劲逆向接口。scrapy-playwright这个插件就是把第二条路和Scrapy无缝衔接的方案。3.2 安装与异步集成配置scrapy-playwright官方文档的安装要求是Python 3.8Scrapy 2.5Playwright库。操作分三步pip install scrapy-playwright playwright install chromium第二步会下载Chromium浏览器内核大概一百多MB网络差的时候要耐心等。装完还没完因为Scrapy默认的Twisted reactor和Playwright的异步机制有冲突必须在settings.py里做两个关键的修改DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor第一个配置强制Scrapy用Playwright的下载处理器来处理所有HTTP/HTTPS请求第二个是把Twisted事件循环切换到asyncio模式让Playwright能正常调度异步任务。这两行不配启动爬虫时大概率会报一个关于reactor的异常提示你需要安装twisted[asyncio]并设置对应的reactor。3.3 异步爬虫写法与等待策略配置好之后在爬虫里用上Playwright只差一个标记。看这个代码import scrapy class DynamicSpider(scrapy.Spider): name dynamic_spider def start_requests(self): yield scrapy.Request( urlhttps://example.com/dynamic-page, callbackself.parse, meta{ playwright: True, playwright_include_page: True, playwright_page_methods: [ {method: wait_for_selector, selector: div.product-list} ], }, ) async def parse(self, response): page response.meta[playwright_page] # 滚动页面加载更多 for _ in range(3): await page.mouse.wheel(0, 2000) await page.wait_for_timeout(1000) html await page.content() # 后续用response或html继续解析 await page.close()这段代码展示了几个关键点。meta里的playwright: True表示这个请求不走原生下载器而是走Playwright渲染playwright_page_methods定义了一个列表Scrapy会在页面加载后依次执行这些操作——上面示例是等待某个选择器出现这比固定sleep要优雅得多因为网络状况不确定时等待具体元素出现比干等一个固定时间要可靠。playwright_include_page则让你能在parse回调里拿到page对象本身这样就能执行滚动、点击等操作。如果parse的方法是async def这意味着把整个页面的后续处理融进了异步循环。我试过在parse里用page.wait_for_timeout(2000)等待某个弹窗出现再关闭这段逻辑在同步方法里是做不到的。需要提醒的是既然用了async def parse用了page对象就记得await page.close()否则浏览器标签页会一直挂着不释放跑久了内存越占越高直到崩掉。对于滚动加载更多这种“无限滚动”页面我上面的示例是一个基础版本。实际抓的时候建议配合while循环判断底部是否满足条件比如滚动后检测某个“加载完成”标记是否出现而不是固定滚动3次——有些页面网络慢了3次根本加载不完有些页面滚动2次就到底了。3.4 iframe嵌套内容的处理方案iframe这块是很多人的知识盲区因为常规的response.css根本穿不透iframe的文档边界。但用Playwright的Frame Locator就能直接定位到iframe内部元素处理起来非常清爽async def parse(self, response): page response.meta[playwright_page] frame page.frame_locator(iframe#content) # 在iframe内部查找元素 items frame.locator(table tr.data-row) count await items.count() for i in range(count): item items.nth(i) title await item.locator(td.title).inner_text() yield {title: title} await page.close()这背后的原理是Playwright在渲染时会同步维护每个iframe对应的DOM树frame_locator并不是去页面里重新搜索而是直接进入子文档的执行上下文所以能拿到iframe内部的完整元素。另外有个经验如果页面里嵌套了多层iframe可以链式调用frame.frame_locator()逐层进入。实测下来这个方法对常见的第三方登录弹窗、支付确认框、社交分享组件的抓取都非常好用。3.5 动态页面下的去重与指纹问题动态渲染页面还有一个隐形坑很多网站的详情URL看起来不同但渲染出来的内容可能是同一套模板。如果爬虫对每个URL都发请求会产生大量重复数据。这时候Scrapy默认的去重机制就不够用了因为它只对Request的URL做SHA1指纹URL不同就认为请求不同。我的做法是在爬虫里自定义指纹去重import hashlib def get_content_fingerprint(title, author): raw f{title.strip()}{author.strip()}.encode(utf-8) return hashlib.md5(raw).hexdigest()在parse里计算出内容指纹放进Item然后pipeline里维护一个集合遇到已存在的指纹就raise DropItem。要注意这个集合不能只放在内存里因为爬虫一重启内存就清空了。我一般用Redis的SET做持久化指纹为key这样即使爬虫重跑也能接着去重。这个方案对增量采集特别实用——只需要抓新出现的数据老数据不用再重复抓。4. 数据落地的工程化细节Pipeline、去重与常见坑4.1 Pipeline的正确打开方式清洗、校验、存储分离Pipeline是Scrapy工程化的精华所在但很多人只拿它写个mongodb.insert_one就草草收工。实际上一个合格的Pipeline体系应该分三层写。第一层做清洗和校验。比如抓下来的价格字段可能是字符串¥1,299存进数据库前要转成浮点数抓下来的时间字段可能是各种格式的字符串要统一转成ISO格式字段缺失的Item可以直接raise DropItem丢弃不让脏数据进入下一层。第二层做数据增强比如给每条数据自动补充采集时间、来源URL、内容指纹。第三层才是存储把清洗好的数据写入数据库或消息队列。打个比方Pipeline就像工厂里的质检线一层层筛选、打标、装箱。如果所有事都塞在一层完成将来想换数据库就要改一大坨代码分好层之后换存储只需要替换最后一层的实现。4.2 Scrapy常见报错与排查速查表这里我整理了实际踩坑最多的几类问题和排查思路现象常见原因排查与解决频繁出现403 ForbiddenUA被识别或触发反爬检查DEFAULT_REQUEST_HEADERS换真实浏览器UA降低请求频率请求被302重定向到登录页触发登录校验或Cookie失效先请求登录接口拿Cookie或用中间件维护SessionTimeoutError大量出现并发过高或目标服务器限流调低CONCURRENT_REQUESTS增加DOWNLOAD_DELAY抓下来内容是乱码页面编码不是UTF-8在解析前用response.encoding检查设置FEED_EXPORT_ENCODINGNotSupported关于reactor没切换asyncio reactorsettings里配置TWISTED_REACTOR为AsyncioSelectorReactor内存无限上涨动态页面page对象未关闭确保await page.close()不要长期持有page引用4.3 增量抓取与断点续爬的实现有时候目标站点数据量非常大一次抓不完或者抓的过程中网络中断了。这时候最需要的就是断点续爬。Scrapy官方提供了JOBDIR参数来支持这个功能scrapy crawl quotes -s JOBDIRjobs/quotes-1Job目录里保存了调度器的队列状态、去重指纹和爬虫的状态信息。中断之后重新执行同样的命令Scrapy会从之前暂停的位置继续抓取而不是从头开始。这个功能对爬长尾数据或者定时增量采集特别有用。但用JOBDIR有一个要注意的坑如果你修改了爬虫代码逻辑恢复了job继续跑新旧代码处理同一条数据时行为可能不一致产生脏数据。我建议只在“没改代码、只是网络中断需要续抓”的情况下使用JOBDIR如果改了解析逻辑宁可从头跑。4.4 分布式扩展一个方向性的思路最后提一下分布式扩展。Scrapy单机跑得再快也有上限目标站点数据量过亿时就得考虑分布式架构了。scrapy-redis是业内最常用的方案思路是用Redis替换Scrapy默认的调度器和去重组件多个爬虫进程共享同一个Redis队列谁空闲了就去取请求来跑去重指纹也在Redis里做这样天然实现了多机横向扩展。我已经用这套方案搭建过8个节点的采集集群抓几千万条数据不在话下。不过分布式是另一个话题了初学阶段先把单机吃透更重要。回到我自己实操中的体会Scrapy最宝贵的不是某个具体的API而是它帮你规范了爬虫开发的整个思维路径——从请求的调度、解析的策略、数据的清洗到去重的管理、异常的处理整个链路都有章可循。我最早用requests写的那版爬虫三个月后自己都不想再看但用Scrapy写出来的采集管道一年后维护起来依然顺手。如果这个10.6章节你已经学完动手写一个自己的项目去抓一个你真正需要数据的网站比反复看十遍教程都管用。等遇到问题了再翻回来对照这篇里讲到的那些坑相信你会理解得比别人快一截。