文章

php联动python不完全攻略

本文写于较早之前,文章内容可能过期。

写在前面

最近在做的一个php项目有需要用到python作为其中某一个功能的实现。尽管这个功能也不是不能用php实现,但是python上有现成的轮子,本着能用轮子就不自己造的原则,决定使用php调用python的形式实现。

虽然但是,目前这个解决方案并不是完美方案,个人评价仅为方便可用(doable),在文章中我会探讨一些我没有采用或实现的可能方法,欢迎指导。

环境

OS:Ubuntu 18.04.3 LTS
PHP: 7.2.6
python: 3.6.8 (+NLTK, json两个库)

环境配置方面其实有一个小坑,python如果有需要用到比较多的包(如tensorflow、beautifulsoup之类)就会涉及到如何配置服务器环境的问题。提供一些可行的思路:使用anaconda的环境配置文件通过这个文件在服务器上重新下载对应的包;使用pyinstaller将python打包成可执行程序(win上打包成exe,linux也有对应的运行程序,但由于win只能打包exe,若要打包成linux的需要用linux开发,因此我没有使用这种方法,理论上这种方法是最舒服的);使用docker等。

php调用python基础

php要调用python有几种大的方向:一是python作为独立的脚本被php运行并获取输出结果(PHP执行系统外部命令函数);二是php和python通过某种方式通信,实现真正意义上的交互(Python作为PHP的扩展——ppython简介)。

这里第二种方法涉及到的知识点比较复杂,涉及到了socket等,有关的介绍也比较少,因此没有使用。

第一种方法主要是通过执行系统命令的方式。主要函数有exec()、passthru()、system()、shell_exec()四个。详细的使用可以看上面的第一篇文章,我用的是exec()

首先来看函数声明,主要就是要把命令传过去,然后用一个数组接收返回结果和返回值。这里一共就有三个坑,第一是传值的问题,第二是返回结果的问题,第三就是debug的问题,之后会详细讲。

function exec(string $command,array[optional] $output,int[optional] $return_value)

有了这个之后就可以进行尝试了。

ps: 如果在尝试的时候遇到问题,可以先看文章最后面的debug,能够拿到报错信息对debug真的至关重要。

Babysteps

python这边比较简单,用sys.argv提取命令行参数,然后用print输出即可。

import sysparams = sys.argv[1]
print(params)

php这边我用的是tp框架,因此路径用了框架的预定义常量,其他情况正确填写路径即可。(推荐使用绝对路径,但不要写死绝对路径,用常量的方式去定义根目录等,方便更改环境的时候只需要改一个位置。)

坑!! command这里一定不要漏掉中间的空格。

要执行的命令如:python3 FILEDIR/try_trans.py 111就不能漏掉py和111中间的空格。

public function testPython()
    {
        $fileDir = ROOT_PATH . 'python_script/';

        $value = 111;

        exec('python3 '.$fileDir. 'try_trans.py '.$value, $output);

        var_dump($output);
    }

用postman请求,可以看到正确返回了111。

之所以称之为babystep是因为这里只涉及到了简单的数值传值,说明是能用的,然后我们继续下去。

ps: 如果提示exec() has been disabled for security reasons之类的,需要把php.ini中disabled_functions = passthru, exec, ...对应的删掉,然后重启php-fpm即可。

安全问题:这里值得商榷的是这些函数默认是禁用的,因为启用这些功能之后比较容易出现提权问题,可以通过php执行系统命令了。

传值

考虑到项目中需要传递结构比较复杂的数据,用json是比较方便的。当然如果只需要传数值这种简单的可以跳过这一节不看。

传值涉及的问题主要有编码和转义。解决传值这个坑实际上花了最多的时间,而最后只能采取一种折中的方式,属实难受。

编码

首先在开发过程中最好就是所有的编码都使用UTF-8,包括读写文件、序列化json等等。一般而言指定UTF-8能解决八成的编码问题。

