杰瑞科技汇

electron 调用python

由于 Electron 本质上是使用 ChromiumNode.js 的技术栈,而 Python 是一门独立的解释型语言,两者无法直接相互调用,我们需要通过 进程间通信 的方式来建立桥梁。

electron 调用python-图1
(图片来源网络,侵删)

下面我将介绍几种主流的方法,并给出详细的步骤和代码示例,从简单到复杂,你可以根据你的项目需求来选择。


使用 child_process 模块(推荐,最直接)

这是最基本也是最直接的方法,Electron(通过 Node.js)可以直接启动一个 Python 子进程,向它传递参数,并捕获其输出。

工作原理

  1. Electron (主进程) 使用 Node.js 的 child_process 模块中的 spawnexec 方法来执行 Python 脚本。
  2. spawn 更适合处理大量数据流,因为它可以持续地返回数据。
  3. exec 更适合执行命令并获取完整的输出(stdout 和 stderr),但有一个缓冲区大小限制。
  4. 通过标准输入、标准输出 和标准错误流进行通信。

优点

  • 无需任何第三方库,是 Node.js 的原生功能。
  • 简单直接,适合一次性脚本调用或简单的任务。

缺点

  • 通信方式是字符串,如果需要传递复杂的数据结构(如 JSON 对象),需要手动序列化和反序列化。
  • 不适合需要长时间、频繁交互的场景,因为每次调用都需要启动和关闭 Python 解释器,开销较大。

详细步骤与代码示例

项目结构

my-electron-python-app/
├── main.js              # Electron 主进程
├── preload.js           # 预加载脚本,用于渲染进程安全地调用主进程功能
├── renderer.js          # 渲染进程的 JavaScript 代码
├── package.json
└── python_script/
    └── my_script.py     # Python 脚本

Python 脚本 (python_script/my_script.py)

electron 调用python-图2
(图片来源网络,侵删)

这个脚本会从命令行参数中读取输入,执行计算,然后将结果打印到标准输出。

# python_script/my_script.py
import sys
import json
import time
def process_data(data):
    """一个模拟的耗时计算函数"""
    print(f"[Python] Received data: {data}", flush=True)
    # 模拟一个耗时操作
    time.sleep(2)
    # 进行一些计算
    result = {
        "original_input": data,
        "processed_value": data * 2,
        "status": "success",
        "message": "Data processed by Python!"
    }
    return result
if __name__ == "__main__":
    # 从命令行参数获取输入,通常是 JSON 字符串
    if len(sys.argv) > 1:
        input_json = sys.argv[1]
        try:
            input_data = json.loads(input_json)
            result = process_data(input_data)
            # 将结果以 JSON 字符串的形式打印到标准输出
            # 这一步至关重要,Electron 会捕获这个输出
            print(json.dumps(result), flush=True)
        except (json.JSONDecodeError, TypeError) as e:
            error = {
                "status": "error",
                "message": f"Invalid input JSON: {e}"
            }
            print(json.dumps(error), flush=True)
    else:
        print(json.dumps({"status": "error", "message": "No input provided"}), flush=True)

注意: flush=True 确保输出被立即发送,而不是被缓冲。

Electron 主进程 (main.js)

主进程负责创建窗口,并处理与 Python 脚本的通信逻辑。

electron 调用python-图3
(图片来源网络,侵删)
// main.js
const { app, BrowserWindow, ipcMain, spawn } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            // 关键:将预加载脚本的路径告诉渲染进程
            preload: path.join(__dirname, 'preload.js'),
            // 出于安全考虑,建议设置为 false,并通过 preload 暴露必要的 API
            contextIsolation: true, 
            nodeIntegration: false,
        }
    });
    mainWindow.loadFile('index.html'); // 假设你的HTML文件是index.html
}
app.whenReady().then(createWindow);
// 监听从渲染进程发来的 'run-python' 事件
ipcMain.on('run-python', (event, arg) => {
    console.log('[Main] Received request from renderer to run Python script.');
    // 1. 确保你的系统已经安装了 Python
    const pythonPath = 'python'; // 在 macOS/Linux 上
    // const pythonPath = 'python.exe'; // 在 Windows 上
    // 或者使用完整路径,如 'C:/Users/YourUser/AppData/Local/Programs/Python/Python39/python.exe'
    // 2. 准备要传递给 Python 脚本的参数
    // arg 是从渲染进程传来的数据,我们需要将其转为 JSON 字符串
    const scriptPath = path.join(__dirname, 'python_script', 'my_script.py');
    const args = [scriptPath, JSON.stringify(arg)];
    // 3. 使用 spawn 启动 Python 进程
    const pythonProcess = spawn(pythonPath, args);
    let result = '';
    let error = '';
    // 4. 捕获标准输出
    pythonProcess.stdout.on('data', (data) => {
        console.log(`[Python stdout] ${data}`);
        result += data.toString();
    });
    // 5. 捕获标准错误
    pythonProcess.stderr.on('data', (data) => {
        console.error(`[Python stderr] ${data}`);
        error += data.toString();
    });
    // 6. 监听进程结束事件
    pythonProcess.on('close', (code) => {
        console.log(`[Python process] exited with code ${code}`);
        if (code === 0) {
            // 成功,将结果发送回渲染进程
            try {
                // 将 Python 输出的 JSON 字符串解析为 JavaScript 对象
                const parsedResult = JSON.parse(result);
                event.reply('python-result', { success: true, data: parsedResult });
            } catch (e) {
                event.reply('python-result', { success: false, error: 'Failed to parse Python output.' });
            }
        } else {
            // 失败,将错误信息发送回渲染进程
            event.reply('python-result', { success: false, error: error || 'Python script exited with an unknown error.' });
        }
    });
});
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

