上周帮一个朋友排查爬虫数据量骤降的问题,日志里没有任何报错,数据却从每天三万条掉到三千条。翻了一整圈,最后定位到是调度器队列积压,下载器超时重试占满了带宽,爬虫本身根本没空跑。这件事让我觉得还是值得把基于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 + BeautifulSoup | Scrapy |
|---|---|---|
| 并发模型 | 需要自己写线程池或异步 | 内置异步并发,默认开启 |
| 去重机制 | 自己维护URL集合 | 内置指纹去重,可持久化 |
| 重试/失败处理 | 自己写异常逻辑 | 下载中间件自动重试 |
| 数据管道 | 手动清洗入库 | Pipeline链式处理 |
| 扩展生态 | 无 | DownloaderMiddleware、Item Pipeline等 |
| 运维监控 | 靠print日志 | 结构化日志加统计扩展 |
这不是说前者一无是处,而是说当项目需要长期维护、数据量持续增长的时候,Scrapy的工程化设计能省掉很多重复工作。最直观的一点,Scrapy的调度器是异步的,请求发出去之后不用傻等页面返回,可以同时处理其他请求。这个底层机制让我第一次从同步脚本切换过来时,速度提升了几十倍。
2. Scrapy的运转逻辑:从Request到入库的一次完整旅程
2.1 项目骨架是模板也是规范
创建项目是规定的姿势,命令就一行。假设项目叫demo_spider:
scrapy 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的价值也正在于此——它把很多调度和并发层面的细节都封装好了,让你有精力去关注真正会出问题的业务环节。