大众点评评论爬取实战:如何绕过_token加密参数获取完整数据

大众点评评论数据获取:从接口分析到加密参数破解的实战指南

最近在做一个本地生活数据分析项目时,需要获取大众点评的商户评论数据。刚开始尝试用传统的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);
}

但实际情况往往更复杂。大众点评可能会:

  1. 代码混淆:变量名被替换为无意义的字符
  2. 逻辑分散:加密逻辑分散在多个函数中
  3. 环境检测:检查浏览器环境,防止代码在非浏览器环境中执行
  4. 动态加载:加密代码可能动态加载或生成

我遇到的一个典型情况是,加密函数被包裹在一个立即执行函数表达式中,并且引用了浏览器特有的对象:

(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生成通常包含以下几个要素:

  1. 时间戳:通常是当前时间的秒级或毫秒级时间戳
  2. 固定密钥:硬编码在JavaScript中的字符串
  3. 用户代理信息:浏览器User-Agent的部分内容
  4. 自定义算法:可能是MD5、SHA1或自定义的哈希函数
  5. 其他环境变量:如屏幕分辨率、时区等

一个比较完整的分析过程如下:

// 在浏览器控制台中调试
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 处理更复杂的加密情况

有时候,大众点评可能会使用更复杂的加密方式,比如:

  1. AES加密:需要对参数进行对称加密
  2. RSA加密:使用非对称加密
  3. 自定义编码:Base64变种或其他编码方式
  4. 多重哈希:多次应用不同的哈希函数

对于这种情况,可能需要更完整的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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值