爬虫技术分享这个主题我在技术群里聊过很多次。聊多了之后发现一个有意思的现象不少人觉得爬虫就是“requests发个GET拿回来解析HTML”简单到不值得单独拿出来讲可真到接手一个稍微像样的真实数据页面时又会一头栽进各种奇怪的问题里列表页打开挺正常脚本跑出来就是空的加了各种请求头还是没用最后只能对着屏幕怀疑人生。今天这篇内容我想从一次具体的抓取经历出发把爬虫技术从“能抓到数据”到“稳定地抓到数据”这条路上真正值得琢磨的点拆开说清楚。内容不限定在某个框架的API调用上而是更多关注思考和排查的过程。无论你是刚接触爬虫的新手还是已经写过一阵子脚本、想在工程化和合规性上补补课的开发者这篇文章应该都能给你一些不一样的参考。1. 先弄清楚数据住在哪一层静态页面、数据接口与渲染进程很多人一上来就写代码这是爬虫最常见的错误姿势。接到一个抓取任务先别急着打开编辑器想清楚一个更基本的问题你要的数据到底是以什么形式存在目标页面里的1.1 静态HTML直取最直观但最脆弱的一层最理想的情况是页面做了服务端渲染数据直接嵌在HTML里。这种情况下写爬虫确实很简单requests拿到响应用解析器提取节点就行。但静态HTML也是所有形式里最脆弱的一层。只要前端工程师调整一下页面结构哪怕只是改了一个CSS类名你的解析逻辑就废了。我见过有人维护了半年的爬虫某天突然一条数据都抓不到了最后发现原因是页面上一个div的层级变了解析规则全盘落空。所以即便是面对静态页面也别把解析规则写得太死。一个靠谱的习惯是把“页面结构指纹”和“数据提取”分开——先判断当前拿到的页面还是不是已知的页面结构再决定要不要执行解析。结构变了就告警而不是闷头跑出一堆空数据。1.2 JSON接口捕获真正省力的路往往被忽略现在的网站哪怕看起来是传统的服务端渲染背后也经常会带一些异步接口。很多时候页面展示的数据并不是直接写在HTML里的而是由浏览器在加载过程中通过XHR请求一个JSON接口拿到的。直接用接口拿数据比解析HTML要干净得多。JSON结构化程度高字段清晰不用考虑各种标签嵌套而且接口请求量通常比页面请求小得多——一个页面可能几百KB一个JSON接口可能只有几十KB。问题在于很多人不会主动去找接口拿到页面就顺手从HTML开始解析。这就导致代码又慢又脆。以后接到一个抓取任务先花几分钟打开开发者工具的面板刷新页面看看有没有XHR请求返回了你要的数据。如果有这条路优先走。1.3 动态渲染兜底前端框架页面的常见解法近几年前端框架普及之后遇到纯静态HTML的概率越来越低。很多页面打开以后浏览器要先执行一堆JavaScript才能把数据填充到DOM上。requests拿到的HTML里只有空壳数据根本不在里面。这种页面抓起来就得上浏览器自动化工具让一个无头浏览器真正把页面跑一遍。这是最稳的兜底方案也是最慢的方案后面我会专门讲怎么在效率和稳定之间取平衡。1.4 一个判断树拿到目标页面后先做这三件事我现在接到任何抓取任务都会先按这个顺序摸一遍底在浏览器里打开目标页面右键查看网页源代码搜索你要的数据字段。如果源代码里能搜到属于静态HTML直接解析。如果源代码里搜不到打开开发者工具的Network面板清空日志后刷新页面看XHR列表里有没有返回数据的接口。有的话优先调接口。如果XHR找不到或者接口参数过于复杂再考虑上无头浏览器渲染页面。这三步做完你对后续方案的复杂度基本就有数了。比如要写的代码量、请求的实效性、解析的稳定性都能预估个大概。2. 模拟项目X连线实录200状态码背后为什么是空列表下面用一次真实的排查过程走一遍完整链路。这个场景我去年在某公开数据平台练习时也遇到过当时还犯了不少蠢错误拆开来看挺有价值的。2.1 现象复现requests返回200解析结果却为空任务是从一个公开数据平台的列表页抓取行业的列表信息页面结构看起来很简单一个列表每个条目有名称、编号、更新时间。按照常规思路requests请求列表页URL状态码200一切正常。然后解析的时候懵了列表区域提取出来的内容是空的。不是解析代码写错是HTML里压根就没有那些条目节点。页面加载成功了响应也回来了但里面没有数据。这个阶段最容易让人产生迷惑。状态码200往往会给新手一种“我已经成功了”的错觉会让人下意识认为是自己解析代码的问题于是反复调整解析规则折腾半天依然什么都拿不到。2.2 第一个转折浏览器里看到的页面和requests看到的不是同一个排查到这一步必须接受一个现实浏览器里展示的最终DOM和requests拿到的原始HTML中间差了好几步。浏览器打开页面以后要做的事情包括加载HTML、解析CSS、执行JavaScript、发起各种额外请求最终才绘制出你看到的页面。验证方法很简单在浏览器里打开目标页面用“查看网页源代码”功能看一眼再对比开发者工具里Elements面板看到的DOM结构。这两个地方不一样就说明页面经过了JavaScript动态渲染。我当时的验证结果就是这样网页源代码里没有列表数据Elements面板里有。等于说requests拿到的只是骨架真正的数据是页面加载过程中通过别的请求拿到的。2.3 第二个转折真正的数据来自一个带签名参数的XHR接口既然数据不在HTML里就得看浏览器到底额外发了什么请求。回到Network面板清空日志刷新页面观察XHR类型的请求。很快就找到一条返回JSON数据的接口里面整整齐齐地列出了列表信息。单独在浏览器地址栏里访问这个接口数据也正常返回。但把接口URL复制到requests里模拟请求时问题来了——服务器返回了一个错误状态提示缺少必要参数。仔细核对浏览器发出的请求发现URL里比直接访问多了一串参数里面有一个类似签名的动态字段每次请求都不一样。这个字段是通过JavaScript在前端计算出来的requests脚本里没有浏览器环境自然生成不了。到这里问题的性质就变了。原来的任务是“抓取列表页”现在的关键是“如何让接口的签名参数通过验证”。2.4 排查结论把“抓页面”重新定义为“模拟动态参数”这个阶段的重要经验是别停留在“我抓不到数据”这样模糊的描述里要把问题拆成可以解决的技术命题。我当时把问题拆成了三个数据在哪个接口里——已定位。接口要求哪些参数——已知有签名、时间戳、分页参数。签名是怎么来的——待分析。这个拆解看起来简单但它决定了后续投入的方向。很多人卡在动态加载页面上就是因为始终在用“抓HTML”的思路处理“抓接口”的问题两者需要的技术栈完全不同。3. 签名参数怎么定位从调用栈回溯到浏览器执行兜底确定了要模拟签名参数之后接下来就是定位这个参数是哪段JavaScript算出来的。这也是爬虫技术里比较核心的一段很多人觉得玄乎但方法论其实是固定的。3.1 XHR断点法让请求停下来自己交代来源定位动态参数我习惯用的第一个工具是浏览器开发者工具的Sources面板里的XHR断点功能。操作方式是切到Sources面板找到XHR/fetch断点配置把之前定位到的接口URL作为断点条件填进去。然后刷新页面当浏览器要发起这个请求时执行会暂停在发请求的那一行代码。这个时候观察调用栈就能看到发起这个请求的函数链条从底层到顶层一层层往上追。这个做法的好处是你不用去大海捞针一样翻JS文件请求会自己告诉你它从哪里来。3.2 调用栈回溯与加密函数输入输出分析沿着调用栈往上走通常会在某个函数里找到签名参数被赋值的过程。在这个函数上再打一个断点重新触发请求查看这个函数接收了哪些输入参数输出了什么结果。我当时看到的情况是签名函数接收了当前时间戳、请求路径和一个看起来像固定密钥的字符串作为输入经过一系列拼接和哈希计算之后输出了一串摘要字符。输入参数变了输出结果就会变这也是为什么直接复制别人的请求URL不能复用——时间戳过期了。这种情况下如果哈希算法不复杂可以尝试用Python重写一遍签名逻辑。关键是要把函数体的每一步操作都推演清楚包括字符串的拼接顺序、编码方式、大小写处理任何一点差异都会导致签名校验失败。3.3 复刻还是渲染两条路怎么选复刻签名逻辑性能和请求效率都好但有两类情况会比较棘手一类是算法复杂里面混了大量环境判断、代码混淆需要花大量时间去逆向投入产出比很低。另一类是签名算法依赖浏览器环境的特征接口单纯靠Python模拟出来非常费劲甚至可能会被对方的风控识别出来。遇到这两种情况我的选择是在效率和稳定性之间做权衡。如果任务对实时性要求不高或者抓取频率本身很低直接上无头浏览器渲染页面等数据接口的响应返回后拦截下来拿到的就是干净的JSON。这种方式“笨”但胜在稳定。页面如果不做大的重构渲染逻辑基本不用改。3.4 无头浏览器的节流改造资源过滤与实例复用用过无头浏览器的人都知道慢是最大的痛点。启动一个浏览器实例可能就要一两秒如果每次请求都重新启动一遍效率会非常感人。而如果页面还加载了各种图片、视频、第三方统计脚本整个抓取过程会被拖得更慢。实际项目中有两个优化手段很好用。一个是通过请求拦截功能在页面加载时屏蔽掉图片、字体、样式等非核心资源。数据接口的JSON响应不受影响但页面整体加载时间能缩短一半以上。另一个是复用浏览器实例。不要每个任务都新开浏览器而是维持一个常驻的无头浏览器进程任务进来通过队列排队执行。这样一次的启动成本被摊薄到多次请求上整体吞吐量能提升不少。我当时用这个方案整个列表分页抓完速度虽然比不上直接调接口但胜在一路顺畅没有因为签名问题断过。4. 从能跑到跑稳并发、退避重试、去重与数据校验的工程落地脚本能跑通只是第一步真正考验爬虫技术的是把它变成一个可以长期稳定运行的任务。这一部分决定你是在“写玩具脚本”还是在“做一个正经的数据采集模块”。4.1 请求节奏是第一优先级波动比特判更重要很多人在上爬虫的第一天就想把并发拉到很高觉得越快越好。这个想法最大的问题不是技术做不到而是会让目标服务的负担陡增进而触发风控策略。一个更合理的做法是先把请求频率控制在一个对对方服务友好的范围内。比如每秒最多请求若干次每次间隔加入随机波动。这个随机波动很重要——固定间隔在服务器日志里会呈现出非常规律的节奏容易被识别为自动化行为加入随机性之后节奏更接近真人操作只要频率本身合理通常问题不大。我做任务时习惯先把单位时间内的请求数写成一个可配置参数从比较低的数值开始跑再根据响应情况逐步上调直到逼近阈值就停下来。宁可慢一点也不想因为贪快导致整个任务被中断。4.2 重试要分级退避要按指数来网络请求没有百分之百成功的说法。偶发超时、连接重置、暂时性错误这些都不能代表任务失败需要重试。但重试不是简单地“失败了就再来一次”错误类型不同处理策略要区分开错误类型处理策略网络超时、连接重置等传输层错误可重试通常连续重试几次能恢复参数缺失、签名过期等业务参数错误不要盲目重试先检查参数是否正确服务器返回5xx说明对方负载较高应拉长间隔再重试限流或风控类响应立即停止当前节奏冷却一段时间再继续重试间隔上用指数退避第一次失败等2秒第二次等4秒第三次等8秒最多重试到第五次就放弃并记录日志。无限重试是另一个坑数据抓取任务一旦遇到持久性问题脚本可能会陷入死循环一直空转。4.3 解析结果也得质检空字段、异常结构与页面指纹数据抓回来不是完事了还得确认抓回来的数据是对的。这一步叫数据质检很多人会忽略。我踩过的坑是某次任务里页面结构被人改了一处解析逻辑依然能运行但所有字段全部提取成空值。日志里每一行都在“成功”记录实际上抓回来的全部是废数据最后整理数据库的时候才发现整批数据得重抓。从那以后我给自己定了一条规矩任何采集任务都必须包含基本的字段校验。比如必填字段不能为空、数值类型是否正确、记录条数是否在合理范围内。一旦发现异常率超过了预设阈值立刻停止任务并发通知而不是让脏数据继续堆积。另外可以给每个页面的解析增加一道“页面指纹”校验——比如检查页面是否包含某个正常的特征节点。特征不存在就说明页面结构可能已经变化直接触发告警。4.4 去重策略URL去重与内容去重结合数据采集过程中重复请求和数据重复是经常发生的事。去重做不好不仅浪费资源还会污染最终数据。URL去重是最基础的管理一个已访问URL集合遇到重复的直接跳过。规模大了以后要考虑内存占用问题可以用带指纹的散列表结构来做。但URL去重挡不住一种情况同一份数据被放在不同URL下或者页面内容更新了但URL没变。这时候就要做内容层面的去重。简单可靠的做法是给每条记录提取关键字段的组合唯一键在写入数据库时用它做唯一性约束重复写入自然就会被拦下来。5. 爬虫的边界意识把合规做成习惯而不是事后补救最后一部分想聊点技术之外但同样重要的事。爬虫技术本身是中性工具但使用方式不同带来的结果可能完全不同。边界意识不是束缚恰恰是为了让这个技术能长期、安心地用下去。5.1 公开数据不等于无限制使用“公开的数据可以随便爬”是一个流传很广的误解。数据公开在页面上不代表抓取和使用它就是完全无限制的。每个站点都会通过robots协议或服务条款明示它的规则这是最基础的参考依据。技术实现之前花几分钟看看目标站点是否允许自动化访问这本身是成本极低但价值极高的一步。另外采集行为也不应该干扰到目标站点的正常运行。控制频率、错峰访问不只是技术手段更是使用习惯问题。5.2 个人敏感数据的“不碰”原则在数据采集领域最需要敬畏的是个人敏感信息。哪怕某个页面不需要登录就能看到一些信息也不代表这些内容适合被批量采集和存储。姓名、电话、地址、证件号、社交关系等字段最好从一开始就避免进入你的采集范围内。判断标准很简单如果一个采集任务的数据字段中包含了能够直接定位到某个具体个人的信息那就需要特别慎重地评估目的和合法性。大多数个人学习、技术演示场景完全不依赖这类数据完全可以绕开没必要给自己找麻烦。5.3 最小影响原则限速、小规模测试与数据闭环我给自己定的习惯是任何抓取任务先小规模跑通确认代码没bug、频率合理之后再逐步放开。不要在写脚本的第一天就拿全量数据做测试这对目标服务器不友好对你自己调试也没帮助。另外采集到的数据要明确它用在哪里、落在哪里、存多久。测试用的临时数据不要随意泄露出去用完了及时清理。数据闭环这件事想清楚了整个采集流程会更从容。关于爬虫技术我想说的核心其实是它的瓶颈从来不在“会不会写几行代码”而在于能不能把问题定义清楚、把数据来源理解透、把工程细节做扎实同时守住使用边界。这几个点的顺序一旦反了——比如一上来就追求高并发或者连数据在哪都没找到就开始写代码——大概率会花好几倍的时间去返工。做个总结式的收尾没太大意义我倒是更想给一个具体建议下次遇到抓不到数据的状况先别急着怀疑代码去开发者工具的Network面板里看一眼找到数据真正的家。这一步做顺了后面所有的技术动作都会自然展开。