大众点评评论数据获取:从接口分析到加密参数破解的实战指南
最近在做一个本地生活数据分析项目时,需要获取大众点评的商户评论数据。刚开始尝试用传统的HTML解析方式,发现只能拿到页面初始加载的几条评论,大部分数据都是通过Ajax动态加载的。更棘手的是,接口请求中有一个关键的_token参数,每次请求都会变化,直接复制粘贴根本行不通。这种加密参数机制是现在很多网站反爬虫的标配,如果无法破解,数据获取就无从谈起。
我花了几天时间研究大众点评的加密逻辑,从浏览器开发者工具一步步追踪JavaScript代码,最终在Python中成功复现了加密过程。这篇文章就是把我踩过的坑和解决方案整理出来,希望能帮到遇到同样问题的开发者。无论你是做竞品分析、用户情感研究,还是构建推荐系统,掌握这套方法都能让你更高效地获取所需数据。
1. 理解大众点评的数据加载机制
要破解一个网站的数据获取,首先要理解它是如何工作的。大众点评采用的是典型的前后端分离架构,页面初始加载时只包含基本的HTML框架和少量静态内容,真正的数据——比如用户评论、评分、商户信息——都是通过JavaScript异步请求从服务器获取的。
1.1 Ajax接口的识别与分析
打开Chrome开发者工具,进入Network面板,筛选XHR或Fetch请求,然后刷新一个商户页面。你会看到一系列请求,其中包含评论数据的接口通常有比较明显的特征:
https://www.dianping.com/ajax/json/shopDynamic/allReview?shopId=H9dNInbVZA9jJ5qH&cityId=1&shopType=10&page=1&pageSize=20&_token=xxxxxxxxxxxx
这个接口返回的是标准的JSON格式数据,结构清晰,比解析HTML要方便得多。关键参数包括:
| 参数名 | 含义 | 示例值 |
|---|---|---|
| shopId | 商户的唯一标识符 | H9dNInbVZA9jJ5qH |
| cityId | 城市ID | 1(代表上海) |
| shopType | 商户类型 | 10(美食类) |
| page | 页码 | 1 |
| pageSize | 每页评论数量 | 20 |
| _token | 加密令牌 | 动态生成 |
注意:
_token参数是大众点评反爬机制的核心,它基于时间戳和特定算法生成,每次请求都需要重新计算。直接复制一次获取的token用于后续请求是行不通的。
1.2 接口响应的数据结构
成功获取数据后,你会发现JSON结构大致如下:
{
"code": 200,
"msg": "success",
"data": {
"reviewList": [
{
"reviewId": "1234567890",
"userId": "user_001",
"userName": "美食探索者",
"userLevel": "LV6",
"reviewScore": 45,
"reviewBody": "这家店的招牌菜确实不错...",
"reviewTime": "2024-03-15 18:30:00",
"likeCount": 24,
"viewCount": 156,
"reviewPics": ["url1", "url2"],
"merchantReply": "感谢您的认可,我们会继续努力!"
}
],
"totalCount": 1250,
"hasNext": true
}
}
这种结构化的数据非常便于后续处理和分析。每条评论都包含了用户信息、评分、内容、时间、互动数据等完整信息,有些还附带图片和商家回复。
2. 逆向分析_token生成逻辑
这是整个过程中最具挑战性的部分。大众点评的_token不是简单的随机字符串,而是基于特定算法生成的加密值。要破解它,需要深入JavaScript代码。
2.1 使用开发者工具追踪加密代码
在Chrome开发者工具中,切换到Sources面板,使用全局搜索功能查找_token或相关关键词。通常你会发现类似这样的代码片段:
// 实际代码可能经过混淆,这是简化后的逻辑
function generateToken() {
var timestamp = Math.floor(Date.now() / 1000);
var secretKey = "dianping_2024_secret";
var rawString = timestamp + secretKey;
return md5(rawString);
}
但实际情况往往更复杂。大众点评可能会:
- 代码混淆:变量名被替换为无意义的字符
- 逻辑分散:加密逻辑分散在多个函数中
- 环境检测:检查浏览器环境,防止代码在非浏览器环境中执行
- 动态加载:加密代码可能动态加载或生成
我遇到的一个典型情况是,加密函数被包裹在一个立即执行函数表达式中,并且引用了浏览器特有的对象:
(function() {
var n = window.navigator;
var u = n.userAgent;
var t = Math.floor((new Date).getTime() / 1000);
var r = "dianping_" + t + "_" + u.substring(0, 10);
var e = function(s) {
// 复杂的加密逻辑
for(var i = 0, h = 0; h < s.length; h++) {
i = (i << 5) - i + s.charCodeAt(h);
i = i & i;
}
return (i >>> 0).toString(16);
};
window._token = e(r);
})();
2.2 提取关键加密逻辑
经过分析,我发现大众点评的_token生成通常包含以下几个要素:
- 时间戳:通常是当前时间的秒级或毫秒级时间戳
- 固定密钥:硬编码在JavaScript中的字符串
- 用户代理信息:浏览器User-Agent的部分内容
- 自定义算法:可能是MD5、SHA1或自定义的哈希函数
- 其他环境变量:如屏幕分辨率、时区等
一个比较完整的分析过程如下:
// 在浏览器控制台中调试
function debugTokenGeneration() {
// 设置断点或使用debugger语句
debugger;
// 查找生成_token的函数
// 通常可以通过搜索 "_token=" 或 "token:" 来定位
var tokenGenerators = [];
// 遍历所有函数,查找可能生成token的
for(var prop in window) {
if(typeof window[prop] === 'function') {
var funcStr = window[prop].toString();
if(funcStr.includes('token') || funcStr.includes('Token')) {
tokenGenerators.push({
name: prop,
func: window[prop]
});
}
}
}
console.log('找到的token生成函数:', tokenGenerators);
return tokenGenerators;
}
2.3 验证加密逻辑
提取出疑似加密函数后,需要在控制台中验证:
// 测试提取的函数
var testTimestamp = Math.floor(Date.now() / 1000);
var testToken = extractedTokenFunction(testTimestamp);
// 与实际请求中的token对比
console.log('生成的token:', testToken);
console.log('实际的token:', '从Network面板复制的token');
// 如果一致,说明找到了正确的函数
if(testToken === actualToken) {
console.log('✓ 加密函数验证成功!');
} else {
console.log('✗ 加密函数验证失败,需要继续分析');
}
3. 在Python中复现JavaScript加密
一旦在浏览器中确定了加密逻辑,下一步就是在Python中实现相同的算法。这通常需要处理几个技术难点。
3.1 环境准备与依赖安装
首先确保你的Python环境已经安装了必要的库:
# 基础请求库
pip install requests
# 用于执行JavaScript代码
pip install pyexecjs
# 或者使用更现代的替代品
pip install js2py
# 日期时间处理
pip install python-dateutil
# 进度显示(可选)
pip install tqdm
如果加密逻辑特别复杂,可能需要安装Node.js环境,因为有些JavaScript特性在Python的JavaScript执行环境中可能不被完全支持。
3.2 实现加密参数生成器
基于前面的分析,我创建了一个Token生成器类:
import time
import hashlib
import execjs
import random
from urllib.parse import quote
class DianpingTokenGenerator:
"""大众点评_token参数生成器"""
def __init__(self, method='python'):
"""
初始化token生成器
Args:
method: 生成方式,'python'或'js'
"""
self.method = method
if method == 'js':
# 如果需要在Python中执行JavaScript代码
self._init_js_environment()
def _init_js_environment(self):
"""初始化JavaScript执行环境"""
# 这里可以加载从浏览器提取的JavaScript加密代码
js_code = """
// 这是从大众点评网站提取的简化版加密函数
function generateToken(timestamp, userAgent) {
// 实际代码会更复杂,这里只是示例
var key = "dianping_secret_key_" +
new Date().getFullYear();
// 组合字符串
var rawStr = timestamp.toString() +
key +
userAgent.substring(0, 16);
// 模拟一个简单的哈希函数
var hash = 0;
for (var i = 0; i < rawStr.length; i++) {
var char = rawStr.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
// 转换为16进制
return (hash >>> 0).toString(16);
}
// 如果需要更复杂的加密,可以在这里定义
function complexEncrypt(input) {
// 实际的加密逻辑
return input;
}
"""
try:
self.ctx = execjs.compile(js_code)
except Exception as e:
print(f"JavaScript环境初始化失败: {e}")
print("回退到Python实现")
self.method = 'python'
def generate_with_python(self, user_agent=None):
"""
使用Python实现token生成
Args:
user_agent: 浏览器User-Agent字符串
Returns:
生成的_token字符串
"""
if user_agent is None:
user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36")
# 获取当前时间戳(秒级)
timestamp = int(time.time())
# 大众点评的密钥可能按年份变化
current_year = time.strftime("%Y")
secret_key = f"dianping_{current_year}_secret"
# 组合原始字符串
raw_string = f"{timestamp}{secret_key}{user_agent[:16]}"
# 计算MD5(实际算法可能不同)
# 这里只是示例,实际需要根据逆向分析结果实现
md5_hash = hashlib.md5(raw_string.encode('utf-8')).hexdigest()
# 可能还需要进一步处理
token = md5_hash[:12] # 取前12位,实际长度可能不同
return token
def generate_with_js(self, user_agent=None):
"""
通过执行JavaScript代码生成token
Args:
user_agent: 浏览器User-Agent字符串
Returns:
生成的_token字符串
"""
if user_agent is None:
user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36")
timestamp = int(time.time())
try:
token = self.ctx.call("generateToken", timestamp, user_agent)
return token
except Exception as e:
print(f"JavaScript执行失败: {e}")
# 回退到Python实现
return self.generate_with_python(user_agent)
def generate(self, user_agent=None):
"""
生成_token参数
Args:
user_agent: 浏览器User-Agent字符串
Returns:
生成的_token字符串
"""
if self.method == 'js' and hasattr(self, 'ctx'):
return self.generate_with_js(user_agent)
else:
return self.generate_with_python(user_agent)
def generate_signature(self, params, secret_key=None):
"""
生成请求签名(如果接口需要)
Args:
params: 参数字典
secret_key: 签名密钥
Returns:
签名字符串
"""
if secret_key is None:
secret_key = "dianping_signature_key"
# 按参数名排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 构建参数字符串
param_parts = []
for key, value in sorted_params:
if key != 'sign' and value is not None:
param_parts.append(f"{key}={value}")
param_str = '&'.join(param_parts)
# 添加密钥
sign_str = param_str + secret_key
# 计算MD5
signature = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
return signature
3.3 处理更复杂的加密情况
有时候,大众点评可能会使用更复杂的加密方式,比如:
- AES加密:需要对参数进行对称加密
- RSA加密:使用非对称加密
- 自定义编码:Base64变种或其他编码方式
- 多重哈希:多次应用不同的哈希函数
对于这种情况,可能需要更完整的JavaScript环境:
import subprocess
import json
class AdvancedTokenGenerator:
"""处理复杂加密情况的token生成器"""
def __init__(self, js_file_path=None):
"""
初始化高级token生成器
Args:
js_file_path: 包含加密逻辑的JavaScript文件路径
"""
self.js_file_path = js_file_path
def generate_with_nodejs(self, params):
"""
使用Node.js执行复杂的JavaScript加密逻辑
Args:
params: 需要加密的参数
Returns:
加密后的token
"""
if not self.js_file_path:
raise ValueError("需要提供JavaScript文件路径")
# 准备输入数据
input_data = {
'params': params,
'timestamp': int(time.time() * 1000), # 毫秒级时间戳
'userAgent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
# 创建Node.js脚本
node_script = f"""
const fs = require('fs');
const crypto = require('crypto');
// 加载加密函数
const encryptModule = require('{self.js_file_path}');
// 准备参数
const inp


4002

被折叠的 条评论
为什么被折叠?



