Java运行Python脚本

    前段时间遇到了在JavaWeb项目中嵌入运行Python脚本的功能的需求。想到的方案有两种,一种是使用Java技术(Jython或Runtime.exec)运行Python脚本,另一种是搭建一个Python工程对外提供相应http或webservice接口。两种方案我都有实现,简单的测试了一下,本机环境两者的执行效率没有太大的差距。考虑到项目的情况,最终选择了第一种方案。
    闲话少说,我们赶紧看看Java怎么实现运行Python脚本的吧~

Jython实现

    简单来说,我们常说的Python是指CPython,由C语言编写。而这里说的Jython是由Java编写,在JVM中运行。显然,Jython天然就对Java有很高的亲和度(本身就是Java写的),能够利用JVM相关的技术,库和函数等资源。
    Jython从2.0版本开始就和Python的版本保持一致,目前最新的版本为2.7.2。Jython的官方下载网址:https://www.jython.org/download。网址中有两个下载包Jython Installer、Jython Standalone。Jython Installer类似一个应用程序(exe),双击安装即可在cmd中使用Jython;而Jython Standalone可以作为jar包直接引入到项目中使用。对于项目编程,直接引入Maven依赖即可。

<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython-standalone</artifactId>
    <version>2.7.1</version>
</dependency>

注意:引用的不是org.python.jython,这将会产生一堆疑难杂症。引入的是jython-standalone,这是专门给java调用jython的包,有较多的jar库支持。

Jython运行Python脚本

    Jython主要是使用PythonInterpreter类,调用方法大致有两种形式。

面向函数调用

    根据需要调用的方法名获取对应的PyFunction对象,调用__call__方法获取方法的返回值。
    在这种调用方式下,python脚本里面是定义了一些函数,且用Jython调用的时候,需要知道所要调用函数的名称。比如python脚本为:

# coding=utf-8
def main(str):
	print(str)

    那么java的关键代码如下:

PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec(script);
// python脚本中的方法名为main
PyFunction function = interpreter.get("main",PyFunction.class);
// 将参数代入并执行python脚本
PyObject o = function.__call__(Py.newString(params));

面向对象调用

    另外一种调用方式是,Python脚本是定义了一个类。可以通过类名获取对应的python对象,再用该对象调用类中定义的方法获取返回值。比如python脚本为:

# coding=utf-8
class Test(object):
	def main(self,str):
		print(str)

    那么java的关键代码如下:

PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec(script);
// 获取Python对象
PyObject pyObject = interpreter.get("Test");
// 调用方法,方法名为main。将参数代入并执行python脚本,获取返回值
// 传入参数对象的个数要与调用的方法一致
PyObject o= pyObj.invoke("main",new PyObject[]{Py.newString(params)})); 

引入第三方包

    Python作为最流行的开源编程语言之一,拥有广泛的第三方包,应该说当下的Python编程已无法离开这丰富的第三方包的支持。Jython本身也包含较多的jar包支持日常的编程,同时也支持引入第三方包。引入的方式如下两种,第一种是引入Jython本身提供的jar包:以引入site为例:

Properties props = new Properties();
props.put("python.import.site", "false");
Properties preprops = System.getProperties();
PythonInterpreter.initialize(preprops, props, new String[0]);
PythonInterpreter interpreter = new PythonInterpreter();

    第二种可以直接指定引入的第三方包所在的目录:

PythonInterpreter interpreter = new PythonInterpreter();
PySystemState sys = interpreter.getSystemState();
sys.path.add("C:/jython2.7.1/Lib");

    第二种方法有另一种写法:

PythonInterpreter interpreter = new PythonInterpreter();
// 下面是加入jython的库,需要指定jar包所在的路径。如果有自己写的包或者第三方,可以继续追加
interpreter.exec("import sys");
interpreter.exec("sys.path.append('C:/jython2.7.1/Lib')");
interpreter.exec("sys.path.append('C:/jython2.7.1/Lib/site-packages')");

Jython实现优缺点

    优点:本身就是Java编写的,使用起来比较方便,且相比后面的Runtime.exec的路子相对比较简单。
    缺点:现在最流行的还是Python(即CPython),不管是社区活跃程度还是更新迭代速度都是Jython望尘莫及的,这也意味着Python许多强大的第三方包是无法即时更新到Jython库中,即便Jython提供引入第三方包的功能,但是总会有莫名其妙的问题,总是莫名的提示导包出错。编程的时候有不少时间浪费在处理导包问题,很影响使用的心情。
    而最致命的问题是,Jython竟然不支持中文!!!只要Python脚本中包含中文,就会一直报错。我在网上找了很多资料,也尝试去在代码中转码,还是无法解决,也没有找到解决方法。由于我在使用的时候还是使用2.7.1版本,不清楚2.7.2版本有没有解决这个问题。
    如果有朋友解决了这个问题,欢迎分享一下解决方式,谢谢~~
    总之在项目中,我是彻底放弃使用Jython了。

