在爬虫开发里有一种非常常见的卡壳场景你用 Python 的 requests 写了十几行代码请求头也带了Cookie 也带了页面却返回一个 401 或者一串“签名错误”你打开浏览器开发者工具发现同样的 URL 明明白白返回了完整 JSON 数据。对比之后你会发现浏览器请求里多了一个参数而且这个参数每次请求都不一样。这个“每次都不一样”的参数就是很多人嘴里说的动态参数。它看起来玄乎背后的本质却很简单浏览器在发起请求之前执行了一段 JavaScript用当前时间戳、随机数、固定密钥等原料算出了一个值塞进请求里。爬虫新手真正缺的不是 Python 代码而是“怎么在浏览器里定位这段 JS并且把它的逻辑在 Python 里复现出来”的方法。这篇文章不打算讲多么高深的混淆破解只讲一套可以反复复用的入门流程抓包定位、全局搜索、断点调试、还原逻辑。读完你能跑通一个完整的动态参数逆向小案例也知道真实项目中遇到这类参数时应该从哪里下手。1. 为什么你的爬虫会卡在“动态参数”上先还原一个非常典型的开发场景。假设你要抓某个网站的文章列表在浏览器里打开页面F12 的 Network 面板里能看到一个list?page1的 XHR 请求点开看响应结构化数据整整齐齐。于是你打开 PyCharm用 requests 模拟同样的请求只带了User-Agent和page1结果服务器返回的不是数据而是一个很含糊的拦截页面。这时候大多数人会犯一个方向性错误反复补请求头。实际上浏览器和你的脚本之间差的不只是头信息可能还有一个sign参数、一段cookie值或者一个隐藏在 URL 里的token。这些参数直接决定服务器是否认为“你是真人”。更让人头疼的是这类参数通常满足三个特征每次请求都会变不能直接复制粘贴一条写死。即使是同一个参数换一个时间、换一个请求路径值就完全不同。在页面源代码里很难直接搜到因为它是运行过程中由 JavaScript 生成的。这就是动态参数的基本盘。我的核心判断是如果你能回答“浏览器里的这个值是怎么算出来的”你就已经完成一次最基本的 JS 逆向。所谓逆向并不是要你把整份压缩混淆的 JS 全部读懂而是找到生成目标参数的函数理解它的输入和输出然后在你自己的环境里重新算一遍。这篇文章的安全边界也需要提前说清楚本文只演示本地自建的公开演示接口目标是理解公开网页中参数生成的通用思路不涉及破解登录态、绕过任何商业防护、爬取用户隐私数据。你在真实项目中分析任何一个平台之前也要先确认对方服务条款是否允许自动化访问并控制好请求频率。2. 动态参数是什么先分清原料、规则和签名“动态参数”这个词在爬虫圈里被用滥了很多人把时间戳、随机数、加密串混为一谈。为了后面实操不糊涂这里先把概念拆开。从请求形式上看动态参数一般出现在四个位置位置典型例子说明URL 查询串/api/list?page1signabc最常见的动态参数直接拼在 URL 后面请求头 HeadersX-Timestamp: 1699999999常见的时间戳、客户端指纹、会话标识Cookiebuvid3xxxx; sessionyyyy由前端 JS 写入后续请求携带请求体 Body{data: 加密串}常见于 POST 接口内容被整体加密从生成逻辑上看可以把它们分成三类类型特点反爬思路静态参数永远不变写死在代码里直接复制到脚本里动态原料每次变化但本身只是时间戳、随机数、自增 ID在 Python 里生成即可签名参数由动态原料和固定规则计算得到必须找到加密算法并复现这里最关键的认知是动态原料和签名参数不是一个东西。时间戳只是原料客户端用时间戳加上固定密钥经过 SHA256、MD5、AES 或其他算法得到的那一串乱码才是签名。很多教程说“动态参数就是时间戳”其实只讲对了一半。打个比方。你去餐厅排队门口会发一张号码票。号码票上的“取号时间”就是动态原料而票面上的防伪码是门店用“取号时间 门店密钥”算出来的签名。服务员不关心你怎么算的只看防伪码对不对、时间有没有过期。网站服务端验证动态参数和这个逻辑几乎一模一样。所以面对任何一个动态参数先问四个问题这个参数出现在哪个位置它的每一次取值都一样吗它是由什么“原料”计算出来的它的“计算规则”在哪一段 JS 里带着这四个问题去做抓包效率会高很多。3. 服务端是怎么校验动态参数的想理解动态参数为什么能“挡住”爬虫还得理解服务端的校验流程。大多数带签名参数的接口校验逻辑可以简化成下面这张流程客户端收集原料比如ts当前时间戳、path请求路径、secret前端隐藏的固定密钥。客户端按照一套规则把原料拼接起来算出一个摘要值sign然后把ts和sign一起发给服务端。服务端拿到ts后用同一个密钥、同一套规则重新计算一遍sign。如果服务端算出来的值和请求里的sign一致并且当前时间在ts的合法时间窗口内校验通过否则拒绝请求。这个流程能成立核心前提是“这个网站的服务端也在使用相同的密钥和算法”。也就是说动态参数并不是不可解密的它更像是客户端和服务端之间约定了一个共同的“暗号”。我们做 JS 逆向本质上就是把这个“暗号”的生成规则找出来然后用 Python 或 Node 重新实现。不同网站的校验强度差别很大有的只校验时间戳是否过期不校验密钥那你只需要生成一个有效时间戳就行。有的签名参数和请求路径绑定路径一变签名就失效所以要按不同接口分别生成。有的还会加入一次性 Nonce防重放同一个参数值第二次使用就失效。更复杂的会把指纹、加密算法、混淆后的 JS 都掺和进来但这属于进阶内容。入门阶段不用想得太复杂。你只需要记住一个结论只要你能构造出和浏览器一致的参数值服务端就无法区分这个请求来自浏览器还是来自你的 Python 脚本。这就是所有请求参数逆向的通用目标。4. 入门环境准备在开始实战之前先准备好一套最小环境。不用贪多初学者把这几个装齐就够了。4.1 Python 环境Python 3.8 及以上版本都可以核心依赖只有一个requests。安装命令pip install requests如果你还装了PyExecJS或execjs后面用 Node 执行前端 JS 会更方便但并不是必须的。第一遍跑流程时先用 Python 的hashlib手动复现算法最直观。4.2 Node.js之所以推荐 Node.js是因为真实网站里的算法千奇百怪你很难在 Python 里一个不漏地重写。更稳妥的方案是直接把浏览器的加密函数抽出来放到 Node.js 里执行生成参数后交给 Python 发请求。安装 Node.js 后可以在终端确认版本node -v npm -v4.3 浏览器开发者工具Chrome 或 Edge 都行。后面的抓包、搜索、打断点全部在 F12 开发者工具里完成。你需要重点熟悉三个面板面板作用Network查看所有请求的 URL、参数、请求头、响应Sources查看页面加载的 JS 文件打断点调试Console执行临时 JS 代码验证函数输入输出这里不需要额外安装任何插件浏览器自带功能完全够用。准备完环境之后下一步就是进入核心方法论。请记住这套四步法适用于绝大多数“参数在 URL 或 Header 里”的动态参数场景不管你面对的是列表接口、详情接口还是带反爬的搜索接口。5. 核心流程拆解定位、搜索、调试、还原很多人学 JS 逆向最大的障碍是打开浏览器开发者工具之后不知道先点哪里。这里给出一套固定顺序照着做就行。5.1 抓包定位打开开发者工具切到 Network 面板勾选 XHR 或 Fetch 过滤条件然后正常触发一次页面请求。这样能把页面里真正返回数据的接口过滤出来而不是被图片、CSS 等静态资源干扰。找到目标接口后依次看三块内容Headers 里的 Query String Parameters确认哪些参数是动态的。响应内容确认这个接口返回的数据是不是你要的。Initiator 标签查看这个请求是由哪一段 JS 发起的。第一步不写代码只做一件很朴素的事把请求参数抄下来。如果你发现某个参数明显是一段 32 位十六进制字符串或者 16 位随机串那它大概率就是一个签名参数需要重点追踪。5.2 全局搜索拿到参数名之后在 Sources 面板里按CtrlShiftF打开全局搜索输入这个参数名。比如参数叫sign就搜索sign:、sign 、sign尽量多搜几个变体。搜索结果可能有两类直接找到生成函数比如sign hex_md5(ts secret)。找到一堆无关匹配因为sign太常见了这时候需要缩小范围把参数名和接口路径拼接起来搜索比如/api/list或者搜索你熟悉的加密函数关键词md5、sha256、encrypt。如果仍然找不到还有一个更稳的办法回到 Network 面板在发起请求那一行点击鼠标右键选择 “Show function definition” 或者查看 Initiator 里的调用链直接跳到发请求的 JS 代码行。在那里打断点之后往上找变量sign是在哪里赋值的。5.3 打断点观察找到可疑代码之后在赋值语句那一行打上断点。刷新页面浏览器执行到该行时会暂停这时候切换到 Scope 面板能看到当前作用域里的所有变量值。这里要注意观察三样东西加密函数接收了什么参数。常见原料有ts、path、token、某个固定字符串。加密函数返回了什么值。把返回值和你抓包里的sign对比一致就说明定位正确。调用栈里上一层函数是谁。如果这一层只是包装再往上找一层就能看到更完整的入参。很多新手卡在“找到了但不确定是不是它”。验证方法很简单在 Console 里手动调用这个函数传入当前时间戳看结果和 Network 请求里的参数是否一致。如果一致就是它。5.4 还原与验证定位到算法之后接下来有两种还原路径还原方式适用场景优点缺点Python 手动重写算法算法简单比如 MD5、SHA256、Base64 组合请求速度快无需额外依赖算法复杂时工作量巨大Node.js 执行原 JS算法复杂依赖浏览器环境几乎 1:1 还原每次请求要启动 Node 进程速度较慢入门阶段建议两条路都走一遍先用 Python 手动重写一个简单案例再用 Node 执行原 JS跑通同一个接口。这样既理解了算法原理也掌握了后续应对复杂场景的通用手段。到这里四步法已经讲完。下面用一个本地演示项目把这些步骤完整走一遍。6. 入门实战本地跑一个带签名的接口为了让流程可控且可复现我在本地写了一个最小演示接口。它模拟了真实网站最常见的逻辑前端生成ts和token服务端用同一个密钥重新计算并校验。你不需要访问任何外部平台也不用担心触发线上风控。6.1 项目结构js-reverse-demo/ ├── server.py # 本地演示接口 ├── index.html # 返回给浏览器的页面内含前端 JS ├── fetch_fail.py # 第一次请求不带签名失败 ├── fetch_python.py # 第二次请求用 Python 重写签名算法 ├── token.js # 用 Node 执行原 JS 逻辑 └── fetch_node.py # 第三次请求调用 Node 生成签名6.2 服务端校验逻辑创建server.py复制以下代码# 文件路径js-reverse-demo/server.py from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs import hashlib import time import json SECRET csdn-demo-secret def make_token(ts: str, path: str) - str: raw f{ts}|{SECRET}|{path} return hashlib.sha256(raw.encode(utf-8)).hexdigest()[:16] class Handler(BaseHTTPRequestHandler): def do_GET(self): url urlparse(self.path) if url.path /: with open(index.html, encodingutf-8) as f: html f.read() self.send_response(200) self.send_header(Content-Type, text/html; charsetutf-8) self.end_headers() self.wfile.write(html.encode(utf-8)) return if url.path /api/data: params parse_qs(url.query) ts params.get(ts, [])[0] token params.get(token, [])[0] if not ts or not token: self._send_json({code: 401, msg: missing token}) return expect make_token(ts, /api/data) try: ts_int int(ts) except ValueError: self._send_json({code: 403, msg: token invalid}) return if token expect and time.time() - ts_int 60: self._send_json({code: 0, data: {msg: hello csdn, ts: ts}}) else: self._send_json({code: 403, msg: token invalid or expired}) return self._send_json({code: 404, msg: not found}) def _send_json(self, obj): data json.dumps(obj).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.end_headers() self.wfile.write(data) if __name__ __main__: server HTTPServer((127.0.0.1, 8000), Handler) print(server on http://127.0.0.1:8000) server.serve_forever()这段代码的关键点在make_token函数它把ts、SECRET、path三个字段用|拼接再做sha256摘要并取前 16 位。这就是我们模拟的“服务端签名规则”。真实网站里的算法可能更长但验证思路完全一样。6.3 前端 JS 生成动态参数创建index.html前端页面会通过fetch请求/api/data并在请求里带上动态生成的ts和token!-- 文件路径js-reverse-demo/index.html -- !DOCTYPE html html head meta charsetutf-8 / title动态参数演示/title /head body h2demo: fetch /api/data/h2 script srchttps://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js/script script function makeToken() { var ts String(Math.floor(Date.now() / 1000)); var secret csdn-demo-secret; var path /api/data; var raw ts | secret | path; var token CryptoJS.SHA256(raw).toString().substring(0, 16); return { ts: ts, token: token }; } function loadData() { var param makeToken(); fetch(/api/data?ts param.ts token param.token) .then(function (res) { return res.json(); }) .then(function (data) { document.body.appendChild(document.createTextNode(JSON.stringify(data))); }); } loadData(); /script /body /html注意看makeToken函数它和server.py里的make_token逻辑是一模一样的。浏览器打开首页时这段 JS 会算出当前时间戳ts再把ts、secret、path拼起来做 SHA256取前 16 位然后发请求。这正好模拟了真实网站最典型的动态参数生成方式前端有一个函数服务端有一个校验函数二者共享同一套规则。6.4 启动服务并验证先启动本地服务python server.py浏览器打开http://127.0.0.1:8000你会在 Network 面板里看到/api/data请求ts 是一串时间戳token 是一段 16 位十六进制字符串。然后创建一个不带签名的请求脚本fetch_fail.py# 文件路径js-reverse-demo/fetch_fail.py import requests resp requests.get(http://127.0.0.1:8000/api/data) print(resp.json())运行python fetch_fail.py预期输出{code: 401, msg: missing token}这一步说明不带动态参数直接请求接口会被服务端拒绝。这就模拟了爬虫新手常见的问题明明浏览器能打开用 Python 请求却被拦住。6.5 用 Python 复现签名参数接下来在 Python 里复现前端的makeToken逻辑。创建fetch_python.py# 文件路径js-reverse-demo/fetch_python.py import time import hashlib import requests SECRET csdn-demo-secret PATH /api/data ts str(int(time.time())) raw f{ts}|{SECRET}|{PATH} token hashlib.sha256(raw.encode(utf-8)).hexdigest()[:16] resp requests.get( http://127.0.0.1:8000/api/data, params{ts: ts, token: token}, ) print(resp.json())运行python fetch_python.py预期输出{code: 0, data: {msg: hello csdn, ts: 1710000000}}到这里你已经完成了一次最基础的动态参数还原。整个过程中最关键的一步不是在 Python 里写 SHA256而是你能从浏览器代码里看出raw的拼接规则。真实项目里这份拼接规则可能藏在混淆后的代码中需要花更多时间定位。6.6 用 Node.js 执行前端 JS 生成参数如果算法的复杂度很高手动在 Python 里重写会很吃力。这时推荐直接用 Node.js 执行原 JS。先把前端的makeToken逻辑抽出来创建token.js// 文件路径js-reverse-demo/token.js const crypto require(crypto); const SECRET csdn-demo-secret; const PATH /api/data; function makeToken() { const ts String(Math.floor(Date.now() / 1000)); const raw ts | SECRET | PATH; const token crypto.createHash(sha256).update(raw).digest(hex).substring(0, 16); return { ts, token }; } console.log(JSON.stringify(makeToken()));然后在 Python 里调用它# 文件路径js-reverse-demo/fetch_node.py import json import subprocess import requests out subprocess.check_output([node, token.js]) param json.loads(out) resp requests.get(http://127.0.0.1:8000/api/data, paramsparam) print(resp.json())运行python fetch_node.py预期输出同样是{code: 0, data: {msg: hello csdn, ts: 1710000000}}这种方式看起来比 Python 手动重写“笨”一点因为它每请求一次就要启动一个 Node 进程。但它的优势非常明显前端 JS 怎么算Node 里就怎么算省去了手动翻译算法的过程和可能出现的翻译误差。初学者先用 subprocess 跑通流程完全没问题。等后续接触更多真实项目再考虑用常驻的 Node 服务或 JS 运行时方案来提升性能。7. 常见问题与排查思路入门阶段不可避免会踩坑。下面这张表格列出的都是真实项目中很容易遇到的现象建议收藏备用。问题现象可能原因排查方式解决方案参数名在 Sources 里搜不到参数名由后端接口返回或代码里被打散拼接切换 Network 面板的 Initiator 查看调用栈搜索接口路径从发起请求的代码段向上逐层找赋值逻辑参数出现在 Cookie 中而不是 URL 里Cookie 由页面加载时的 JS 写入在 Console 里搜索 Cookie 名查看 Set-Cookie 来源在页面初始化 JS 里找写 Cookie 的赋值语句全局搜索时匹配结果太多参数名太常见比如 sign、data换用更具体的赋值上下文搜索比如sign 、sign:组合关键词或者直接搜加密函数名断点触发后页面行为异常页面内置了反调试逻辑比如循环 debugger观察暂停位置是否反复跳转阅读逻辑即可不建议写工具绕过风控本地执行原 JS 得到的结果与浏览器不一致Node 环境缺少浏览器特有对象如 window、document逐步打印日志对比入参差异在 JS 里 mock 掉缺失对象最小化补环境同一个参数脚本早上能跑下午失效算法或密钥升级重新抓包从入口再次定位将参数生成逻辑模块化建立回归用例Node 执行 JS 速度太慢每个请求都启动一次新进程使用 time 统计请求耗时真实项目里可用常驻 Node 服务或直接移植为 Python 计算补充一个最容易忽略的细节很多初学者在网上找到一段“能跑的 JS”复制到本地后却得到和浏览器不一致的结果。原因往往不是代码问题而是这段 JS 依赖的外部变量没有补齐比如它内部读取了某个全局变量、当前时间、navigator.userAgent或者一个由其他 JS 提前写入的 Cookie 值。遇到这种问题不要急着怀疑算法复制错了先对比入参。8. 工程化与合规建议跑通一个演示案例只是起点。如果你正在维护一个真实项目的爬虫或自动化流程下面这些工程化建议能帮你减少后续维护成本。8.1 把“参数生成”和“请求逻辑”解耦不要在一个 requests 脚本里既写页面解析、又写签名算法、又管数据库存储。更合理的做法是把参数生成抽成独立模块输入是 ts、path、其他原料输出是完整的参数字典。这样算法一旦更新你只需要改一个文件而不是在一大堆代码里查找替换。# 推荐结构示例 # param_generator.py def generate_params(path: str) - dict: ts str(int(time.time())) token compute_token(ts, path) return {ts: ts, token: token}8.2 建立参数过期监控大多数签名参数都会绑定时间窗口。如果服务端校验的是“当前时间与 ts 的差值小于 60 秒”那你的脚本在发出请求前才生成参数而不是在启动时生成一次然后反复使用。一个常见错误是把 token 写进配置文件运行一小时后 token 已经过期导致请求全部失败。建议在每次请求前动态生成参数并记录请求时间、参数有效期两个字段出现大面积失败时可以快速定位是不是参数过期。8.3 控制请求频率避免影响线上服务逆向分析公开接口的技术本身是中性的但高频请求会对对方服务器造成压力也可能触发更严格的风控。合理做法是请求间隔加随机延迟而不是固定 sleep 一个值。设置退避策略连续失败后自动降频。只在必要时抓取数据不要做永久性全量爬取。优先查看对方是否有公开的官方 API能用官方 API 就不要逆向前端。8.4 合规红线要提前想清楚这里必须强调一个容易被忽略的原则你有权理解公开网页里的技术实现但不代表你有权对任何平台做不受控的自动化访问。涉及用户登录态、付费内容、验证码绕过、个人隐私数据的逆向不仅技术上难度更大法律风险也完全不同。建议只对你有权访问、且服务条款允许自动化的接口做技术分析并在本地测试环境中验证逻辑。8.5 日志里不要记录敏感字段调试时打日志可以但不要把签名密钥、完整 Cookie、用户 Token 原样打到日志文件里。因为日志往往会长期保存一旦日志泄露等于把请求的“通行证”一起交了出去。用脱敏方式记录即可比如只记录参数前几位和后几位。9. 总结与后续学习方向回到开头的问题为什么浏览器能拿到数据而你的 requests 却拿不到根本原因是请求里少了动态参数。这篇文章把入门最关键的方法论浓缩成了四步先抓包定位请求和动态参数再在 Sources 里全局搜索参数名或接口路径然后打断点观察函数输入输出确认算法上下文最后用 Python 或 Node 把算法复现出来让请求参数和浏览器一致。这个四步法适用于大多数 URL 参数、请求头参数、Cookie 参数。像一些平台里常见的buvid3类浏览器标识参数本质上也是前端脚本在本地生成 UUID、时间戳等信息后编码得到的结果。用同一套定位流程先看它被写进 Cookie 的位置再顺着赋值语句找到生成函数最后复现编码规则思路完全一致。下一步值得学习的方向有三个第一巩固 JavaScript 基础语法哪怕不会写复杂前端至少能读懂函数、对象、字符串拼接和常见加密库的调用。第二练习浏览器开发者工具的调试技巧特别是条件断点、调用栈分析、Console 里手动调用函数。第三了解常见混淆手段比如十六进制字符串、变量名乱码、控制流扁平化以及如何用 Node 环境执行混淆后的代码。这些内容都比“背一个加密算法”更通用也更值得投入时间。建议你先照着本文的本地案例跑一遍然后把四步法套到一个自己日常使用的公开页面上练习定位。如果在某个参数上卡住了优先检查定位环节是否找到了正确的函数入口而不是急着去研究算法本身。方向和操作顺序正确动态参数就不再是拦路虎。