Scrapy爬虫工程化实战:从调度原理到数据采集稳定上线
上周帮一个朋友排查爬虫数据量骤降的问题日志里没有任何报错数据却从每天三万条掉到三千条。翻了一整圈最后定位到是调度器队列积压下载器超时重试占满了带宽爬虫本身根本没空跑。这件事让我觉得还是值得把基于Scrapy的整套开发流程梳理一遍——很多人对Scrapy的印象停留在“能写爬虫”但对它内部怎么调度、哪些配置会互相影响、日志怎么看平时缺少系统性的认识。这篇文章围绕Scrapy框架的完整开发流程来写覆盖从需求拆解、工程搭建、核心组件原理到反爬识别、数据完整性、断点续爬以及最后的调试和上线运维。无论是刚入门想搭第一个爬虫还是已经写了一些脚本但对工程化没有把握都能在里面找到可以直接照做的内容。1. 起步前先画好两条边界采集需求和访问红线1.1 先把需求拆成可量化的目标很多人上手就写爬虫写了一半才发现字段不够用、频率不对、目标页面结构变了。我习惯在写任何代码之前先回答四个问题数据字段有多少哪些是必须的哪些是可以后期补的数据量级是多少是几万条还是几亿条更新的频率要求是每天一次、每小时一次还是实时数据要落到哪里本地文件、数据库还是消息队列这四个问题直接决定了后续怎么设计Item、怎么配置调度以及要不要上分布式。比如一个只需要每天同步一次的行情数据跟一个需要实时抓取评论数据的项目整个工程结构会有很大差异。前者单机加个定时任务就够了后者要仔细考虑并发、限速还有断点续爬。一个快速估算请求量的方法目标站点页面总数乘以每个页面的请求数再除以计划耗时就能算出需要的并发上限。如果QPS只需要个位数Scrapy默认的并发配置完全够用根本不需要分布式。1.2 合法性边界robots协议和访问频率爬虫领域的合规问题不是写不写代码的事情而是项目能不能长期跑下去的底线。我的做法是分三步确认边界先看目标站点有没有robots.txt明确哪些路径允许访问。再看站点有没有公开的API或数据接口很多网站其实提供了比网页更友好的数据出口能走API就不走页面解析。最后是控制访问节奏请求频率要设置在“不给对方服务器造成压力”的水平。这几个点如果从一开始没有明确项目做到一半被限制IP或者收到警告返工成本非常高。这个边界也影响技术选型。如果你抓的是完全公开、更新稳定的数据Scrapy的简单Spider就够了。如果目标页面对访问行为敏感那后面第三章要讲的请求头管理、限速策略就更重要。我一般建议在项目初始化时就设置好DOWNLOAD_DELAY不要等到被限制之后再手忙脚乱。1.3 为什么选Scrapy而不是requests加BeautifulSoup不少新手会用requests加BeautifulSoup写一个脚本跑通之后发现维护性很差。如果只是抓几十个页面这种组合完全没有问题但一旦目标页面数量上到几万甚至几百万两者差距就体现出来了。我做过一个简单的对比整理成表格对比维度requests BeautifulSoupScrapy并发模型需要自己写线程池或异步内置异步并发默认开启去重机制自己维护URL集合内置指纹去重可持久化重试/失败处理自己写异常逻辑下载中间件自动重试数据管道手动清洗入库Pipeline链式处理扩展生态无DownloaderMiddleware、Item Pipeline等运维监控靠print日志结构化日志加统计扩展这不是说前者一无是处而是说当项目需要长期维护、数据量持续增长的时候Scrapy的工程化设计能省掉很多重复工作。最直观的一点Scrapy的调度器是异步的请求发出去之后不用傻等页面返回可以同时处理其他请求。这个底层机制让我第一次从同步脚本切换过来时速度提升了几十倍。2. Scrapy的运转逻辑从Request到入库的一次完整旅程2.1 项目骨架是模板也是规范创建项目是规定的姿势命令就一行。假设项目叫demo_spiderscrapy startproject demo_spider这个命令会生成一套标准的目录结构每个文件都有明确的职责。很多老手也知道这个命令但未必意识到这套结构本身就是工程规范——spiders目录下放爬虫逻辑items.py定义数据字段pipelines.py处理数据清洗入库middlewares.py放请求和响应的钩子。团队协作时新人只需要看目录结构就能理解整个项目的数据流向这比自定义一堆脚本文件要容易维护。命令行还支持直接在项目目录里生成一个具体的爬虫模板scrapy genspider bookspider books.example.com生成的模板里含有一个基础Spider类默认的parse回调函数等着你填充解析逻辑。用模板而不是手写还有一个好处是parse函数的签名、allowed_domains的写法这些都是框架约定好的不用去查文档。2.2 一次请求的完整生命周期理解Scrapy核心是搞清楚一个Request的完整生命周期。这个生命周期大致分七步引擎从爬虫拿到初始的起始请求交给调度器放入队列。调度器按优先级出队交给下载器。下载器执行下载前会依次经过下载器中间件。请求头、代理、Cookie等都在这里处理。下载完成后响应对象同样经过下载器中间件回到引擎。引擎把响应交给爬虫的parse回调函数。parse里解析出数据封装成Item交给Item Pipeline处理。parse里还能生成新的Request继续回到第2步循环。这个流程直接解释了一个常见问题为什么Scrapy的代码里看不到明显的for循环去抓取页面因为整个过程是事件驱动的写爬虫本质上是在定义“拿到响应后做什么”和“生成哪些新请求”而不是自己控制循环。2.3 Item设计决定数据质量的上限很多入门教程把Item当成一个可有可无的容器随便定义几个字段就行。实际项目里Item是数据质量的源头我见过最多的问题就是字段定义不完整到入库阶段发现缺字段只能回炉重跑。以爬取一个图书列表页面为例import scrapy class BookItem(scrapy.Item): title scrapy.Field() author scrapy.Field() price scrapy.Field() publish_date scrapy.Field() isbn scrapy.Field() source_url scrapy.Field() fetched_at scrapy.Field()注意多了一个source_url和fetched_at这俩字段看起来不起眼但价值极大。source_url能让你在数据出问题时溯源fetched_at能让你切分增量数据和判断数据新鲜度。加这么两个字段只花几秒钟但后期排查问题能省几个小时。2.4 Pipeline执行顺序与常见操作Item从parse里产出后会依次经过Item Pipeline。这里是做数据处理的最后一站典型操作包括清洗字段、转格式、去重和入库。Pipeline的执行顺序是在settings.py里通过数字优先级控制的数值越小越先执行ITEM_PIPELINES { demo_spider.pipelines.CleanPipeline: 300, demo_spider.pipelines.DuplicatesPipeline: 400, demo_spider.pipelines.MongoPipeline: 800, }比如要清洗价格字段把一个字符串类似¥99.00转成浮点数99.00在Pipeline里处理比在parse里散落处理要干净得多。而且Pipeline有个方便之处同一个Spider里的所有Item都会走同一条管道如果后面加了新爬虫清洗和入库逻辑也可以复用同一套Pipeline不需要在每个Spider里重复写。3. 实战里最容易翻车的三块地方及排查链路3.1 请求头细节被识别为机器人的常见原因做过一段时间爬虫基本都会遇到被封策略最常见的被封原因其实是请求头不对劲。目标站点的反爬系统会从请求头里提取大量特征User-Agent、Accept-Language、Referer、Cookie的一致性。如果有一个地方不自然就会被识别。一个非常常见的反面示例是所有请求都使用同一个User-Agent。这在数据量小的时候问题不大一旦请求量上去特征会变得非常明显。我的做法是在DownloaderMiddleware里给每个请求随机分配一个常见的浏览器UA同时保证请求头的顺序尽量和真实浏览器一致。这里要注意UA策略不是越乱越好。使用完全不常见的UA组合反而比固定一个常见UA更可疑因为这个UA在真实用户群体里根本不存在。我一般维护一个大约20条的UA池覆盖主流浏览器的最近几个大版本每次请求从中取一个。3.2 下载延时与并发自动限速不是万能钥匙Scrapy默认的并发设置是16个并发请求没有下载延时。对很多网站来说这个频率已经偏高所以第一件事就是设置一个合理的DOWNLOAD_DELAY。比如0.5秒的延时意味着每个请求间隔至少0.5秒对于中小型站点已经足够温柔。但设了延时之后可能太慢这时候可以配合AUTOTHROTTLE配置让框架自动动态调整延迟。有一个配置组合我强烈建议在settings里开启AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY 4.0这套配置的意思是起步延时1秒根据服务器的响应速度自动调节最多加到10秒。不要手动把并发调得很大Target concurrency设为4是个比较稳的起点。如果你发现爬虫经常超时不要太急着加大并发先看目标服务器的响应时间是否已经过载Scrapy日志里的慢请求条目就是最直接的提示。3.3 断点续爬与去重指纹设计决定增量能力爬虫跑得越久越能感受到断点续爬的重要性。如果抓了一千万条突然网络中断或者机器重启结果要从头跑谁都受不了。Scrapy默认使用RFPDupeFilter做去重它的核心是把请求的方法、URL和请求体计算出一个SHA1指纹这个请求指纹一旦出现就不再重复调度。默认去重在同一个爬虫进程内是有效的但重启之后去重记录就丢了。要做持久化去重通常重写DupeFilter把指纹存到数据库里。这样可以实现跨重启甚至跨机器的去重。这个概念虽然主要讨论断点续爬但同样可用于处理增量采集第一次全量抓取后把指纹记录存下来后续只需判断新请求的指纹是否已存在。一个经验是保存指纹时连带保存请求的时间和来源方便后面排查数据缺口时定位原因。3.4 一次完整的数据量骤降排查过程回到开头说的那个案例朋友的项目数据量从每天三万掉到三千日志没有任何报错我把排查链路完整走了一遍第一步看系统日志的INFO级别。这里没有错误只有大量超时重试记录。第二步看下载器统计。通过scrapy stats打印出来的downloader/response_count和downloader/response_status_count发现大部分响应是504超时而不是200。第三步看调度器队列大小。scheduler/enqueued远超scheduler/dequeued说明请求积压得越来越严重。第四步看延时配置发现他把DOWNLOAD_DELAY设成了0.1秒同时并发开到了32目标服务器响应逐渐变慢超时重试又占满了带宽。第五步回退配置。把延时改成1秒并发降到8重新观察统计数据量就恢复到正常水平了。整个排查过程没有涉及到复杂的代码改动纯粹是对配置项之间关系的理解。像这样的问题如果只看错误日志很可能一整天都摸不着头脑。4. 从debug到上线调试习惯、监控指标与部署要点4.1 开发期先学会使用scrapy shell开发调试爬虫时打开终端一行一行解析页面比反复改代码重新启动要高效太多。scrapy shell可以直接抓取一个URL并进入交互式环境scrapy shell https://books.example.com/在交互环境里response对象已经就绪可以直接试各种CSS选择器或XPath表达式确认之后再复制回代码。这个流程能把调试时间缩短一半。使用快捷键view(response)能在浏览器里直接看到抓回来的原始页面比一个个打印HTML安全可靠。不过要注意一个使用场景有些目标页面是异步渲染的页面内容由JavaScript后续加载scrapy shell默认只拿到初始HTML看不到动态内容。遇到这种情况先确认数据是不是在初始HTML的script标签或嵌入的JSON里很多站点会把初始数据放在window.__INITIAL_STATE__这样的变量中正则解析比无头浏览器更轻量。4.2 日志系统的分级与统计Scrapy的日志体系是被很多开发者低估的价值点。设置好日志之后线上异常排查会变得非常高效。推荐设置LOG_LEVEL INFO LOG_FILE logs/scrapy.log日常开发建议设成DEBUG看Request细节上线后改成INFO减少噪音。日志级别从低到高是DEBUG、INFO、WARNING、ERROR、CRITICAL。如果不设LOG_FILE日志会直接打到标准输出更适合开发调试一旦进入定期任务运行的场景要尽早配置日志文件并做好轮转。更关键的是stats统计每个爬虫跑完后都会输出一长串数据包括start_time、finish_time、item_scraped_count、log_count/ERROR、downloader/request_bytes等。养成每次爬虫结束都扫一眼这些统计的习惯比上线后看监控报表管用得多。item_scraped_count和downloader/response_count一对比就能看出数据清洗阶段丢了多少条。4.3 存储与调度的工程化细节爬虫拿到的数据大概率不是落到本地文件就算完。一个典型的做法是Pipeline层把清洗好的数据写入数据库同时记录抓取时间和来源URL。这里有一个容易忽略的点写数据库的Pipeline一定要做幂等设计。消费方可能因为网络抖动重复投递数据如果写入操作没有幂等约束就会出现重复记录。简单做法是给业务表建一个唯一索引以source_url或请求指纹作为唯一键重复插入时报错就忽略。定时调度的推荐姿势是Scrapy负责抓取调度交给Cron或任务平台。但要注意不要在Cron里用粗暴的方式重启爬虫例如直接杀掉进程再启动这种方法容易留下半截数据。一个更稳的做法是爬虫从队列里取任务抓完一批之后正常退出把“下一轮什么时候开始”交给调度器控制。4.4 上线前的检查清单上线之前先把清单过一遍能提前排掉最常见的坑。完整的检查清单包括以下项目检查项预期结果CONCURRENT_REQUESTS根据服务器响应能力设置不要超过目标站承受能力DOWNLOAD_DELAY有明确的延时值或开启自动限速请求头有UA池且有合理的浏览器特征日志级别上线环境设INFO并有日志文件输出去重持久化重启后不重复抓取数据表唯一键有幂等入库约束告警机制连续几次错误或采集量为零时有提醒这份清单看起来很短但每条背后都是实际踩过的坑。特别是最后一条采集量归零这个问题不容易被直接发现因为爬虫可能还在运行进程也没有退出只是所有请求都失败了。如果上线前配置好“低于阈值就告警”的机制就能及时介入。跑过一段时间后我再回看这个清单发现真正决定一个爬虫项目稳定性的往往不是解析能力而是这些基础配置的合理程度。而Scrapy的价值也正在于此——它把很多调度和并发层面的细节都封装好了让你有精力去关注真正会出问题的业务环节。