返回

Python 执行 CMD 命令的几种方法及最佳实践

python

Python 执行 CMD 命令行指令的几种方法

最近有个小项目,需要用 Python 调用外部程序。具体来说,就是在 Python 脚本里,让 Windows 的 cmd(命令提示符)去执行一些命令行指令。 比方说,我想先用cd命令切换工作目录,然后执行一个外部程序 externalprogram,带着一堆参数那种。

刚开始有点懵,查了半天资料,踩了不少坑。最后总算搞定了,把几种常用的方法总结一下。

问题出在哪?

原始代码大概是这样:

import os

os.chdir('C:\Program Files (x86)\<externalProgram>')
os.system('<externalCommand> "<destination>\file.fileType" "<source>\*.*"')

这段代码虽然不会报错,但实际上可能没达到预期效果。主要有两个问题:

  1. os.chdir:这个函数在 Python 里用来改变 Python 解释器 的当前工作目录,而不是 cmd 的。换句话说,你改变了 Python 自己“站”的位置,但 cmd “站”的位置没变。后续在 Python 里用相对路径访问文件可能会受到影响,但 os.system 打开的 cmd 不会“理会”这个变化。

  2. os.system: 这个函数直接执行,缺乏错误处理。比如调用失败,不容易排查。并且直接使用字符串拼接执行命令有安全风险。

下面具体说说怎么解决这些问题。

解决方案

方法一:os.system,简单粗暴

os.system 是最直接的方法。它会创建一个子进程,在子进程中执行你给的命令。

原理:

os.system 底层调用了操作系统的 shell(在 Windows 上就是 cmd.exe)来执行命令。你传给 os.system 的字符串,会被 shell 直接解释执行。

代码示例:

import os

# 方法1: 使用 & 连接符 (不推荐,因为不好处理错误,且容易有注入风险)
command = 'cd "C:\\Program Files (x86)\\<externalProgram>" & <externalCommand> "<destination>\\file.fileType" "<source>\\*.*"'
os.system(command)
# 或拆分成多次 os.system 调用(避免使用)
# os.chdir('C:\\Program Files (x86)\\<externalProgram>')  # 通常不需要,见下方解释
# os.system('<externalCommand> "<destination>\\file.fileType" "<source>\\*.*"')

注意事项:

  • 使用&符号可能不太安全, 不容易 debug。
  • 因为os.system的返回值只是一个命令执行的退出状态码(一般 0 表示成功,非 0 表示失败)。所以,你最好用下面的其他方法。

方法二:subprocess.run,灵活可靠

Python 的 subprocess 模块是官方推荐的执行外部命令的工具。它比 os.system 更强大、更灵活。 推荐用 subprocess.run

原理:

subprocess.run 也是创建子进程来执行命令。但它提供了更精细的控制:

  • 你可以捕获命令的输出(标准输出、标准错误)。
  • 可以检查命令的执行结果(成功还是失败)。
  • 可以设置超时时间。
  • 可以更安全地传递参数(避免 shell 注入攻击)。

代码示例:

import subprocess

# 完整例子:

external_program_path = "C:\\Program Files (x86)\\<externalProgram>\\<externalCommand>.exe"  # 最好给出完整路径
destination = "<destination>"
source = "<source>"
file_type = "file.fileType"
file_name = "newName"
# 使用列表传递参数,避免 shell 注入风险
args = [
    external_program_path,
    "-w",  # 假设 -w 是你的外部程序需要的参数
    f"{destination}\\{file_name}.{file_type}",  # 使用 f-string 构造目标路径
    f"{source}\\*.*",
]

try:
    result = subprocess.run(args, capture_output=True, text=True, check=True, cwd="C:\\Program Files (x86)\\<externalProgram>")

    # 如果需要查看命令的输出:
    print("标准输出:", result.stdout)
    print("标准错误:", result.stderr)

except subprocess.CalledProcessError as e:
    print(f"命令执行失败: {e}")
    print("标准输出:", e.stdout)
    print("标准错误:", e.stderr)
    print("返回码", e.returncode)

except FileNotFoundError:
    print(f"找不到程序:{external_program_path}")

# 其他方法: 指定 shell=True (不推荐,除非必要,可能有安全风险)
# result = subprocess.run('your_command', shell=True, capture_output=True, text=True, check=True)

参数说明:

  • args:一个列表,包含要执行的命令及其参数。强烈建议使用列表,避免 shell 注入。
  • capture_output=True:捕获命令的标准输出和标准错误。
  • text=True:将输出解码为文本(字符串)。
  • check=True:如果命令执行失败(返回码非 0),抛出 subprocess.CalledProcessError 异常。
  • cwd: 指定程序的工作目录,相当于先cd
  • shell=True: 如果为Ture,允许使用字符串而不是数组传递参数.有安全风险,不建议。

安全建议:

  • 尽量避免使用 shell=True。如果必须使用,务必确保命令字符串中的用户输入部分经过了严格的过滤和转义,防止 shell 注入攻击。
  • 使用列表来传递参数,不要手动拼接命令字符串。

方法三:subprocess.Popen,精细控制

subprocess.Popensubprocess 模块中最底层的接口。它提供了最精细的控制,但使用起来也更复杂一些。

原理:

subprocess.Popen 创建一个子进程,并返回一个 Popen 对象。你可以通过这个对象与子进程进行交互:

  • 可以向子进程的标准输入发送数据。
  • 可以读取子进程的标准输出和标准错误。
  • 可以等待子进程结束,或者手动终止子进程。

代码示例:

import subprocess

external_program_path = "C:\\Program Files (x86)\\<externalProgram>\\<externalCommand>.exe"
destination = "<destination>"
source = "<source>"
file_type = "file.fileType"
file_name = "newName"

args = [
    external_program_path,
    "-w",
    f"{destination}\\{file_name}.{file_type}",
    f"{source}\\*.*",
]

# 创建 Popen 对象
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,cwd="C:\\Program Files (x86)\\<externalProgram>")

# 获取输出(可以实时获取,也可以等进程结束后一次性获取)
# 实时获取:
for line in process.stdout:
   print("输出:", line.strip())

for line in process.stderr:
   print("错误:", line.strip())
# 等待进程结束
return_code = process.wait()

if return_code != 0:
    print(f"命令执行失败,返回码:{return_code}")

#一次性获取
# stdout, stderr = process.communicate()
# print("标准输出:", stdout)
# print("标准错误:", stderr)

进阶技巧:

  • subprocess.PIPE 用于创建一个管道。这样可以用于读取命令行的输出。
  • 你可以使用communicate()方法一次性读取标准输出和标准错误, 或者用循环分别从process.stdoutprocess.stderr中一行行读取(上面的示例代码中已给出).

总结一下

如果只是简单地执行一下命令,不关心输出,也不需要特别精细的控制,os.system 最方便,但不建议。

大多数情况下,subprocess.run 就够用了。它既安全又灵活。

如果你需要与子进程进行更复杂的交互(比如实时处理输出、向子进程发送输入),那就用 subprocess.Popen