Python 模拟执行 JavaScript 教程

目录

  1. 为什么需要这技能?
  2. 环境快速搭建
  3. 实战:破解某球星站加密Token
  4. 避坑指南+性能小技巧
  5. 总结

为什么需要这技能?

如今 Web 开发的核心逻辑早已不再是单纯的后端渲染 HTML。前端渲染、接口参数加密、动态 Cookie 生成,这些活儿几乎全由 JavaScript 扛起。作为开发者或爬虫工程师,你大概率会碰到下面这些场景:

  • 抓取 B 站番剧或评论,得先搞定 bili_jct_signature 这类加密参数。
  • 爬电商平台的商品库存,需要拿到动态生成的 anti_scrape_token
  • 补全前后端分离项目的自动化测试,Python 构造的数据必须经过前端加密才能通过后端校验。

面对这些情况,能在 Python 里直接跑 JavaScript 逻辑,就能省下大量逆向和重写的时间。本教程将用 PyExecJS 这个轻量库,教你如何把前端 JS 代码无缝搬进 Python,全程不牵扯高深数学,只讲实用的执行技巧。


环境快速搭建

核心依赖

想要 Python 模拟执行 JavaScript,你需要两样东西:

  1. PyExecJS:Python 和 JS 运行环境之间的桥梁。
  2. JS 运行环境:强烈推荐 Node.js。它的 V8 引擎原生支持 ES6+ 以及 crypto-js 这类第三方加密库,比 Windows 自带的 JScript 好用太多。

一步一步安装

1. 安装 PyExecJS

pip install PyExecJS

2. 安装 Node.js

去官网下载最新的 LTS 版本(稳定省心):

https://nodejs.org/

安装完成后,打开终端验证:

node -v   # 例如输出 v20.x.x
npm -v    # 后面安装第三方库时会用到

3. 强制绑定 Node.js(关键!)

很多新手装完 PyExecJS 后,系统默认会使用 Windows 自带的 JScript(不支持 ES6+),导致代码一跑就报错。所以一定要主动检查并绑定 Node.js

import execjs

# 查看所有可用的 JS 运行时
print("可用的 JS 环境:", execjs.runtimes().keys())

# 强制指定 Node.js
ctx = execjs.get(execjs.runtime_names.Node)
print("当前绑定的 JS 环境:", ctx.name)  # 应输出类似 "Node.js (V8)"

看到 Node.js (V8) 就说明环境已经稳妥了。


实战:破解某球星站加密 Token

我们拿官方提供的 SPA7 爬虫练习站https://spa7.scrape.center/)练手。这个站点里每个球星信息都由一个加密的 token 保护,只有正确生成该 token,才能请求到完整的球员数据。

第一步:前端加密逻辑逆向

先别急着写 Python 代码,打开浏览器的开发者工具(F12),跟着下面步骤走,锁定加密逻辑:

  1. 切换到 Network(网络)标签。
  2. 刷新页面,找出返回完整球员信息的请求(通常包含 apidetail)。
  3. 查看请求参数,你会看到有一个很显眼的加密字段:token
  4. 点击 Initiator(启动器)标签,找到发起该请求的 JS 文件位置。
  5. 进去后用 Ctrl+F 搜索 getToken(加密函数的常见命名),定位具体加密代码。

第二步:提取并准备加密代码

在练习站中,getToken 函数依赖了 crypto-js 这个第三方库来执行 DES 加密。为了让它在 Node.js 里正常运行,我们需要做两件事:

  1. 获取 crypto-js 的压缩版 crypto-js.min.js(可以直接从 npm 或 CDN 下载)。
  2. 把加密函数和 crypto-js.min.js 合并到同一个 JS 文件里,并手动解决全局变量挂载问题。

第三步:Python 无缝调用

先整理加密文件(crypto_utils.js)

Node.js 默认不会把第三方库暴露成全局对象,因此我们需要用一个自执行函数手动返回 CryptoJS,并挂到全局:

// 1. 手动挂载全局 CryptoJS(解决 Node.js 环境下的变量问题)
var CryptoJS = (function() {
    // 这里粘贴完整的 crypto-js.min.js 内容,篇幅所限此处省略
    return e();
})();

