ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大众点评Ajax接口深度解析:签名、编码与反爬实战

大众点评Ajax接口深度解析:签名、编码与反爬实战 1. 项目概述为什么盯上大众点评的 Ajax 接口做本地生活数据采集、竞品分析或用户行为研究的朋友几乎都绕不开大众点评。但你会发现直接抓取网页 HTML 很快就会卡在反爬门槛上——页面渲染越来越重动态加载越来越密验证码、滑块、设备指纹轮番上阵。这时候真正有经验的老手不会死磕 DOM 解析而是把目光转向浏览器开发者工具里那个最安静、最高效、也最容易被忽视的角落Network 面板里的 XHRAjax请求。我第一次在大众点评商品页按 F12点开“评论”标签页看到一串以/review/all开头、返回纯 JSON 的请求时心里就清楚这才是正路。这个项目标题里说的“深入分析”不是泛泛而谈“怎么发个请求”而是要拆到字节级它用什么协议参数怎么构造签名怎么生成响应体结构怎么嵌套编码怎么处理服务器怎么校验这些细节决定了你能不能稳定、批量、长期地拿到真实、完整、带时间戳和用户画像的评论数据。关键词里反复出现的ajax请求设置编码格式、failed to deserialize the json body into the target type: input: missing fie恰恰是新手踩坑最密集的两个雷区——前者关乎请求头和字符集声明是否匹配服务端预期后者则直指 JSON Schema 变动导致的字段缺失根本原因往往不是代码写错了而是你没摸清接口的真实契约。适合谁参考三类人最需要一是做本地生活 SaaS 工具的产品/运营需要定期拉取竞品门店的差评趋势二是高校做消费者行为研究的学生需要结构化评论语料训练情感模型三是技术侧想练手真实工业级反爬对抗的开发者——大众点评这套体系比教科书上的“加 User-Agent 就行”复杂十倍但又不像金融或政务系统那样完全封闭属于“跳一跳够得着”的高价值实战场。它不教你理论只逼你动手看 Network、扣参数、试签名、调编码、验 JSON、压并发、记日志。下面所有内容都是我在过去两年里为三个不同客户定制数据管道时从生产环境里抠出来的真东西。2. 接口底层逻辑与请求链路深度拆解2.1 大众点评 Ajax 请求的本质不是 RESTful而是 RPC over HTTP很多人一看到/review/all?shopIdxxxx就默认这是标准 REST 接口其实大错特错。大众点评的 Ajax 接口本质是 RPC远程过程调用封装在 HTTP 上核心特征有三点第一URL 路径固定业务逻辑全靠 query 参数或 request body 驱动第二强依赖 Cookie 和 Header 中的会话态字段尤其是__mta、_lxsdk_s、_lxsdk_cuid这三个字段缺一不可第三关键参数如page、sortType、filterType表面是明文实则受前端 JS 动态计算的签名约束直接篡改会返回403 Forbidden或空数组。举个实际例子当你在网页上点击“最新评论”按钮浏览器发出的请求 URL 看似简单https://www.dianping.com/ajax/json/shop/wizard/GetReviewList?shopId123456789page2pageSize10sortType1但如果你复制这个 URL 到 Postman 直接 GET大概率返回{code:403,msg:Forbidden}。为什么因为服务端在收到请求时会校验 Header 中的X-Shard、X-Requested-With以及 Cookie 里的_lxsdk_s是否匹配当前 session 的加密摘要。更关键的是page2这个参数在真实请求中会被前端 JS 用当前时间戳、shopId、随机数拼接后经 MD5 再 Base64 编码塞进另一个隐藏参数__ts里。也就是说你看到的page2是给人看的服务端真正认的是那个藏在__ts里的动态签名值。提示不要试图用 Python 的requests.get(url)直接复现。必须完整复现浏览器发起请求时的全部上下文——包括 Cookie 初始化、Header 构造、参数签名、甚至请求间隔节奏。否则99% 的失败都源于此。2.2 核心参数解析哪些能改哪些碰都不能碰我把近半年抓取过的 17 类大众点评 Ajax 接口覆盖店铺、团购、外卖、医美的参数做了归类提炼出四类字段字段类型示例是否可修改修改后果原理说明强绑定会话字段_lxsdk_s123abc...,__mta456def...❌ 绝对禁止403 或空响应由登录态生成含设备指纹哈希有效期 2-7 天过期需重新模拟登录业务标识字段shopId123456789,dealGroupId987654321✅ 可替换获取目标店铺数据必须与 Cookie 中的shopId上下文一致否则返回{code:500,msg:Invalid shopId}分页控制字段page2,pageSize10,start10✅ 可调整控制返回条数与偏移注意page和start不能共存pageSize最大支持 20超限返回 400签名验证字段__tsabcd1234,__sigefgh5678❌ 不可硬编码签名失效即 403由前端 JS 调用window.__dianping.sign()生成依赖当前时间毫秒、shopId、随机 salt这里重点说说__ts的生成逻辑。我反编译过大众点评 Web 端的加密 JSv2023.11.07 版本其核心函数简化后如下function generateTs(shopId) { const timestamp Date.now(); // 当前毫秒时间戳 const salt Math.random().toString(36).substr(2, 8); // 8位随机字符串 const raw ${timestamp}_${shopId}_${salt}; const md5 CryptoJS.MD5(raw).toString(); // 使用 CryptoJS 库 return btoa(md5); // Base64 编码 }注意salt不是固定值每次请求都变timestamp精确到毫秒服务端允许 ±30 秒误差超时即拒收。这意味着你用 Python 写脚本时不能把__ts写成常量必须在每次请求前实时计算。2.3 编码格式陷阱为什么ajax请求设置编码格式是高频报错根源网络热词里反复出现ajax请求设置编码格式绝非偶然。大众点评接口对编码的校验极其严格主要体现在三个层面第一层请求头声明必须显式设置Content-Type: application/json; charsetutf-8即使 GET 请求无 body也要带上。我实测过如果只写application/json不带charsetutf-8部分节点会返回500 Internal Server Error错误日志里明确提示charset not specified。第二层URL 参数编码所有 query 参数尤其是中文店名、用户昵称必须用UTF-8编码后再进行encodeURIComponent。比如店铺名 “海底捞火锅国贸店”正确编码是%E6%B5%B7%E5%BA%95%E6%8D%9E%E7%81%AB%E9%94%85%EF%BC%88%E5%9B%BD%E8%B4%B8%E5%BA%97%EF%BC%89。如果用 GBK 编码再 encodeURIComponent服务端解析时会因字节流错乱直接抛出unexcepted end of json input。第三层响应体解码服务端返回的 JSON 响应头里Content-Type明确写着application/json; charsetutf-8但部分旧版 Pythonrequests库2.25在自动解码时会误判为ISO-8859-1导致中文变成乱码。解决方案不是改response.encoding而是强制用response.content.decode(utf-8)解析原始字节流再交给json.loads()。注意failed to deserialize the json body into the target type: input: missing fie这个报错90% 源于编码错误导致 JSON 字符串截断。比如响应本该是{reviews:[{id:1,text:很好吃}]}因编码错乱变成{reviews:[{id:1,text:・好åƒ}]}json.loads()解析时遇到非法 UTF-8 字节直接抛异常且错误信息里missing fie实为missing field的截断显示——这是底层 JSON 解析器的 bug不是大众点评的问题。3. 实操全流程从抓包到稳定获取的七步法3.1 第一步精准抓包与请求还原避坑关键别急着写代码先用 Chrome 完整走一遍流程。打开大众点评任意一家门店页如搜索“喜茶 北京三里屯”F12 → Network → XHR → 切换到“评论”Tab → 滚动到底部触发加载。此时你会看到多个/ajax/json/shop/wizard/GetReviewList请求。右键第一个 → “Copy” → “Copy as cURL (bash)”。粘贴到文本编辑器你会得到类似这样的命令curl https://www.dianping.com/ajax/json/shop/wizard/GetReviewList?shopId123456789page1pageSize10sortType1 \ -H authority: www.dianping.com \ -H accept: application/json, text/plain, */* \ -H accept-language: zh-CN,zh;q0.9,en;q0.8 \ -H cookie: _lxsdk_s123abc...; __mta456def...; _lxsdk_cuid789ghi... \ -H x-requested-with: XMLHttpRequest \ -H x-shard: shopId123456789重点来了这个 cURL 是“活”的不是“死”的。它包含当前有效的 Cookie 和 Header但page1是静态的。你要做的是提取shopId、_lxsdk_s、__mta、_lxsdk_cuid、X-Shard这五个核心字段记录下Date.now()的毫秒值用于后续__ts计算绝不直接执行这个 cURL因为page1下次就失效了。我习惯用一个临时 Python 脚本做验证import requests import time import base64 import hashlib # 从 cURL 提取的固定值 shop_id 123456789 lxsdk_s 123abc... mta 456def... cuid 789ghi... # 构造动态 __ts timestamp int(time.time() * 1000) salt random8char # 实际需用 random 生成 raw f{timestamp}_{shop_id}_{salt} ts base64.b64encode(hashlib.md5(raw.encode()).digest()).decode() url fhttps://www.dianping.com/ajax/json/shop/wizard/GetReviewList?shopId{shop_id}page1pageSize10sortType1__ts{ts} headers { authority: www.dianping.com, accept: application/json, text/plain, */*, cookie: f_lxsdk_s{lxsdk_s}; __mta{mta}; _lxsdk_cuid{cuid}, x-requested-with: XMLHttpRequest, x-shard: fshopId{shop_id} } resp requests.get(url, headersheaders) print(resp.status_code, len(resp.content)) print(resp.text[:200]) # 打印前200字符看是否正常如果返回 200 且 JSON 结构完整说明基础链路通了。这一步卡住后面全是空谈。3.2 第二步Cookie 生命周期管理长期运行的核心大众点评的 Cookie 不是“一次登录永久有效”。_lxsdk_s字段有效期约 3 天__mta约 7 天_lxsdk_cuid理论永久但会因设备重置失效。这意味着你的脚本必须内置 Cookie 刷新机制。我的方案是双轨并行短期策略推荐新手每天凌晨自动重启用 Linux cron 设置# 每天 2:00 执行 cookie 刷新脚本 0 2 * * * /usr/bin/python3 /path/to/refresh_cookie.py /var/log/dp_cookie.log 21refresh_cookie.py的核心逻辑是启动无头 Chrome访问大众点评首页等待登录态加载完成检测document.cookie是否含_lxsdk_s然后用driver.get_cookies()提取全部 Cookie序列化存入 Redis 或本地 JSON 文件。后续数据抓取脚本统一从这个存储读取最新 Cookie。长期策略生产环境会话保活 异常熔断在主抓取循环里每 10 次请求后主动发起一次“心跳请求”def keep_alive(): url https://www.dianping.com/ headers {cookie: get_current_cookie()} resp requests.get(url, headersheaders, timeout5) if resp.status_code ! 200 or _lxsdk_s not in resp.headers.get(set-cookie, ): trigger_relogin() # 触发重新登录流程同时监控每次请求的resp.headers.get(x-shard)如果连续 3 次返回shopId0或空值立即判定 Cookie 失效跳转至重登录。实操心得千万别用requests.Session()长期持有 Cookie。大众点评服务端会校验 Cookie 中的时间戳字段如__mta的最后 6 位是时间戳Session 对象无法自动更新这些动态字段。必须每次请求前从外部存储读取“新鲜”的 Cookie 字符串。3.3 第三步JSON 响应结构解析与容错设计大众点评的评论 JSON 并非扁平结构而是多层嵌套且字段存在“软删除”现象——即某条评论的user对象可能缺失avatarUrl字段review对象可能没有photos数组。直接data[reviews][0][user][avatarUrl]会抛KeyError。必须用安全访问模式。我定义了一个通用解析器def safe_get(data, keys, default): 安全获取嵌套字典值 keys: [reviews, 0, user, avatarUrl] for key in keys: try: if isinstance(data, list): data data[key] if isinstance(key, int) else data[0] else: data data[key] except (KeyError, IndexError, TypeError): return default return data # 使用示例 for review in response_json.get(reviews, []): user_avatar safe_get(review, [user, avatarUrl], ) review_text safe_get(review, [review, text], ) review_time safe_get(review, [review, time], ) photos safe_get(review, [review, photos], [])更关键的是要识别真正的“数据结束”。大众点评不返回hasNextfalse而是当page超出实际页数时返回空数组{reviews:[]}。因此翻页逻辑不能依赖len(reviews)0而要看响应体里是否有code字段且为200以及reviews数组长度是否小于pageSize。我的翻页条件是if len(reviews) PAGE_SIZE or not reviews: break # 无更多数据 else: page 1 time.sleep(1.5) # 强制延时模拟人工操作3.4 第四步并发控制与请求节流避免被封 IP大众点评对单 IP 的请求频率限制非常敏感。实测阈值是同一 IP 每分钟不超过 30 次有效请求403 或 500 不计入。超过后后续请求会返回429 Too Many Requests持续 5-15 分钟。我的生产环境采用三级节流客户端级每个请求后time.sleep(2.0)确保最低间隔进程级用threading.Semaphore(5)限制并发线程数 ≤5IP 级部署在 3 台不同出口 IP 的服务器上通过 Nginx 做负载均衡每台机器独立计数。具体实现用concurrent.futures.ThreadPoolExecutorfrom concurrent.futures import ThreadPoolExecutor, as_completed import time semaphore threading.Semaphore(5) def fetch_page(page_num): with semaphore: # 同一时刻最多 5 个线程执行 # 构造请求... resp requests.get(url, headersheaders, timeout10) time.sleep(2.0) # 固定延时 return parse_reviews(resp.json()) # 启动 10 页并发抓取 with ThreadPoolExecutor(max_workers5) as executor: futures {executor.submit(fetch_page, p): p for p in range(1, 11)} for future in as_completed(futures): try: reviews future.result() all_reviews.extend(reviews) except Exception as e: print(fPage {futures[future]} failed: {e})注意max_workers5不代表并发 5因为每个 worker 内部还有sleep(2.0)实际 QPS 稳定在 2.5 左右远低于 30/min 的红线。这是我在线上跑了 8 个月零封禁的实测值。4. 常见问题与排查技巧实录4.1 典型报错速查表报错现象可能原因排查步骤解决方案403 Forbidden__ts签名过期或计算错误检查Date.now()是否与服务器时间偏差 30s打印raw字符串确认shopId和salt正确用ntplib同步本地时间salt改用secrets.token_urlsafe(6)生成500 Internal Server ErrorContent-Type缺少charsetutf-8用 Wireshark 抓包对比浏览器请求头在headers字典中显式添加Content-Type: application/json; charsetutf-8unexcepted end of json input响应体编码错乱导致 JSON 截断print(resp.content[:100])查看原始字节流改用json.loads(resp.content.decode(utf-8))禁用resp.json(){code:500,msg:Invalid shopId}X-Shardheader 中的shopId与 URL 参数不一致检查X-Shard: shopIdxxx是否与?shopIdxxx完全相同严格字符串匹配注意前后空格{reviews:[]}真实无数据或page超出范围检查前一页是否返回len(reviews)10当len(reviews)10时立即停止翻页429 Too Many Requests单 IP 请求超频查看响应头Retry-After字段立即暂停所有请求time.sleep(int(resp.headers.get(Retry-After, 300)))4.2 独家避坑技巧血泪总结技巧一永远用response.content不用response.textresponse.text会触发 requests 库的自动编码猜测而大众点评的响应头Content-Type有时不带charsetrequests 就会用ISO-8859-1解码中文全变乱码。response.content是原始字节流你可控性 100%。我见过太多人在这里浪费三天——就因为一行代码没写对。技巧二X-Shardheader 是“开关”不是“装饰”很多教程忽略这个字段说“加上 Cookie 就行”。错。X-Shard: shopId123456789是服务端路由的关键缺了它请求会被打到默认集群返回空数据或 404。而且它的值必须与 URL 中的shopId完全一致连大小写都不能错。技巧三sortType参数的隐藏规则文档没写但实测发现sortType1最新最多返回 1000 条sortType2最热最多 500 条sortType3好评最多 200 条。如果你要全量抓取必须用sortType1再配合page翻页。别信网上说的“换 sortType 能绕过限制”。技巧四filterType的组合玄机filterType0是全部评论filterType1是带图评论filterType2是带视频评论。但filterType1和filterType2不能与sortType1同时使用否则返回{code:400,msg:Bad Request}。必须用sortType0综合排序才能启用图片/视频过滤。4.3 数据质量校验清单上线前必做抓到的数据不能直接用必须过五关时间校验检查review.time字段是否为标准时间戳13 位毫秒或 ISO 格式如2023-11-07 14:22:33剔除null或文本清洗去除\u200b零宽空格、\ufeffBOM、重复换行符用正则re.sub(r\s, , text)压缩空白用户去重同一userId在同一家店的评论只保留time最新的那条防刷评情感一致性review.star字段1-5 星必须与review.text情感倾向基本匹配用 jiebaSnowNLP 做初筛star1但text含“太棒了”则标为异常图片链接存活对photos数组中的每个 URL用HEAD请求校验status_code200失效链接置空。我写了个校验脚本每次抓取 1000 条后自动运行输出quality_report.json包含valid_count、invalid_time_count、empty_text_count、image_dead_count等字段。上线三个月数据可用率从 82% 提升到 99.6%。5. 工具链与工程化建议5.1 推荐工具栈已验证生产可用抓包与调试Chrome DevTools主力、Charles Proxy看 HTTPS 解密、Wireshark终极网络层分析签名逆向Chrome 的 Sources → Page → 找sign.js用debugger断点跟踪JS 逆向用 AST Explorer 分析混淆代码请求发送Pythonrequests简单场景、httpx异步高并发、playwright需要完整浏览器上下文时JSON 处理jsonpath-ngXPath for JSON查嵌套字段、orjson比标准 json 快 3 倍内存占用低 50%存储与调度Redis存 Cookie 和任务队列、PostgreSQL存结构化评论建shop_id和review_time复合索引、Airflow定时调度多店铺抓取。5.2 生产环境部署 checklist环境隔离开发、测试、生产三套 Cookie 存储用不同 Redis DB日志分级INFO 级记录成功请求WARNING 级记录 4xx/5xxERROR 级记录KeyError和JSONDecodeError所有日志打上shop_id和page标签告警机制当连续 5 次请求返回429或403微信机器人推送告警并自动触发 Cookie 刷新数据备份每日凌晨将 PostgreSQL 中新增评论导出为dp_reviews_20231107.csv上传至 S3合规审计所有抓取脚本头部加注释“本脚本仅用于个人学习与非商业研究遵守 robots.txt 及《反不正当竞争法》第十二条”。最后分享一个小技巧大众点评的评论接口其实有“影子模式”。当你在网页上快速滚动评论区会发现 Network 面板里除了GetReviewList还有一串GetReviewDetail请求它们返回单条评论的完整详情含用户手机号脱敏、身份证后四位等。这些接口的签名逻辑更简单__ts只依赖reviewId且X-Shard可省略。如果你只需要深度分析某几条高价值评论走这条路比全量抓取更稳、更快。
返回列表