如何在Java中运行Python解释器并获取输出?

3 投票
2 回答
4376 浏览
提问于 2025-04-18 15:21

可以用Java来获取Python的控制台输出吗?下面是一个这样的输出示例:

Python 3.3.4 (v3.3.4:7ff62415e426, Feb 10 2014, 18:13:51) [MSC v.1600 64 bit (AMD64)]
on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 2+2
4
>>>

现在,主要目标是通过Java调用Python解释器来获得上面的输出。以下是我的尝试:

//...
//Irrelevant code omitted

ProcessBuilder processBuilder = new ProcessBuilder("cmd");
processBuilder.redirectErrorStream(true);
processBuilder.start();
processBuilder.command("python2");
Process pythonProcess = processBuilder.start();
OutputStream outputStream = pythonProcess.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(outputStream);
osw.write("2+2\r\nquit()\r\n");
osw.flush();
osw.close();
InputStream inputStream = pythonProcess.getInputStream();
BufferedReader bufferedReader = new BufferedReader(
                  new InputStreamReader(inputStream));
String line;

while( (line=bufferedReader.readLine())!=null) {

    System.out.println(line);

}

//...
//Irrelevant code omitted

我明白调用start方法会启动一个新的进程,并为其设置执行环境。往一个进程的输出流写入python2会导致创建另一个进程。这时候问题就来了。我一直没找到办法把命令2+2发送给Python解释器(它是CMD的子进程),而不是发送给它的父进程。

总结一下:我该如何运行Python解释器,在里面执行一些命令,最后把结果打印到标准输出上呢?

2 个回答

5

Asim Ihsan的回答指出了问题的根本原因——Python会知道它没有连接到一个交互式终端,因此不会按照你想要的方式运行。不过,你可以通过在启动Python时加上-i这个参数,让Python进入交互模式。这样做比Asim提议的方法要简单。你只需要直接启动Python(不需要先打开cmd)。你可以使用类似下面的命令:

ProcessBuilder pb = new ProcessBuilder('python', '-i');

然后接下来就可以按照原问题中的方式进行操作。启动进程,获取相关的流,然后对这些流进行读写。

5

Python的执行程序能够判断你是在非交互模式下运行命令。一旦它意识到自己是在这种模式下,它就不会再尝试和你互动了;如果没有人可以回应,打印信息到标准输出或从标准输入读取又有什么意义呢?

要验证这一点,你可以尝试运行像“ls”或“ps”这样的命令,看看它们在你的程序中能正常工作,但如果你运行“ftp”或“telnet”或“python”,就会发现它们不工作,而且没有任何输出。

在Linux的术语中,问题在于我们运行进程的方式没有给它们连接一个TTY。解决这个问题的方法是通过创建一个PTY来欺骗它们,让它们相信另一端有一个TTY。

欺骗一个应用程序,让它认为它的标准输入是交互式的,而不是管道

在:

  • 我的Mac OS X 10.9.4笔记本上,使用的是CPython 2.7.5和Java 1.8.0_05,以及
  • 一台运行Ubuntu 12.04.4 LTS的服务器,使用的是CPython 2.7.5和Java 1.7.0_55

以下代码可以工作,尽管方式很丑陋:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

class Foo {
    public static void main(String[] args) throws IOException, InterruptedException {
        // https://stackoverflow.com/questions/1401002/trick-an-application-into-thinking-its-stdin-is-interactive-not-a-pipe
        // 
        // Using script or unbuffer is the important catch. Without this
        // step you cannot use stdin in Python interactively, even with
        // python -u. At least script comes with Linux/Mac OS X, but
        // unbuffer works fine too.
        ProcessBuilder pb;
        switch(System.getProperty("os.name")) {
            case "Mac OS X":
                pb = new ProcessBuilder(
                    "/usr/bin/script", "-q", "/dev/null", "/usr/bin/python");
                break;
            default:
                // Linux
                pb = new ProcessBuilder(
                    "/usr/bin/script", "-qfc", "/usr/bin/python", "/dev/null");

        }
        // This doesn't make a difference.
        // pb.redirectErrorStream(true);

        Process p = pb.start();

        char[] readBuffer = new char[1000];
        InputStreamReader isr = new InputStreamReader(p.getInputStream());
        BufferedReader br = new BufferedReader(isr);
        int charCount;
        boolean written = false;
        while(true) {
            if (!br.ready() && !written) {
                // Ugly. Should be reading for '>>>' prompt then writing.
                Thread.sleep(1000);
                if (!written) {
                    written = true;
                    OutputStream os = p.getOutputStream();
                    OutputStreamWriter osw = new OutputStreamWriter(os);
                    BufferedWriter bw = new BufferedWriter(osw);
                    bw.write("2+2");
                    bw.newLine();
                    bw.write("quit()");
                    bw.newLine();
                    bw.flush();
                    bw.close();
                }
                continue;
            }
            charCount = br.read(readBuffer);
            if (charCount > 0)
                System.out.print(new String(readBuffer, 0, charCount));
            else
                break;
        }
    }
}

我不会这样做。相反,我会使用线程来保持交互性,避免在读取时阻塞,并像expect那样,在写出之前等待特定的提示。在上面的代码中,我盲目地让程序休眠,然后希望一切顺利。

不过,我注意到你在使用Windows,因为你运行了“cmd”。我不知道如何在Windows上创建PTY,抱歉。不过我想你可以在Windows上获取Expect;unbuffer是Expect中的一个工具:

http://expect.sourceforge.net/

或者试试cygwin,但我也没有测试过这个。

关于TTY和PTY的更多背景知识可以参考:

如何创建一个伪TTY以读取输出和写入输入

撰写回答