You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
    

问题根因

  1. PHP命令拼接存在语法错误
    非digital分支的命令参数拼接时遗漏空格,实际生成的命令为python3 路径/create_pdf.py hmml{inv值},hmml和传入的inv参数连为一个字符串,导致Python读取sys.argv时只能拿到1个参数,访问第二个参数时直接抛出索引错误,脚本提前退出,根本执行不到写debug.txt的逻辑。
  2. 执行用户权限不匹配
    命令行手动执行脚本时,使用的是当前登录SSH的用户(通常为root或具备全目录权限的运维用户);而PHP通过mod_fcgid执行脚本时,使用的是web服务专用低权限用户(通常为www-data、apache),该用户默认没有/mnt挂载目录的读取权限、PDF输出目录的写入权限,脚本因权限不足直接终止。
  3. 相对路径导致日志写入位置异常
    Python脚本中写debug.txt使用的是相对路径,命令行执行时的工作目录是用户当前所在的SSH目录,而PHP执行shell命令时的工作目录是web服务的运行目录(通常为/或Apache根目录),就算脚本正常执行,日志文件也会写入到其他路径,而非手动执行时查看的目录。
  4. 长耗时操作触发fcgid管道断开
    PDF生成涉及大量图片读取、拼接操作,属于长耗时任务。mod_fcgid默认有严格的请求执行超时限制,若前端用户提前关闭页面、或脚本执行时长超过fcgid的超时阈值,fcgid会强制终止运行中的脚本进程,触发Broken pipe报错。
  5. 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脚本的路径与异常处理
    1. 将日志文件路径改为绝对路径,避免相对路径导致的写入位置错误
    2. 增加全局异常捕获,将所有报错信息写入日志,方便排查问题
    3. 增加参数、目录存在性校验,避免无提示直接退出
      参考修改后的核心逻辑:
    #!/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运行用户授权对应目录的读写权限:
    1. 执行chown -R www-data:www-data /home/你的站点路径/public_html/wp-content/uploads/,保证web用户对日志目录、PDF输出目录有写入权限
    2. 给web用户开通/mnt下对应图片目录的读取权限
    3. 确认Python3及依赖库(fpdf、Pillow、PyPDF2)安装在系统全局路径,而非仅个人用户可见的虚拟环境或用户目录下,保证web用户可以正常调用依赖。
  • 解决fcgid管道断开问题
    1. 最优方案是改为异步执行:PHP收到请求后只将生成任务写入队列,立刻给前端返回响应,通过系统定时任务轮询队列,后台执行Python脚本生成PDF,生成完成后更新状态通知用户,完全避免前台请求阻塞超时。
    2. 若暂时无法改造为异步,可调整mod_fcgid配置,将FcgidIOTimeout、FcgidProcessLifeTime参数调大到足够PDF生成的时长(比如300秒);同时修改PHP调用命令,让脚本脱离请求进程后台执行,避免请求断开导致进程被杀死,命令修改示例:
      $command = escapeshellcmd('python3 '. dirname(__file__). '/create_pdf.py hmml '.$inv.' > /dev/null 2>&1 &');
      

内容的提问来源于stack exchange,提问作者Martha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:15:32