☰
大众点评Ajax接口深度解析:5层校验与动态签名实战
2026/10/3 10:25:15 网站建设 项目流程

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,少一个/或多一个?from=map都会失败。更隐蔽的是,它还会校验 Referer 中的shopId是否与当前请求 URL 中的shopId一致。我曾遇到一个案例:前端 JS 用window.location.href拼接请求 URL,但用户通过分享链接进入页面时,URL 带有utm_source参数,导致 Referer 包含?utm_source=xxx,而后端校验时直接截断?后内容,造成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 小时,且每次页面加载都会重新生成。这意味着:

  1. 你无法长期复用一个 Cookie;
  2. 即使你成功注入了 Cookie,若cy对应的设备指纹参数(如屏幕分辨率)与当前请求环境不一致,服务端会返回x-forbid-reason: "f-1024"(设备指纹不匹配)。

我实测过:用 Puppeteer 启动无头浏览器,设置--window-size=1920,1080,但navigator.hardwareConcurrency返回 8,而真实 iPhone 13 的该值为 6——这个差异足以让cy解密失败。

2.3 第三层:动态 Header 的实时生成机制

除了常规的User-Agent、Accept,大众点评强制要求两个动态 Header:

  • X-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,否则视为自动化脚本;
  • 滚动深度校验:请求offset=20(第 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 提取sessionKey:WebSocket 消息的 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-8;
  • CryptoJS 的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_id="123456789", timestamp=1717023456789, nonce=123, session_key="abc123def456" ) print(sign) # 输出:a1b2c3d4e5f67890

3.4 处理nonce的同步难题:前端状态的可靠传递

nonce是前端 JS 中的全局变量window.__dp_nonce,每次请求后自增。如果你用 Python 后端生成签名,就必须知道当前nonce的值。最稳妥的方式不是“猜”,而是让前端主动上报:

  1. 在页面中注入一段 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); };
  1. Python 后端从请求参数中读取_nonce,用于生成签名。
  2. 请求成功后,前端 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.js):

async 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.platform
iPhone 133908446iPhone
Samsung S223607808Linux arm64
Windows PC1920108012Win32

Puppeteer 启动时动态注入:

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,并重新建立 WebSocket
f-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 class="highlight">)、广告语(“老板人超好!”、“送了小礼物!”)和无效信息(“位置很好找”、“地铁直达”)。直接解析会导致 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 t=JSON.stringify(JSON.parse(document.querySelector('pre').textContent),null,2);prompt('评论JSON',t);})()——点击即可弹出格式化后的 JSON,适合快速调试。

技术没有善恶,但使用技术的场景有边界。深入分析大众点评 Ajax 接口的价值,不在于“能不能拿到数据”,而在于“在什么条件下,以什么代价,拿到什么质量的数据,服务于什么目标”。当你看清这背后的权衡,选择本身就变得清晰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询