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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章