精通 Python:轻松执行 Shell 命令并捕获输出
在现代软件开发和系统管理中,与操作系统底层进行交互是一项常见的需求。Shell 命令作为与操作系统沟通的桥梁,其强大功能不言而喻。Python 作为一种功能全面且易于上手的编程语言,提供了多种执行 Shell 命令并捕获其输出的方法。本文将深入探讨 Python 中执行 Shell 命令的各种技术,从简单直接的 os.system
到功能强大且推荐的 subprocess
模块,帮助您轻松驾驭这一关键技能。
为什么需要 Python 执行 Shell 命令?
在许多场景下,我们可能需要从 Python 脚本中调用外部程序或 Shell 命令:
- 自动化系统任务:例如,执行备份脚本、管理用户、配置网络服务等。
- 利用现有工具:许多强大的命令行工具(如
grep
,awk
,sed
,git
,ffmpeg
等)已经存在,直接调用它们比用 Python 重写功能更高效。 - 获取系统信息:例如,获取磁盘空间、内存使用情况、网络状态等。
- 与其他程序集成:调用其他语言编写的命令行程序,实现跨语言协作。
- 简化复杂工作流:将一系列 Shell 命令串联起来,并通过 Python 脚本进行控制和管理。
Python 提供了灵活的机制来满足这些需求,让开发者能够无缝地将 Shell 的力量融入到 Python 应用中。
方法一:os.system()
—— 简单但不推荐
os.system()
是执行 Shell 命令最简单直接的方式。它接受一个字符串参数,该参数即为要执行的命令。
“`python
import os
示例 1: 列出当前目录文件 (Linux/macOS)
command = “ls -l”
示例 1: 列出当前目录文件 (Windows)
command = “dir”
return_code = os.system(command)
print(f”命令 ‘{command}’ 已执行完毕,返回码: {return_code}”)
示例 2: 创建一个新目录
new_dir_command = “mkdir my_new_directory” # Linux/macOS
new_dir_command = “md my_new_directory” # Windows
os.system(new_dir_command)
“`
工作原理:
os.system()
会在子 Shell 中执行命令。在 Unix-like 系统上,它通常调用 /bin/sh -c command
;在 Windows 上,它调用 cmd.exe /c command
。
优点:
* 简单易用:一行代码即可执行命令。
缺点:
* 无法捕获输出:os.system()
将命令的输出直接打印到标准输出流(通常是控制台),Python 脚本本身无法直接获取这些输出内容进行处理。
* 返回码有限:它只返回命令的退出状态码。通常,0 表示成功,非 0 表示错误,但具体的非 0 值的含义取决于被调用的命令。
* 安全风险:如果命令字符串中包含用户输入,且未经过严格的过滤和转义,很容易造成 Shell 注入漏洞。例如,如果 user_input
是 "; rm -rf /"
, 那么 os.system(f"echo {user_input}")
可能会带来灾难性后果。
* 平台依赖性:命令本身是平台相关的(如 ls
vs dir
)。
* 缺乏精细控制:无法控制标准输入、标准错误流,也无法设置超时。
何时使用:
由于上述缺点,os.system()
通常不推荐在生产环境或需要捕获输出、关注安全性的场景中使用。它可能适用于一些非常简单的、内部使用的、命令固定的快速脚本。
方法二:os.popen()
—— 捕获输出的初步尝试
os.popen()
是 os.system()
的一个改进,它允许我们像操作文件一样打开一个到命令的管道,从而可以读取命令的输出或向命令写入输入。
os.popen(command, mode='r', buffering=-1)
command
: 要执行的 Shell 命令字符串。mode
: 模式,可以是'r'
(读取,默认) 或'w'
(写入)。buffering
: 可选的缓冲参数。
“`python
import os
示例 1: 读取命令输出 (Linux/macOS)
command_to_run = “ls -l”
示例 1: 读取命令输出 (Windows)
command_to_run = “dir”
‘r’模式表示读取命令的标准输出
pipe = os.popen(command_to_run, ‘r’)
output = pipe.read() # 读取所有输出
output = pipe.readlines() # 按行读取输出到一个列表
关闭管道非常重要,它会等待命令完成并返回退出状态
退出状态存储在 close() 方法的返回值中,但格式与 os.system 不同
需要右移8位获取实际的退出码
exit_status_raw = pipe.close()
if exit_status_raw is not None:
exit_code = os.waitstatus_to_exitcode(exit_status_raw) # Python 3.9+
# 对于旧版本 Python,可以使用 exit_status_raw >> 8
# exit_code = exit_status_raw >> 8
print(f”命令 ‘{command_to_run}’ 的输出:\n{output}”)
print(f”命令退出码: {exit_code}”)
else:
print(f”命令 ‘{command_to_run}’ 执行失败或无法获取退出码。”)
print(f”命令输出:\n{output}”)
示例 2: 向命令写入 (不常用,因为 subprocess 提供了更好的方式)
假设有一个命令 ‘sort’ 可以从标准输入读取数据并排序
sort_command = “sort” # Linux/macOS
pipe_write = os.popen(sort_command, ‘w’)
pipe_write.write(“banana\n”)
pipe_write.write(“apple\n”)
pipe_write.write(“cherry\n”)
exit_status_raw_write = pipe_write.close()
if exit_status_raw_write is not None:
print(f”Sort命令退出码: {os.waitstatus_to_exitcode(exit_status_raw_write)}”)
“`
优点:
* 可以捕获标准输出:通过返回的类文件对象,可以读取命令的标准输出。
缺点:
* 仍然存在安全风险:与 os.system()
类似,如果命令字符串来自不可信来源,容易受到 Shell 注入攻击。
* 功能有限:无法同时处理标准输出和标准错误,难以获取精确的错误信息。
* 已部分废弃:官方文档建议使用 subprocess
模块替代 os.popen()
。
* 获取退出码复杂:需要通过 pipe.close()
的返回值并进行转换。
os.popen()
相较于 os.system()
有了进步,但其功能和安全性仍有不足。现代 Python 开发中,它已被更强大的 subprocess
模块所取代。
方法三:subprocess
模块 —— 现代 Python 的首选
subprocess
模块是 Python 官方推荐的用于创建和管理子进程的模块。它提供了比 os.system()
和 os.popen()
更强大、更灵活、更安全的功能。
该模块的核心思想是创建一个新的子进程,连接到它的输入/输出/错误管道,并获取其返回码。
3.1 subprocess.run()
—— 推荐的高级接口 (Python 3.5+)
subprocess.run()
是执行外部命令并等待其完成的最简单且推荐的方法。它提供了非常灵活的参数配置。
基本语法:
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
常用参数解释:
args
: 要执行的命令。推荐传入一个列表,其中第一个元素是命令本身,后续元素是命令的参数。例如['ls', '-l', '/tmp']
。如果shell=True
,则可以传入一个字符串。capture_output=True
: 如果设置为True
,则会捕获标准输出和标准错误,并将它们存储在返回的CompletedProcess
对象的stdout
和stderr
属性中。text=True
(或universal_newlines=True
,在 Python 3.7+ 中text
是推荐的别名): 如果设置为True
,stdout
和stderr
将被解码为字符串(使用encoding
参数指定的编码,默认为系统区域设置)。否则,它们将是字节串。check=True
: 如果设置为True
,并且命令以非零退出码结束,则会抛出CalledProcessError
异常。这对于快速错误处理非常有用。shell=False
(默认):- 当
shell=False
时,命令和参数应该以列表形式传递(如['ls', '-l']
)。这种方式更安全,因为它不涉及 Shell 的解析,避免了 Shell 注入的风险。 - 当
shell=True
时,命令可以是一个包含 Shell 元字符(如|
,*
,>
,&&
)的字符串(如"ls -l | grep .py"
)。但要极其小心,如果命令字符串中包含任何外部输入,务必确保输入是安全的,否则可能导致严重的安全漏洞。 通常应避免shell=True
,除非你确切知道自己在做什么,并且命令是完全受控的。
- 当
stdout
,stderr
: 可以设置为subprocess.PIPE
来捕获相应的输出,或者设置为文件对象以重定向输出。capture_output=True
是stdout=subprocess.PIPE
和stderr=subprocess.PIPE
的简写。input
: 一个字节串(或字符串,如果text=True
)传递给子进程的标准输入。timeout
: 命令执行的超时时间(秒)。如果超时,子进程将被杀死,并抛出TimeoutExpired
异常。cwd
: 设置子进程的当前工作目录。
返回值:
subprocess.run()
返回一个 CompletedProcess
对象,它包含以下常用属性:
* args
: 传递给 run()
的 args
参数。
* returncode
: 子进程的退出状态码。0 通常表示成功。
* stdout
: 捕获的标准输出(如果 capture_output=True
或 stdout=subprocess.PIPE
)。字节串或字符串(取决于 text
参数)。
* stderr
: 捕获的标准错误(如果 capture_output=True
或 stderr=subprocess.PIPE
)。字节串或字符串(取决于 text
参数)。
示例:
“`python
import subprocess
示例 1: 基本用法,列出文件 (推荐列表形式,shell=False)
command_list = [‘ls’, ‘-l’, ‘.’] # Linux/macOS
command_list = [‘dir’, ‘.’] # Windows
try:
result = subprocess.run(command_list, capture_output=True, text=True, check=True, timeout=10)
print(“命令执行成功!”)
print(“返回码:”, result.returncode)
print(“标准输出:\n”, result.stdout)
if result.stderr:
print(“标准错误:\n”, result.stderr)
except subprocess.CalledProcessError as e:
print(f”命令执行失败: {e}”)
print(“返回码:”, e.returncode)
print(“标准输出:\n”, e.stdout)
print(“标准错误:\n”, e.stderr)
except subprocess.TimeoutExpired as e:
print(f”命令执行超时: {e}”)
# e.stdout 和 e.stderr 可能包含超时前捕获的部分输出
if e.stdout:
print(“部分标准输出:\n”, e.stdout.decode(errors=’ignore’) if isinstance(e.stdout, bytes) else e.stdout)
if e.stderr:
print(“部分标准错误:\n”, e.stderr.decode(errors=’ignore’) if isinstance(e.stderr, bytes) else e.stderr)
except FileNotFoundError:
print(f”命令 ‘{command_list[0]}’ 未找到。请确保它在系统 PATH 中或提供完整路径。”)
except Exception as e:
print(f”发生未知错误: {e}”)
print(“-” * 30)
示例 2: 执行一个不存在的命令,演示 check=False 和 check=True 的区别
non_existent_command = [‘nonexistentcmd’]
result_no_check = subprocess.run(non_existent_command, capture_output=True, text=True) # check=False (默认)
print(f”‘{non_existent_command[0]}’ (check=False) 返回码: {result_no_check.returncode}”)
print(f”‘{non_existent_command[0]}’ (check=False) stderr: {result_no_check.stderr}”)
try:
subprocess.run(non_existent_command, check=True, text=True) # check=True
except subprocess.CalledProcessError as e:
print(f”‘{non_existent_command[0]}’ (check=True) 捕获到 CalledProcessError: {e}”)
except FileNotFoundError:
print(f”‘{non_existent_command[0]}’ (check=True) 捕获到 FileNotFoundError: 命令未找到。”)
print(“-” * 30)
示例 3: 使用 shell=True (谨慎使用!)
假设我们需要使用管道
shell_command = “echo ‘Hello Python’ | wc -w” # Linux/macOS
shell_command_windows = “echo Hello Python && echo World” # Windows,使用 && 连接两个命令
try:
# 注意:在 Windows 上,dir | findstr .py
这样的管道可能需要 cmd /c "dir | findstr .py"
# 或者直接使用 shell=True
,但要确保命令安全。
# result_shell = subprocess.run(shell_command, shell=True, capture_output=True, text=True, check=True)
result_shell = subprocess.run(shell_command_windows, shell=True, capture_output=True, text=True, check=True)
print(f”Shell命令 ‘{shell_command_windows}’ 输出: {result_shell.stdout.strip()}”)
except subprocess.CalledProcessError as e:
print(f”Shell命令执行失败: {e}”)
print(“-” * 30)
示例 4: 向命令传递输入 (input 参数)
sort_command_list = [‘sort’] # Linux/macOS
windows 下没有直接的 sort 从 stdin 读取,这里用一个 Python 脚本模拟
创建一个 test_sort.py:
import sys
for line in sorted(sys.stdin):
print(line, end=”)
sort_command_list = [‘python’, ‘-c’, “import sys; print(”.join(sorted(sys.stdin.readlines())))”]
input_data = “banana\napple\ncherry\n”
try:
result_input = subprocess.run(sort_command_list, input=input_data, capture_output=True, text=True, check=True)
print(“排序后的输出:\n”, result_input.stdout)
except subprocess.CalledProcessError as e:
print(f”带输入的命令执行失败: {e}\nStderr: {e.stderr}”)
except FileNotFoundError:
print(“Python 解释器未找到或模拟排序的脚本路径不正确。”)
“`
subprocess.run()
是大多数场景下的理想选择,因为它简洁、功能全面且易于理解。
3.2 subprocess.Popen()
—— 更底层的接口,实现更高级的交互
当需要对子进程进行更精细的控制时,例如非阻塞 I/O、与长时间运行的进程进行持续交互、或者构建复杂的管道时,subprocess.Popen
类提供了更大的灵活性。
Popen
对象在创建后,子进程会立即开始执行,而 Python 脚本可以继续执行其他任务,或者通过 Popen
对象的方法与子进程交互。
主要方法:
* p.communicate(input=None, timeout=None)
: 与进程交互。向 stdin 发送数据(如果 input
提供),然后读取 stdout 和 stderr 直到文件结束符。它会等待进程终止。返回一个 (stdout_data, stderr_data)
元组。
* p.wait(timeout=None)
: 等待子进程终止。返回退出码。
* p.poll()
: 检查子进程是否已经终止。如果终止,返回退出码,否则返回 None
。这是一个非阻塞调用。
* p.send_signal(signal)
: 向子进程发送信号。
* p.terminate()
: 终止子进程 (发送 SIGTERM
)。
* p.kill()
: 杀死子进程 (发送 SIGKILL
)。
* p.stdin
, p.stdout
, p.stderr
: 如果在 Popen
构造函数中将相应的参数设置为 subprocess.PIPE
,则这些属性是连接到子进程标准流的文件对象。
示例 1: 基本的 Popen 用法和 communicate
“`python
import subprocess
command = [‘ls’, ‘-lha’] # Linux/macOS
command = [‘dir’] # Windows
try:
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# communicate() 会等待进程结束,并一次性获取所有输出
# 这对于输出量不大的命令很方便
stdout, stderr = process.communicate(timeout=15) # 设置超时
return_code = process.returncode # 或者 process.wait() 也可以获取
if return_code == 0:
print("Popen 命令执行成功!")
print("标准输出:\n", stdout)
if stderr:
print("标准错误:\n", stderr)
else:
print(f"Popen 命令执行失败,返回码: {return_code}")
print("标准输出:\n", stdout)
print("标准错误:\n", stderr)
except subprocess.TimeoutExpired:
process.kill() # 如果超时,确保杀死进程
stdout, stderr = process.communicate() # 尝试获取残留的输出
print(“Popen 命令超时!”)
if stdout:
print(“部分标准输出 (超时前):\n”, stdout)
if stderr:
print(“部分标准错误 (超时前):\n”, stderr)
except FileNotFoundError:
print(f”命令 ‘{command[0]}’ 未找到。”)
except Exception as e:
print(f”Popen 执行时发生错误: {e}”)
“`
示例 2: 构建管道 (例如 dmesg | grep -i error
)
这展示了 Popen
如何连接多个进程的输入输出。
“`python
import subprocess
仅在 Linux/macOS 上有意义
p1_command = [‘dmesg’]
p2_command = [‘grep’, ‘-i’, ‘error’]
try:
# 第一个进程,输出到管道
p1 = subprocess.Popen(p1_command, stdout=subprocess.PIPE, text=True)
# 第二个进程,从第一个进程的输出管道读取输入
# p1.stdout 作为 p2 的 stdin
p2 = subprocess.Popen(p2_command, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# 关闭 p1.stdout,允许 p1 在 p2 读取完毕后接收 SIGPIPE (如果 p2 提前退出)
p1.stdout.close()
# 从第二个进程获取最终输出
stdout_final, stderr_final = p2.communicate(timeout=10)
if p2.returncode == 0:
print(f”管道命令 ‘{‘ ‘.join(p1_command)} | {‘ ‘.join(p2_command)}’ 执行成功。”)
print(“最终输出:\n”, stdout_final)
if stderr_final:
print(“最终错误输出:\n”, stderr_final)
elif p2.returncode == 1 and not stderr_final and not stdout_final: # grep没找到内容通常返回1
print(f”管道命令 ‘{‘ ‘.join(p1_command)} | {‘ ‘.join(p2_command)}’ 执行完毕,grep 未找到匹配项。”)
else:
print(f”管道命令 ‘{‘ ‘.join(p1_command)} | {‘ ‘.join(p2_command)}’ 执行失败。”)
print(f”P2 返回码: {p2.returncode}”)
if stdout_final: print(“最终输出:\n”, stdout_final)
if stderr_final: print(“最终错误输出:\n”, stderr_final)
except subprocess.TimeoutExpired:
print(“管道命令执行超时。”)
p1.kill()
p2.kill()
except FileNotFoundError as e:
print(f”命令未找到: {e.filename}”)
except Exception as e:
print(f”管道执行时发生错误: {e}”)
Windows 上的管道示例: dir | findstr “.py”
注意:Windows 的 findstr 如果找不到,返回码也是 1
try:
p1_cmd_win = [‘dir’]
p2_cmd_win = [‘findstr’, ‘.py’] # 注意:findstr 是区分大小写的,/I 不区分
p1_win = subprocess.Popen(p1_cmd_win, stdout=subprocess.PIPE, text=True, shell=True) # shell=True 对 dir 更方便
p2_win = subprocess.Popen(p2_cmd_win, stdin=p1_win.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
p1_win.stdout.close() # 允许 p1 在 p2 读取完后结束
stdout_final_win, stderr_final_win = p2_win.communicate(timeout=10)
print(f"Windows 管道命令 '{' '.join(p1_cmd_win)} | {' '.join(p2_cmd_win)}'")
if p2_win.returncode == 0:
print("执行成功。")
print("最终输出:\n", stdout_final_win)
elif p2_win.returncode == 1 and not stderr_final_win: # findstr 没找到
print("执行完毕,findstr 未找到匹配项。")
else:
print(f"执行失败,P2 返回码: {p2_win.returncode}")
if stderr_final_win:
print("最终错误输出:\n", stderr_final_win)
if stderr_final_win:
print("最终错误输出:\n", stderr_final_win)
except subprocess.TimeoutExpired:
print(“Windows 管道命令执行超时。”)
if ‘p1_win’ in locals() and p1_win.poll() is None: p1_win.kill()
if ‘p2_win’ in locals() and p2_win.poll() is None: p2_win.kill()
except FileNotFoundError as e:
print(f”命令未找到: {e.filename}”)
except Exception as e:
print(f”Windows 管道执行时发生错误: {e}”)
``
communicate()
**注意的死锁风险**:
p.stdout.read()
如果你使用或
p.stderr.read()并且子进程产生了大量输出到其中一个管道,而你正在等待另一个管道,或者子进程正在等待你的输入,就可能发生死锁。因为操作系统的管道缓冲区是有限的。当缓冲区满时,子进程会阻塞等待 Python 脚本读取。如果 Python 脚本也在等待其他事件,就会死锁。
p.communicate()通过在内部使用非阻塞读取(可能通过线程)来避免这个问题,它会同时读取 stdout 和 stderr。因此,对于大多数情况,
communicate()` 是与子进程交互并获取其输出的首选方法。
3.3 subprocess
模块中的其他函数
subprocess
模块还提供了一些旧的便捷函数,它们本质上是 Popen
的简单封装:
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, timeout=None)
: 执行命令并等待其完成,返回退出码。不捕获输出。类似于os.system()
但更安全(当shell=False
)。subprocess.check_call(args, ...)
: 同call()
,但如果返回非零退出码,则抛出CalledProcessError
。subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, text=None, timeout=None, ...)
: 执行命令并返回其标准输出。如果返回非零退出码,则抛出CalledProcessError
。
这些函数在 subprocess.run()
出现之前被广泛使用。现在,subprocess.run()
提供了更统一和强大的接口,通常是更好的选择。
例如,subprocess.check_output("ls -l", shell=True, text=True)
的效果与 subprocess.run("ls -l", shell=True, capture_output=True, text=True, check=True).stdout
类似。
安全注意事项 (shell=True
)
重复强调:当使用 shell=True
时,必须非常小心。如果命令字符串的任何部分来自用户输入或不可信的外部源,应避免使用 shell=True
,或者对输入进行极其严格的清理和转义(但这很难做到完美)。
不安全的例子:
“`python
危险!不要这样做!
filename = input(“请输入要查看的文件名: “) # 用户输入 “my_file.txt; rm -rf /”
command = f”cat {filename}”
subprocess.run(command, shell=True) # 这会导致执行 “cat my_file.txt; rm -rf /”
“`
安全的替代方案 (shell=False):
“`python
import subprocess
filename = input(“请输入要查看的文件名: “)
# 将命令和参数作为列表传递,不使用 shell 解析
try:
# 对于 ‘cat’ 这类简单命令,可以这样
result = subprocess.run([‘cat’, filename], capture_output=True, text=True, check=True)
# 或者,如果只是想读取文件内容,Python 内置的文件操作更好:
# with open(filename, ‘r’) as f:
# content = f.read()
# print(content)
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f”Error: {e}”)
except FileNotFoundError:
print(f”Error: ‘cat’ command not found or file ‘{filename}’ not found.”)
``
shell=False
当(默认) 时,
args` 列表中的每个元素都被视为一个单独的参数,不会被 Shell 解释。这消除了 Shell 注入的风险。
编码问题
当使用 text=True
(或 universal_newlines=True
) 时,subprocess
会尝试使用默认编码(通常由 locale.getpreferredencoding(False)
给出)或 encoding
参数指定的编码来解码 stdout
和 stderr
。如果子进程的输出编码与 Python 期望的编码不匹配,可能会遇到 UnicodeDecodeError
。
在这种情况下,你可以:
1. 明确指定 encoding
参数,例如 encoding='utf-8'
或 encoding='gbk'
。
2. 将 text
设置为 False
(或不设置),获取原始的字节串 (bytes
),然后手动解码,并可能使用 errors='ignore'
或 errors='replace'
来处理无法解码的字节。
“`python
import subprocess
command_list = [‘echo’, ‘你好世界’] # Linux/macOS
command_list = [‘cmd’, ‘/c’, ‘echo 你好世界’] # Windows,需要指定 cmd /c
假设命令输出的编码是 utf-8
try:
# 尝试1: 使用 text=True 和默认编码 (如果系统默认是 utf-8 通常没问题)
result_default_encoding = subprocess.run(command_list, capture_output=True, text=True, check=True)
print(“默认编码输出:”, result_default_encoding.stdout.strip())
# 尝试2: 明确指定编码
result_utf8 = subprocess.run(command_list, capture_output=True, text=True, encoding='utf-8', check=True)
print("UTF-8 编码输出:", result_utf8.stdout.strip())
# 尝试3: 获取字节串,手动解码
result_bytes = subprocess.run(command_list, capture_output=True, check=True) # text=False
stdout_bytes = result_bytes.stdout
# Windows cmd.exe 的 echo 默认输出可能是 gbk 或 cp936
# 这里我们假设它输出的是GBK,如果不是,需要相应调整
try:
stdout_decoded_gbk = stdout_bytes.decode('gbk')
print("手动解码 (GBK) 输出:", stdout_decoded_gbk.strip())
except UnicodeDecodeError:
# 如果 GBK 解码失败,尝试 UTF-8 或其他编码,或处理错误
stdout_decoded_utf8_replace = stdout_bytes.decode('utf-8', errors='replace')
print("手动解码 (UTF-8, replace errors) 输出:", stdout_decoded_utf8_replace.strip())
except subprocess.CalledProcessError as e:
print(f”命令执行失败: {e}”)
print(f”Stderr: {e.stderr.decode(errors=’ignore’) if e.stderr else ‘N/A’}”)
except FileNotFoundError:
print(“命令未找到。”)
``
cmd.exe
在 Windows 的中,输出编码可能比较复杂(通常是
cp936或
gbk`)。如果使用 PowerShell,它更倾向于使用 UTF-8(尤其是在较新版本中)。了解子进程实际使用的输出编码是正确处理文本的关键。
最佳实践总结
- 首选
subprocess.run()
: 对于大多数需求,它是最现代、最简单、最安全的选择。 shell=False
是王道: 默认情况下,将命令和参数作为列表传递给args
。这能避免 Shell 注入漏洞。- 仅在绝对必要且命令完全受控时使用
shell=True
: 当你需要 Shell 的特性(如管道、通配符扩展)并且无法通过Popen
的组合或 Python 自身功能实现时,才考虑shell=True
,并确保命令字符串是安全的。 - 使用
capture_output=True
和text=True
: 方便地捕获和解码输出为字符串。 - 使用
check=True
: 简化错误处理,当命令失败时自动抛出异常。 - 合理使用
timeout
: 防止脚本因外部命令卡死而无限期挂起。 - 处理异常: 始终使用
try...except
块来捕获CalledProcessError
,TimeoutExpired
,FileNotFoundError
等潜在异常。 - 注意编码: 如果
text=True
导致解码错误,尝试指定encoding
或手动处理字节串。 - 使用
Popen
进行高级交互: 当需要非阻塞操作、与长时间运行的进程交互或构建复杂管道时,使用Popen
。优先使用communicate()
方法与Popen
对象交互以避免死锁。 - 考虑可移植性: Shell 命令本身可能在不同操作系统上不同(如
ls
vsdir
)。如果需要跨平台,你的 Python 脚本需要处理这些差异,或者寻找平台无关的 Python 库来完成任务。 - Pythonic 方式优先: 在调用 Shell 命令之前,先考虑是否有纯 Python 的库或方法可以完成同样的任务。通常,Python 原生解决方案更易于维护、更安全且跨平台性更好。例如,文件操作用
os
和shutil
模块,网络请求用requests
库等。
结论
Python 提供了从简单到复杂的多种方式来执行 Shell 命令并与之交互。虽然 os.system()
和 os.popen()
因其局限性和安全风险已不再被推荐,但 subprocess
模块,特别是其 run()
函数和 Popen
类,为开发者提供了强大、安全且灵活的工具集。
通过理解这些工具的特性、优点和潜在陷阱,尤其是关于 shell=True
的安全警告和输出编码的处理,您可以自信地在 Python 项目中集成外部命令,实现自动化、系统管理和与其他程序的无缝协作。记住,选择正确的工具并遵循最佳实践,将使您的代码更健壮、更安全、更易于维护。掌握了这些,您就能真正做到“精通 Python:轻松执行 Shell 命令并捕获输出”。