拼多多社会招聘反爬参数逆向实战(Anti-Content生成)

想要批量获取拼多多社招岗位做行业薪酬分析、人才画像搭建这类技术调研?先过眼前这道动态关卡——瑞数动态安全防护下的核心反爬参数:anti-content

本文以拼多多社招的真实场景为锚点,带你梳理从环境检测到核心函数Hook的完整逆向流程,最后给出可复用的Python+Nodejs(execjs)实现方案。

前置要求:熟悉Chrome DevTools的使用,了解基础的JS逆向逻辑,知道execjs如何调用JS代码。


概述

1.1 场景与核心

拼多多社招平台(https://careers.pddglobalhr.com/jobs)的接口数据,必须携带anti-content才能正常返回——这个参数由混淆后的JS动态生成,每次刷新前端代码可能有细微混淆变化,但核心加密逻辑的框架是稳定的。

1.2 本次面临的技术挑战

和静态加密(比如MD5+盐、AES)不同,瑞数的防护有几个“硬骨头”:

  1. 动态混淆防静态分析:抠下的JS代码经常“缺东少西”(依赖瑞数自注入的Webpack模块或全局变量)
  2. 严格的环境指纹检测:会检查navigator.webdriverwindow.chrome、插件列表等是否是自动化工具伪造
  3. 基础的反重放与链路绑定:携带的Cookie、Referer、UA必须保持一致性,参数里还会嵌入时间戳防止旧请求复用
  4. 动态构造器调用:加密函数不是暴露在顶层的静态md5(),而是通过混淆生成的window.hhh(4)这样的动态构造器

网页逆向分析思路

2.1 定位目标接口

直接按F12打开DevTools,切换到Network面板,刷新页面或点击“下一页”筛选岗位,很快就能找到返回岗位列表的接口:

POST https://careers.pinduoduo.com/api/recruit/position/list

查看请求Payload,anti-content就是我们要逆向的核心参数。

2.2 定位参数生成位置

瑞数的参数通常不会在静态JS里写死,我们可以用以下两种常用方法:

方法1:全局搜索关键词

在DevTools的Sources面板,按Ctrl+Shift+F(Windows)或Cmd+Option+F(Mac)打开全局搜索,输入:

  • anti-content
  • antiContent
  • getAnti
  • hhh(后续会发现这是核心构造器的名字)

很快就能找到构造器调用的地方。

方法2:XHR/Fetch断点

在Network面板找到目标接口后,右键选择“XHR/fetch Breakpoints”添加断点,刷新页面,页面会停在发送请求的代码处——往上倒推调用栈,就能找到生成anti-content的函数。


核心加密逻辑简化解析

通过调试和Hook,我们可以把核心逻辑简化为以下三个步骤:

3.1 第一步:补全合法浏览器环境

瑞数会在加密前检测大量环境变量,不补全的话,生成的anti-content会被后端判定为无效。我们需要补的关键变量包括:

// 最基础的自动化特征
Object.defineProperty(navigator, 'webdriver', {
    get: () => false
});

// 模拟Chrome环境的扩展API(瑞数会检查这个)
window.chrome = {
    runtime: {},
    loadTimes: () => {},
    csi: () => {}
};

// 模拟合法的插件列表
navigator.plugins = [
    {name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer'},
    {name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai'}
];

3.2 第二步:调用动态构造器

瑞数会通过自执行的混淆代码,在window上注入类似hhh的构造函数——这个构造函数的名字可能会变,但调用方式和参数(比如4代表“加密岗位列表接口的anti-content”) 是稳定的:

// 简化后的核心构造器调用
function get_anti() {
  // window.hhh(4) 是岗位列表接口专属的加密构造器
  // serverTime 是当前毫秒时间戳,防止重放攻击
  const encryptor = new window.hhh(4)({
    serverTime: new Date().getTime()
  });
  // 调用序列化方法生成最终的字符串
  return encryptor.messagePack();
}

3.3 第三步:序列化输出

messagePack() 方法会把加密后的二进制数据编码成类似Base64的字符串(但不是标准Base64,有自定义的字符映射),这就是我们要的anti-content


可复用的Python实现方案

我们可以用execjs调用补好环境的JS文件,生成anti-content,再用requests发送请求。

4.1 补好环境的JS文件(demo.js)

注意:这里只给出了环境补全的框架——真实的window.hhh需要从瑞数的混淆代码中完整提取并补全所有依赖的模块,不能直接复制粘贴框架。

// 先补全环境变量(放在代码最前面)
Object.defineProperty(navigator, 'webdriver', {
    get: () => false
});
window.chrome = { runtime: {}, loadTimes: () => {}, csi: () => {} };
navigator.plugins = [
    {name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer'},
    {name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai'}
];
navigator.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36';

// 【关键部分】从瑞数混淆代码中提取并补全依赖的 window.hhh 和相关模块
// 这部分需要你自己通过Chrome DevTools的调试和Hook来完成

// 暴露给Python调用的函数
function get_anti() {
  const encryptor = new window.hhh(4)({
    serverTime: new Date().getTime()
  });
  return encryptor.messagePack();
}

4.2 Python调用代码

# -*- coding: utf-8 -*-
import json
import requests
import execjs

def get_anti_content():
    """
    调用补好环境的JS文件生成 anti-content
    """
    try:
        with open('demo.js', 'r', encoding='utf-8') as f:
            js_code = f.read()
        ctx = execjs.compile(js_code)
        return ctx.call('get_anti')
    except Exception as e:
        print(f"生成 anti-content 失败: {e}")
        return None

def fetch_pdd_jobs(page=1, page_size=10):
    """
    获取拼多多社招岗位列表
    """
    # 生成核心参数
    anti_content = get_anti_content()
    if not anti_content:
        return None

    # 请求头和Cookie必须保持一致性
    headers = {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
        "content-type": "application/json",
        "origin": "https://careers.pinduoduo.com",
        "referer": "https://careers.pinduoduo.com/jobs",
        "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": '"Windows"',
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"
    }
    cookies = {
        # 【注意】这里需要你自己从浏览器中获取合法的 Cookie
        "_nano_fp": "替换成你自己的_nano_fp",
        "api_uid": "替换成你自己的api_uid"
    }
    url = "https://careers.pinduoduo.com/api/recruit/position/list"
    payload = {
        "job": "",
        "page": page,
        "pageSize": page_size,
        "name": "",
        "workLocationList": [],
        "anti_content": anti_content
    }

    try:
        # 发送POST请求,注意payload要转为紧凑的JSON字符串
        response = requests.post(
            url,
            headers=headers,
            cookies=cookies,
            data=json.dumps(payload, separators=(',', ':'))
        )
        response.raise_for_status()  # 抛出HTTP错误
        return response.json()
    except Exception as e:
        print(f"获取岗位列表失败: {e}")
        return None

if __name__ == "__main__":
    jobs = fetch_pdd_jobs(page=1, page_size=10)
    if jobs:
        print("获取成功!")
        print(json.dumps(jobs, indent=2, ensure_ascii=False))

注意事项与总结

5.1 注意事项

  1. 不要频繁请求:拼多多的反爬会检测请求频率,太快会被封IP或Cookie
  2. Cookie需要定期更新:Cookie里的_nano_fp等参数会过期
  3. 瑞数代码会动态更新:混淆后的window.hhh名字和内部逻辑可能会变,需要定期调试
  4. 仅供技术学习使用:批量爬取可能违反拼多多的服务条款,请注意合法合规

5.2 核心知识点总结

关键步骤具体内容
定位接口通过Network面板找到POST请求的岗位列表接口
定位参数用XHR/Fetch断点或全局搜索关键词找到生成位置
补全环境重点补navigator.webdriverwindow.chrome、插件列表
提取逻辑从混淆代码中提取window.hhh和依赖模块
生成请求用execjs调用JS生成参数,保持Cookie/Referer一致性

希望本文能帮你入门瑞数动态安全防护的逆向——如果有问题,欢迎在评论区交流。