预加载脚本 (preload.js)

这个脚本是连接渲染进程和主进程的安全桥梁。

// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 向渲染进程暴露一个名为 'electronAPI' 的对象
contextBridge.exposeInMainWorld('electronAPI', {
    // 暴露一个方法,渲染进程可以调用它来触发主进程的 'run-python' 事件
    runPython: (data) => ipcRenderer.send('run-python', data),
    // 暴露一个监听器,用于接收主进程返回的 'python-result' 事件
    onPythonResult: (callback) => ipcRenderer.on('python-result', (_event, result) => callback(result)),
    // 为了防止内存泄漏,提供一个方法来移除监听器
    removePythonListener: () => ipcRenderer.removeAllListeners('python-result')
});

渲染进程 (renderer.jsindex.html)

index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">Electron + Python</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
        #output { margin-top: 20px; padding: 10px; border: 1px solid #ccc; min-height: 100px; white-space: pre-wrap; }
    </style>
</head>
<body>
    <h1>Electron 调用 Python 示例</h1>
    <button id="runBtn">运行 Python 脚本</button>
    <div id="output">点击按钮查看结果...</div>
    <script src="./renderer.js"></script>
</body>
</html>

renderer.js:

// renderer.js
document.addEventListener('DOMContentLoaded', () => {
    const runBtn = document.getElementById('runBtn');
    const outputDiv = document.getElementById('output');
    // 监听来自主进程的结果
    window.electronAPI.onPythonResult((result) => {
        console.log('[Renderer] Received result from main process:', result);
        if (result.success) {
            outputDiv.textContent = `成功!\n${JSON.stringify(result.data, null, 2)}`;
        } else {
            outputDiv.textContent = `失败!\n错误: ${result.error}`;
        }
    });
    runBtn.addEventListener('click', () => {
        outputDiv.textContent = '正在调用 Python...';
        // 准备要发送给 Python 的数据
        const dataToSend = { number: 42 };
        // 通过预加载脚本暴露的方法,向主进程发送请求
        window.electronAPI.runPython(dataToSend);
    });
});

使用 HTTP 服务器(适合复杂交互)

如果你的 Python 应用是一个独立的服务(比如一个 Web API),或者你需要频繁地、双向地通信,那么让 Python 启动一个 HTTP 服务器,然后让 Electron 作为客户端去调用它,是一个非常好的选择。

工作原理

  1. Python 端: 使用 Flask 或 FastAPI 等框架创建一个本地 HTTP 服务器。
  2. Electron 端: 使用 Node.js 的 axiosfetch API 向本地服务器发送 HTTP 请求(GET, POST 等),并接收响应。

优点

  • 通信协议标准化: 使用 HTTP/JSON,是业界标准,易于调试和扩展。
  • 双向通信: 可以轻松实现 Electron 主动请求 Python,也可以通过 WebSocket 实现服务器主动推送消息给 Electron。
  • 解耦: Python 和 Electron 代码可以完全独立开发和部署,只要网络接口保持不变。

缺点

  • 架构更复杂,需要管理一个额外的服务器进程。
  • 需要处理 HTTP 请求和响应,比简单的进程调用有更多的开销。

简单示例 (Python Flask + Electron)

Python 脚本 (app.py)

# app.py
from flask import Flask, request, jsonify
import time
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process():
    data = request.get_json()
    if not data or 'number' not in data:
        return jsonify({"status": "error", "message": "Missing 'number' in request"}), 400
    print(f"[Python Server] Received: {data['number']}")
    time.sleep(2) # 模拟耗时任务
    result = {
        "original_input": data['number'],
        "processed_value": data['number'] * 2,
        "status": "success"
    }
    return jsonify(result)
if __name__ == '__main__':
    # 在 5000 端口启动服务器,允许任何来源的请求(开发时用)
    app.run(port=5000, debug=True)

Electron 主进程 (main.js)

你需要先安装 axios: npm install axios

然后在 main.js 中添加逻辑来启动和停止 Python 服务器。

