pdd-recruitment-anticontent-reverse

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

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

前置准备:熟悉 Chrome DevTools 的基本操作,了解 JavaScript 逆向的常用手法,知道 execjs 是怎么在 Python 里调用 JavaScript 代码的。


1. 概述

1.1 场景与核心参数

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

1.2 这次要啃的“硬骨头”

跟静态加密(比如 MD5+盐、AES)相比,瑞数这款动态安全防护产品有几个让人头疼的地方:

  1. 动态混淆,静态分析容易缺依赖:抠下来的 JS 代码经常“缺东少西”,因为它依赖瑞数自动注入的 Webpack 模块或者某些全局变量,不补全会直接报错。
  2. 严格的环境指纹检测:会仔细检查 navigator.webdriverwindow.chrome、浏览器插件列表等,判断你是不是在用自动化工具伪造环境。
  3. 反重放与链路绑定:请求里携带的 Cookie、Referer、User-Agent 必须保持一致性,参数里还会嵌入时间戳,防止旧请求被复用。
  4. 动态构造器调用:加密函数不是暴露在顶层的静态 md5(),而是通过混淆生成的类似 window.hhh(4) 这样的动态构造器。

2. 网页逆向分析思路

2.1 定位目标接口

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

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

点击这条请求,查看 Payload,就会发现 anti-content 就是我们这次要逆向的核心参数。

2.2 定位参数生成位置

瑞数的参数一般不会直接写在某个静态 JS 文件里,通常藏在动态注入的代码中。两种常用定位方法:

方法一:全局搜索关键词

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

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

一般很快就能定位到构造器调用的地方。

方法二:XHR/Fetch 断点

在 Network 面板中,右键点击目标接口,选择“XHR/fetch Breakpoints”添加断点,然后刷新页面。请求发出时会自动断在发送代码的地方——沿着调用栈往上回溯,就能找到生成 anti-content 的函数。


3. 核心加密逻辑简化解析

通过调试和 Hook,我们把核心逻辑拆分成三步:

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

瑞数在加密前会检测大量环境变量。如果这些特征不补全,即便生成出的 anti-content 格式正确,后端校验也会失败。下面这些是必须要补的关键变量:

// 最基础的自动化特征 — 隐藏 webdriver 痕迹
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


4. 可复用的 Python 实现方案

我们可以用 Python 的 execjs 库来执行补好环境的 JavaScript 文件,生成 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' }
];
// User-Agent 也需要补上
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 需要和你提取 anti-content 时的环境保持一致
    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:
        # 注意 payload 要转成紧凑的 JSON 字符串,不然后端可能校验失败
        response = requests.post(
            url,
            headers=headers,
            cookies=cookies,
            data=json.dumps(payload, separators=(',', ':'))
        )
        response.raise_for_status()
        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. 注意事项与总结

5.1 注意事项

  1. 控制请求频率:拼多多的风控会检测请求频率,频率过高容易导致 IP 或 Cookie 被封。
  2. Cookie 需要定期更新:Cookie 里的 _nano_fpapi_uid 等字段会过期,失效后需要重新从浏览器拿一份。
  3. 瑞数代码会动态更新:混淆后的变量名(比如 window.hhh)和内部逻辑可能不定期变化,你得定期调试跟进。
  4. 仅供技术学习使用:批量抓取可能违反拼多多的服务条款,请务必注意合法合规使用。

5.2 核心知识点回顾

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

希望本文能帮你顺利迈过瑞数动态防护的门槛。如果你在复现过程中遇到问题,欢迎在评论区一起交流讨论。