ppython的研究与实践:认识ppython

最近接到一个任务,是在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个月前。
github源码截图

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

不足之处,还请各位读者批评指正
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值