// main.js (部分代码)
const { spawn } = require('child_process');
const axios = require('axios');
const path = require('path');
// ... (其他 Electron 代码)
let pythonServerProcess;
async function startPythonServer() {
    const scriptPath = path.join(__dirname, 'app.py');
    pythonServerProcess = spawn('python', [scriptPath]);
    pythonServerProcess.stdout.on('data', (data) => {
        console.log(`[Server stdout] ${data}`);
    });
    pythonServerProcess.stderr.on('data', (data) => {
        console.error(`[Server stderr] ${data}`);
    });
    // 等待服务器启动
    await new Promise(resolve => setTimeout(resolve, 3000)); // 简单等待,实际中可以用更健壮的方式检测端口
}
async function callPythonServer(data) {
    try {
        const response = await axios.post('http://127.0.0.1:5000/process', data);
        return { success: true, data: response.data };
    } catch (error) {
        return { success: false, error: error.message };
    }
}
// 在 IPC 处理程序中
ipcMain.on('run-python-via-http', async (event, arg) => {
    if (!pythonServerProcess) {
        await startPythonServer();
    }
    const result = await callPythonServer(arg);
    event.reply('python-http-result', result);
});
// 在应用退出时停止服务器
app.on('before-quit', () => {
    if (pythonServerProcess) {
        pythonServerProcess.kill();
    }
});

然后在 preload.jsrenderer.js 中相应地修改事件名称和调用逻辑即可。


使用 Pyodide(在浏览器中直接运行 Python)

这是一个非常现代和强大的方法,它允许你将 Python 解释器本身编译成 WebAssembly (WASM),直接在 Electron 的渲染进程中运行 Python 代码,无需任何外部 Python 环境

工作原理

  1. 将 Pyodide (CPython 的 WASM 端口) 打包进你的 Electron 应用。
  2. 在渲染进程中加载 Pyodide 的 JavaScript 加载器。
  3. 通过 Pyodide 提供的 API 加载 Python 模块和执行 Python 代码。

优点

  • 零依赖: 最终用户无需安装 Python。
  • 高性能: WASM 的性能接近原生。
  • 简单集成: 对于需要在前端执行的 Python 脚本非常方便。

缺点

  • 无法使用所有 Python 库: 只有纯 Python 或可以编译成 WASM 的 C 扩展才能使用,像 NumPy, Pandas 这样依赖大量 C 代码的库支持有限(虽然社区正在努力)。
  • 打包体积大: Pyodide 自身有十几兆,会增加你的应用体积。
  • 不适合 CPU 密集型任务: WASM 的性能仍然不如原生代码,特别是对于科学计算。

简单示例

  1. 安装 Pyodide: 你可以使用 electron-pyodide-adapter 这样的封装库,或者手动集成。 npm install electron-pyodide-adapter

  2. 修改 renderer.js:

    // renderer.js
    const { loadPyodide } = require('electron-pyodide-adapter');
    document.getElementById('runBtn').addEventListener('click', async () => {
        const outputDiv = document.getElementById('output');
        outputDiv.textContent = 'Loading Pyodide...';
        try {
            // 加载 Pyodide
            const pyodide = await loadPyodide();
            outputDiv.textContent = 'Pyodide loaded. Running Python...';
            // 运行 Python 代码
            // 注意:这里不能直接导入复杂的 C 扩展库
            const result = await pyodide.runPythonAsync(`
                import json
                data = json.loads('${JSON.stringify({ number: 42 })}')
                result = {
                    "original_input": data['number'],
                    "processed_value": data['number'] * 2,
                    "status": "success"
                }
                json.dumps(result)
            `);
            const jsResult = JSON.parse(result);
            outputDiv.textContent = `成功!\n${JSON.stringify(jsResult, null, 2)}`;
        } catch (error) {
            outputDiv.textContent = `失败!\n错误: ${error.message}`;
        }
    });

总结与选择

方法 优点 缺点 适用场景
child_process 简单、原生、无需额外依赖 通信方式原始、不适合高频交互、启动开销大 一次性任务、脚本调用、简单的数据处理
HTTP 服务器 标准化、易于调试、支持双向通信、架构解耦 架构复杂、有网络开销、需管理服务器进程 需要频繁交互、Python 服务化、复杂业务逻辑
Pyodide 零 Python 依赖、前端直接执行、性能尚可 库支持有限、打包体积大、不适合重计算 在浏览器/前端执行纯 Python 脚本、教学、轻量级任务

给你的建议:

  • 如果你只是想偶尔调用一个 Python 脚本来完成一个特定任务(比如文件转换、数据分析),使用 child_process 是最快、最直接的选择。
  • 如果你的 Python 逻辑非常复杂,或者你希望未来能独立部署或扩展这个 Python 服务使用 HTTP 服务器 是更专业、更具扩展性的方案。
  • 如果你的应用需要在用户电脑上完全独立运行,不希望用户安装任何东西,且你的 Python 代码不依赖复杂的 C 库可以考虑 Pyodide

对于大多数开发者来说,从 child_process 开始是最稳妥和常见的入门方式。

分享:
扫描分享到社交APP
上一篇
下一篇