1. 项目缘起与整体设计思路
做数据采集这行的朋友,多多少少都碰过本地生活服务平台的数据抓取需求。我自己因为工作关系,前后接触过好几个跟商户信息聚合相关的项目,其中绕不开的一个典型场景,就是围绕大众点评这类平台的公开商户数据做结构化整理。这个项目标题叫“大众点评反爬虫解析”,说白了就是研究平台方布设了哪些拦截机制,以及作为采集方该怎么合规、稳定地把公开信息拿下来。
先把话说在前头:这篇文章讨论的所有内容,都建立在只采集平台公开展示的商户基础信息这个前提上,比如店铺名称、地址、营业时间、评分区间这类不涉及用户隐私、不涉及账号体系的数据。任何涉及用户个人信息、需要登录才能查看的内容,都不在讨论范围内。这一点必须先讲清楚,否则后面所有技术细节都失去了正当性。
那这个项目到底解决什么问题?简单讲,就是当你用常规的请求方式去拉取页面时,会发现返回的内容要么是空的、要么是一段加密的乱码、要么直接给你弹一个验证页面。这就是平台的反爬机制在起作用。我们要做的,就是搞清楚这些机制分别是什么、它们各自拦截的是什么行为、以及在不触发风控的前提下,怎么把数据完整地取回来。
适合谁来参考?我觉得有三类人比较对口。第一类是做数据分析的,需要一批商户样本做行业研究;第二类是做竞品监测的,想定期跟踪某些品类的店铺变化;第三类就是纯粹想学习反爬对抗思路的技术爱好者。不管你是哪一类,我都建议先把“合规”两个字刻在脑子里,后面讲技术才有意义。
整个项目的设计思路,我采用的是“先侦察、再分类、后逐个击破”的路线。为什么不直接上来就写采集脚本?因为这类平台的反爬是分层级的,你如果不知道对面有几道防线,写出来的代码大概率是东墙补了西墙漏。所以第一步一定是被动侦察:用最朴素的请求去试探,观察返回状态码、响应头、响应体特征,把拦截点一个个标记出来。这个思路跟做安全测试是一个道理,先摸清边界,再谈突破。
具体来说,我把整个反爬体系拆成了四个层面来理解:请求头校验层、参数签名层、行为风控层、内容混淆层。这四层不是孤立的,而是层层递进的关系。请求头校验是最外层的门槛,参数签名是核心的加密逻辑,行为风控是动态的、基于你访问模式的判断,内容混淆则是最后一道防线,就算你前面都过了,拿到的数据也可能是加密的。理解这个分层结构,是后面所有实操的基础。
我选择这个拆解方式,是因为它符合大多数中大型平台的实际架构。很多新手一上来就盯着某个加密参数死磕,结果发现就算破解了参数,请求频率一高照样被封。原因就是忽略了行为风控这一层。所以我的建议是,先把四层结构在脑子里建立起来,再动手写代码,效率会高很多。
2. 四层反爬机制的核心细节解析
2.1 请求头校验层:最容易被忽视的第一道门
请求头校验听起来简单,但恰恰是很多人栽跟头的地方。平台会检查你的请求里带了哪些头、这些头的值是否符合真实浏览器的特征。最基础的几个头包括User-Agent、Referer、Accept、Accept-Language、Accept-Encoding等等。如果你用 Python 的 requests 库直接发请求,默认的User-Agent是python-requests/2.x.x,这等于在脑门上写着“我是脚本”,秒封。
但光改User-Agent就够了吗?远远不够。我实测下来,平台还会校验一些不太常见的头,比如sec-ch-ua、sec-ch-ua-mobile、sec-ch-ua-platform这一组,这是浏览器指纹的一部分。还有Upgrade-Insecure-Requests、Connection这些。更细的还会看头的顺序,真实浏览器发送请求头是有固定顺序的,你用字典随便拼一个,顺序不对也可能被识别。
这里有个实操心得:不要手动去拼请求头,而是直接从浏览器开发者工具里把某个真实请求的头完整复制下来,包括顺序。然后在代码里用一个有序字典或者列表来构造。我试过,光是保证头的顺序和真实浏览器一致,就能把拦截率降低一大截。
注意:复制请求头的时候,
Cookie字段要特别小心。如果你复制的是登录状态下的 Cookie,那涉及的就是账号数据了,这超出了我们讨论的公开数据范围。建议用无登录状态的匿名会话,只保留平台给游客下发的基础标识。
另外,Referer这个头很多人会忽略。平台会检查你是从哪个页面跳转过来的。如果你直接请求详情页,但Referer是空的,或者指向一个不相关的域名,风控系统就会标记你。正确的做法是把Referer设置成该平台的搜索结果页或者列表页地址,模拟正常的点击跳转路径。
2.2 参数签名层:核心加密逻辑的拆解思路
这一层是技术含量最高的部分。平台会在请求的 URL 参数或者请求体里,加入一个由 JavaScript 动态生成的签名参数。这个签名通常是对当前请求的某些参数、时间戳、随机数做哈希运算得到的。你如果直接构造请求而不带这个签名,或者签名算错了,服务器直接拒绝。
常见的签名参数命名有_token、_signature、mtgsig这类。以我接触过的案例来看,签名的生成逻辑一般藏在混淆过的 JS 文件里。你需要做的第一步是定位:在开发者工具的 Network 面板里找到那个关键的请求,看它的参数里哪个是动态变化的、哪个看起来像哈希值。然后去 Sources 面板里全局搜索这个参数名,找到生成它的 JS 代码。
找到代码之后,就是逆向分析。这里有个技巧:不要一上来就硬读混淆代码,先用格式化工具把代码展开,然后搜索关键词比如sign、token、encrypt、md5、sha这些。很多时候签名逻辑就是几个字符串拼接之后做一次哈希。我遇到过最简单的案例,签名就是把时间戳加上一个固定盐值做 MD5,这种半小时就能搞定。
但也有复杂的,比如用了自定义的加密算法,或者把关键逻辑拆散在多个函数里,甚至用了 JS 虚拟机保护。这种时候,与其硬逆向,不如考虑直接调用:用 Node.js 把那段 JS 代码跑起来,传入参数,拿到签名结果。这就是所谓的“补环境”方案。你需要把 JS 代码运行所需的浏览器环境对象(比如window、document、navigator)用 Node.js 模拟出来,让代码能正常执行。
提示:补环境是个细致活。JS 代码里可能会检测
navigator.userAgent、window.location、document.cookie等对象,你需要根据报错信息一个个补全。我的经验是,先跑起来看报什么错,缺什么补什么,不要试图一次性把所有环境都模拟好。
2.3 行为风控层:动态判断的隐形防线
前面两层是静态的,你只要把请求构造对了就能过。但行为风控是动态的,它看的是你的访问模式。平台会记录你每个 IP、每个会话的请求频率、请求间隔、访问路径、鼠标轨迹(如果是浏览器环境)等,然后用一套规则或者模型来判断你是不是机器人。
最直观的指标是请求频率。你如果一秒钟发十个请求,那不用想,肯定被封。但频率低就安全吗?也不一定。平台还会看你的访问路径是否合理。正常用户是先搜索、再翻列表、再点进详情页,路径是有逻辑的。你如果直接批量请求详情页,跳过了前面的步骤,也会被标记。
还有一个容易被忽略的点是请求间隔的规律性。如果你用time.sleep(1)这种固定间隔,虽然频率不高,但间隔太规律了,反而像机器。我的做法是加入随机抖动,比如在 1 到 3 秒之间随机取一个值,模拟人的操作节奏。
另外,平台还会用蜜罐链接来抓爬虫。就是在页面里埋一些正常用户看不到的链接(比如用 CSS 隐藏),只有解析 HTML 的爬虫才会去点。你如果点了这些链接,就直接暴露了。所以解析页面的时候,要注意过滤掉那些display:none或者尺寸为 0 的元素。
2.4 内容混淆层:拿到数据也可能看不懂
就算你前面三层都过了,成功拿到了响应,也不代表数据就能直接用。平台会对返回的内容做混淆处理。常见的手段有几种:一是字体加密,页面上的数字用自定义字体渲染,你看到的“8”在 HTML 里其实是某个特殊字符,需要对照字体文件做映射才能还原;二是CSS 偏移,文字的位置被 CSS 打乱,视觉上正常但源码里顺序是错的;三是数据加密,关键信息用 JS 解密后再渲染。
字体加密是最常见的。平台会定期更换字体文件,你需要下载对应的字体文件,用字体解析库(比如 Python 的 fontTools)把字符和真实数字的映射关系提取出来,然后在解析 HTML 时做替换。这个映射关系不是固定的,每次字体文件更新都要重新提取,所以需要做成自动化的流程。
注意:字体文件的 URL 通常藏在 CSS 里,你需要先请求 CSS 文件,从中提取字体 URL,再下载字体文件。这个过程本身也可能触发风控,所以请求频率依然要控制。
3. 实操过程与核心环节实现
3.1 侦察阶段:用最小成本摸清防线
我一般会先用一个最简单的脚本去试探,不写任何反爬对抗逻辑,就看平台怎么回应。具体操作是:用 requests 发一个 GET 请求到某个商户详情页,打印出状态码、响应头、响应体前 500 个字符。如果状态码是 200 但响应体里没有商户信息,说明有内容混淆;如果状态码是 403 或者 302 跳转到验证页,说明请求头或者行为被拦截了。
这个阶段的关键是记录。我会建一个表格,把每次试探的请求特征和响应结果都记下来,方便后面分析规律。比如下面这种:
| 试探编号 | User-Agent | Referer | 是否带签名 | 状态码 | 响应特征 |
|---|---|---|---|---|---|
| 1 | 默认 | 无 | 无 | 403 | 直接拒绝 |
| 2 | 真实浏览器 | 无 | 无 | 200 | 返回空数据 |
| 3 | 真实浏览器 | 列表页 | 无 | 200 | 返回加密数据 |
| 4 | 真实浏览器 | 列表页 | 有 | 200 | 返回正常数据 |
从这张表就能看出来,请求头和 Referer 是基础门槛,签名是核心门槛。有了这个判断,后面的工作就有方向了。
3.2 签名逆向:从定位到复现的完整流程
定位签名参数之后,我会把相关的 JS 代码下载到本地,用 Node.js 试着跑。第一步是补环境,把代码里用到的浏览器对象模拟出来。我通常会先写一个空壳:
// env.js const window = {}; const document = {}; const navigator = { userAgent: '真实浏览器的UA' }; const location = { href: 'https://www.example.com' }; // 然后把目标 JS 代码引入进来 require('./target.js'); // 调用生成签名的函数 const sign = window.generateSign(params); console.log(sign);跑起来之后,根据报错信息逐步补全缺失的对象和方法。这个过程可能需要反复几次,但一旦跑通,签名问题就彻底解决了。
补环境的时候有个坑:有些 JS 代码会检测window对象的某些属性是否存在,如果不存在就认为不是真实浏览器,直接返回假签名。所以你不能只补用到的部分,还要把一些常见的浏览器属性也加上,比如window.chrome、window.screen、window.history等。我的经验是,找一个真实的浏览器环境,把window的常用属性列出来,照着补。
签名跑通之后,我会写一个简单的测试:用同样的参数,分别调用 JS 生成的签名和实际请求中的签名,对比是否一致。如果一致,说明逆向成功;如果不一致,说明还有隐藏的参数或者逻辑没考虑到。
3.3 请求调度:控制节奏比破解签名更重要
很多人以为签名破解了就万事大吉,其实真正的难点在于稳定运行。我见过太多案例,签名逻辑写得完美,但跑不了几百条就被封了。问题就出在请求调度上。
我的做法是设计一个请求队列,每个请求之间加入随机延迟,并且定期切换会话标识。具体参数上,我把延迟设置在 2 到 5 秒之间,每请求 20 到 30 条就休息 30 秒到 1 分钟。这个节奏是我多次实测后总结出来的,既能保证一定的采集效率,又不会触发频率风控。
另外,我会给每个会话分配一个请求配额,比如一个会话最多请求 50 条,用完就换下一个。会话标识主要靠 Cookie 来维持,所以需要定期清理 Cookie 或者使用新的匿名会话。这里要注意,不要用同一个 IP 短时间内创建大量会话,那样反而更可疑。
提示:如果你的采集量比较大,建议把请求分散到不同的时间段。比如白天少采一点,凌晨多采一点,模拟真实用户的活跃规律。我试过全天均匀采集和分时段采集,后者的稳定性明显更好。
3.4 内容还原:字体解密与数据清洗
拿到加密内容之后,最后一步是还原。以字体加密为例,我的处理流程是这样的:先从响应中提取字体文件的 URL,下载字体文件;然后用 fontTools 解析字体,提取每个字符的编码和对应的字形;接着根据字形的轮廓特征,匹配到真实的数字或文字;最后在解析 HTML 时,把加密字符替换成真实值。
这个流程听起来简单,但实际操作中有几个细节要注意。一是字体文件可能是动态生成的,每次请求的映射关系都不一样,所以不能缓存映射表,必须每次重新提取。二是字体文件可能被混淆过,字符的编码顺序是打乱的,需要根据字形的坐标信息来判断真实值。三是有些平台会把字体文件做成多个,需要合并处理。
数据清洗这块,我一般会把还原后的数据存成结构化的格式,比如 JSON 或者 CSV。字段包括商户名称、地址、评分、评论数、营业时间等。存储之前要做一次校验,比如评分是否在 0 到 5 之间、地址是否为空等,把异常数据标记出来人工复核。
4. 常见问题与排查技巧实录
4.1 请求返回 403 的排查顺序
403 是最常见的拦截状态码,排查的时候我一般按这个顺序来:先检查User-Agent是不是默认的,改成真实浏览器的;再看Referer有没有带,带上列表页地址;然后检查请求头的顺序和完整性,从浏览器完整复制一份;最后看是不是频率太高了,降低频率再试。如果这些都做了还是 403,那可能是 IP 被标记了,需要换一个网络环境。
4.2 签名正确但请求失败的几种可能
签名算对了但请求还是失败,通常有几种原因。一是签名的时间戳过期了,很多签名是跟时间戳绑定的,超过一定时间就失效,需要重新生成。二是签名依赖的参数不全,你可能漏了某个隐藏参数,需要对比真实请求和你的请求,找出差异。三是签名算法更新了,平台定期会更换加密逻辑,你需要重新逆向。四是 Cookie 失效了,有些签名是跟会话绑定的,会话过期签名也就无效了。
4.3 数据采集过程中的稳定性维护
稳定性维护是个持续的工作。我会定期检查采集日志,看有没有突然增多的失败请求。如果失败率上升,第一时间降低频率,观察是否恢复。同时,我会准备多套请求头模板,轮换使用,避免单一特征被识别。另外,我会监控响应内容的变化,如果发现返回的数据格式变了,说明平台可能更新了混淆逻辑,需要及时调整解析代码。
下面这张表是我整理的常见问题速查表,遇到问题可以对照排查:
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 403 拒绝 | 请求头不合格 | 检查 UA、Referer、头顺序 | 完整复制浏览器请求头 |
| 200 但无数据 | 内容混淆 | 检查是否有字体加密 | 解析字体文件做映射 |
| 签名无效 | 算法更新或参数缺失 | 对比真实请求参数 | 重新逆向或补全参数 |
| 频繁封禁 | 行为风控触发 | 检查请求频率和路径 | 降低频率、模拟正常路径 |
| 数据错乱 | CSS 偏移或加密 | 检查 HTML 结构 | 按视觉顺序重新排序 |
4.4 几个踩过的坑和独家技巧
第一个坑是过度依赖固定延迟。我一开始用time.sleep(2),结果发现跑了一段时间后失败率飙升。后来改成随机延迟,并且加入了“偶尔长休息”的策略,稳定性才上来。这个经验告诉我,机器和人的区别不在于快慢,而在于规律性。
第二个坑是忽略了字体文件的更新频率。有一次我缓存了字体映射表,结果第二天数据全乱了。后来改成每次请求都重新提取映射,虽然慢了一点,但准确率上去了。
第三个技巧是用真实浏览器的开发者工具做对照。当你搞不清楚某个请求该怎么构造时,直接在浏览器里操作一遍,把请求完整复制下来,然后逐项对比你的代码。这个方法虽然笨,但最有效。
第四个技巧是做好请求日志。我会把每个请求的 URL、参数、响应状态、响应时间都记下来,出问题的时候可以回溯。有一次我发现某个时间段的失败率特别高,查日志才发现是那个时间段平台的风控策略收紧了,调整采集时间后就正常了。
最后再分享一个小经验:不要试图一次性采集大量数据。我见过很多人一上来就想把整个城市的数据都拉下来,结果没跑多久就被封了。正确的做法是小步快跑,先采集一小批样本,验证流程通畅,再逐步扩大规模。这样即使出问题,损失也可控。而且平台的风控策略是动态调整的,你今天能跑通的方法,明天不一定能用,保持小规模、高频次的测试,比一次性大规模采集要稳妥得多。