Runtime.exec实现

    Jdk提供Runtime类用于执行JVM外的程序,其效果类似于调用命令行执行指令。在安装了Python的前提下,我们可以模拟命令行窗口执行Python的脚本。如该Runtime类除了可以执行Python代码,也可执行Java、shell代码等,具有非常强大的功能。但需要注意的是,Runtime类并不等同于命令行。
    在命令行中,我们可以逐行执行脚本代码:
在这里插入图片描述
    然而使用Runtime的时候,我发现并不能每行代码像命令行那样逐行用exec执行,猜测是每次exec后返回的结果之间没有联系。
    如果要使用Runtime.exec执行Python脚本,就要模拟命令行的命令:python py文件的路径 参数...

    该命令的第一个参数为字符串“python”,指定命令的执行程序为python。这个需要在系统中安装python程序。有些系统可能同时安装了python2和python3,这个需要先在命令行中执行一次python看调用的是哪个。一般默认python调用python2,python3调用python3。
    第二个参数是需要执行的Python脚本文件的绝对路径。也就是说Python脚本文件在执行的过程中需要存放在本地。
    第三个参数是参数列表,如果脚本中需要传入参数,那么这里需要根据形参的顺序依次传入对应的实参。

    比方说,如果需要执行的python脚本名字为Hello.py,存放在E盘下,那么其绝对路径为E:/Hello.py,该脚本需要传入一个字符串参数,假如为“world”,那么在cmd执行的命令为:python E:/Hello.py "world"
    在Java中可以用一个字符串数组存储上面的命令:

String command = "E:/Hello.py";
String params = "world";
String[] cmd = new String[]{"python",command,params};
Process process = Runtime.getRuntime().exec(cmd);

这里有个坑,当params是一个包含空格的字符串时,脚本执行结果会有问题。
比如String params=“this is a brand new day”。那么实际模拟cmd执行的命令是这样的:python E:/Hello.py this is a brand new day
看出问题了吗?我们理想认为参数是一个(this is a brand new day),但实际上却变成了6个,第一个是this,第二个是is,依次类推。由于脚本只需要一个参数,所以只会把第一个参数(this)传入到脚本中执行!
解决方法很简单:只要在params字符串前后加上双括号即可!String params="\"this is a brand new day\""

    在获取到Process对象后,通过getInputStream()获取子进程标准输入流、getErrorStream()获取子进程错误流,从而得到脚本的执行结果、错误反馈信息。并使用waitFor()使当前线程等待至子进程结束。但如果在同一个线程中,主进程和子进程有可能会出现相互等待对方结束而出现死锁的情况。这时候需要新开一个线程将获取标准输入流或错误流分开。完整的关键代码如下:

/* 注意:cmd的格式:“python py文件的路径 参数...”
 *  注意2:参数是字符串的时候,有可能会出现参数只解析第一个词的情况,此时必须在整个字符串参数首尾手动添加双引号(单引号都不行)
 *  则下面的cmd=python E:/test/pythontest/Demo.py “params”
 */
//String cmd = String.format("python %s \"%s\"",command,params);
// 也可以用String[],但是params传入前也需要手动在字符串前后加双引号
params = "\"" + params + "\"";
String[] cmd = new String[]{"python",command,params};
Process process = Runtime.getRuntime().exec(cmd);

// error的要单独开一个线程处理。其实最好分成两个子线程分别处理标准输出流和错误输出流
ProcessStream stderr = new ProcessStream(process.getErrorStream(), "ERROR", charset);
stderr.start();
// 获取标准输出流的内容
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), charset));
while ((line = stdout.readLine()) != null) {
    rtnSb.append(line).append("\r\n");
}
rtnMap.put("result",rtnSb.toString());
rtnMap.put("error",stderr.getContent());
//关闭流
stdout.close();
int status = process.waitFor();
if (status != 0) {
    System.out.println("return value:"+status);
}
process.destroy();

    到此,两种方法均已介绍完毕。附上完整运行工具类和相应的进程工具类:

import org.python.core.Py;
import org.python.core.PyFunction;
import org.python.core.PyObject;
import org.python.core.PySystemState;
import org.python.util.PythonInterpreter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * java执行python的工具类
 *  使用条件,python代码中有一个main方法,其他代码都在main方法中执行
 * @author hjw
 */

public class RunPython {

    private static Logger logger = LoggerFactory.getLogger(RunPython.class);

