
Python 爬虫入门容易写一个requests请求也不难但一旦目标页面或接口加入了动态签名、加密参数、Cookie 校验很多人就会卡住。这时候要解决的不再是 Python 本身而是前端 JavaScript 里那一小段“看不见的生成逻辑”也就是常说的 JS 逆向。所谓 JS 逆向并不是去破解什么高深算法而是通过抓包、断点、关键字搜索等调试手段定位一个参数是怎么生成的再用 Python 或 Node 把它等价还原出来。本文围绕这条主线走一遍从环境准备、基础爬虫、参数定位到 Python 调用 JS 完成联调的完整过程并给出一个可复现的本地案例、常见报错和合规边界。这类“全系列课程”的标题通常会把学习周期压缩成“七天从小白到大神”但实际技能成长依然要靠持续练习。对新手来说更有价值的不是收藏一套视频而是跟着一条可复现的路径亲手把一个带签名校验的接口从抓包分析做到 Python 联调成功。完成后你不仅知道sign参数怎么来的也知道下一次遇到同类问题时该从哪里下手。1. 先理解 Python 爬虫与 JS 逆向之间是什么关系1.1 爬虫的本质是模拟客户端请求无论是网页还是 App客户端要向服务器拿数据本质上都是发起一次 HTTP 请求。服务器返回 HTML、JSON、图片等内容爬虫把这些内容请求下来之后再解析、清洗、存储。所以一个最基础的爬虫只需要三件事构造正确的请求地址和参数携带服务器能识别的请求头或 Cookie解析返回内容提取目标数据。在早期很多网站没有反爬机制只要 URL 正确就能拿到数据。现在情况完全不同服务器很难确认请求是否真的来自自己的客户端于是会在请求参数上增加校验逻辑。常见做法是让前端 JS 动态生成一个签名、一个 token、一个加密后的 Cookie再随请求一起发给服务器。爬虫如果只复制了 URL却不知道这些动态参数如何生成就会得到 403、400 或一段无法解析的密文。1.2 反爬为什么会让参数变得复杂服务器无法只靠 URL 判断请求者身份所以需要一套“参数校验”机制。典型流程是页面加载时前端 JS 读取当前时间、随机数、设备信息或用户行为通过某种算法把这些信息拼成一个字符串对这个字符串做哈希、加密或自定义混淆把结果作为sign、token、cookie等参数附加到请求里服务器用同样的算法重新计算并比对不一致就拒绝。这就是为什么浏览器能打开页面而 Python 脚本请求同样地址却失败。问题不一定出在 IP 或频率而是请求里缺少了由 JS 动态生成的那部分参数。不理解这个过程时最常见的表现是现象直接原因请求返回 403缺少 Cookie、签名或请求头参数每次请求都不一样参数依赖时间戳或随机数返回结果是密文响应内容也被 JS 加密浏览器成功但脚本失败请求头或前置 JS 步骤未还原1.3 JS 逆向解决什么问题JS 逆向要回答三个问题这个动态参数在哪里生成它由哪些输入计算得到用 Python 或 Node 如何得到同样的输出。它的核心能力不是“破解”而是定位和复现。定位依赖浏览器开发者工具、抓包工具和断点调试复现依赖对拼接规则、加密算法和编码方式的理解。这里需要强调边界学习 JS 逆向适合用于理解前端防护机制、测试自有系统或者在明确授权的场景下完成数据采集。不要用它去绕过登录、抓取需要身份认证的数据、攻击验证码系统也不要大规模请求真实站点。技术本身是中性的使用方式决定了它是否合规。2. 环境准备从 Python 到抓包与调试工具链在开始写代码之前先把环境对齐。很多初学者卡在环境问题上不是代码不对而是 Python 版本、依赖库或 Node.js 环境没有准备好。2.1 Python 版本与虚拟环境建议使用 Python 3.10 及以上版本。新版本对类型注解、异步和部分新库支持更好如果你的工作环境必须使用 Python 3.8需要额外确认依赖库是否兼容。创建独立虚拟环境避免多个项目之间的依赖冲突python -m venv .venv激活虚拟环境# Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate激活后确认当前环境python --version pip --version2.2 常用 Python 库与用途本篇文章用到的库不多但实际爬虫项目中会持续扩展。库用途安装命令requests发送 HTTP 请求pip install requestsflask搭建本地测试接口pip install flaskbeautifulsoup4解析 HTML 文档pip install beautifulsoup4lxml高性能解析器pip install lxmlpandas数据整理与导出pip install pandasPyExecJS在 Python 中调用 JavaScriptpip install PyExecJS其中flask用来搭一个本地签名校验服务方便你完整复现“前端生成参数、服务端校验、Python 请求成功”的闭环。不要一开始就去真实站点上反复试探那样既容易封 IP也容易在没搞懂原理时浪费大量时间。2.3 Node.js 与抓包调试工具JS 逆向经常需要执行一段从页面中提取出来的 JavaScript 代码。虽然 Python 的PyExecJS可以调用 JS但在真实项目中更多人直接使用 Node.js 来执行 JS输出结果再返给 Python。建议安装 Node.js 18 或更高版本安装后验证node -v npm -v抓包和调试工具方面优先使用浏览器自带的开发者工具也就是 Chrome 或 Edge 的 DevToolsNetwork 面板查看请求 URL、请求头、Query 参数、响应内容Sources 面板查看页面 JS 文件、设置断点、观察调用栈Console 面板在页面上下文中执行临时 JS、验证函数输出。如果后面要分析 App 接口再考虑 Fiddler、Charles 等抓包工具。学习阶段先以 Web 接口为主App 抓包涉及证书设置和设备授权需要在你具备测试条件的情况下进行。2.4 环境检查清单在开始写代码前执行一遍检查python --version node -v pip list | findstr requests pip list | findstr flask如果requests或flask未安装执行pip install requests flask这一步完成后本地环境就满足本篇文章所有示例的运行条件。3. 先跑通一个基础爬虫再谈逆向不要一上来就分析加密先把最简单的“请求一个接口、拿到 JSON、保存数据”链路跑通。只有基础流程稳定后面加入签名时才不会把问题混在一起。3.1 用 Flask 搭建一个带签名校验的测试接口我使用一个本地 Flask 服务模拟真实站点的接口。这个接口要求请求参数里必须包含ts和sign其中sign的规则是sign sha256(path ts SECRET)path是接口路径ts是秒级时间戳SECRET是一个固定字符串。新建文件app.pyfrom flask import Flask, request, jsonify, send_file import hashlib import time app Flask(__name__) SECRET test-secret-2026 def calc_sign(path, ts): raw f{path}{ts}{SECRET} return hashlib.sha256(raw.encode(utf-8)).hexdigest() app.route(/) def index(): return send_file(index.html) app.route(/api/data) def api_data(): ts request.args.get(ts, ) sign request.args.get(sign, ) if not ts or not sign: return jsonify({code: 403, msg: missing param}), 403 try: ts_num int(ts) except ValueError: return jsonify({code: 403, msg: bad ts}), 403 if abs(int(time.time()) - ts_num) 300: return jsonify({code: 403, msg: expired}), 403 expected calc_sign(/api/data, ts) if sign ! expected: return jsonify({code: 403, msg: invalid sign}), 403 return jsonify({ code: 0, data: [ {id: 1, name: python crawler}, {id: 2, name: js reverse} ] }) if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)启动服务python app.py这个接口的逻辑很简单先检查参数是否存在再检查时间戳是否在五分钟内最后校验签名。任何一步不通过都会返回 403。3.2 用 requests 请求一个没有签名的接口打开另一个终端进入同一个虚拟环境写一个最简单的请求脚本basic_request.pyimport requests url http://127.0.0.1:5000/api/data resp requests.get(url, timeout5) print(resp.status_code) print(resp.json())运行python basic_request.py预期输出403 {code: 403, msg: missing param}这一步的关键是让你先看到“缺少参数”的真实报错。很多入门教程直接给最终代码导致读者不知道服务器为什么拒绝。先看 403再补上参数才能理解签名的作用。3.3 解析 JSON 响应与保存结果当签名正确后接口返回的是标准 JSON。处理方式很简单import requests import json import subprocess import time def calc_sign_via_node(path, ts): result subprocess.run( [node, sign.js, path, str(ts)], capture_outputTrue, textTrue, encodingutf-8, timeout10, ) if result.returncode ! 0: raise RuntimeError(result.stderr) return result.stdout.strip() ts int(time.time()) path /api/data sign calc_sign_via_node(path, ts) resp requests.get( http://127.0.0.1:5000/api/data, params{ts: ts, sign: sign}, timeout5, ) print(resp.status_code) data resp.json() print(json.dumps(data, ensure_asciiFalse, indent2)) with open(result.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)代码中把结果同时打印并保存到result.json。这里保存文件时必须指定encodingutf-8否则中文内容在 Windows 环境下可能乱码。3.4 基础爬虫的检查点基础爬虫完成后按以下清单确认状态码是 200 还是 403返回内容是否真的是 JSON中文字段是否正常显示保存后的文件能否用编辑器正常打开请求失败时是否抛出异常。不要只验证“程序能跑”还要验证“输出内容符合预期”。这是后续排查复杂签名问题时最重要的习惯。4. 分析加密参数JS 逆向的核心流程现在进入 JS 逆向的核心部分。目标不是背命令而是掌握一套“从浏览器定位到参数生成逻辑”的方法。4.1 从 Network 面板定位加密参数启动 Flask 服务后在浏览器中打开http://127.0.0.1:5000/页面里只有一个按钮。点击按钮页面会发起/api/data请求。打开开发者工具切到 Network 面板再点一次按钮会看到一条/api/data请求。点击这条请求在 Query String Parameters 或 Payload 区域能看到两个参数ts秒级时间戳sign一段 64 位十六进制字符串。ts很容易理解它就是当前时间。sign是动态的每次请求都不一样。此时要做的是找到sign在前端 JS 中如何生成。4.2 用关键字搜索定位生成逻辑在开发者工具的 Sources 面板里按CtrlShiftF打开全局搜索输入sign或calcSign。搜索结果通常指向某个 JS 文件或 HTML 内联脚本中的函数。在示例页面中calcSign函数位于index.html内联脚本里。它的逻辑是function calcSign(path, ts) { const raw path ts SECRET; return sha256(raw); }这里的重点是不要把整段前端代码都复制到 Python 里而是先理解它的输入输出。输入path、ts、SECRET处理把三者拼接成一个字符串输出对这个字符串做 SHA-256 哈希。从浏览器里看到的复杂代码拆开之后往往只是“拼接 哈希 编码”的组合。4.3 用断点调试观察函数入参和返回值在 Sources 面板里找到calcSign函数在return那一行设置断点点击页面按钮刷新请求。代码会在断点处暂停此时可以观察path的值是/api/datats的值是当前秒级时间戳raw的值是拼接后的完整字符串函数返回值是raw的 SHA-256 哈希。再打开 Console手动执行calcSign(/api/data, 1700000000)会得到与 Python 端hashlib.sha256相同的结果。这一步验证很关键它说明你已经拿到了完整的生成逻辑后面只需要用另一种语言等价实现。4.4 本地复现与验证把calcSign函数提取到本地sign.js文件使用 Node.js 执行const crypto require(crypto); function calcSign(path, ts) { const raw path ts test-secret-2026; return crypto.createHash(sha256).update(raw, utf8).digest(hex); } if (require.main module) { const path process.argv[2] || /api/data; const ts process.argv[3] || String(Math.floor(Date.now() / 1000)); console.log(calcSign(path, ts)); } module.exports { calcSign };命令行验证node sign.js /api/data 1700000000输出应该与 Python 端hashlib.sha256结果一致。这里有一个容易忽略的坑如果 JS 中拼接时用的是毫秒时间戳而 Python 请求时用的是秒两边永远对不上。遇到这种问题优先检查两侧的单位是否一致再检查拼接顺序是否一致。5. 在 Python 中执行 JSPyExecJS 与 Node 联调拿到 JS 函数后有两条路一是把算法翻译成 Python二是让 Python 直接调用 JS。算法简单时推荐翻译逻辑复杂、混淆度高时推荐调用 JS。5.1 方案对比方案优点缺点适用场景翻译成 Python无额外依赖性能好混淆代码翻译成本高算法简单、拼接规则清晰PyExecJS快速验证 JS 逻辑对require支持不完整依赖系统 JS 运行时小程序、少量函数验证js2py纯 Python 实现无需 Node支持 ECMAScript 版本有限简单 JS 函数subprocess node稳定、隔离、可控需要装 Node进程启动有开销生产环境常用方案5.2 PyExecJS 的最小示例安装pip install PyExecJS使用示例import execjs js_code function calcSign(path, ts) { const raw path ts test-secret-2026; return sha256(raw); } ctx execjs.compile(js_code) sign ctx.call(calcSign, /api/data, 1700000000) print(sign)不过这里有一个很实际的问题如果 JS 代码里直接使用require(crypto)PyExecJS 的默认运行时可能无法识别require会报require is not defined。处理方式有两种在 JS 代码里把哈希函数改为纯 JavaScript 实现不经过 PyExecJS直接让 Python 调用 Node。在实际项目中第二种方式更稳定因此下面的完整案例使用subprocess node。5.3 用 subprocess 调用 Node.js在 Python 里调用 Node 执行sign.jsimport subprocess def calc_sign_via_node(path, ts, script_pathsign.js): result subprocess.run( [node, script_path, path, str(ts)], capture_outputTrue, textTrue, encodingutf-8, timeout10, ) if result.returncode ! 0: raise RuntimeError(fnode exec failed: {result.stderr}) return result.stdout.strip()关键点capture_outputTrue用来捕获标准输出textTrue让输出以字符串形式返回encodingutf-8避免 Windows 下中文编码问题timeout10防止 JS 执行死循环导致 Python 卡住检查returncode只要非 0 就说明 JS 执行失败要把stderr打印出来。调用ts int(time.time()) sign calc_sign_via_node(/api/data, ts) print(sign)5.4 编码、超时和进程资源问题使用 Node 联调时最常遇到三个问题问题现象处理方式编码不一致中文签名字符串结果不同统一使用 UTF-8JS 和 Python 都显式指定编码执行超时Python 一直卡住增加timeout参数并在异常时打印stderr频繁调用每秒启动几十次 Node 进程在高频场景下改用常驻服务或直接翻译算法如果请求量很小subprocess完全够用。如果同一个签名函数会被调用成百上千次建议把签名逻辑封装成一个本地 Node 服务或者把算法翻译成 Python。6. 完整案例从抓包定位到 Python 调用 JS 生成动态签名前面章节把步骤拆开了这一节串成一条完整链路。案例会用到本地 Flask 服务、浏览器开发者工具和 Node.js适合按顺序完整操作一遍。6.1 项目结构js-reverse-demo/ ├── app.py ├── index.html ├── sign.js ├── basic_request.py └── main.py其中app.py是 Flask 服务index.html是页面sign.js是提取出来的签名函数main.py是最终的 Python 爬虫脚本。6.2 浏览器端签名页面index.html中展示了一个简单的浏览器端签名逻辑!DOCTYPE html html langzh-CN head meta charsetUTF-8 title签名示例/title /head body button idload请求 /api/data/button pre idresult/pre script const SECRET test-secret-2026; function calcSign(path, ts) { const raw path ts SECRET; return sha256(raw); } async function sha256(text) { const buffer await crypto.subtle.digest(SHA-256, new TextEncoder().encode(text)); return Array.from(new Uint8Array(buffer)) .map(b b.toString(16).padStart(2, 0)) .join(); } async function loadData() { const ts Math.floor(Date.now() / 1000); const sign await calcSign(/api/data, String(ts)); const url /api/data?ts${ts}sign${sign}; const resp await fetch(url); const body await resp.json(); document.getElementById(result).textContent JSON.stringify(body, null, 2); } document.getElementById(load).addEventListener(click, loadData); /script /body /html这个页面只是为了模拟真实场景前端动态生成参数服务器校验后会返回数据。6.3 从抓包到定位的完整步骤按以下顺序操作启动 Flask 服务python app.py浏览器打开http://127.0.0.1:5000/打开开发者工具切到 Network点击“请求 /api/data”按钮找到/api/data请求查看 Query String Parameters看到ts和sign切到 Sources全局搜索sign定位到calcSign函数在return sha256(raw)行设置断点重新点击按钮观察raw的值是/api/data、当前时间戳、SECRET拼接后的结果。这一步完成后你会得到签名算法的输入、输出和内部拼接规则。6.4 Python 联调脚本最终脚本main.py会做三件事生成签名、请求接口、解析并保存结果。import json import subprocess import time import requests def calc_sign_via_node(path, ts, script_pathsign.js): result subprocess.run( [node, script_path, path, str(ts)], capture_outputTrue, textTrue, encodingutf-8, timeout10, ) if result.returncode ! 0: raise RuntimeError(result.stderr) return result.stdout.strip() def main(): path /api/data ts int(time.time()) sign calc_sign_via_node(path, ts) url http://127.0.0.1:5000/api/data resp requests.get(url, params{ts: ts, sign: sign}, timeout5) print(status:, resp.status_code) data resp.json() print(json.dumps(data, ensure_asciiFalse, indent2)) with open(result.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) if __name__ __main__: main()运行python main.py预期输出status: 200 { code: 0, data: [ { id: 1, name: python crawler }, { id: 2, name: js reverse } ] }6.5 结果校验与日志建议成功拿到 200 后再做一个负向验证把sign改错一位看服务器是否返回 403。这能确认请求不是靠运气成功而是签名真的被服务器校验通过。生产环境还需要注意记录每次请求的 URL、状态码、耗时对响应中的异常字段做容错处理避免连续高频率请求保存数据前做去重和格式校验。7. 常见反爬机制、合规边界与学习环境选择爬虫和反爬是一对长期博弈关系。掌握 JS 逆向之后你会看到更多反爬手段。这里只做合规学习和工程防御视角的梳理。7.1 常见反爬层次层次典型手段学习方向请求头校验User-Agent、Referer、Accept模拟完整请求头分析缺失项Cookie 与会话首次请求种 Cookie后续接口依赖 Cookie先访问首页或预请求接口再带 Cookie 访问目标接口参数签名URL 中携带动态sign、token分析前端生成逻辑等价复现响应加密返回内容为密文或自定义编码定位解密函数解密后再解析字体反爬数字或文字被替换为自定义字体分析字体文件映射字符编码行为验证滑动验证、点选验证、设备指纹研究验证逻辑但注意只用于防御自测频率限制短时间大量请求触发封禁限速、重试、代理池但注意合规设备指纹通过 canvas、WebGL 等生成设备标识了解指纹生成机制避免滥用这些手段往往组合出现。学习时不要试图一次性解决所有问题先从一个接口、一个参数开始。7.2 进阶工具与方向当 JS 代码被压缩、混淆后直接阅读会非常困难。此时可以借助工具辅助分析AST 解析与还原把混淆后的代码转换成可读性更好的结构浏览器断点调试观察函数调用栈找到关键函数本地 JS 执行环境把提取后的代码放到 Node 中运行和验证无头浏览器Playwright、Selenium 等技术适合自动化测试和兼容性验证。这些工具本身都是正当开发工具但它们是否合规取决于使用场景。不要用无头浏览器绕过验证码攻击真实系统也不要用它批量采集需要账号权限的数据。7.3 合规底线在公开博客和实际项目中以下几件事必须守住只采集公开数据并优先使用官方 API尊重网站的robots.txt和服务条款不采集个人信息、账号数据、订单数据等敏感内容不绕过登录、不破解验证码、不攻击服务器大规模采集前先做合规评估必要时咨询法务学习阶段优先使用本地服务或公开测试接口。文章中的 Flask 示例就是基于这个原则设计的流程完整、可复现、不依赖任何真实站点也能展示 JS 逆向的完整链路。7.4 学习环境与生产环境的差异学习环境追求快速复现生产环境追求稳定和可控。维度学习环境生产环境目标服务本地 Flask、公开测试接口经过授权或合规评估的数据源错误处理打印异常即可日志、告警、重试、降级请求频率手动点击或低频请求限速、队列、任务调度数据保存直接写入 JSON数据库、对象存储、去重机制签名生成subprocess 调用 Node缓存签名结果或封装本地服务合规无风险需要专门的合规审查不要把学习环境里的粗暴写法直接搬进生产。真实的采集任务还需要考虑异常恢复、监控、数据质量和版本兼容。8. 常见坑、排错清单与学习建议8.1 至少要知道这 5 个坑第一个坑只加sign不加时间戳或者时间戳精度不一致。服务端校验时会把ts一起参与签名计算。如果前端用的是毫秒Python 用秒签名永远对不上。排查时先确认两侧单位再确认拼接顺序。第二个坑拼接顺序与服务端不一致。同一个字符串path ts secret和ts path secret计算出的哈希完全不同。一定要从 JS 源码里确认拼接顺序而不是猜。第三个坑在 PyExecJS 中直接使用require。很多 Node.js 代码依赖require(crypto)但 PyExecJS 的运行时不一定支持。遇到require is not defined要么把函数改成纯 JS 实现要么改用subprocess node。第四个坑忽略编码问题导致中文结果不一致。JS 和 Python 对字符串的 UTF-8 编码方式必须一致。如果签名内容包含中文统一使用 UTF-8并在 Python 中显式指定encoding。第五个坑一上来就抓真实站点。学习阶段直接在未授权站点上反复调试既容易被封 IP也没有调试环境。先用本地 Flask 把流程走通再迁移到真实场景并且要先确认合规。8.2 从现象倒推根因的排错链路遇到请求失败时按以下顺序检查检查 URL包括协议、域名、路径、端口检查请求参数是否缺少ts、sign、token检查请求头是否缺少 User-Agent、Cookie、Referer检查签名先用浏览器 Console 手动调用函数对比输出检查时间戳确认是秒还是毫秒是否过期检查日志特别是服务端日志或响应体中的错误信息检查依赖版本Node.js、Python、第三方库是否兼容。例如请求返回 403现象优先检查处理方式提示缺少参数请求是否真的带上了参数打印完整 URL 和 params提示签名过期时间戳单位或服务器时间确认秒/毫秒校准系统时间提示签名错误拼接顺序、SECRET、编码在浏览器中手动验证函数输出浏览器能过、脚本不能过请求头差异对比浏览器和脚本的请求头8.3 可复用的学习清单每开始一个新的逆向任务先按清单走一遍确认目标接口属于公开数据且合规打开 Network记录请求参数和响应结构全局搜索动态参数名定位生成位置在关键行下断点确认输入和输出把生成函数提取到本地 JS 文件用命令行验证 JS 输出用 Python 调用 JS再拼接完整请求先做一次负向验证再确认正向结果记录排错过程形成自己的笔记。这份清单同样适合后续学习 AST 还原、App 抓包、验证码防御等更复杂方向。8.4 下一步扩展方向如果已经能独立完成一个带签名接口的逆向下一步可以深入学习 AST 解析与反混淆处理压缩后的前端代码研究字体反爬和 CSS 偏移的还原思路了解 Playwright 等无头浏览器在自动化测试中的应用掌握 Python 异步请求、数据去重和任务队列复习 HTTP 协议、加密算法、编码原理补齐底层知识。JS 逆向本身不是终点它背后涉及的调试能力、代码阅读能力和工程化能力才是长期有价值的东西。真正决定你能否独立解决问题的不是某一段代码而是遇到问题后能否按照“定位参数、理解逻辑、本地复现、验证结果”这条路径持续走下去。学习时优先选择本地服务和公开测试接口先保证流程闭环再逐步扩大到真实场景同时始终守住合规底线。