// 2. 从练习站提取的加密函数
function getToken(player) {
    // 固定密钥(练习站逆向得到,实际项目需要自行分析)
    const key = "XwKsGlMcdPMEhR1B";
    // 只提取加密所需字段
    const plainObj = {
        name: player.name,
        birthday: player.birthday,
        height: player.height,
        weight: player.weight
    };
    // 转为 JSON 字符串
    const plainText = JSON.stringify(plainObj);
    // 执行 DES-ECB-Pkcs7 加密
    const encrypted = CryptoJS.DES.encrypt(
        CryptoJS.enc.Utf8.parse(plainText),
        CryptoJS.enc.Utf8.parse(key),
        {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
        }
    );
    // 返回 Base64 编码的加密结果
    return encrypted.toString();
}

再写 Python 调用代码

import execjs

def generate_spa7_token(player_info):
    # 1. 预编译 JS 文件(只编译一次,后续复用性能更高)
    with open('crypto_utils.js', 'r', encoding='utf-8') as f:
        js_ctx = execjs.compile(f.read())
    
    # 2. 直接调用 JS 函数
    token = js_ctx.call('getToken', player_info)
    return token

if __name__ == "__main__":
    # 练习站的球星数据
    lebron = {
        "name": "LeBron James",
        "birthday": "1984-12-30",
        "height": "2.06m",
        "weight": "113.4kg"
    }
    
    # 生成 Token
    spa7_token = generate_spa7_token(lebron)
    print(f"加密成功!Token:\n{spa7_token}")

运行这段代码,你应该能看到一串 Base64 格式的加密 token,和浏览器请求里的一模一样。


避坑指南+性能小技巧

1. 避坑:CryptoJS 未定义

原因:Node.js 环境下直接 require('crypto-js') 不会自动挂到全局,我们的自执行函数如果没正确返回,也会导致 CryptoJS is not defined 报错。

解决方案

  • 严格使用上文的自执行函数 + 全局挂载方式。
  • 或者在 Python 里预先安装并引入:
    ctx.eval("const CryptoJS = require('crypto-js')")
    前提是当前目录下已经执行 npm install crypto-js

2. 避坑:中文字符乱码

表现:如果加密对象中含有中文,生成出来的 token 和浏览器里的不一致,请求会被拒绝。

解决方案

  • 确保 JS 文件保存为 UTF-8 without BOM 编码(避免 Windows 记事本的默认编码)。
  • Python 读取 JS 文件时务必指定 encoding='utf-8'

3. 性能优化:别每次都编译 JS!

PyExecJS 的 编译过程最耗时,因为每编译一次都会新开一个 Node.js 子进程。如果你要爬 1000 个球星数据,却在循环里反复 execjs.compile,性能会直接崩掉。

优化方式:把 execjs.compile() 提到循环外面,整个脚本只执行一次,一直复用同一个 ctx 对象。

import execjs
import time

# 循环外预编译,只做一次
with open('crypto_utils.js', 'r', encoding='utf-8') as f:
    js_ctx = execjs.compile(f.read())

# 模拟爬取 10 个球星
players = [
    {"name": "LeBron James", "birthday": "1984-12-30", "height": "2.06m", "weight": "113.4kg"},
    # ...省略另外 9 个球星数据
]

start_time = time.time()
for p in players:
    token = js_ctx.call('getToken', p)
end_time = time.time()

print(f"优化后爬取 10 个球星耗时:{end_time - start_time:.2f}s")
# 如果每次循环都重新 compile,耗时可能会膨胀 10 倍以上

这条小技巧在批量调用 JS 函数时能带来质的提升。


总结

通过这篇教程,你掌握了以下核心能力:

  1. 如何正确安装并绑定 PyExecJS + Node.js 的运行环境。
  2. 从浏览器开发者工具逆向出前端加密逻辑的基本思路。
  3. 将依赖第三方库的 JS 加密代码无缝迁移到 Python 中执行。
  4. 解决 CryptoJS 未定义、中文乱码等常见坑,以及通过预编译提升执行性能。

如果未来遇到更复杂的加密——比如高度混淆的 JS 或 WebAssembly,你也可以选择两条路:

  • 进一步研究 JS 逆向(例如 AST 还原、脚本行为分析)。
  • 使用 Selenium / Playwright 这类浏览器自动化工具,牺牲一些性能,但跳过了手动逆向的步骤。

完整示例代码可访问对应的 GitHub 仓库:
https://github.com/Python3WebSpider/ScrapeSpa7