最近接到一个任务,是在php上将office文件转换为pdf格式。首先想到的是利用php的COM插件调用office的COM组件,试了一下,然后就一直报错调用不通。先说一下,笔者是13版的office。后来又考虑到其他原因,索性放弃这个方案,在查询资料的时候看到了这个ppython技术。
一、ppython介绍
以下这篇文章讲的很详细,笔者就不在这儿过多阐述。
链接如下:
https://linux.cn/article-10856-1.html
github源码地址:
https://github.com/maiwang79/PPython-again
补充说明:
github上说了如何将ppython放在linux环境下以服务的方式运行,其实windows下大同小异。都是得先有个python运行环境,然后将php_python.py 与 process.py放置在一个固定目录,比如ppython,然后用cmd命令,或直接双击php_python.py运行。运行时是一个cmd窗口,下篇会说如何打包成一个服务放到后台运行。
使用时,一定要将php_python.php放置到你的php运行环境下面,然后在php里用$result = ppython({python_func},{params}…)的方式调用吧。
二、ppython源码分析
1、下载源码
看了一下源码发现此项目的确已经很久不维护了,最新时间是4个月前。
2、运行源码
将源码导入PyCharm源码后发现,核心代码一共有两个php_python.py、process.py。检查了一下,代码还有语法错误大概是后面python版本升级,未及时维护的原因吧。修改后代码文末附上。(注:以下都是我修改后代码)
代码逻辑非常清晰,原理也很简单(跟java的反射非常类似)。php是通过socket的方式与python进行通信,比起传统的exec、system方式好很多。过程如下:
- 首先,php将模块名称及函数、参数等,以socket的方式传递给python
- 检查预编译字典中没有此编译模块,没有的话调用import执行动态的导入
- 调用getattr加载函数(此处个人感觉直接用getattr的返回值执行就行,不知为何又单独去加载函数执行,大概是为了判断函数是否可用吧)
- compile加载函数并执行
废话不多数,直接上源码。如下(源码我加入了log日志功能,字符编码改为gb2312):
php_python.py
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import time
import socket
import logging
from logging.handlers import TimedRotatingFileHandler
import process
# -------------------------------------------------
# 基本配置
# -------------------------------------------------
LISTEN_PORT = 10240 # 服务侦听端口
CHARSET = "gb2312" # 设置字符集(和PHP交互的字符集)
# -------------------------------------------------
# 日志配置
# -------------------------------------------------
# 自定义 Logger 配置
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[TimedRotatingFileHandler(filename="logs\\ppython.log", when='D', encoding="utf-8")])
_Log = logging.getLogger("php_python")
# -------------------------------------------------
# 主程序
# 请不要随意修改下面的代码
# -------------------------------------------------
if __name__ == '__main__':
_Log.info("-------------------------------------------")
_Log.info("- PPython Service")
_Log.info("- Time: %s" % time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
_Log.info("-------------------------------------------")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP/IP
sock.bind(('', LISTEN_PORT))
sock.listen(5)
_Log.info("Listen port: %d" % LISTEN_PORT)
_Log.info("charset: %s" % CHARSET)
_Log.info("Server startup...")
while 1:
connection, address = sock.accept() # 收到一个请求
# print ("client's IP:%s, PORT:%d" % address)
# 处理线程
try:
process.ProcessThread(connection).start()
except Exception as e:
_Log.error(e)
process.py
# -*- coding: UTF-8 -*-
# -------------------------------------------------
# 请不要随意修改文件中的代码
# -------------------------------------------------
import sys
import threading
import json
import numpy
import logging
import php_python
REQUEST_MIN_LEN = 10 # 合法的request消息包最小长度
TIMEOUT = 180 # socket处理时间180秒
pc_dict = {} # 预编译字典,key:调用模块、函数、参数字符串,值是编译对象
global_env = {} # global环境变量
_Log = logging.getLogger("php_python")
def index(bytes, c, pos=0):
"""
查找c字符在bytes中的位置(从0开始),找不到返回-1
pos: 查找起始位置
"""
for i in range(len(bytes)):
if i <= pos:
continue
if bytes[i] == c:
return i
else:
return -1
def z_encode(p):
"""
encode param from python data
"""
if p is None: # None->PHP中的NULL
return "N;"
elif isinstance(p, int): # int->PHP整形
return "i:%d;" % p
elif isinstance(p, str): # String->PHP字符串
p_bytes = p.encode(php_python.CHARSET)
ret = 's:%d:"' % len(p_bytes)
ret = ret.encode(php_python.CHARSET)
ret = ret + p_bytes + '";'.encode(php_python.CHARSET)
ret = str(ret, php_python.CHARSET)
return ret
elif isinstance(p, bool): # boolean->PHP布尔
b = 1 if p else 0
return 'b:%d;' % b
elif isinstance(p, float): # float->PHP浮点
return 'd:%r;' % p
elif isinstance(p, list) or isinstance(p, tuple): # list,tuple->PHP数组(下标int)
s = ''
for pos, i in enumerate(p):
s += z_encode(pos)
s += z_encode(i)
return "a:%d:{%s}" % (len(p), s)
elif isinstance(p, dict): # 字典->PHP数组(下标str)
s = ''
for key in p:
s += z_encode(key)
s += z_encode(p[key])
return "a:%d:{%s}" % (len(p), s)
elif isinstance(p, numpy.ndarray): # add by yaoer,多维数组结构
s = ''
i = 0
for d in p:
s += 'i:%d;%s' % (i, z_encode(d))
i += 1
return "a:%d:{%s}" % (len(p), s)
elif isinstance(p, numpy.complex128): # add by yaoer,复数结构
t1 = str(p.real)
t2 = str(p.imag)
return 'O:7:"complex":2:{s:4:"real";d:%s;s:4:"imag";d:%s;}' % (t1, t2)
else: # 其余->PHP中的NULL
return json.dumps(p)
# return "N;"
def z_decode(p):
"""
decode php param from string to python
p: bytes
"""
if p[0] == 0x4e: # NULL 0x4e-'N'
return None, p[2:]
elif p[0] == 0x62: # bool 0x62-'b'
if p[2] == 0x30: # 0x30-'0'
return False, p[4:]
else:
return True, p[4:]
elif p[0] == 0x69: # int 0x69-'i'
i = index(p, 0x3b, 1) # 0x3b-';'
return int(p[2:i]), p[i + 1:]
elif p[0] == 0x64: # double 0x64-'d'
i = index(p, 0x3b, 1) # 0x3b-';'
return float(p[2:i]), p[i + 1:]
elif p[0] == 0x73: # string 0x73-'s'
len_end = index(p, 0x3a, 2) # 0x3a-':'
str_len = int(p[2:len_end])
end = len_end + 1 + str_len + 2
v = p[(len_end + 2): (len_end + 2 + str_len)]
return str(v, php_python.CHARSET), p[end + 1:]
elif p[0] == 0x61: # array 0x61-'a'
list_ = [] # 数组
dict_ = {} # 字典
flag = True # 类型,true-元组 false-字典
second = index(p, 0x3a, 2) # 0x3a-":"
num = int(p[2:second]) # 元素数量
pp = p[second + 2:] # 所有元素
for i in range(num):
key, pp = z_decode(pp) # key解析
if i == 0: # 判断第一个元素key是否int 0
if (not isinstance(key, int)) or (key != 0):
flag = False
val, pp = z_decode(pp) # value解析
list_.append(val)
dict_[key] = val
return (list_, pp[2:]) if flag else (dict_, pp[2:])
else:
return p, ''
def parse_php_req(p):
"""
解析PHP请求消息
返回:元组(模块名,函数名,入参list)
"""
while p:
v, p = z_decode(p) # v:值 p:bytes(每次z_decode计算偏移量)
params = v
module_func = params[0] # 第一个元素是调用模块和函数名
# print("模块和函数名:%s" % module_func)
# print("参数:%s" % params[1:])
pos = module_func.find("::")
module = module_func[:pos] # 模块名
func = module_func[pos + 2:] # 函数名
return module, func, params[1:]
class ProcessThread(threading.Thread):
"""
preThread 处理线程
"""
def __init__(self, socket):
threading.Thread.__init__(self)
# 客户socket
self._socket = socket
def run(self):
# ---------------------------------------------------
# 1.接收消息
# ---------------------------------------------------
try:
self._socket.settimeout(TIMEOUT) # 设置socket超时时间
first_buf = self._socket.recv(16 * 1024) # 接收第一个消息包(bytes)
if len(first_buf) < REQUEST_MIN_LEN: # 不够消息最小长度
_Log.error("非法包,小于最小长度: %s" % first_buf)
self._socket.close()
return
firstComma = index(first_buf, 0x2c) # 查找第一个","分割符
totalLen = int(first_buf[0:firstComma]) # 消息包总长度
_Log.info("消息长度:%d" % totalLen)
reqMsg = first_buf[firstComma + 1:]
while len(reqMsg) < totalLen:
reqMsg = reqMsg + self._socket.recv(16 * 1024)
# 调试
# print ("请求包:%s" % reqMsg)
except Exception as e:
_Log.error('接收消息异常', e)
self._socket.close()
return
# ---------------------------------------------------
# 2.调用模块、函数检查,预编译。
# ---------------------------------------------------
# 从消息包中解析出模块名、函数名、入参list
module, func, params = parse_php_req(reqMsg)
if module not in pc_dict: # 预编译字典中没有此编译模块
# 检查模块、函数是否存在
try:
callMod = __import__(name=module) # 根据module名,反射出module
pc_dict[module] = callMod # 预编译字典缓存此模块
except Exception as e:
_Log.error('模块不存在:%s' % module)
self._socket.sendall(("F" + "module '%s' is not exist!" % module).encode(php_python.CHARSET)) # 异常
self._socket.close()
return
else:
callMod = pc_dict[module] # 从预编译字典中获得模块对象
try:
getattr(callMod, func)
except Exception as e:
_Log.error('函数不存在:%s' % func)
self._socket.sendall(("F" + "function '%s()' is not exist!" % func).encode(php_python.CHARSET)) # 异常
self._socket.close()
return
# ---------------------------------------------------
# 3.Python函数调用
# ---------------------------------------------------
try:
params = ','.join([repr(x) for x in params])
# print ("调用函数及参数:%s(%s)" % (module+'.'+func, params) )
# 加载函数
compStr = "import %s\nret=%s(%s)" % (module, module + '.' + func, params)
# print("函数调用代码:%s" % compStr)
rpFunc = compile(compStr, "", "exec")
if func not in global_env:
global_env[func] = rpFunc
local_env = {}
exec(rpFunc, global_env, local_env) # 函数调用
# print (global_env)
# print (local_env)
except Exception as e:
_Log.error('调用Python业务函数异常', e)
errType, errMsg, traceback = sys.exc_info()
self._socket.sendall(("F%s" % errMsg).encode(php_python.CHARSET)) # 异常信息返回
self._socket.close()
return
# ---------------------------------------------------
# 4.结果返回给PHP
# ---------------------------------------------------
# retType = type(local_env['ret'])
# print ("函数返回:%s" % retType)
rspStr = z_encode(local_env['ret']) # 函数结果组装为PHP序列化字符串
try:
# 加上成功前缀'S'
rspStr = "S" + rspStr
# 调试
# print ("返回包:%s" % rspStr)
self._socket.sendall(rspStr.encode(php_python.CHARSET))
except Exception as e:
_Log.error('发送消息异常', e)
errType, errMsg, traceback = sys.exc_info()
self._socket.sendall(("F%s" % errMsg).encode(php_python.CHARSET)) # 异常信息返回
finally:
self._socket.close()
return
3、其他
ppython源码还考虑了多线程并发执行的情。另外,以作者所说,还加入了对复数、多维数组的支持,这一点笔者未做测试。
4、附件
我改造的PPython下载地址:
https://gitee.com/minpsn/ppython