
简介面向Python爬虫进阶学习者的JS解密逆向实战资源精选多个真实站点逆向案例适合毕业设计、大作业或数据采集项目参考。压缩包共86个文件以JavaScript解密脚本和Python爬虫脚本为主包含42个js文件、31个py文件并配5份md说明文档与少量图片样图整体仅1.13MB体积紧凑但案例密集。目前已有1102人学习浏览。内容涵盖Cookie生成、密码加密、参数签名、滑块验证及AES/RSA/DES等常见加密逻辑覆盖携程、小红书、淘宝搜索、芒果TV等平台场景每个案例目录通常包含加密JS、爬虫入口脚本与README说明可快速定位关键断点与调用链。资源整体围绕“JS解密Python抓取”两条主线组织模块独立便于按需查阅和二次改造。对想弄清前端加密与Python联动、提升反爬应对能力的开发者来说这套资源能提供直接可运行的参考脚本和拆解思路。1. 从能爬到爬不动之间隔着一层 JS 加密做 Python 爬虫的人大概率经历过这种反转昨天还能正常采集的接口今天返回 403 或者一串密文打开浏览器却一切正常。问题往往不在 IP 被封而是服务端给请求参数加了签名校验签名逻辑用 JavaScript 在前端算好Python 这边拿不到。所谓 JS 解密逆向就是把这层浏览器里执行的计算搬到 Python 里让请求看起来和真实用户毫无差别。本文按我实际处理这类问题的顺序展开先定位加密入口再选执行方案接着识别算法并还原最后讨论签名验证和工程化落地。适合已经能熟练写 requests 和解析数据、但第一次直面加密参数的爬虫开发者。2. 定位加密入口从浏览器断点到调用栈别上来就翻源码2.1 先判断加密形态决定用哪种逆向路线JS 加密逆向第一步不是读代码而是确定要逆的对象是什么。常见形态有三种参数型加密、响应型加密、请求头加密。参数型加密最常见表现为 URL 查询串里有sign、token、_signature之类的字段值是一串十六进制或 Base64 字符。响应型加密是服务器返回的数据本身是密文前端用 JS 解密后再渲染典型特征是浏览器 Network 面板里响应内容是一堆乱码页面却正常显示。请求头加密则是把时间戳、设备指纹、随机数拼进 header比如x-sign或authorization校验失败返回 401 或 403。判断方法很直接在 Network 面板里逐个请求查看把可疑字段复制下来刷新页面看值是否变化。每次都变的字段大概率是动态签名固定不变的字段可能是设备指纹或时间戳处理策略完全不同。我一般先用这个分类决定后续优先级动态签名优先处理因为它直接影响能否拿到数据。2.2 用全局搜索锁定关键字缩小函数范围打开 DevTools 的 Sources 面板按CtrlShiftF全局搜索。关键词从请求参数里提取看到sign就搜sign看到token就搜token也可以直接搜参数名加上的组合比如sign或sign:. 搜索结果会列出所有包含该关键字的 JS 文件和行号优先看文件体积小的、名字带chunk、min、encrypt、crypto的文件。这里有个容易踩的坑很多站点会做 JS 混淆变量名被替换成_0x3f2a这种形式搜sign可能搜不到因为属性名也被改了。这种情况下改用搜索值的特征比如签名字段的值以md5开头或长度固定为 32 位就去搜加密算法常见的特征字符串比如md5(、sha256(、AES、CryptoJS。搜到结果后不要急着读全部代码先看函数入口和 return 语句。2.3 XHR 断点加调用栈回溯定位加密调用位置搜索能告诉你加密函数在哪但不知道它在哪个调用链路里被触发。这时候用 XHR/fetch 断点效率最高在 Sources 面板右侧的 XHR/fetch breakpoints 里勾选Any XHR然后刷新页面请求发出前代码会停在发起请求的那一行。此时右侧 Call Stack 面板能看到完整的调用链从上往下点凡是执行到包含加密操作的函数右侧 Scope 面板里通常能看到加密前的明文参数和加密后的结果。具体流程我一般这样走在fetch或XMLHttpRequest.send处设置断点刷新页面等待断点命中在 Call Stack 里逐层往上找观察每层函数的参数和局部变量找到sign值第一次出现的函数在那一行重新下断点刷新后单步执行这一个过程基本能把明文参数如何在内存里变成密文的路径走通。定位到具体函数后右键该函数名选择Show function definition跳到源码处把函数体完整复制出来这就是后面在 Python 里执行或还原的原材料。提示混淆严重的代码里单步执行比读源码可靠。每执行一行就看一次变量面板变量从什么值变成什么值比猜测函数逻辑直观得多。3. 把 JS 从浏览器搬到 Python三种执行方式与选型边界3.1 PyExecJS适合临时验证不适合生产拿到加密函数后最省事的做法是直接用 PyExecJS 在 Python 里调用。安装方式pip install PyExecJS它需要一个 JS 运行时系统里装了 Node.js 或 PhantomJS 即可。import execjs # 从文件读取逆向得到的完整 JS 代码 with open(sign.js, r, encodingutf-8) as f: js_code f.read() # 编译 JS 环境 ctx execjs.compile(js_code) # 调用 JS 里的签名函数参数按原函数签名传入 sign ctx.call(getSign, param1_value, param2_value) print(sign)这段代码的逻辑是先用compile把 JS 代码加载进运行时再用call调用其中暴露的函数。getSign是 JS 里定义好的函数名参数顺序必须和它在 JS 中的定义完全一致多传或少传都会直接报错。PyExecJS 的优势是上手快三五行代码就能验证逆向结果是否正确。但它每次调用都要启动一次 JS 运行时性能差且依赖系统环境里的 Node 版本换机器容易出兼容问题。我的用法是拿它做验证确认 JS 代码在 Python 里能跑出和浏览器一致的结果然后再决定是否要改写或接入执行引擎。3.2 Node.js 子进程工程化首选方案生产环境我更推荐用 Node.js 子进程。思路是把 JS 代码封装成一个独立脚本Python 通过subprocess调用它用 stdin/stdout 交换数据。这样 JS 和 Python 完全解耦JS 部分可以独立测试、独立更新。// sign.js —— 由浏览器逆向得到的加密函数 这个启动入口 function getSign(param1, param2) { // 这里是从浏览器里复制的原始加密逻辑 return md5(param1 param2 salt); } // 从 stdin 读取 JSON 参数 let input ; process.stdin.on(data, (chunk) { input chunk; }); process.stdin.on(end, () { const params JSON.parse(input); const result getSign(params.param1, params.param2); process.stdout.write(JSON.stringify({ sign: result })); });import json import subprocess def call_js(params): # 启动 node 子进程传入脚本路径 proc subprocess.run( [node, sign.js], inputjson.dumps(params), # 参数以 JSON 形式写入 stdin capture_outputTrue, textTrue, timeout10 ) # 解析 stdout 返回的 JSON 结果 if proc.returncode 0: return json.loads(proc.stdout)[sign] else: raise RuntimeError(fJS 执行失败: {proc.stderr})subprocess.run的input参数负责把 Python 侧的字典序列化成 JSON 传给子进程capture_outputTrue收集标准输出timeout10防止 JS 死循环拖垮爬虫进程。这种方式的代价是每次调用多一次进程启动开销但好在签名计算通常不需要高频触发每秒几次的量级完全扛得住。如果并发量大可以做一次小优化启动常驻 Node 进程Python 端持续往 stdin 写请求、从 stdout 读结果省去反复启动进程的开销。不过这要求 JS 端写成循环模式复杂度会上升我一般等到单机 QPS 超过两位数才做这一步。3.3 Playwright 直接执行绕开剥离代码的麻烦有个场景比上面两种都棘手加密函数依赖浏览器环境比如用了window.navigator、document.cookie或者 WebCrypto API直接用 Node 跑会因为环境缺失而报错。这时候换 Playwright 在浏览器上下文中执行是最稳的。import asyncio from playwright.async_api import async_playwright async def get_sign_js(page, param1, param2): # 在页面上下文中执行 JS可以访问 window 和 document sign await page.evaluate( ([p1, p2]) window.getSign(p1, p2), [param1, param2] ) return sign async def main(): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page() # 先访问目标站点这样 JS 环境与真实场景一致 await page.goto(https://example.com) result await get_sign_js(page, a, b) print(result) await browser.close() asyncio.run(main())这段代码的核心是page.evaluate它把参数列表传给页面里的函数并在浏览器环境里执行返回结果直接可被 Python 使用。先goto目标站点是为了让window上的全局变量和 cookie 都处于真实状态很多 JS 逆向结果在这个环境里才能跑通。这种方案鉴权成功率最高但性能和资源占用也最重每开一个浏览器实例就是几百 MB 内存。我的选择标准是Node 跑不通且不想剥离浏览器依赖时用它同时用连接池复用同一个浏览器实例避免频繁开关。4. 从算法特征到 Python 还原MD5、SHA、AES、RSA 的判定与实现4.1 识别加密算法的特征对照有些场景不适合保留 JS 代码执行比如签名函数依赖的 JS 文件过大导致启动慢或者想彻底去掉 Node 依赖那就需要把 JS 逻辑翻译成 Python。翻译前先识别算法特征对照表如下特征对应算法佐证线索输出长度 32 位十六进制字符MD5JS 里常出现md5(或CryptoJS.MD5输出长度 40 或 64 位SHA1 / SHA256搜索sha关键字hash.update加密结果是 Base64 串且每次带有随机 IVAES-CBC出现CryptoJS.AES、enc.Utf8.parse加密结果超长且用到公钥文件RSA出现setPublicKey、JSEncrypt、pkcs1输出固定 8 位或 16 位且与时间戳拼接自定义或 HMAC搜索hmac、btoa、encodeURIComponent这个表不是绝对判定只是一个起点。真正有效的方法是动态对比在浏览器里手动改一个明文参数观察密文对应位置的字符变化。改动一小节明文密文整体改变八成是块加密算法只有局部变化则可能是个性化编码或哈希截断。4.2 AES 类 JS 加密的 Python 还原AES 在 JS 逆向里出现频率极高通常是 AES-CBC 或 AES-ECB 模式加上 PKCS7 填充。JS 侧代码常见是这样// CryptoJS AES 加密的典型写法 function encrypt(data, key) { var keyHex CryptoJS.enc.Utf8.parse(key); var ivHex CryptoJS.enc.Utf8.parse(0123456789abcdef); var encrypted CryptoJS.AES.encrypt(data, keyHex, { iv: ivHex, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }对应到 Python 侧用pycryptodome库可以完整还原from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def js_aes_encrypt(plaintext: str, key: str, iv: str) - str: # 构造 AES-CBC 加密器key 和 iv 必须是 bytes cipher AES.new(key.encode(utf-8), AES.MODE_CBC, iv.encode(utf-8)) # PKCS7 填充CryptoJS 默认也是 PKCS7 padded pad(plaintext.encode(utf-8), AES.block_size) encrypted cipher.encrypt(padded) # CryptoJS 的 encrypted.toString() 默认输出 Base64 return base64.b64encode(encrypted).decode(utf-8)AES.new的第一个参数是密钥必须和 JS 里的Utf8.parse(key)结果一致也就是直接取字符串的 UTF-8 字节。IV 同理。如果 JS 里没显式指定ivCryptoJS 会用随机 IV并且密文会带上 IV 前缀这时候需要从密文前 16 字节提取 IV 再做解密否则无法还原。经常被忽略的一个坑是填充模式。CryptoJS 默认Pkcs7但有些自定义实现会写ZeroPadding或NoPaddingPython 侧就要相应地用pad(..., stylepkcs7)换成zeros或干脆不填充。识别方法是看明文长度是否为 16 的整数倍不是却还能加密成功的必然有填充逻辑。4.3 MD5 与 RSA 的翻译要点MD5 和 SHA 是纯哈希没有密钥概念翻译最简单。JS 里CryptoJS.MD5(abc).toString()对应 Python 的hashlib.md5(babc).hexdigest()。但实际逆向里它很少单独出现更多是和字符串拼接组合比如md5(timestamp nonce secret)。翻译时注意 JS 的在字符串上是直接拼接Python 里不要漏掉转型import hashlib def js_md5_concat(timestamp: str, nonce: str, secret: str) - str: raw timestamp nonce secret return hashlib.md5(raw.encode(utf-8)).hexdigest()RSA 相对复杂因为标准库不提供 RSA 加密需要rsa或pycryptodome。JS 里JSEncrypt默认使用 PKCS1 v1.5 填充公钥是 PEM 格式的字符串。Python 侧还原import rsa def js_rsa_encrypt(plaintext: str, public_key_pem: str) - str: # 加载 PEM 格式公钥 pub_key rsa.PublicKey.load_pkcs1_openssl_pem(public_key_pem.encode(utf-8)) # PKCS1 v1.5 加密对应 JSEncrypt 默认行为 encrypted rsa.encrypt(plaintext.encode(utf-8), pub_key) # JSEncrypt 的 output 默认是 Base64 编码 return base64.b64encode(encrypted).decode(utf-8)一个容易出错的点JSEncrypt 加密结果每次都可能不同因为 PKCS1 v1.5 有随机填充如果服务端只认第一次的密文说明你还漏掉了时间戳或随机数参与拼接RSA 本身不是问题所在。另外 RSA 对明文长度有限制1024 位密钥最多加密 117 字节明文长数据通常是先 AES 加密再用 RSA 加密 AES 密钥这种混合模式在接口逆向里也常见。5. 签名参数验证与绕过反爬的收尾比对、持久化、动态盐值处理5.1 本地反向验证用同一组输入比对 JS 与 Python 输出逆向完成不等于能上线先做一致性验证。方法是用一组固定的时间戳和随机数分别在浏览器、Node 执行的原始 JS、Python 还原版里计算签名三者结果必须完全一致。不一致时按优先级排查先看字符串拼接顺序再看编码方式UTF-8 还是有 BOM最后看填充模式。一个实用的验证脚本import hashlib import time def verify_md5_impl(): # 固定输入避免随机因素干扰比对 timestamp 1735689600 nonce abc123 secret test_secret # 浏览器/Node 环境下运行原始 JS 得到的值替换成你的实测值 js_result 19a5c5b4b2f6c1d0e0a1b2c3d4e5f6a7 # Python 本地实现 py_result hashlib.md5( (timestamp nonce secret).encode(utf-8) ).hexdigest() # 直接比对 assert js_result py_result, f不一致: JS{js_result}, Python{py_result} print(校验通过签名实现一致) verify_md5_impl()这种验证脚本要保留在项目里因为服务端更新 JS 后你只需要用新 JS 跑一遍同样的固定输入如果 Python 结果对不上就知道算法变了而不是等线上接口 403 才发现。5.2 动态盐值别写死从响应或本地存储中提取逆向到后期最容易踩的坑是盐值固定。很多站点的签名公式是md5(params salt)salt 不是写死在 JS 里而是服务器在登录接口或页面初始化接口里下发的存在 localStorage 或全局变量里。这类盐值会定期轮换一旦写死签名校验必挂。处理上我采用一个折中方案爬虫启动时先请求一次页面初始化接口从响应 JSON 或 HTML 里用正则提取新盐值再注入到签名函数里。盐值的生命周期通常和会话绑定所以每个会话任务开始时重新提取一次import re import requests def fetch_realtime_salt(session, init_url): # 请求初始化接口拿到包含盐值的页面 resp session.get(init_url) # 用正则从返回内容中截取 salt具体模式按站点结构调整 match re.search(rsalt\s*:\s*([^]), resp.text) if not match: raise ValueError(未找到盐值站点可能已更新下发逻辑) return match.group(1)session对象保持 cookie 一致性盐值从响应正文里动态提取而不是直接读配置这样就算服务端定期轮换盐值爬虫也能自动适应。5.3 签名与代理、Cookie 的组合使用签名逆向解决的是请求参数合法问题但爬虫稳定运行还需要 Cookie 和出口 IP 配合。加密签名只是服务端校验的一环通常还会配合 cookie 中的 session 字段和页面浏览行为做风控。常见做法是按并发量配置代理 IP 池每个代理 IP 绑定一套独立的 Cookie 和签名参数避免多个请求共享同一套身份特征被识别。同时在代码里对签名函数做缓存同一秒内的相同参数不重复计算签名降低 CPU 开销也减少 JS 进程频繁启动。5.4 遗留问题排查清单到上线阶段如果仍然遇到 403 或校验失败按下面顺序排查先确认签名值是否与浏览器当前环境一致排除时间戳不同步再检查请求头里是否缺少User-Agent或Origin很多站点把 UA 纳入签名计算最后看 Cookie 是否有浏览器指纹字段比如_fp或device_id这类字段不参与签名但参与风控评分。整套流程跑通后把签名函数、盐值提取、验证脚本打包成一个独立模块这样 JS 更新时只需替换模块内的加密逻辑爬虫主体不用动。本文还有配套的精品资源点击获取