news 2026/9/14 14:57:00

Python爬虫JS加密逆向实战:从定位加密入口到工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫JS加密逆向实战:从定位加密入口到工程化落地

简介:面向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 查询串里有signtoken_signature之类的字段,值是一串十六进制或 Base64 字符。响应型加密是服务器返回的数据本身是密文,前端用 JS 解密后再渲染,典型特征是浏览器 Network 面板里响应内容是一堆乱码,页面却正常显示。请求头加密则是把时间戳、设备指纹、随机数拼进 header,比如x-signauthorization,校验失败返回 401 或 403。

判断方法很直接:在 Network 面板里逐个请求查看,把可疑字段复制下来,刷新页面看值是否变化。每次都变的字段大概率是动态签名,固定不变的字段可能是设备指纹或时间戳,处理策略完全不同。我一般先用这个分类决定后续优先级,动态签名优先处理,因为它直接影响能否拿到数据。

2.2 用全局搜索锁定关键字,缩小函数范围

打开 DevTools 的 Sources 面板,按Ctrl+Shift+F全局搜索。关键词从请求参数里提取:看到sign就搜sign,看到token就搜token,也可以直接搜参数名加上=的组合,比如"sign="sign:. 搜索结果会列出所有包含该关键字的 JS 文件和行号,优先看文件体积小的、名字带chunkminencryptcrypto的文件。

这里有个容易踩的坑:很多站点会做 JS 混淆,变量名被替换成_0x3f2a这种形式,搜sign可能搜不到,因为属性名也被改了。这种情况下改用搜索值的特征,比如签名字段的值以md5开头,或长度固定为 32 位,就去搜加密算法常见的特征字符串,比如md5(sha256(AESCryptoJS。搜到结果后不要急着读全部代码,先看函数入口和 return 语句。

2.3 XHR 断点加调用栈回溯,定位加密调用位置

搜索能告诉你加密函数在哪,但不知道它在哪个调用链路里被触发。这时候用 XHR/fetch 断点效率最高:在 Sources 面板右侧的 XHR/fetch breakpoints 里勾选Any XHR,然后刷新页面,请求发出前代码会停在发起请求的那一行。此时右侧 Call Stack 面板能看到完整的调用链,从上往下点,凡是执行到包含加密操作的函数,右侧 Scope 面板里通常能看到加密前的明文参数和加密后的结果。

具体流程我一般这样走:

  1. fetchXMLHttpRequest.send处设置断点
  2. 刷新页面,等待断点命中
  3. 在 Call Stack 里逐层往上找,观察每层函数的参数和局部变量
  4. 找到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', encoding='utf-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'], input=json.dumps(params), # 参数以 JSON 形式写入 stdin capture_output=True, text=True, timeout=10 ) # 解析 stdout 返回的 JSON 结果 if proc.returncode == 0: return json.loads(proc.stdout)['sign'] else: raise RuntimeError(f'JS 执行失败: {proc.stderr}')

subprocess.runinput参数负责把 Python 侧的字典序列化成 JSON 传给子进程,capture_output=True收集标准输出,timeout=10防止 JS 死循环拖垮爬虫进程。这种方式的代价是每次调用多一次进程启动开销,但好在签名计算通常不需要高频触发,每秒几次的量级完全扛得住。

如果并发量大,可以做一次小优化:启动常驻 Node 进程,Python 端持续往 stdin 写请求、从 stdout 读结果,省去反复启动进程的开销。不过这要求 JS 端写成循环模式,复杂度会上升,我一般等到单机 QPS 超过两位数才做这一步。

3.3 Playwright 直接执行:绕开剥离代码的麻烦

有个场景比上面两种都棘手:加密函数依赖浏览器环境,比如用了window.navigatordocument.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.AESenc.Utf8.parse
加密结果超长,且用到公钥文件RSA出现setPublicKeyJSEncryptpkcs1
输出固定 8 位或 16 位,且与时间戳拼接自定义或 HMAC搜索hmacbtoaencodeURIComponent

这个表不是绝对判定,只是一个起点。真正有效的方法是动态对比:在浏览器里手动改一个明文参数,观察密文对应位置的字符变化。改动一小节明文密文整体改变,八成是块加密算法;只有局部变化则可能是个性化编码或哈希截断。

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 里没显式指定iv,CryptoJS 会用随机 IV,并且密文会带上 IV 前缀,这时候需要从密文前 16 字节提取 IV 再做解密,否则无法还原。

经常被忽略的一个坑是填充模式。CryptoJS 默认Pkcs7,但有些自定义实现会写ZeroPaddingNoPadding,Python 侧就要相应地用pad(..., style='pkcs7')换成zeros或干脆不填充。识别方法是看明文长度是否为 16 的整数倍:不是却还能加密成功的,必然有填充逻辑。

4.3 MD5 与 RSA 的翻译要点

MD5 和 SHA 是纯哈希,没有密钥概念,翻译最简单。JS 里CryptoJS.MD5('abc').toString()对应 Python 的hashlib.md5(b'abc').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 加密,需要rsapycryptodome。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(r'"salt"\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-AgentOrigin,很多站点把 UA 纳入签名计算;最后看 Cookie 是否有浏览器指纹字段,比如_fpdevice_id,这类字段不参与签名但参与风控评分。整套流程跑通后,把签名函数、盐值提取、验证脚本打包成一个独立模块,这样 JS 更新时只需替换模块内的加密逻辑,爬虫主体不用动。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:56:56

MiniOB源码解析:用C++亲手实现一个数据库内核

简介:这份基于C的MiniOB数据库系统源码包,是OceanBase与华中科技大学联合开发的数据库内核入门实践项目。它面向在校学生和数据库初学者,重点帮助理解存储管理、查询优化、事务处理等模块,通过简化实现降低学习门槛,并…

作者头像 李华
网站建设 2026/9/14 14:56:09

MATLAB实现MIT-BIH心电信号预处理:从WFDB读取到QRS检测

简介:面向MIT-BIH心律失常数据库的MATLAB心电信号预处理程序,适合生物医学工程、数据科学及心脏病学领域的研究者与工程师,用于去除ECG中的基线漂移、肌电干扰和电源噪声,提升后续分析可靠性。压缩包共含2个文件,以m脚…

作者头像 李华
网站建设 2026/9/14 14:55:44

Windows内存占用居高不下?从任务管理器到RAMMap的排查实战

前几天一个朋友找我说,电脑没开游戏、也没跑渲染,刚开机一会儿内存占用直接飙到90%,鼠标飘得跟喝醉了一样。我远程一看,任务管理器里一堆奇奇怪怪的进程排在前排,罪魁祸首根本不是他以为的“某个软件”,而是…

作者头像 李华
网站建设 2026/9/14 14:53:52

MATLAB时频分析实战:STFT、CWT与EMD-HHT全解析

简介:这份MATLAB时频分析程序包面向信号处理初学者与工程师,覆盖短时傅里叶变换、小波变换、Wigner-Ville分布及EMD/EEMD等常见方法,配套大量带exa编号的示例脚本,可系统学习时频分析原理与实现。压缩包共40个文件,以3…

作者头像 李华
网站建设 2026/9/14 14:51:50

TurboQuant技术:大模型KV Cache高效压缩方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:48:41

火鸟门户V4.8部署指南:PHP地方门户框架环境配置与五端同步实践

简介:这是一套面向地方政务与社区服务场景的PHP开源门户系统,适用于有本地化运营需求的开发者、中小型企业及地方政府信息化建设团队,可快速搭建集资讯发布、用户互动、养老服务与社群管理于一体的五端同步(PC、H5、小程序、APP、…

作者头像 李华