执行 Python (Manim) 代码
本文档介绍了基于 Java-Linux 项目环境下如何通过 Java 程序执行 Python 代码(主要用于执行 Manim 脚本)的实现方案。文档包括整体架构、工作流程说明以及完整的代码实现,确保不遗漏您提供的任何代码。
1. 项目概述
该项目旨在提供一个基于 Java 后端的 Python 代码执行服务。用户通过 HTTP 请求提交包含 Manim 脚本的代码,系统将自动执行脚本并返回执行结果。项目主要包含两个模块:
请求处理层(ManimHanlder)
负责接收 HTTP 请求、提取代码内容,并调用业务逻辑处理后返回 JSON 格式的执行结果。业务逻辑层(ManimService)
负责代码预处理(目录创建、占位符替换)、将代码保存为脚本文件,启动 Python 进程执行脚本,捕获输出日志,并检查生成的 Manim 视频文件。
2. 工作流程与架构
2.1 整体流程
接收请求
用户通过 HTTP 请求提交 Manim 代码,ManimHanlder
的index
方法接收请求,并将代码内容记录到日志中。代码预处理与文件存储
ManimService.executeCode(String code)
方法中:- 创建用于缓存执行结果的目录(如
cache
)。 - 利用 Snowflake 算法生成唯一任务 ID,并在
cache
和scripts
目录下分别创建对应的子目录。 - 替换代码中的占位符
#(output_path)
为生成的输出路径。 - 将代码写入生成的 Python 脚本文件中。
- 创建用于缓存执行结果的目录(如
执行 Python 脚本
使用ProcessBuilder
构造适合当前操作系统的命令(Windows 下调用python
,Linux/Unix 下调用python3
),启动 Python 进程执行脚本,并捕获标准输出和错误输出。返回执行结果
收集执行结果(标准输出、错误输出、退出码),检查生成的视频文件(路径为cache/{taskId}/videos/1080p30/CombinedScene.mp4
),将结果封装到ProcessResult
对象中,并通过 HTTP 响应返回给客户端。
2.2 异常处理
- 在请求处理层和业务逻辑层均采用异常捕获机制,确保在出现异常时:
- 日志中记录详细错误信息,便于问题排查;
- HTTP 响应返回状态码 500,并附带错误信息。
3. 完整代码
3.1 ManimHanlder.java
package com.litongjava.linux.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.linux.ProcessResult;
import com.litongjava.linux.service.ManimService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimHanlder {
ManimService manimService = Aop.get(ManimService.class);
public HttpResponse index(HttpRequest request) {
String code = request.getBodyString();
log.info("code:{}", code);
HttpResponse response = TioRequestContext.getResponse();
try {
ProcessResult executeScript = manimService.executeCode(code);
executeScript.setExecuteCode(code);
response.setJson(executeScript);
} catch (Exception e) {
log.error(e.getMessage(), e);
response.setStatus(500);
response.setString(e.getMessage());
}
return response;
}
}
3.2 ManimService.java
package com.litongjava.linux.service;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import com.jfinal.kit.Kv;
import com.jfinal.template.Engine;
import com.jfinal.template.Template;
import com.litongjava.linux.ProcessResult;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimService {
public ProcessResult executeCode(String code) throws IOException, InterruptedException {
new File("cache").mkdirs();
long id = SnowflakeIdUtils.id();
String subFolder = "cache" + File.separator + id;
code = code.replace("#(output_path)", subFolder);
//Template template = Engine.use().getTemplateByString(code);
//code = template.renderToString(Kv.by("output_path", subFolder));
String folder = "scripts" + File.separator + id;
File fileFolder = new File(folder);
if (!fileFolder.exists()) {
fileFolder.mkdirs();
}
String scriptPath = folder + File.separator + "script.py";
FileUtil.writeString(code, scriptPath, StandardCharsets.UTF_8.toString());
ProcessResult execute = execute(scriptPath);
execute.setTaskId(id);
String filePath = subFolder + File.separator + "videos" + File.separator + "1080p30" + File.separator + "CombinedScene.mp4";
File file = new File(filePath);
if (file.exists()) {
execute.setOutput(filePath.replace("\\", "/"));
} else {
log.info("file is not exists:{}", filePath);
}
return execute;
}
public static ProcessResult execute(String scriptPath) throws IOException, InterruptedException {
// 构造 ProcessBuilder
String osName = System.getProperty("os.name");
ProcessBuilder pb = null;
if (osName.toLowerCase().contains("windows")) {
pb = new ProcessBuilder("python", scriptPath);
} else {
pb = new ProcessBuilder("python3", scriptPath);
}
pb.environment().put("PYTHONIOENCODING", "utf-8");
Process process = pb.start();
// 读取标准输出 (可能包含base64以及脚本本身的print信息)
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
// 读取错误输出
BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8));
// 用于存放所有的标准输出行
StringBuilder outputBuilder = new StringBuilder();
String line;
while ((line = stdInput.readLine()) != null) {
outputBuilder.append(line).append("\n");
}
// 收集错误输出
StringBuilder errorBuilder = new StringBuilder();
while ((line = stdError.readLine()) != null) {
errorBuilder.append(line).append("\n");
}
// 等待进程结束
int exitCode = process.waitFor();
// 构造返回实体
ProcessResult result = new ProcessResult();
result.setExitCode(exitCode);
result.setStdOut(outputBuilder.toString());
result.setStdErr(errorBuilder.toString());
return result;
}
}
4. 使用说明
4.1 代码提交与执行流程
提交代码
用户通过 HTTP POST 请求将包含 Manim 脚本的代码提交至系统,请求体中包含待执行的代码内容。请求处理
ManimHanlder.index
方法接收请求,并记录代码日志,然后调用ManimService.executeCode
方法进行处理。脚本预处理与执行
在ManimService
中:- 创建
cache
和scripts
目录; - 替换代码中的
#(output_path)
占位符; - 将代码写入
script.py
文件; - 通过
ProcessBuilder
调用 Python 解释器执行脚本,并捕获标准输出与错误输出。
- 创建
返回执行结果
执行结果封装在ProcessResult
对象中,其中包含:- 任务 ID
- 脚本执行的标准输出和错误输出
- 执行退出码
- 若生成视频文件存在,则返回视频文件的路径
异常处理
如果执行过程中发生异常,则返回 HTTP 500 状态,并在日志中记录详细的异常信息,便于调试和维护。
4.2 环境要求
Python 环境
- Linux/Unix 系统需安装
python3
; - Windows 系统下安装
python
。
- Linux/Unix 系统需安装
目录权限
服务器需要具备在项目目录下创建cache
和scripts
子目录及写入文件的权限。
6. 总结
本项目通过 Java 后端动态执行 Python (Manim) 代码,利用目录隔离、占位符替换和标准输出捕获技术,实现了一个灵活且高效的代码执行器。完整的代码实现已在本文档中展示,确保不遗漏您提供的任何代码。该方案不仅便于用户提交和执行 Manim 脚本,还通过日志记录和异常处理机制提高了系统的健壮性和可维护性。