做爬虫时间长了,你会发现一个铁律:能拿到的数据,都是服务器允许你拿到的;拿不到的数据,才是真正值得研究的对象。刚开始用requests写爬虫时,只要处理好 headers、cookie、代理,大部分网站都能轻松拿下。但一旦遇到需要生成动态参数的反爬机制,例如签名(sign)、动态 token、加密的buvid之类的参数,事情就变得不那么简单了。
很多新手在第一次看到请求参数里有一串看不懂的加密字符串时,第一反应是“这网站是不是不想让人爬了”。其实不然,动态参数本质上就是网站前端为了防止自动化脚本批量请求而设计的一道门槛。想跨过这道门槛,就需要打开浏览器开发者工具,进入 JS 的世界,去定位“参数是怎么生成的”。这也是爬虫进阶路上最值得投入时间掌握的技能——JS 逆向。
本文将围绕“动态参数怎么生成”这个问题,从概念铺垫、抓包定位、JS 断点调试、Python 模拟实现四个维度,带你完成一次完整的 JS 逆向入门实操。文章默认你已经有 Python 基础,会用requests写简单爬虫,但还没系统接触过 JS 逆向。
1. 先说清楚:什么是动态参数,为什么网站要生成它?
1.1 静态参数与动态参数的区别
在写爬虫时,我们通常会先通过浏览器 F12 打开开发者工具,看某个数据请求的 URL、Headers 和 Payload。看到的结果往往分成两类:
| 类型 | 特点 | 示例 |
|---|---|---|
| 静态参数 | 固定不变,直接写死在代码里 | appid=123456、version=1.0.0、platform=web |
| 动态参数 | 每次请求都会变化,由前端 JS 动态计算 | sign=2e3f4d5c...、token=abc123、timestamp=1699999999 |
静态参数没什么好说的,写死即可。动态参数才是麻烦所在,因为同一个接口,你刷新一次页面,参数值就变了,直接复制浏览器里的参数到requests代码中,下一次请求大概率会失效。
1.2 网站为什么需要动态参数?
从网站开发者的视角来看,动态参数通常承担以下职责:
- 防止请求重放:通过时间戳 + 签名的方式,确保一个请求在短时间内有效,过期作废。
- 防止自动化脚本:参数由前端 JS 加密生成,Python 的
requests库默认不会执行 JS,所以不处理动态参数,就拿不到合法的请求数据。 - 区分正常用户与爬虫:正常用户通过浏览器访问页面,浏览器会执行 JS,动态参数自然生成;爬虫直接构造请求,缺少动态参数,服务器就可以直接拒绝返回数据。
- 风控与反爬追踪:部分平台通过动态参数埋入设备指纹、浏览器环境信息,用于识别同一台设备或同一 IP 的异常行为。
1.3 动态参数的常见形式
不同网站的封装方式不一样,但常见的动态参数主要有以下几类:
- 时间戳型动态参数:参数值就是当前时间戳,服务器用来判断请求时间差。
- MD5 / SHA 系列散列:把 URL、密钥、时间戳按一定规则拼接后做散列处理。
- AES / RSA 对称或非对称加密:把请求体加密后传输,服务器端再做解密。
- 自定义混淆算法:前端把明文参数经过字符替换、数组打乱、编码嵌套等混淆手段生成动态值,例如 B 站早期
buvid参数、某些加速乐 cookie 参数就属于此类。 - 环境指纹型参数:基于 Canvas 指纹、WebGL 信息、浏览器版本等生成的参数。
理解了上面这些,你就明白为什么学爬虫必须学 JS 逆向。因为动态参数的生成逻辑写在 JS 文件里,你不读 JS,就永远只能猜。
2. 环境准备与工具清单
学习 JS 逆向最忌讳空谈理论,必须打开真实项目来练手。下面先把开发环境准备好。
2.1 基础运行环境
- 操作系统:Windows / macOS / Linux 均可,本文示例以 Windows 10/11 为主。
- Python:建议使用 Python 3.9 及以上版本。
- Node.js:建议使用 Node.js 16 及以上版本,用于本地运行和调试 JS 片段。
- 浏览器:Chrome 或 Edge,本文以 Chrome 为例。
2.2 Python 需要安装的库
建议创建一个独立的虚拟环境,避免污染全局 Python:
python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install requests pyexecjs如果你打算做更复杂的 JS 逆向,还可以用到这些库:
| 库名 | 用途 |
|---|---|
requests | 发送 HTTP 请求 |
pyexecjs | 在 Python 中调用 JS 代码 |
PyMiniRacer | 更快的 JS 执行环境 |
node子进程 | 通过 Python 调起 Node 执行 JS 文件 |
playwright/selenium | 浏览器自动化,协助分析动态参数 |
需要注意的是,pyexecjs依赖本地的 JS 运行环境。在 Windows 上默认使用 JScript,执行 ES6 语法可能会报错,更建议在系统安装 Node.js,然后在pyexecjs中配置使用 Node。
2.3 抓包工具
- Chrome DevTools:最常用,也是新手优先掌握的。
- Charles / Fiddler:用于 Android 模拟器、真机 App 抓包,做 App 逆向时再用。
本文主要用 Chrome DevTools,因为 JS 逆向的核心场景是 Web 端动态参数分析。对 App 里的 JS 逆向,抓包工具和脱壳工具又是另一套体系,这里先不展开。
3. JS 逆向的第一课:学会在 DevTools 里定位动态参数生成位置
这一节是整个 JS 逆向的核心基本功。很多人卡在“不知道从哪里下手”,本质上是 DevTools 的使用方法没掌握好。下面我把定位流程拆解成四步。
3.1 第一步:Network 面板找请求与参数
打开 Chrome 访问目标网站,按F12打开开发者工具,切换到Network面板。刷新页面,在请求列表中点击你要分析的数据请求,例如一个 JSON 数据接口。
在请求详情里关注两个地方:
- Headers 下的 Query String Parameters或Payload。
- Initiator列,它表示该请求是由哪个 JS 文件发起的。
例如,你在 Payload 里看到一个参数叫sign,值是一串 32 位的十六进制字符串,那基本可以断定它的生成算法大概率是 MD5 或某种自定义拼接后散列。此时,点击该请求,右键选择Copy -> Copy as cURL,可以快速拿到完整的请求信息,方便后续模拟。
3.2 第二步:通过全局搜索定位参数名
在 DevTools 的Sources面板中,按Ctrl + Shift + F进行全局搜索,输入动态参数名,比如sign、token、buvid,回车后浏览器会在所有加载的 JS 文件中查找包含该关键词的代码。
这里有一个关键技巧:搜索时不要只搜参数名,也要搜参数赋值前后的关联字符串。举个例,如果请求参数是{"data": "xxx", "sign": "yyy"},那在 JS 源码中大概率会出现sign:或sign =或"sign"这样的片段。搜索时尽量用带引号的完整写法,减少匹配到无关内容的情况。
3.3 第三步:XHR 断点回溯调用栈
如果全局搜索找到很多匹配文件,看不出哪个是真正的生成位置,可以改用XHR/fetch Breakpoints断点。
在Sources面板右侧,找到XHR/fetch Breakpoints,点击加号,输入接口 URL 中包含的关键词,例如api/list。然后刷新页面,浏览器会在发请求之前中断执行。此时点击右侧Call Stack面板,从上往下查看调用栈,重点关注(anonymous)或某个函数名中出现的 JS 文件。
调用栈会把请求发起前的所有 JS 执行步骤列出来,你只需要往调用栈下面找,通常就能找到参数被拼接的那个函数。
3.4 第四步:在函数调用处设置断点
找到可疑函数后,点击对应 JS 文件,在函数内部行号处单击设置断点。再次触发请求,代码执行到这里就会暂停。此时:
- 把鼠标悬停在变量名上,可以查看变量的当前值。
- 在Console面板里输入变量名,回车可以看到值。
- 使用
F10下一步,F11进入函数内部。
通过这种方式,你可以逐行观察sign参数是怎么被计算出来的。这是 JS 逆向最核心的调试手段。
下面用一个简化示例来展示这个流程。
假设某个接口的 JS 代码经过格式化后长这样:
// 文件路径:/assets/js/chunk-xxxx.js function getSign(params) { var sorted = Object.keys(params).sort(); var str = ""; for (var i = 0; i < sorted.length; i++) { str += sorted[i] + "=" + params[sorted[i]] + "&"; } str += "key=9a5f2c1e"; return md5(str); }你在断点处停留时,可以在 Console 中执行:
// 查看当前传入的参数 JSON.stringify(params) // 查看拼接后的字符串 str // 查看最终生成的签名 md5(str)通过这种代入式调试,很快就能还原整个动态参数的生成流程。
4. 完整实战案例:通过浏览器环境模拟生成动态参数
下面用一个通用场景来演示动态参数生成的完整链路。我们模拟的是一个需要timestamp和sign两个动态参数的接口,sign的生成规则是:把参数按字典序拼接后,加盐做 MD5。
注意:以下为教学演示示例,目标地址是虚构的,只用于演示 JS 逆向的流程。实际项目请以你遇到的站点为主,并确保操作合法合规。
4.1 创建项目结构
先在本地创建一个项目文件夹:
js_reverse_demo/ ├── main.py # Python 爬虫主程序 ├── sign.js # 从页面提取并整理后的签名生成 JS └── requirements.txt # 依赖清单4.2 整理页面中的 JS 算法
假设我们从网站的 JS 文件中还原出了下面的签名算法:
// 文件路径:sign.js function generateSign(params) { // 1. 过滤空值 var filtered = {}; for (var key in params) { if (params[key] !== undefined && params[key] !== "") { filtered[key] = params[key]; } } // 2. 按键名升序排序 var keys = Object.keys(filtered).sort(); var arr = []; for (var i = 0; i < keys.length; i++) { arr.push(keys[i] + "=" + filtered[keys[i]]); } // 3. 拼接字符串并加盐 var baseStr = arr.join("&") + "&secret=a1b2c3d4e5f6"; // 4. MD5 哈希,输出 32 位小写字符串 return md5(baseStr); }如果你是在浏览器里调试,还有一个更快的方法:在 Console 面板直接执行页面加载的generateSign函数,验证它对某个输入输出的结果是否符合预期。
4.3 在 Node.js 中补齐 MD5 工具函数
上面的md5函数不是原生 JS 内置的,如果要在 Node 中跑,还需要引入一个 MD5 实现。实际开发中,你可以把网站的 MD5 函数直接抠出来,也可以自己安装js-md5包:
npm init -y npm install js-md5然后在sign.js里补充引入:
// 文件路径:sign.js const md5 = require('js-md5'); // ... generateSign 函数定义不变 ... // 导出函数,方便外部调用 module.exports = { generateSign };4.4 使用 Python 调用 JS 生成签名
现在来到 Python 侧。最直接的方式是通过subprocess调起 Node 执行 JS 文件,把结果打印出来再读取。这种方式的好处是 JS 执行环境与浏览器一致,兼容性最好。
# 文件路径:main.py import json import subprocess import requests def generate_sign_by_node(params: dict) -> str: """ 通过 Node.js 执行 sign.js 中的 generateSign 函数 """ js_code = f""" const {{ generateSign }} = require('./sign.js'); const params = {json.dumps(params, ensure_ascii=False)}; console.log(generateSign(params)); """ result = subprocess.run( ["node", "-e", js_code], capture_output=True, text=True, encoding="utf-8" ) if result.returncode != 0: raise RuntimeError(f"Node 执行失败: {result.stderr}") return result.stdout.strip()然后在主流程里组装请求参数:
# 文件路径:main.py(接上面的代码) def fetch_data(): # 准备基础参数 params = { "keyword": "python爬虫", "page": 1, "pageSize": 20, "timestamp": int(__import__("time").time()), } # 生成动态签名 sign = generate_sign_by_node(params) params["sign"] = sign # 发起请求 headers = { "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", "Content-Type": "application/json", "Referer": "https://example.com/search", } resp = requests.post("https://api.example.com/search", json=params, headers=headers, timeout=10) print(resp.status_code) print(resp.json()) if __name__ == "__main__": fetch_data()4.5 运行与验证
先测试 JS 函数本身是否能正确输出:
node -e "const { generateSign } = require('./sign.js'); console.log(generateSign({page: 1, keyword: 'test'}))"如果 Node 环境没问题,它会输出一个 32 位的 MD5 字符串。接着运行 Python 脚本:
python main.py如果签名生成正确,服务器会正常返回 JSON 数据;如果签名错误,服务器通常会返回sign error、invalid request之类的提示。
这个例子展现的是一种通用思路:把网站 JS 里的算法提取出来,在本地用 Node 或 Python 重新执行,从而在每一次请求中动态生成合法参数。
5. 另一种思路:用浏览器自动化直接拿到动态参数
有时候,某些 JS 逆向难度极高,代码经过了重度混淆、控制流平坦化、正则替换、虚拟机保护,短时间无法还原算法。这种情况下,可以考虑用浏览器自动化工具来“代执行” JS。
5.1 Playwright 示例
以 Playwright 为例,它的page.evaluate方法可以把任意 JS 代码放到页面上下文中执行,从而复用页面加载过的全部 JS 函数。
# 文件路径:browser_demo.py from playwright.sync_api import sync_playwright def get_sign_from_browser(keyword: str) -> str: with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com/search", wait_until="networkidle") # 在页面上下文中执行 JS,调用页面已有的函数 sign = page.evaluate( """(keyword) => { // 模拟页面内部生成签名的逻辑 const params = { keyword: keyword, page: 1 }; return window.generateSign ? window.generateSign(params) : "fallback"; }""", keyword ) browser.close() return sign if __name__ == "__main__": print(get_sign_from_browser("python爬虫"))这种方案的核心优势是:不需要完全理解 JS 算法。缺点是执行速度慢,需要启动浏览器,且会增加资源消耗。在实际工程中,一般用于算法复杂度极高、短期无法还原的情况,或者用于验证自己还原出的算法是否正确。
5.2 何时选择浏览器自动化,何时选择本地执行 JS?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| JS 代码轻度混淆,能定位到算法 | 本地执行 JS(Node) | 速度快,适合大规模请求 |
| JS 代码重度混淆,算法还原成本高 | 浏览器自动化 | 快速上线,减少人力投入 |
| 需要验证自己的算法是否正确 | 浏览器自动化对照 | 对比输出结果是否一致 |
| 生产环境对请求速度要求高 | 本地执行 JS | 避免启动浏览器,性能更优 |
6. 从实战角度拆解:JS 逆向中常见的动态参数类型与应对思路
上面完成了整体流程演示。下面再从实战案例角度,把动态参数按照“生成方式”做一次分类总结。这会帮助你在面对陌生网站时,更快判断应该从哪个方向入手。
6.1 时间戳类参数
特征:参数名通常是timestamp、_t、time、ts,值是一串 10 位或 13 位数字。
生成方式:
// 10位秒级时间戳 Math.round(new Date().getTime() / 1000) // 13位毫秒级时间戳 Date.now()应对方案:直接在 Python 里生成,不需要读 JS。
import time timestamp = int(time.time()) # 秒级 timestamp_ms = int(time.time() * 1000) # 毫秒级6.2 拼接散列类参数
特征:参数值通常是 32 位或 64 位的十六进制字符串,算法的关键词带有md5、sha1、sha256、hash、digest等。
生成方式:将请求参数、时间戳、固定密钥按某种规则拼接后散列。
应对思路:
- 在 JS 文件中搜索
md5(、sha1(、CryptoJS、js-md5等关键词。 - 找到传入函数的参数,逐步向上回溯拼接逻辑。
- 在本地用 Python 的
hashlib或 Node 的crypto复现。
例如 JavaScript 里是:
md5("keyword=python&page=1&secret=abc123")Python 里就可以写:
import hashlib def make_sign(keyword: str, page: int, secret: str) -> str: raw_str = f"keyword={keyword}&page={page}&secret={secret}" return hashlib.md5(raw_str.encode("utf-8")).hexdigest()这类参数是最常见的入门练手目标,建议优先掌握。
6.3 Cookie 动态生成类参数
特征:每次刷新页面,Cookie 中某个字段的值都会变化,例如_signature、acw_sc__v2、buvid3等。
生成思路:这类 Cookie 通常是在页面加载早期的 JS 中生成,有时是服务端返回一段 JS 代码,浏览器执行后设置 Cookie。
应对方案:
- 先用抓包工具确认 Cookie 是哪个响应头或哪个 JS 设置的。
- 在 JS 中搜索 Cookie 名,定位生成函数。
- 如果 JS 代码需要浏览器环境(例如
document.cookie),可以考虑在 Python 中借助execjs补环境,或者用浏览器自动化处理。
这里提醒一句:Cookie 类动态参数往往与风控系统绑定更深,技术难度比普通 sign 参数更高,需要掌握补环境技能。
6.4 请求头动态参数
特征:Headers中某个字段,例如Authorization、X-Sign、X-Token,值是一长串加密字符串。
应对思路:定位方式与普通参数一致,只是在Network面板中要看Request Headers,搜索关键词时在 Sources 面板中搜 Header 名称即可。
6.5 滑块验证与行为参数
特征:请求参数中包含captcha、verify、risk、trace等字段,值可能是加密的行为轨迹数据。
应对思路:这类参数涉及浏览器环境检测、鼠标轨迹采集、Canvas 指纹等,已经超出了入门范畴。新手不建议一上来就碰,先把基础的 sign 参数搞清楚,再逐步接触滑块逆向。
7. JS 逆向调试的实用技巧与常见问题排查
7.1 实用调试技巧
技巧一:格式化压缩后的 JS 文件
大部分网站的 JS 文件都是压缩过的,函数名可能是单个字母,可读性极差。在 Sources 面板左下角点击Pretty-print(花括号图标{}),代码会自动格式化。格式化后配合全局搜索,定位效率会提高很多。
技巧二:利用 Console 在断点处执行代码
断点暂停时,Console 依然可以执行代码,而且可以访问当前作用域内的变量。这是逆向过程中最常用的手段。你不需要把所有逻辑一次性看懂,只需要在 Console 里不断执行变量名,观察每个中间变量的值,通常很快就能拼凑出完整逻辑。
技巧三:Hook 函数定位
有时候 JS 里对某个 API 的封装层很多,很难直接找到调用的位置。这时可以在 Console 中提前改写关键函数,插桩打印调用信息。
例如某个参数使用了JSON.stringify,你可以先执行:
(function() { var originalStringify = JSON.stringify; JSON.stringify = function() { console.log('JSON.stringify called with:', arguments); return originalStringify.apply(this, arguments); }; })();然后在页面里正常触发请求,Console 就会打印出所有JSON.stringify的调用参数。这种方法叫做Hook,在 JS 逆向中非常实用。
技巧四:使用 Override 保存修改后的 JS
如果需要对 JS 文件做修改(例如把某个函数的返回值直接改为固定值),可以在 Sources 面板中右键文件,选择Override content,结合本地文件夹实现文件替换。这样每次刷新页面都会加载修改后的 JS,方便验证算法。
7.2 常见问题排查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
本地执行 JS 报window is not defined | JS 依赖浏览器环境 | 在代码中补充window、document等环境变量,或用浏览器自动化替代 |
pyexecjs执行 ES6 报语法错误 | 默认执行环境不支持 ES6 | 安装 Node.js 并让pyexecjs使用 Node 环境 |
| 生成的签名与浏览器不一致 | 拼接顺序、编码格式、大小写不一致 | 在 Node 中打印每一段中间结果,与浏览器 Console 对比 |
| 请求返回 403 或风控提示 | 请求头缺失、IP 被标记、频率过高 | 模拟完整请求头,降低请求频率,使用代理池 |
| 全局搜索参数名找不到任何结果 | 参数名被混淆加密 | 搜索参数名的一部分、搜索请求 URL 关键字,或改用 XHR 断点回溯调用栈 |
JS 算法里包含atob、Base64嵌套编码 | 多层编码混淆 | 从最内层开始解码,先还原内层明文,再分析外层逻辑 |
7.3 新手最容易犯的三个错误
错误一:拿到一个参数就去搜“Python 实现”,而不是先分析 JS。
很多新手看到sign是 MD5,就立刻在 Python 里写 MD5,但忽略了拼接规则中的顺序、分隔符、盐值。只有先把 JS 里的完整拼接逻辑搞清楚,Python 才能正确模拟。
错误二:忽略了字符集和编码问题。
中文参数在 JS 中可能经过encodeURIComponent,在 Python 中则需要使用urllib.parse.quote处理。直接拼接中文,生成的签名往往不一致。
错误三:只关注参数值,不关注请求头。
很多网站的签名校验不只校验参数内容,还会校验User-Agent、Referer、Origin等请求头。即使你签名生成了,请求头不完整,依然拿不到数据。所以模拟请求时要尽量把浏览器里的请求头带全。
8. JS 逆向入门学习路线与工程建议
8.1 推荐学习顺序
如果完全零基础,建议按照下面的路线推进:
- 掌握浏览器开发者工具:Network、Sources、Console、Application 四个面板的常用功能。
- 掌握 JavaScript 基础语法:变量、函数、对象、数组、字符串处理、回调函数。
- 掌握常见加密算法识别:MD5、SHA、AES、RSA、Base64,能通过特征判断是哪种算法。
- 练习简单站点逆向:找一两个技术博客或开源网站练手,完成“定位参数 -> 还原算法 -> Python 模拟”的完整闭环。
- 学习补环境与 Hook 技术:当 JS 依赖浏览器环境时,学会在本地模拟
window、document、navigator等对象。 - 学习浏览器自动化兜底方案:Playwright / Selenium 常备,用于验证或兜底。
8.2 工程化实践建议
在实际项目里,JS 逆向不应该是一个“一次性脚本”,而是需要做成可持续维护的模块。建议从第一天就注意几点。
第一,把 JS 算法与爬虫逻辑分离。
不要在主爬虫文件里写一大段加密逻辑。把sign.js这类文件独立存放,Python 侧只负责调用,这样算法更新时,只需要修改 JS 文件,不需要改动整个爬虫主程序。
第二,增加参数校验日志。
在生成签名后,建议先把自己的参数和浏览器中的参数做一次对比。可以在代码里加一个 debug 开关,打印每一步的中间值,方便快速定位问题。
第三,控制请求频率,预留重试机制。
即使动态参数生成正确,请求频率过高也会触发风控。建议在爬虫中加入随机延迟、请求重试、异常报警机制。
第四,遵守 robots 协议与网站条款。
爬虫技术本身是中性的,但使用方式必须合规。用于学习、测试、公开数据采集没有问题,但不要对未授权接口发起高频请求,不要绕过付费墙、验证码、登录授权获取非公开数据。尤其是涉及用户隐私、付费内容、版权内容时,要特别谨慎。
第五,注意 JS 逆向的维护成本。
网站的 JS 算法会随时更新,今天还原的签名算法可能过几天就变了。因此,JS 逆向不是一劳永逸的方案,需要建立持续监控机制,例如定时检查页面 JS 文件是否变更。
9. 小结与动手实践建议
本文从动态参数的概念讲起,介绍了动态参数产生的背景,梳理了使用 Chrome DevTools 定位 JS 动态参数的完整流程,并通过一个可运行的实战案例,演示了“提取 JS 算法 -> 本地 Node 执行 -> Python 模拟请求 -> 获取数据”的完整链路。同时,还介绍了几类常见的动态参数类型、应对思路、调试技巧和工程建议。
对于刚开始接触 JS 逆向的读者,下一步可以这样练:
- 找一台自己常用的网站,打开 DevTools,定位一个带签名参数的接口,尝试还原它的签名算法。
- 把本文的实战案例代码跑通,理解
subprocess调 Node 和playwright调页面的两套思路。 - 尝试在不使用浏览器自动化的情况下,用 Python 或 Node 生成动态参数,再对比浏览器中的真实请求是否一致。
JS 逆向是一条需要耐心积累的路,遇到难啃的混淆代码,不要急着放弃。先从小参数入手,一步一步记录中间变量,随着经验增加,你对动态参数的直觉会越来越准。如果本文对你有帮助,可以收藏备用,后续我会继续写 JS 逆向中更进阶的内容,比如补环境、AST 混淆还原、验证码处理等方向。