PHP执行shell调用Python脚本与命令行运行不一致问题排查
WordPress调用Python生成PDF故障排查
问题场景
在WordPress的functions.php文件中编写PHP逻辑,通过shell命令调用Python脚本实现PDF生成能力。
PHP侧实现代码:
if($dig!=""){ $command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py digital '.$inv); }else{ $command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py hmml'.$inv); }
被调用的create_pdf.py脚本内容:
#!/usr/bin/env python3 import os import glob from fpdf import FPDF from PIL import Image import time import sys from PyPDF2 import PdfFileMerger from PyPDF2 import PdfFileReader imagetype = sys.argv[1:][0] pdf_id = sys.argv[1:][1] #path containing images - to change to mnt if imagetype == 'digital': root = '/mnt/../Digital/' rootPDF = '/home/../public_html/wp-content/uploads/Scans/Digital/' else: root = '/mnt/../VHMML/' rootPDF = '/home/../public_html/wp-content/uploads/Scans/VHMML/' for i in os.listdir(root): if pdf_id in i: files = i imageList = sorted(glob.glob(root+files+'/*.jpg')+sorted(glob.glob(root+files+'/*.JPG'))) pdf = glob.glob(root+"*/"+"*.pdf") name = root.split(" ")[0] #path where pdfs are to be stored namePath = rootPDF+pdf_id+".pdf" noImages = len(imageList) factorL = 0.25 factorP = 0.5 im_list = [] start = time.time() with open('debug.txt', 'a') as f: f.write(imagetype) f.write(pdf_id) f.write(str(noImages))
故障现象
- 直接在服务器命令行运行上述Python脚本时,脚本可正常执行并写入
debug.txt调试文件 - 通过PHP触发shell执行该脚本时,
debug.txt文件无写入记录,脚本未按预期运行 - Apache错误日志记录报错信息:
(32)Broken pipe: [client] mod_fcgid: ap_pass_brigade failed in handle_request_ipc function
问题根因
- PHP命令拼接存在语法错误
非digital分支的命令参数拼接时遗漏空格,实际生成的命令为python3 路径/create_pdf.py hmml{inv值},hmml和传入的inv参数连为一个字符串,导致Python读取sys.argv时只能拿到1个参数,访问第二个参数时直接抛出索引错误,脚本提前退出,根本执行不到写debug.txt的逻辑。 - 执行用户权限不匹配
命令行手动执行脚本时,使用的是当前登录SSH的用户(通常为root或具备全目录权限的运维用户);而PHP通过mod_fcgid执行脚本时,使用的是web服务专用低权限用户(通常为www-data、apache),该用户默认没有/mnt挂载目录的读取权限、PDF输出目录的写入权限,脚本因权限不足直接终止。 - 相对路径导致日志写入位置异常
Python脚本中写debug.txt使用的是相对路径,命令行执行时的工作目录是用户当前所在的SSH目录,而PHP执行shell命令时的工作目录是web服务的运行目录(通常为/或Apache根目录),就算脚本正常执行,日志文件也会写入到其他路径,而非手动执行时查看的目录。 - 长耗时操作触发fcgid管道断开
PDF生成涉及大量图片读取、拼接操作,属于长耗时任务。mod_fcgid默认有严格的请求执行超时限制,若前端用户提前关闭页面、或脚本执行时长超过fcgid的超时阈值,fcgid会强制终止运行中的脚本进程,触发Broken pipe报错。 - Python脚本缺少异常判断
脚本中遍历/mnt目录查找匹配pdf_id的子目录时,如果没有找到匹配项,files变量不会被定义,后续拼接图片路径时会直接抛出变量未定义的错误,脚本无捕获直接退出。
修复方案
- 修复PHP命令拼接bug
给非digital分支的参数前补充空格,保证参数能被Python正确识别:if($dig!=""){ $command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py digital '.$inv); }else{ // 补全hmml和$inv之间的空格 $command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py hmml '.$inv); } - 修正Python脚本的路径与异常处理
- 将日志文件路径改为绝对路径,避免相对路径导致的写入位置错误
- 增加全局异常捕获,将所有报错信息写入日志,方便排查问题
- 增加参数、目录存在性校验,避免无提示直接退出
参考修改后的核心逻辑:
#!/usr/bin/env python3 import os import glob import traceback from fpdf import FPDF from PIL import Image import time import sys from PyPDF2 import PdfFileMerger from PyPDF2 import PdfFileReader # 定义绝对路径的日志文件,放到站点uploads目录下保证可写 LOG_PATH = '/home/你的站点路径/public_html/wp-content/uploads/python_exec.log' try: # 校验参数数量 if len(sys.argv) < 3: with open(LOG_PATH, 'a') as f: f.write(f"[{time.ctime()}] 参数错误,收到参数:{str(sys.argv)}\n") sys.exit(1) imagetype = sys.argv[1] pdf_id = sys.argv[2] if imagetype == 'digital': root = '/mnt/对应业务路径/Digital/' rootPDF = '/home/对应站点路径/public_html/wp-content/uploads/Scans/Digital/' else: root = '/mnt/对应业务路径/VHMML/' rootPDF = '/home/对应站点路径/public_html/wp-content/uploads/Scans/VHMML/' # 校验根目录存在 if not os.path.isdir(root): with open(LOG_PATH, 'a') as f: f.write(f"[{time.ctime()}] 图片根目录不存在:{root}\n") sys.exit(1) # 查找匹配的子目录 files = None for i in os.listdir(root): if pdf_id in i and os.path.isdir(os.path.join(root, i)): files = i break if not files: with open(LOG_PATH, 'a') as f: f.write(f"[{time.ctime()}] 未找到ID为{pdf_id}的对应图片目录\n") sys.exit(1) imageList = sorted(glob.glob(os.path.join(root, files, '*.jpg')) + sorted(glob.glob(os.path.join(root, files, '*.JPG')))) namePath = os.path.join(rootPDF, f"{pdf_id}.pdf") noImages = len(imageList) # 原有PDF生成逻辑... # 写调试日志 with open(LOG_PATH, 'a') as f: f.write(f"[{time.ctime()}] 执行成功,类型:{imagetype}, ID:{pdf_id}, 图片数量:{noImages}, 输出路径:{namePath}\n") except Exception as e: with open(LOG_PATH, 'a') as f: f.write(f"[{time.ctime()}] 执行报错:{str(e)}\n{traceback.format_exc()}\n") - 配置正确的目录权限
给web运行用户授权对应目录的读写权限:- 执行
chown -R www-data:www-data /home/你的站点路径/public_html/wp-content/uploads/,保证web用户对日志目录、PDF输出目录有写入权限 - 给web用户开通
/mnt下对应图片目录的读取权限 - 确认Python3及依赖库(fpdf、Pillow、PyPDF2)安装在系统全局路径,而非仅个人用户可见的虚拟环境或用户目录下,保证web用户可以正常调用依赖。
- 执行
- 解决fcgid管道断开问题
- 最优方案是改为异步执行:PHP收到请求后只将生成任务写入队列,立刻给前端返回响应,通过系统定时任务轮询队列,后台执行Python脚本生成PDF,生成完成后更新状态通知用户,完全避免前台请求阻塞超时。
- 若暂时无法改造为异步,可调整mod_fcgid配置,将
FcgidIOTimeout、FcgidProcessLifeTime参数调大到足够PDF生成的时长(比如300秒);同时修改PHP调用命令,让脚本脱离请求进程后台执行,避免请求断开导致进程被杀死,命令修改示例:$command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py hmml '.$inv.' > /dev/null 2>&1 &');
内容的提问来源于stack exchange,提问作者Martha
相关产品推荐
相关产品推荐