    /**
     * 使用jython运行py代码,缺点:一旦引用第三方库容易报错,而即便手动设置第三方库,也有可能出现错误
     * @param script python解析代码
     * @param params python代码中的参数
     * @return
     */
    public static Map<String,Object> runPythonByJython(String script, String params){
        Map<String,Object> rtnMap = new HashMap<>();

        Properties props = new Properties();
        props.put("python.import.site", "false");
        Properties preprops = System.getProperties();
        PythonInterpreter.initialize(preprops, props, new String[0]);
        PythonInterpreter interpreter = new PythonInterpreter();
//        // 下面是加入jython的库,需要指定jar包所在的路径。如果有自己写的包或者第三方,可以继续追加
//        interpreter.exec("import sys");
//        interpreter.exec("sys.path.append('C:/jython2.7.1/Lib')");
//        interpreter.exec("sys.path.append('C:/jython2.7.1/Lib/site-packages')");
        try {
            interpreter.exec(script);
            // 假设python有一个main方法,包含所有实现需求的代码。换言之,传来的python代码只需要执行main方法就能完成需求
            PyFunction function = interpreter.get("main",PyFunction.class);
            // 将报文代入并执行python进行解析
            PyObject o = function.__call__(Py.newString(params));
            rtnMap.put("result",o);
            interpreter.cleanup();
            interpreter.close();
        } catch (Exception e) {
            e.printStackTrace();
            rtnMap.put("error",e);
        }
        return rtnMap;
    }
    
    /**
     * 使用Runtime.getRuntime().exec()解析运行python
     * @param command 解析的python代码,即py文件的路径
     * @param params python代码中的参数
     * @param charset 码表
     * @return
     */
    public static Map<String,Object> runPythonByRuntime(String command, String params, String charset) {
        Map<String,Object> rtnMap = new HashMap<>();
        String line;
        StringBuffer rtnSb = new StringBuffer();
        try {
            /* 注意:cmd的格式:“python py文件的路径 参数...”
             *  注意2:参数是字符串的时候,必须在首尾手动添加双引号(单引号都不行)
             *  则下面的cmd=python E:/test/pythontest/Demo.py “params” */
//            String cmd = String.format("python %s \"%s\"",command,params);
            // 也可以用String[],但是params传入前也需要手动在字符串前后加双引号
            String[] cmd = new String[]{"python",command,params};
            Process process = Runtime.getRuntime().exec(cmd);
            // error的要单独开一个线程处理。其实最好分成两个子线程处理标准输出流和错误输出流
            ProcessStream stderr = new ProcessStream(process.getErrorStream(), "ERROR", charset);
            stderr.start();
            // 获取标准输出流的内容
            BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), charset));
            while ((line = stdout.readLine()) != null) {
                rtnSb.append(line).append("\r\n");
            }
            rtnMap.put("result",rtnSb.toString());
            rtnMap.put("error",stderr.getContent());
            //关闭流
            stdout.close();
            int status = process.waitFor();
            if (status != 0) {
                System.out.println("return value:"+status);
                logger.info("event:{}", "RunExeForWindows",process.exitValue());
            }
            process.destroy();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return rtnMap;
    }
}

线程工具类:

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * java执行python的线程工具类
 */
public class ProcessStream extends Thread {

    private static Logger logger = LoggerFactory.getLogger(ProcessStream.class);
    private InputStream inputStream;
    private String streamType;
    private StringBuffer buf;
    private String charset;
    private volatile boolean isStopped = false; // 用于判断本线程是否执行完毕,用volatile保证线程安全

    public ProcessStream(InputStream inputStream, String streamType, String charset) {
        this.inputStream = inputStream;
        this.streamType = streamType;
        this.buf = new StringBuffer();
        this.charset = charset;
        this.isStopped = false;
    }

    @Override
    public void run() {
        try {
            // 默认编码为UTF-8,如果有传入编码,则按外部编码
            String exactCharset = StringUtils.isBlank(charset) ? "UTF-8" : charset;
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, exactCharset));
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                this.buf.append(line + "\n");
            }
            bufferedReader.close();
        } catch (IOException e) {
            logger.error("Failed to successfully consume and display the input stream of type " + streamType + ".", e);
        } finally {
            this.isStopped = true;
            synchronized (this) {
                notify();
            }
        }
    }

    /**
     * 当主线程调用本方法获取本子线程的输出时,若本子线程还没执行完毕,主线程阻塞到子线程完成后再继续执行
     */
    public String getContent() {
        if (!this.isStopped) {
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException ignore) {
                    ignore.printStackTrace();
                }
            }
        }
        return this.buf.toString();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章