搞爬虫这些年,真正让我觉得"值回票价"的项目,往往不是那些搭建超大规模集群的方案,反而是小而精、边界清晰的定向采集任务。这次的爬虫项目就是这样一个典型——目标明确,数据量不算夸张,但背后涉及的解析策略、异步调度、防封控意识和数据清洗逻辑,一套走下来相当完整。我把这次的全过程做个小结,从需求拆解到方案选型,从核心代码到踩坑实录,一次性梳理清楚。
不管你是刚接触爬虫的新手,还是想看看别人怎么处理反爬和调度问题的老手,这篇内容应该都能给你一些可复用的思路和实打实的经验教训。
1. 项目定位与需求分析
1.1 这次爬虫到底想解决什么问题
这个项目最初的需求非常朴素:某业务方需要一批结构化的商品数据,包含名称、价格、销量、评价数量和若干规格参数,用于做市场分析和价格走势追踪。数据源是一个中型电商平台,页面结构中等复杂,有一定的反爬机制,但谈不上顶级难度。
我接到任务后没有立刻写代码,先做了一轮需求确认,这一步比很多人想象的重要得多。爬虫最容易翻车的点不是技术,而是需求本身含糊。比如"把商品信息爬下来"这句话,听着简单,实际执行时会冒出无数细节问题:
- 商品范围是按分类拿、按关键词搜索拿,还是按店铺维度拿?
- 价格字段是否需要包含促销价、划线价、到手价这种多级结构?
- 评价数、销量这类高频变动字段,是只取当前值还是需要历史快照?
- 数据更新频率是一天一次、每小时一次,还是实时?
这些问题如果在项目启动前没有敲定,开发过程中大概率会返工。这个项目的情况是:范围锁定某个分类下的热销商品,每日增量更新,需要保留每日快照以支持后续趋势分析。需求边界清晰之后,技术方案的选型就顺理成章了。
在这个阶段我还有一个体会:爬虫工程师最核心的竞争力不是破解反爬有多强,而是对数据需求的拆解能力。同样一条商品页,不同业务角色关注的点可能完全不同,运营看价格竞争力,供应链看库存周转,财务看毛利结构。如果在写代码之前不能把这些视角差异收敛到一个统一的字段模型里,后面所有解析逻辑都会摇摆不定。
1.2 原始诉求之外还需要考虑什么
需求确认过程中,有几个隐藏项需要在设计时一并处理,否则后患无穷:
数据时效性。业务方说"每日更新",但实际使用时发现他们上午十点要基于数据做定价决策,所以真正的要求是每天上午八点前必须产出当天数据。这意味着爬虫调度要避开平台的访问高峰时段,同时给异常重试预留缓冲时间。
去重与增量识别。商品数据并不是每抓一次就必须全量更新。同一商品在不同时刻被抓取,价格可能变了,销量可能变了,但商品ID和标题没变。如果每次都无脑覆盖旧数据,后续做趋势分析时就没有了"历史轨迹"这个维度。所以当时在表设计上单独拉了一张价格快照表,主表存最新状态,快照表记录每次变化。
数据质量校验。爬虫抓回来的数据不可能百分百干净,缺字段、格式异常、明显越界的数值都有可能。如果这些脏数据直接落库,下游分析报表里就会出现"销量-1""价格12000元"这种奇葩记录。需要在入库前加一套校验规则,该丢弃的丢弃,该修补的修补。
把这些附加需求梳理清楚之后,整体方案的骨架就出来了:采集层采用异步并发提升吞吐,解析层用层级选择器配合正则处理动态内容,调度层用分布式队列控制节奏,存储层分主表和快照表双写。接下来逐个环节讲细节。
2. 整体架构与方案选型
2.1 技术栈选型:为什么是Python + Scrapy +异步扩展
语言和框架的选择是爬虫项目的第一个分岔路口。这个项目我选了Python,这个选择背后有非常实际的考量:
生态成熟度。Python在爬虫领域有最完整的工具链:requests处理HTTP、BeautifulSoup和lxml负责解析、Scrapy提供完整采集框架、配合pandas做数据清洗。这种"一站式"生态让项目落地速度极快,不需要到处拼凑不同语言的库。
团队协作成本。爬虫不是写完就完事的工程,后续会有维护、加字段、调逻辑这些需求。Python代码的可读性天然比某些语言好一些,接手的人上手成本低。如果全用底层语言实现,性能是上去了,但团队成员的心智负担也上去了。
异步机制带来的收益。IO密集型场景下,Python的asyncio和Scrapy自带的异步并发已经能榨干大部分网络等待时间的价值。这批商品数据的请求基本都在百毫秒到两秒之间返回,用并发协程跑和用线程池跑,效率差距非常大。
框架层面选了Scrapy。说实话,如果是单次性的小任务,用requests写个脚本循环请求就够了,没必要上Scrapy这种比较重的框架。但这次项目有几个特征决定了Scrapy是合适的:
- 采集目标有明确的结构化字段,需要定义一个统一的数据模型。
- 爬取规模有持续更新的需求,不是跑一次就结束。
- 需要处理分页、重试、去重、并发控制这些"爬虫标配"能力。
- 后期可能增加新的数据源,希望有一个可插拔的扩展架构。
Scrapy的架构里,Spider负责页面解析和提取URL,Pipeline负责数据处理和存储,Downloader Middleware负责请求前后处理,这种分层设计让每一个关注点都能独立修改和测试。实际开发中,我把请求头管理、延时控制、代理切换逻辑全部收拢到下载中间件里,spider的文件就干干净净只剩业务解析逻辑,这后面调试问题、改需求要舒服得多。
2.2 并发策略与频率控制的平衡
爬虫新手最常见的问题是贪:并发开得越高越好,抓取速度越快越好。但实际跑起来就知道,这种思路在小规模任务上可能会把目标站点拖到响应超时,甚至触发账号封禁,最后反而断断续续,效率远低于稳定匀速的采集方式。
这个项目采用了一种比较成熟的策略:异步并发 + 限速双管齐下。
并发层面,通过Scrapy的CONCURRENT_REQUESTS设置控制同时进行的请求数。我没有追求极限值,而是结合目标平台的响应时间和本机带宽,设置为16个并发。测试下来的结果,这个并发数既能保持较高的抓取吞吐量,又不会让请求集中到某个时间点形成明显的波动特征。
频率控制则有三种手段叠加:
- 自定义延时。在
DOWNLOAD_DELAY之外,我还写了一个动态延时中间件,在0.5秒到2.5秒之间随机浮动。固定的延时模式很容易被识别,随机化的本质是让请求间隔的统计特征更接近真人操作。 - 并发上限保护。即使并发设置很高,如果合起来每秒钟发出的请求数超过阈值,还是会造成压力。所以在调度层加了一个并发信号量,全局限制每秒请求不超过8个。这个阈值是怎么定出来的?观察目标平台的情况,同时参考同类电商站点能正常应对的频度,在安全区间内取了一个较为保守的值。
- 单一大批量任务拆分。全量商品列表不是一次性把所有URL都丢到调度队列,而是先抓分类入口页,每页数据解析后生成商品详情页的请求,再进入等待队列。这种"漏斗式"的请求漏斗天然实现了速率平滑,不会出现前几分钟疯狂请求、后几分钟无事可干的波峰波谷现象。
注意:其实很多平台的反爬策略已经不是看单个指标,而是综合请求频率、UA分布、行为路径、账号历史记录等多种信号。所以不要抱着"我把延时调大总不会封我吧"的心态,更关键的是让整体行为模式接近自然用户,而不是把指标压低就算安全。
2.3 数据存储设计:主表 + 快照表双写
存储选型上,项目用了MySQL。选择关系型数据库而不是MongoDB这种文档型,原因在于数据关联关系清晰、字段类型稳定,后续业务方要用SQL做统计分析也方便。
表结构上核心是两张表:
商品主表。保存商品的静态属性,包括商品唯一标识、标题、分类路径、规格参数、上下架状态、首次发现时间、最近更新时间。每个商品在这个表里只有一条记录,通过商品ID做唯一索引,重复抓取时用ON DUPLICATE KEY UPDATE做幂等更新。
价格快照表。保存每次抓取时的价格、销量、评价数、优惠信息,同时记录抓取时间。这张表是趋势分析的数据基础。每次增量抓取时,如果价格或销量相比上一次发生了变化,就插入一条新记录;如果完全没变化,则跳过不写,减少无效数据量。
这里有一个细节值得展开:判断"是否变化"不能直接比对浮点数。某些平台上价格看似没变,但底层数值可能因为四舍五入产生了微小差异,比如18.00和18.001。所以我做了字段归一化处理——价格统一保留两位小数后再比较;销量则看整数部分是否相同。这样一来,快照表里不会产生大量"无意义的变化记录",数据量能控制在合理范围。
存储层的另一个关键点是字段设计的可扩展性。商品规格参数可能因类目不同而字段各异,比如手机类目有"存储容量"“电池容量”,服装类目有"尺码"“材质”。我把这部分统一存在一个JSON列里,查询时用JSON_EXTRACT提取。这样主表结构不用跟着业务变化频繁改来改去,新加一个类目只是新增JSON里的几个key而已。
3. 核心实现细节与踩坑实录
3.1 页面解析策略:选择器与表达式的取舍
页面解析是整个爬虫里最需要耐心的环节。目标平台的部分数据是直接渲染在HTML里的,这部分用XPath或CSS选择器就能处理;但也有一些字段是藏在JavaScript变量里或者通过异步接口加载的,这就不能用单纯的选择器搞定了。
我当时的做法是分层解析:
第一层,先用XPath定位主体结构。比如商品列表页,每条商品记录都在一个li元素下,里面包含标题、价格、链接。对这类结构规整的页面,XPath的效率和可读性都很不错。项目里我统一用XPath而不是CSS选择器,原因是XPath可以基于父节点做上下文查找,比如"找当前商品节点下的第一个价格元素",这种相对定位场景XPath表达得更自然。
第二层,处理JavaScrip嵌入数据。很多平台会把商品信息序列化成一个JSON对象,直接塞进<script>标签里。这时候用正则从脚本内容中提取这个JSON,再用json.loads解析,比硬用选择器去抠HTML片段可靠得多。关键是正则的写法要足够精准,我见过很多同行用通配符匹配JSON导致抓下来的数据是截断的,正确做法是先定位到var data = {这个边界,再用括号匹配抓完整块。
第三层,兜底策略。如果前两层都没能取到期望字段,就降级到直接请求接口。现在很多平台的前端是SPA架构,列表页的数据其实来自一个JSON接口,页面只是渲染层。这种情况直接调用接口拿JSON是效率最高的方式。但要注意接口经常会有签名校验,需要分析请求头里的加密参数生成逻辑。这个项目刚好接口的签名算法比较简单,就是一个固定的header拼接再算哈希,破解成本很低,所以直接用接口方案解决了大部分动态内容的获取问题。
3.2 异常处理与重试机制的设计
网络请求不可能永远成功,超时、连接重置、返回异常状态码、内容被反爬策略拦截,这些都是常态。对于这些异常,处理不好就会出现数据缺失或者整个任务卡死。
我在项目的下载中间件里做了一整套异常处理逻辑:
超时分级重试。请求设置了连接超时5秒、读取超时10秒。超时后不会立刻重试,而是先进入一个等待队列,延迟一段时间后再发起。重试次数上限为3次。如果3次都失败,就把这个URL记录到失败队列里,不阻塞主流程,等全量任务跑完后单独处理。
HTTP状态码分类策略。200是正常;404和410说明资源已不存在,直接标记为失效,不做重试;500、502、503这些是服务端临时故障,会走重试逻辑;403、429这类则说明可能触发了反爬策略,处理方式完全不同——暂停当前爬虫、延迟较长时间、然后换策略再试。
反爬触发后的降级策略。如果连续出现多个403或429,说明当前行为特征已经被识别,再继续低头猛跑只会加重封禁风险。这时我会触发一个全局熔断开关,暂停整个爬虫,设置一个异常等待时间,同时启动二次验证识别逻辑。值得一提的是,识别验证页最简单的方式是检查响应内容里有没有特征关键字,比如验证码提示、安全验证字样等有明确特征的中文关键词,一旦命中就按被拦截处理。
这里有个实操小技巧:重试不能无脑重试同样的请求。如果第一次请求因为缺少某个header失败了,重试时必须先把问题修复了再发。尤其遇到被风控的情况,必须分析目标返回的页面或响应头,找到被封的原因——是UA被识别出来了?是IP段被标记了?还是请求频率过高?只有找出根因,重试才有价值,否则只是把同样的错误重复执行几次而已。
3.3 数据清洗与入库前的质量门槛
抓回来的数据不是拿到就能用的。以价格字段为例,我这边遇到过字符串带货币符号、带千分位逗号、含"促销价/到手价"这类复合文本、甚至直接是空白的情况。不做清洗,这些数据入库后就是一堆垃圾。
清洗模块分三层校验:
格式清洗层。把所有价格类字段统一格式化为纯数字,去掉货币符号、逗号和多余空格,再把空字符串统一转成NULL。销量字段如果有"1万+"这种缩写表达,做单位换算后转成实际数字。这里要特别提一个坑:某些平台的价格在HTML里是分开渲染的,整数部分一个元素、小数部分又一个元素,直接抓取文本会得到"18""99"两个片段,必须通过拼装逻辑还原成完整的18.99。
逻辑校验层。所有数字字段做边界检查:价格必须大于0且小于某个合理上限,销量大于等于0,评价数小于等于销量(这个规则在多数平台成立,但也有例外,所以只能设为警告不能设为拦截)。日期字段校验格式并统一为ISO标准时区。标题和描述这类文本字段做长度校验,异常短的通常是解析失败的残留。
唯一性校验层。每个商品ID在入库前做一次查重,重复数据直接丢弃。这里有一个值得说的经验:去重不能只依赖数据库的唯一索引,如果重复数据在到达数据库之前就在内存里占了一份,纯靠数据库层面拦截会白白浪费请求和解析资源。我在Spider产出item时就会用BloomFilter做一次前置去重,内存中过滤得差不多了再交到Pipeline里,这样既能省MySQL的连接开销,也能减少无效写入。
去重这个点我觉得可以多说两句。BloomFilter这种数据结构对爬虫场景特别合适——它有一定的误判率,会把极少数不重复的URL误判为已抓取,但绝不会把已抓取的URL漏过去。这个特性放在爬虫的去重场景里正好合适:宁可漏抓一两条(后续可以用其他策略补偿),也不要重复抓一堆给平台增加压力。实际配置时我把误判率设置为1%,这个比例下空间效率和可用性是很平衡的。
4. 常见问题排查与效率优化实录
4.1 抓取数据量与预期不符时的排查路径
项目运行过程中遇到过一次很头疼的问题:明明列表页解析出了5000个商品链接,最终入库只有3000条,大量数据凭空消失了。
排查过程是这样的:
第一步,检查失败日志。重试3次依然失败的URL被记录在failure.log里,数量大概200多条,和500差距不小,说明不是全部丢失的原因。
第二步,检查去重模块。BloomFilter的误判率虽然是1%,但这个概率按5000个样本算也只有50条级别的误杀,不符合情况。
第三步,抓包对比。发现部分列表页返回的商品链接其实是同一个商品的不同SKU变体,比如同一款手机有黑色和白色两个链接,点进去详情页的商品ID相同。去重模块把这些判定为重复数据丢弃了,一统计才发现这种变体占比接近30%。
找到根因后,处理方案不是简单放开去重——如果放开,那同一商品的多SKU会变成多条独立记录,只想要热销款数据的业务这边也不希望这样。最终方案是:详情页解析时额外提取SKU规格信息,如果是同商品ID的不同SKU,就把规格字段合并到该商品记录的SKU集合里,只保留一条主记录。既避免了重复采集,也没有丢失规格数据。
这个问题的经验就是:排查数据差异时永远不要只看总量,要拆到每个处理环节看经过前后的数量变化,才能快速定位哪个环节出了问题。总量对不上一定意味着某个环节的过滤或转换逻辑和预期不一致,一步步对照日志和中间结果,远比凭空猜想快得多。
4.2 异步并发下的性能瓶颈与优化
爬虫跑起来速度慢,大部分人第一反应是加并发。但我在项目里遇到过瓶颈根本不在并发数,而是在其他环节:
DNS解析阻塞。项目初期,请求的域名解析都是同步完成的,高并发下DNS解析变成一个隐藏的串行点。后来在系统层面启用了DNS缓存,同时把解析超时降到了2秒,整体吞吐量提升明显。
数据库写入瓶颈。并发抓取时,Spider产出item的速率远高于数据库单条插入的速率,结果就是Pipeline里的item堆积,内存占用飙升,数据库连接池被占满。最初的写法是每条item各自commit,效率很低。后来改成批量写入方案,攒够50条或者每隔5秒批量flush一次,写入性能提升了不止一个量级,代码复杂度也并没有增加多少。
CPU解析过载。XPath解析和正则匹配是CPU密集型操作,如果全部在主线程跑,异步IO带来的性能优势会被部分抵消。实际处理时我在数据提取环节用了一个进程池,把HTML解析任务分发到多个进程并行执行。这个优化尤其适合大量列表页的场景——每页都要解析几十个商品节点,串行解析和并行解析的差距非常可观。
4.3 增量更新场景下的稳定性保障
这个项目是每日增量更新的模式,不是一次性全量抓完就结束。这种持续运行的爬虫,稳定性要求比一次性任务高得多,因为一旦挂了,没有及时发现,当天的数据就是缺失的,整个快照序列就断了一截。
我在增量更新场景里加了几个保障机制:
断点续跑。任务执行过程中如果出现进程崩溃或手动终止,重启后能从上一次标记的位置继续抓取,而不是从头来过。实现方式是维护一个游标文件,记录已完成的翻页页码和已抓取的商品ID集合,每次启动时读入恢复现场。这个概念和数据库的binlog位点有点像,本质都是给流水账记位置。
定时巡检。写了一个独立的巡检脚本,每天固定时间检查当天任务是否按预期完成,包括数据量是否达到预期、快照表里有没有新增记录、失败队列里残留了多少条。如果有异常,自动触发一次补偿性抓取任务,把失败的部分补回来。巡检本身不依赖爬虫主进程,即使爬虫整体崩了,巡检也会按计划运行,起到保险丝的作用。
结果校验报告。每次抓取任务结束后,自动生成一个报告:本次采集了多少条、成功多少条、失败多少条、重复多少条、各分类覆盖情况如何、耗时多久。这个报告对于判断"今天的数据是否可信"非常有用。就算业务方没要求这个功能,我也会建议做上,因为运维人员和后续接手者都需要靠它快速了解采集的健康状态。
4.4 采集过程中容易被忽视的合规问题
最后一个话题要单独拿出来说,因为它比任何技术细节都重要:爬虫的合规性。
我个人的原则是,采集行为必须合法合规,不能触碰红线。项目启动前要确认数据来源公开可见、没有明确的访问限制,而且使用目的限于个人或团队的研究分析和正常业务需求,不会对外进行不当传播。实际操作中,我会做几件事:
- 遵循平台的服务条款,不强行绕过或突破平台正常的机制和权限边界,不采集需要登录或特定身份才能访问的非公开数据。
- 在请求中带上可联系的User-Agent标识,明确告知源站这个爬虫的身份。
- 抓取频率保持保守,不给目标站点带来过大的访问压力。
- 只采集处理任务真正需要的最少字段,不把整个页面内容全量拉下来存储。
这两年被大数据和AI带动的数据需求越来越多,但数据采集始终要在法律法规框架内进行。技术能力是工具,用什么方式去用这个工具,是每个从业者自己要守住的底线。
5. 对爬虫工程化的几点体会
这次项目做完之后,我对"爬虫小结"这个词的理解深了一层。爬虫看起来是一个"发请求、收响应、解析数据"的简单流水线,但真正工程化了之后,它实际上是一个完整的数据系统工程:需求分析、任务调度、并发控制、异常管理、数据清洗、存储设计、稳定性保障、合规审查,每一个环节都是独立的学问,缺一个环节后面都会找补回来。
有几个具体的体会分享给同行:
第一,把"爬到数据"和"爬对数据"分开来看。很多人验证爬虫是否完成的标准是"跑完了没报错",但真正专业的标准是"入库数据经过校验后依然准确可靠的比例"。这个标准倒逼你写数据校验逻辑、写覆盖率统计、写报告,而不是跑完就撒手不管。
第二,日志和监控系统前置配置。爬虫跑起来容易出现各种隐蔽问题——某个字段偶发解析失败、某些页面响应偏慢、某个接口改了返回结构。如果没有完善的日志记录,这些问题只能在业务方反馈"数据对不上"时才能发现,那时候排查成本就很高了。我在这类项目中习惯把所有关键节点都打上日志,包括每次请求的URL、状态码、耗时、解析出的字段数量,配合简单的统计面板,基本能做到问题当日发现、当日定位。
第三,不要在单一方案上堆时间。有些页面结构复杂,你在XPath选择器上调来调去几个小时,其实换一种思路比如直接用渲染工具配合脚本模拟操作,可能几分钟就解决问题。爬虫领域没有银弹,每个平台的情况都不一样,工具箱越丰富,现场处理问题时就越从容。
第四,温和采集是长期可持续的基础。高频请求、暴力采集这种策略,短期能看到漂亮的数据量,但很容易导致后续无法采集,而且对目标站点也是一种负担。我在长期运行的项目里,会把节流、重试、分布式调度都做成可配置项,随时按情况调整,而不是固定死一套参数打天下。
爬虫这个领域一直在变化,新框架、新协议、新反爬手段层出不穷,但底层的能力模型是相对稳定的:理解HTTP和Web运作原理,懂页面结构分析,有耐心处理数据质量问题,以及最重要的,保持对数据合法性的敬畏。技术本身不复杂,复杂的是把它放到真实场景里,用可靠、稳定、合规的方式持续提供有价值的数据。
这次的小结就写到这里。希望对正在做类似项目的朋友有参考价值,也欢迎踩过更多坑的同行补充交流。