Python 执行 CMD 命令的几种方法及最佳实践
2025-03-03 02:10:35
Python 执行 CMD 命令行指令的几种方法
最近有个小项目,需要用 Python 调用外部程序。具体来说,就是在 Python 脚本里,让 Windows 的 cmd(命令提示符)去执行一些命令行指令。 比方说,我想先用cd
命令切换工作目录,然后执行一个外部程序 externalprogram
,带着一堆参数那种。
刚开始有点懵,查了半天资料,踩了不少坑。最后总算搞定了,把几种常用的方法总结一下。
问题出在哪?
原始代码大概是这样:
import os
os.chdir('C:\Program Files (x86)\<externalProgram>')
os.system('<externalCommand> "<destination>\file.fileType" "<source>\*.*"')
这段代码虽然不会报错,但实际上可能没达到预期效果。主要有两个问题:
-
os.chdir
:这个函数在 Python 里用来改变 Python 解释器 的当前工作目录,而不是 cmd 的。换句话说,你改变了 Python 自己“站”的位置,但 cmd “站”的位置没变。后续在 Python 里用相对路径访问文件可能会受到影响,但os.system
打开的 cmd 不会“理会”这个变化。 -
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.Popen
是 subprocess
模块中最底层的接口。它提供了最精细的控制,但使用起来也更复杂一些。
原理:
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.stdout
和process.stderr
中一行行读取(上面的示例代码中已给出).
总结一下
如果只是简单地执行一下命令,不关心输出,也不需要特别精细的控制,os.system
最方便,但不建议。
大多数情况下,subprocess.run
就够用了。它既安全又灵活。
如果你需要与子进程进行更复杂的交互(比如实时处理输出、向子进程发送输入),那就用 subprocess.Popen
。