经过探索,控制台(console)中使用的一般是GBK编码(中文),因此如果在exec中直接在参数里传递UTF-8的json,会出现在python中死活读不到的情况。另外postman可能也存在一些自动识别编码的机制使得我很难判断过程中编码的转换,中间还涉及到转义的问题,整个过程十分折腾,而且跨语言debug实在不太方便(后面有提到 debug方法),后来我放弃了…

如果你比较有毅力,

转义

因为json字符串内部有非常多的双引号,所以传值的时候要给python传进去一个完整的json字符串就很麻烦。这里提供一个一般情况可以用的做法:

$value = addslashes(json_encode($value, JSON_UNESCAPED_UNICODE))

这里一般推荐加上JSON_UNESCAPED_UNICODE这个选项,可以避免中文被编码成\uxxxx的问题,方便debug。在这里这个选项可以防止中文字符被加上转义的斜杠。

我最终没有使用成功的原因是json数组里面可能会出现双引号,而这个双引号我已经事先转义,就会出现反转义时整个字符串崩开的情况。虽然可以通过事先把双引号替换成单引号规避这个问题,但总体容易出现问题,不如采用折中一点的办法。

最终解决方案

其实主要的问题就在于用命令行传参的时候的编码和转义问题,那么如果我不通过命令行传递这个json,我把json保存下来,通过传文件名(甚至只用传一个整形值)的形式,不就完美避开这个坑了吗。

贴一下代码

php部分

$value = json_encode($value, JSON_UNESCAPED_UNICODE);

$fileName = ROOT_PATH . "python_script/" .time().'.json';
file_put_contents($fileName, $value);

exec('python3 '.$fileDir. 'main.py '.'"'.$fileName.'"', $output);

$output = json_decode($output[0]);

python部分

param = sys.argv[1]
with open(param+"", 'r', encoding='utf-8') as answer_f:
    answer = json.load(answer_f)

重点就在于用一个time()区分开文件,这里用其他的随机串也可。其实这里传参传了文件的字符串有一些多余了,可以在python里写好路径,然后只传一个time(unix时间戳,一般可以保证唯一),不然还要考虑参数两边要加上双引号的问题。

返回结果

前面说到跟python交互来回都可以使用json,json在返回的时候意外地舒服,基本没有遇到问题。

直接贴代码

python部分

import json
res = {}
...
json_str = json.dumps(res)
print(json_str)

需要注意这里的res是一个字典。python中会把json映射成字典,因此对于json对象的操作都是字典的操作。关于python中json的使用可以参考这篇文章,不多赘述。

另外在直接在控制台运行观察输出时,这样输出的json可能会有中文显示成\uxxxx的问题,可以通过如下的方式使得中文正常输出,但用php接收时要去掉这一个,否则php可能会报无法处理json的问题。

json_str = json.dumps(res, ensure_ascii=False)

php部分

$output = json_decode($output[0]);

php部分就十分简单了,exec会返回一个array,这里注意接收的时候array里面的每一个元素就是一次print就可以了。这里很神奇并没有出现编码的问题,所有中文都能正常处理,很玄学。

Debug

一开始我对自己过分自信,觉得写的python应该不会有问题,因此在很长一段时间里都在盲debug,没有看到错误信息,浪费了很多时间。

首先,在写python的时候就可以直接用命令行进行调试,先解决python内的错误。

然后,在php调用python的时候,在命令后面加一个2>&1,就可以把错误流输出(详情看PHP:用exec()函数输出命令错误信息, php调用python调试)。另外,exec的第三个参数可以接收一个int值,这个值就是python运行之后返回给系统的值(一般正常运行退出会return 0,出错会返回1),也可作debug或异常处理用途。

最后

这个方案确实不太完美,但将就能用。中间一些问题列举如下,欢迎指导

  1. 环境部署可以有更好的解决方案
  2. 通过系统命令行方式执行python可能会有安全问题,且开销比较大,性能比较差
  3. 传值需要用到中间文件,实际使用时可能需要增加垃圾回收机制
  4. 跨语言debug有些麻烦
License:  CC BY 4.0