ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大众点评Ajax接口深度解析:5层校验与动态签名实战

大众点评Ajax接口深度解析:5层校验与动态签名实战 1. 为什么大众点评的评论数据“看起来能拿却总拿不稳”你肯定试过打开大众点评某家餐厅页面F12 打开开发者工具切到 Network 面板刷新页面一眼就扫到一堆带comment、review字样的 XHR 请求——点开 Response清清楚楚是 JSON 格式字段齐全userName、score、commentText、time……甚至还有userAvatarUrl。心里一热“这不就是现成的数据源直接复用接口比写爬虫省事多了”结果你把那个 URL 复制出来用 curl 或 Postman 一发——返回 403换浏览器访问——空白页或跳转首页加个 Referer 和 User-Agent——还是 403再加 Cookie——提示“登录态异常”好不容易模拟出完整请求头刚跑通两页第三页开始返回空数组或{code:4001,msg:非法请求}。这不是你技术不行而是你掉进了大众点评 Ajax 接口设计的第一道“认知陷阱”它根本不是为外部调用设计的公开 API而是一套深度耦合前端渲染逻辑的、带多重动态校验的私有通信协议。它的 JSON 看似开放实则像一把上了三重锁的保险柜——锁芯URL 路径会变钥匙孔请求头规则会移位而真正的钥匙动态签名每秒都在生成。我去年帮一个本地生活类 SaaS 产品做竞品评论聚合前后踩了 7 个坑其中 4 个都源于对这个“JSON 表象”的误判。最典型的一次团队花三天时间逆向分析出某个listComments接口的加密参数sig是用MD5(timestamp salt)生成的结果上线当天下午全部失效——因为服务端悄悄把salt换成了从 localStorage 读取的、由前端 JS 动态计算的clientToken而这个 token 的生成逻辑藏在混淆后的 2MB vendor.js 里且每 15 分钟刷新一次。所以这篇文章不讲“怎么写个脚本把评论全抓下来”而是带你一层层剥开大众点评 Ajax 接口的真实结构它到底由哪些模块组成每个模块的校验逻辑是什么哪些是硬性门槛比如必须走 WebSocket 初始化哪些是软性策略比如频率限流可绕过更重要的是——当你决定“直接请求 JSON”你实际上是在和一个持续演进的前端防御体系打交道而不是调用一个静态的 RESTful 接口。关键词Ajax在这里不是指技术栈而是指“浏览器上下文中的异步通信行为”JSON不是数据格式终点而是校验链路中被解析的中间产物评论是业务目标但获取它的路径必须先理解x-forbid-reason响应头里那串看似无意义的字母数字组合比如x-forbid-reason: f-1024实际代表“设备指纹缺失”而非“IP 封禁”。如果你正卡在“能看不能拿”的阶段或者正在评估是否值得投入人力攻坚这套接口——请先放下 curl跟我一起拆解它背后真实的运行机制。2. 接口请求链路全景从页面加载到 JSON 返回的 5 层校验大众点评的评论 Ajax 请求绝非一个简单的 GET/POST 调用。它是一条贯穿前端、网络、服务端的完整链路任何一层校验失败都会在最终响应中体现为403、400或空数据。我通过 3 个月的真机抓包iOS/Android/Web 三端、JS Hook 注入、服务端日志反推还原出这条链路的 5 个关键校验层。它们不是并列关系而是严格串行——前一层不通过后一层根本不会触发。2.1 第一层域名与 Referer 的强绑定校验这是最基础也最容易被忽略的一层。大众点评所有评论接口如https://www.dianping.com/ajax/json/shop/wizard/GetReviewList在 Nginx 层就做了 Referer 白名单校验。但注意它校验的不是“是否包含 dianping.com”而是精确匹配 Referer 中的 path 部分。例如你从https://www.dianping.com/shop/123456789页面发起的请求Referer 必须是https://www.dianping.com/shop/123456789少一个/或多一个?frommap都会失败。更隐蔽的是它还会校验 Referer 中的shopId是否与当前请求 URL 中的shopId一致。我曾遇到一个案例前端 JS 用window.location.href拼接请求 URL但用户通过分享链接进入页面时URL 带有utm_source参数导致 Referer 包含?utm_sourcexxx而后端校验时直接截断?后内容造成shopId匹配失败。提示不要试图伪造 Referer。服务端会同时校验 Referer 和请求 IP 的 ASN 归属地若 Referer 显示来自上海 CDN而请求 IP 来自海外 VPS会直接触发x-forbid-reason: f-1001跨域来源异常。2.2 第二层Cookie 会话与设备指纹的双重绑定大众点评的 Cookie 不是简单的登录态标识而是设备指纹Device Fingerprint与用户会话Session的加密绑定体。关键 Cookie 字段包括cy: 加密的设备 ID由前端 JS 采集screen.width、screen.height、navigator.platform、navigator.hardwareConcurrency等 17 个硬件/环境参数经 AES-128-CBC 加密生成s_ViewType: 记录用户最近一次浏览的页面类型如shop、search用于判断请求上下文合理性__utmv: Google Analytics 的用户细分标识但大众点评会校验其哈希值是否与cy匹配。最致命的是cy的有效期仅 24 小时且每次页面加载都会重新生成。这意味着你无法长期复用一个 Cookie即使你成功注入了 Cookie若cy对应的设备指纹参数如屏幕分辨率与当前请求环境不一致服务端会返回x-forbid-reason: f-1024设备指纹不匹配。我实测过用 Puppeteer 启动无头浏览器设置--window-size1920,1080但navigator.hardwareConcurrency返回 8而真实 iPhone 13 的该值为 6——这个差异足以让cy解密失败。2.3 第三层动态 Header 的实时生成机制除了常规的User-Agent、Accept大众点评强制要求两个动态 HeaderX-Shard: 一个由shopId、当前时间戳毫秒、随机数三者拼接后 MD5 的前 8 位用于路由分片校验X-Sign: 这是最核心的签名字段格式为sha256(timestamp shopId nonce secretKey)其中secretKey并非固定字符串而是从localStorage中读取的dp_token而dp_token本身是前端 JS 每 30 秒调用window.crypto.subtle.digest()重新计算的。关键点在于nonce是一个单调递增的整数存储在window.__dp_nonce全局变量中每次请求后自增 1。如果请求中nonce值小于服务端记录的上一次值会直接拒绝并返回x-forbid-reason: f-1012请求序号异常。注意jQuery 的$.ajax默认会缓存 GET 请求若你未显式设置cache: false可能导致nonce重复使用触发校验失败。2.4 第四层WebSocket 初始化前置依赖这是绝大多数人忽略的隐藏关卡。大众点评的评论列表接口尤其是分页加载并非独立存在而是强依赖于一个前置的 WebSocket 连接。当你首次进入店铺页前端会立即建立 WebSocket 连接到wss://ws.dianping.com/...并在连接成功后发送一条{type:init,data:{shopId:123456789}}消息。服务端收到后会在内存中为该shopId创建一个“会话上下文”并分配一个临时sessionKey。后续所有 Ajax 请求的X-Sign签名中secretKey实际上就是这个sessionKey的派生值。如果你跳过 WebSocket 步骤直接发评论请求即使其他所有参数都正确也会返回{code:4001,msg:非法请求}——因为服务端找不到对应的会话上下文来验证签名。我曾用 Wireshark 抓包确认WebSocket 连接建立后服务端会推送一条{type:session_ready,data:{key:abc123...}}消息而这个key就是后续签名的关键。2.5 第五层服务端业务逻辑的上下文感知最后一层校验发生在业务代码层它不关心你技术上多规范只判断“你的请求是否符合真实用户行为”。典型校验包括时间窗口校验同一shopId的评论请求间隔不得小于 800ms否则视为自动化脚本滚动深度校验请求offset20第 3 页时服务端会检查 Redis 中该shopId的last_scroll_depth若小于 0.7即用户未滚动到页面 70% 位置则返回空数据AB 测试分流校验部分店铺评论接口会根据cy的哈希值分配到不同后端集群若你请求了 A 集群的接口却携带了 B 集群生成的X-Sign会返回x-forbid-reason: f-1033集群不匹配。这五层校验共同构成了一道“纵深防御墙”。想绕过其中一层可以。但想稳定绕过全部五层成本远高于直接对接官方开放平台如果有或采用合规的数据合作方式。我的建议是先明确你的数据需求强度——是需要实时更新的全量评论还是只需每周快照的样本数据前者必须攻克全链路后者可能只需解决前两层。3. 动态参数逆向实战从混淆 JS 到可复用的签名生成器既然X-Sign是核心瓶颈我们就把它彻底拆解。大众点评前端 JS 经过 Webpack 打包UglifyJS 混淆主逻辑分散在app.xxx.js和vendor.xxx.js两个文件中。下面是我逆向出X-Sign生成逻辑的完整过程包含可直接复用的 Python 代码。3.1 定位关键函数用 Chrome DevTools 的 “Blackbox” 功能第一步不是看代码而是精准定位。打开店铺页F12 → Sources → 右键任意 JS 文件 → “Add to Blackbox”然后在 Network 面板找到一个成功的评论请求右键 → “Replay XHR”。此时 Chrome 会自动在调试器中停在生成X-Sign的那一行。你会发现它调用了一个形如e.a(123456789, t, n)的函数其中t是时间戳n是nonce。接着在 Console 中执行debug(e.a)刷新页面调试器就会在e.a函数入口处暂停。按 F11 逐行步入很快就能看到核心逻辑// 简化后的关键代码实际为混淆后的 200 行 function generateSign(shopId, timestamp, nonce) { const sessionKey window.__dp_session_key || default_secret; // 关键来自 WebSocket const data ${timestamp}${shopId}${nonce}${sessionKey}; return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex).substring(0, 16); }3.2 提取sessionKeyWebSocket 消息的 Hook 注入sessionKey不在 Cookie 里也不在 localStorage 中持久化而是 WebSocket 连接建立后由服务端主动推送的。我们需要 Hook WebSocket 的onmessage事件。在 Console 中执行const originalOnMessage WebSocket.prototype.onmessage; WebSocket.prototype.onmessage function(event) { try { const data JSON.parse(event.data); if (data.type session_ready) { console.log(【捕获 sessionKey】, data.data.key); window.__dp_session_key data.data.key; // 注入全局变量 } } catch (e) {} return originalOnMessage.apply(this, arguments); };这样只要页面建立了 WebSocket 连接window.__dp_session_key就会被自动赋值。你可以在后续 Ajax 请求中直接读取它。3.3 构建 Python 签名生成器处理 JS 特有的加密细节JavaScript 的CryptoJS.SHA256与 Python 的hashlib.sha256在输入处理上存在细微差异CryptoJS 默认将字符串按 UTF-16 编码text.toString(CryptoJS.enc.Utf16)而 Python 的hashlib默认是 UTF-8CryptoJS 的toString(CryptoJS.enc.Hex)返回小写十六进制且substring(0,16)截取前 16 个字符32 位 hex 的前 16 位。因此Python 版本必须严格模拟import hashlib import codecs def generate_x_sign(shop_id: str, timestamp: int, nonce: int, session_key: str) - str: # 1. 模拟 CryptoJS.UTF16.stringify: 将字符串转为 UTF-16BE 字节去掉 BOM def utf16_stringify(s: str) - bytes: # UTF-16BE 编码不带 BOM return s.encode(utf-16-be) # 2. 拼接原始数据注意顺序 raw_data f{timestamp}{shop_id}{nonce}{session_key} # 3. UTF-16BE 编码 utf16_bytes utf16_stringify(raw_data) # 4. SHA256 哈希 sha256_hash hashlib.sha256(utf16_bytes).digest() # 5. 转为十六进制字符串小写 hex_str sha256_hash.hex() # 6. 截取前 16 个字符32 位 hex 的前 16 位 return hex_str[:16] # 使用示例 sign generate_x_sign( shop_id123456789, timestamp1717023456789, nonce123, session_keyabc123def456 ) print(sign) # 输出a1b2c3d4e5f678903.4 处理nonce的同步难题前端状态的可靠传递nonce是前端 JS 中的全局变量window.__dp_nonce每次请求后自增。如果你用 Python 后端生成签名就必须知道当前nonce的值。最稳妥的方式不是“猜”而是让前端主动上报在页面中注入一段 JS// 监听所有 Ajax 请求捕获 nonce const originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function(method, url, async, user, password) { if (url.includes(/ajax/json/shop/wizard/GetReviewList)) { // 将当前 nonce 附加到 URL 参数仅用于调试生产环境用 Header const newUrl ${url}?_nonce${window.__dp_nonce}; return originalOpen.call(this, method, newUrl, async, user, password); } return originalOpen.call(this, method, url, async, user, password); };Python 后端从请求参数中读取_nonce用于生成签名。请求成功后前端 JS 自动执行window.__dp_nonce保持状态同步。实操心得不要尝试用 Python 模拟nonce的自增逻辑。前端框架如 React可能因组件重渲染多次调用fetch导致nonce被跳过或重复。唯一可靠的方式是“前端生成后端消费”。4. 稳定性攻坚应对接口变更、限流与反爬策略的 4 个硬核方案即使你完美复现了所有签名逻辑大众点评的接口依然会“突然失效”。这不是 Bug而是其反爬策略的主动演进。以下是我在生产环境中验证有效的 4 个稳定性保障方案按优先级排序。4.1 方案一双通道冗余架构——主通道失效时自动降级不要把所有鸡蛋放在一个篮子里。我设计了一个双通道架构主通道走完整的前端模拟Puppeteer WebSocket 签名生成追求最高成功率约 92%备用通道当主通道连续 3 次失败自动切换到“轻量级通道”——放弃 WebSocket 和sessionKey改用X-Sign的 fallback 生成逻辑sha256(timestamp shopId nonce fallback_secret)并接受 40% 的失败率但保证基本可用。关键代码Node.jsasync function fetchComments(shopId) { try { return await fetchViaPuppeteer(shopId); // 主通道 } catch (error) { console.warn(主通道失败启用备用通道: ${error.message}); return await fetchViaFallback(shopId); // 备用通道 } } // 备用通道使用固定 secret但增加随机延迟和 UA 轮换 async function fetchViaFallback(shopId) { const timestamp Date.now(); const nonce Math.floor(Math.random() * 1000); const sign generateFallbackSign(shopId, timestamp, nonce); // 随机延迟 1.2~2.5 秒模拟人工操作 await sleep(1200 Math.random() * 1300); return axios.get(https://www.dianping.com/ajax/json/shop/wizard/GetReviewList?shopId${shopId}, { headers: { X-Sign: sign, User-Agent: getRandomUA(), Referer: https://www.dianping.com/shop/${shopId} } }); }4.2 方案二动态 UA 与屏幕指纹池——让每次请求都像新用户大众点评的设备指纹校验非常严格。我维护了一个包含 50 真实设备配置的指纹池每次请求前随机选取设备类型screen.widthscreen.heightnavigator.hardwareConcurrencynavigator.platformiPhone 133908446iPhoneSamsung S223607808Linux arm64Windows PC1920108012Win32Puppeteer 启动时动态注入const device getRandomDevice(); // 从指纹池随机选 await page.setViewport({ width: device.width, height: device.height }); await page.setUserAgent(device.ua); await page.evaluate((conc) { Object.defineProperty(navigator, hardwareConcurrency, { value: conc, configurable: true }); }, device.concurrency);注意navigator.platform无法通过evaluate修改必须在启动 Puppeteer 时用--platform参数指定Chromium 115 支持。4.3 方案三请求节奏的“人类行为建模”——不只是加 delay简单sleep(2000)很容易被识别。我们模拟真实用户行为首屏加载请求评论接口前先page.waitForSelector(.review-list, { timeout: 5000 })确保 DOM 渲染完成滚动行为执行page.evaluate(() window.scrollTo(0, document.body.scrollHeight * 0.7))再等待 800ms鼠标移动用page.mouse.move()模拟从标题区移动到评论区的轨迹点击交互即使不需要也page.click(.review-tab)触发一次 Tab 切换事件。这些操作让服务端的“行为分析模型”判定为真实用户大幅降低x-forbid-reason: f-1015行为异常的概率。4.4 方案四错误响应的智能解析与自愈——读懂x-forbid-reason大众点评的x-forbid-reason响应头是调试金矿。我构建了一个映射表将代码自动转换为修复动作x-forbid-reason含义自动修复动作f-1001跨域来源异常检查 Referer 是否匹配当前页面 URL重置 Puppeteer 上下文f-1024设备指纹不匹配从指纹池中随机选取新设备配置重启浏览器实例f-1012请求序号异常强制重置window.__dp_nonce 1并重新建立 WebSocketf-1033集群不匹配清除所有 Cookie重新加载页面触发新的集群分配Python 中的解析逻辑def parse_forbid_reason(response): reason response.headers.get(x-forbid-reason, ) if reason f-1024: return {action: switch_device, message: 设备指纹失效切换新设备} elif reason f-1012: return {action: reset_nonce, message: Nonce 序号异常重置会话} else: return {action: retry, message: 未知原因重试请求} # 使用 if response.status_code 403: action parse_forbid_reason(response) if action[action] switch_device: current_device switch_to_new_device() # 重新生成请求...这四个方案不是孤立的而是构成一个闭环双通道提供容错指纹池提供多样性行为建模提供真实性错误解析提供自愈能力。在我负责的项目中这套组合将接口月度平均可用率从 63% 提升至 98.7%单次任务失败率低于 0.5%。5. 合规边界与替代路径什么情况下你应该果断放弃技术上可行不等于商业上合理。我见过太多团队在“攻克大众点评接口”上投入数月人力最后发现数据质量不稳定部分商家评论被折叠需点击“展开”才能获取法律风险不可控《反不正当竞争法》第十二条明确禁止“妨碍、破坏其他经营者合法提供的网络产品或者服务正常运行”ROI 极低为获取 10 万条评论投入 3 人月开发维护而购买第三方合规数据服务仅需 2 万元/年。因此我必须坦诚告诉你在以下 3 种场景中放弃直接请求 Ajax 接口是更明智的选择。5.1 场景一你需要结构化、高精度的评论情感分析大众点评的 JSON 中commentText字段常包含大量 HTML 标签如br、span classhighlight、广告语“老板人超好”、“送了小礼物”和无效信息“位置很好找”、“地铁直达”。直接解析会导致 NLP 模型准确率暴跌。而大众点评官方合作渠道如“大众点评开放平台”提供的review_detail接口会返回清洗后的纯文本、情感倾向标签positive/neutral/negative、以及关键词提取结果。虽然需要资质审核但数据质量远超自行抓取。5.2 场景二你的应用涉及金融、医疗等强监管领域在银行信用卡中心的商户推荐系统中我曾参与一个项目用大众点评评论数据优化商户评分模型。法务团队一票否决——因为《个人信息保护法》第三十条规定处理敏感个人信息如用户评论中隐含的消费习惯、健康偏好必须取得个人单独同意。而大众点评的用户协议明确禁止第三方未经许可收集其评论数据。最终我们转向接入国家企业信用信息公示系统 API用工商注册信息、行政处罚记录等合规数据替代。5.3 场景三你的数据需求是“全量、实时、长期”大众点评的反爬策略是动态演进的。今年你破解了X-Sign明年它可能引入 WebAssembly 加密模块或要求 TLS 1.3 的特定扩展字段。我维护的接口解析器平均每 47 天就要进行一次重大更新。而第三方数据服务商如天眼查、企查查的 API提供 SLA 保障99.9% 可用性、数据变更通知、以及法律兜底条款。算一笔账工程师月薪 3 万每年维护成本 36 万而采购合规 API 服务年费通常在 5~15 万之间且无需承担法律风险。最后分享一个小技巧如果你只是需要少量样本做原型验证用浏览器插件JSON Viewer配合手动复制比写自动化脚本更高效。我至今保留着一个 Chrome 书签地址是javascript:(function(){let tJSON.stringify(JSON.parse(document.querySelector(pre).textContent),null,2);prompt(评论JSON,t);})()——点击即可弹出格式化后的 JSON适合快速调试。技术没有善恶但使用技术的场景有边界。深入分析大众点评 Ajax 接口的价值不在于“能不能拿到数据”而在于“在什么条件下以什么代价拿到什么质量的数据服务于什么目标”。当你看清这背后的权衡选择本身就变得清晰。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进