在Python中运行Bash命令

520 投票
12 回答
1032527 浏览
提问于 2025-04-16 07:29

在我的本地电脑上,我运行了一个Python脚本,其中有这一行代码

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
os.system(bashCommand)

这段代码运行得很好。

然后我在服务器上运行同样的代码,却遇到了以下错误信息

'import site' failed; use -v for traceback
Traceback (most recent call last):
File "/usr/bin/cwm", line 48, in <module>
from swap import  diag
ImportError: No module named swap

于是我做了一件事,就是在代码里加了一个print bashCommand,这样在用os.system()运行之前,它会先把命令打印出来。

当然,我还是遇到了同样的错误(是因为os.system(bashCommand)引起的),但在报错之前,它会先在终端打印出命令。然后我把这个输出复制粘贴到终端里,按下回车,结果就成功了……

有没有人知道这是怎么回事?

12 个回答

56

用子进程来调用它

import subprocess
subprocess.Popen("cwm --rdf test.rdf --ntriples > test.nt")

你遇到的错误看起来是因为服务器上没有安装 swap 模块,你需要在服务器上安装 swap,然后再运行这个脚本。

470

不要使用 os.system。这个方法已经被淘汰了,推荐使用 subprocess。根据 官方文档 的说法:“这个模块的目的是替代几个旧的模块和函数: os.systemos.spawn。”

就像你提到的情况:

import subprocess

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
510

为了更详细地解释之前的回答,这里有一些常被忽视的细节。

  • 优先使用 subprocess.run(),而不是 subprocess.check_call(),再往下是 subprocess.call(),然后是 subprocess.Popen(),最后是 os.system()os.popen()
  • 理解并通常使用 text=True,也就是 universal_newlines=True
  • 理解 shell=Trueshell=False 的含义,以及它们如何影响引号的使用和一些命令行便利功能的可用性。
  • 理解 sh 和 Bash 之间的区别。
  • 理解子进程是如何与其父进程分开的,并且通常不能改变父进程。
  • 避免将 Python 解释器作为 Python 的子进程运行。

下面会更详细地讨论这些主题。

优先使用 subprocess.run()subprocess.check_call()

subprocess.Popen() 是一个底层的工具,但正确使用起来比较棘手,最终你可能会复制粘贴多行代码……而这些代码在标准库中已经以更高层次的封装函数存在,下面会详细介绍这些函数。

以下是来自 文档的一段话:

调用子进程的推荐方法是使用 run() 函数来处理所有可以处理的用例。对于更高级的用例,可以直接使用底层的 Popen 接口。

不幸的是,这些封装函数在不同的 Python 版本中可用性不同。

  • subprocess.run() 在 Python 3.5 中正式引入,旨在替代以下所有函数。
  • subprocess.check_output() 在 Python 2.7 / 3.1 中引入,基本等同于 subprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
  • subprocess.check_call() 在 Python 2.5 中引入,基本等同于 subprocess.run(..., check=True)
  • subprocess.call() 在 Python 2.4 中引入,基本等同于 subprocess.run(...).returncode

高层 API 与 subprocess.Popen()

重构和扩展后的 subprocess.run() 比它替代的旧函数更合理、更灵活。它返回一个 CompletedProcess 对象,包含各种方法,可以让你获取退出状态、标准输出以及其他一些结果和状态指示。

如果你只是需要运行一个程序并将控制权返回给 Python,subprocess.run() 是最佳选择。对于更复杂的场景(比如后台进程,可能需要与 Python 父程序进行交互),你仍然需要使用 subprocess.Popen(),并自己处理所有的细节。这需要对所有的组件有相当深入的理解,不应轻率尝试。简单的 Popen 对象 代表了(可能仍在运行的)进程,需要在子进程的整个生命周期内从你的代码中进行管理。

需要强调的是,subprocess.Popen() 仅仅是创建了一个进程。如果你就这样放着,它会和 Python 并行运行,成为一个“后台”进程。如果它不需要输入或输出,也不需要与你协调,它可以与 Python 程序并行执行有用的工作。

避免使用 os.system()os.popen()

自古以来(好吧,自 Python 2.5 起),os 模块文档 一直建议优先使用 subprocess 而不是 os.system()

subprocess 模块提供了更强大的功能来生成新进程并获取它们的结果;使用该模块比使用这个函数更好。

system() 的问题在于它显然依赖于系统,并且不提供与子进程交互的方式。它只是运行,标准输出和标准错误在 Python 的控制范围之外。Python 仅接收到命令的退出状态(零表示成功,尽管非零值的含义在某种程度上也依赖于系统)。

PEP-324(上面已经提到过)包含了更详细的理由,说明为什么 os.system 是有问题的,以及 subprocess 如何尝试解决这些问题。

os.popen() 曾经更是 强烈不推荐

自 2.6 版本起已弃用:这个函数已经过时。请使用 subprocess 模块。

不过,自 Python 3 起,它已经重新实现为简单地使用 subprocess,并重定向到 subprocess.Popen() 的文档以获取详细信息。

理解并通常使用 check=True

你还会注意到 subprocess.call() 有许多与 os.system() 相同的限制。在正常使用中,你通常应该检查进程是否成功完成,这就是 subprocess.check_call()subprocess.check_output() 的作用(后者还会返回完成的子进程的标准输出)。同样,除非你特别需要允许子进程返回错误状态,否则通常应该在 subprocess.run() 中使用 check=True

实际上,使用 check=Truesubprocess.check_* 时,如果子进程返回非零退出状态,Python 会抛出一个 CalledProcessError 异常

一个常见的错误是使用 subprocess.run() 时省略 check=True,当子进程失败时,后续代码会出错。

另一方面,使用 check_call()check_output() 的常见问题是,用户在盲目使用这些函数时,发现当 grep 没有找到匹配项时异常被抛出。(你可能应该用原生的 Python 代码替换 grep,如下所述。)

总的来说,你需要理解 shell 命令如何返回退出代码,以及在什么情况下它们会返回非零(错误)退出代码,并做出明确的决定如何处理这些情况。

理解并通常使用 text=True 也就是 universal_newlines=True

自 Python 3 起,Python 内部的字符串是 Unicode 字符串。但并不能保证子进程生成的输出是 Unicode,或者根本就是字符串。

(如果差异不明显,建议阅读 Ned Batchelder 的 实用 Unicode,这几乎是必读的。如果你更喜欢,可以观看链接后面的 36 分钟视频,但自己阅读页面可能会花费更少的时间。)

从根本上说,Python 需要获取一个 bytes 缓冲区并以某种方式解释它。如果它包含一块二进制数据,就 不应该 解码为 Unicode 字符串,因为这会导致错误和 bug - 这正是许多 Python 2 脚本中存在的麻烦行为,在那之前没有办法正确区分编码文本和二进制数据。

使用 text=True,你告诉 Python,你实际上期望以系统的默认编码返回文本数据,并且它应该尽可能将其解码为 Python(Unicode)字符串(通常在任何适度更新的系统上是 UTF-8,除了 Windows 之外?)

如果这不是你请求的内容,Python 将只会给你 bytes 字符串作为 stdoutstderr 的内容。也许在稍后的某个时刻,你确实知道它们是文本字符串,并且知道它们的编码。然后,你可以对它们进行解码。

normal = subprocess.run([external, arg],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    check=True,
    text=True)
print(normal.stdout)

convoluted = subprocess.run([external, arg],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))

Python 3.7 引入了更短、更具描述性和可理解性的别名 text,替代之前有些误导性的 universal_newlines

理解 shell=Trueshell=False

使用 shell=True 时,你将一个字符串传递给 shell,shell 会接管。

使用 shell=False 时,你将参数列表传递给操作系统,绕过 shell。

当你没有 shell 时,你节省了一个进程,并消除了 相当复杂的隐藏复杂性,这可能会隐藏 bug 或安全问题。

另一方面,当你没有 shell 时,你就没有重定向、通配符扩展、作业控制以及许多其他 shell 功能。

一个常见的错误是使用 shell=True 但仍然向 Python 传递一个参数列表,或者反之亦然。这在某些情况下是有效的,但实际上是模糊不清的,可能会以有趣的方式出错。

# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')

# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
    shell=True)

# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
    shell=True)

correct = subprocess.run(['dig', '+short', 'stackoverflow.com'],
    # Probably don't forget these, too
    check=True, text=True)

# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackoverflow.com',
    shell=True,
    # Probably don't forget these, too
    check=True, text=True)

常见的反驳“但这对我有效”并不是一个有用的反驳,除非你确切理解在什么情况下它可能停止工作。

简而言之,正确的用法如下:

subprocess.run("string for 'the shell' to parse", shell=True)
# or
subprocess.run(["list", "of", "tokenized strings"]) # shell=False

如果你想避免使用 shell,但又懒得或不确定如何将字符串解析为参数列表,可以注意到 shlex.split() 可以为你完成这项工作。

subprocess.run(shlex.split("no string for 'the shell' to parse"))  # shell=False
# equivalent to
# subprocess.run(["no", "string", "for", "the shell", "to", "parse"])

常规的 split() 在这里不起作用,因为它不保留引号。在上面的例子中,注意 "the shell" 是一个单独的字符串。

重构示例

通常,shell 的功能可以用原生 Python 代码替代。简单的 Awk 或 sed 脚本可能应该直接翻译成 Python。

为了部分说明这一点,这里有一个典型但稍显傻的例子,涉及许多 shell 功能。

cmd = '''while read -r x;
   do ping -c 3 "$x" | grep 'min/avg/max'
   done <hosts.txt'''

# Trivial but horrible
results = subprocess.run(
    cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)

# Reimplement with shell=False
with open('hosts.txt') as hosts:
    for host in hosts:
        host = host.rstrip('\n')  # drop newline
        ping = subprocess.run(
             ['ping', '-c', '3', host],
             text=True,
             stdout=subprocess.PIPE,
             check=True)
        for line in ping.stdout.split('\n'):
             if 'min/avg/max' in line:
                 print('{}: {}'.format(host, line))

这里有一些需要注意的事项:

  • 使用 shell=False 时,你不需要 shell 对字符串的引号要求。无论如何加引号可能都是一个错误。
  • 通常,尽量在子进程中运行尽可能少的代码是有意义的。这让你可以在 Python 代码中更好地控制执行。
  • 话虽如此,复杂的 shell 管道在 Python 中重新实现起来是繁琐且有时具有挑战性的。

重构后的代码也说明了 shell 实际上为你提供了多少功能,语法非常简洁——无论是好是坏。Python 说 显式优于隐式,但 Python 代码 确实 相对冗长,并且看起来比实际情况更复杂。另一方面,它提供了多个可以在其他事情中抓取控制的点,正如我们可以轻松地将主机名与 shell 命令输出一起包含的简单示例所示。(这在 shell 中也并不难做到,但代价是又一次的分心,可能还会涉及另一个进程。)

常见的 Shell 构造

为了完整起见,这里简要解释一些 shell 功能,以及它们如何可能被原生 Python 功能替代的一些说明。

  • 通配符扩展可以用 glob.glob() 替代,或者通常可以用简单的 Python 字符串比较来实现,例如 for file in os.listdir('.'): if not file.endswith('.png'): continue。Bash 还有其他各种扩展功能,比如 .{png,jpg} 的大括号扩展和 {1..100} 以及波浪号扩展(~ 扩展为你的主目录,更一般地说,~account 扩展为其他用户的主目录)
  • Shell 变量如 $SHELL$my_exported_var 有时可以简单地用 Python 变量替代。导出的 shell 变量可以通过 os.environ['SHELL'] 获取(export 的意思是使变量对子进程可用——一个对子进程不可用的变量显然也不会在作为 shell 的子进程运行的 Python 中可用,反之亦然。subprocess 方法的 env= 关键字参数允许你将子进程的环境定义为字典,这样可以使 Python 变量对子进程可见)。使用 shell=False 时,你需要理解如何去掉引号;例如,cd "$HOME" 等同于 os.chdir(os.environ['HOME']),而不需要在目录名周围加引号。(通常 cd 并不有用或必要,许多初学者省略变量周围的双引号也能成功 直到有一天……
  • 重定向允许你将文件作为标准输入读取,并将标准输出写入文件。grep 'foo' <inputfile >outputfile 打开 outputfile 进行写入,打开 inputfile 进行读取,并将其内容作为标准输入传递给 grep,其标准输出则进入 outputfile。这通常不难用原生 Python 代码替代。
  • 管道是一种重定向形式。echo foo | nl 运行两个子进程,其中 echo 的标准输出是 nl 的标准输入(在类 Unix 系统的操作系统层面,这是一个文件句柄)。如果你无法用原生 Python 代码替换管道的一个或两个端点,或许可以考虑使用 shell,尤其是当管道有两个或三个以上的进程时(尽管可以查看 pipes 模块 或一些更现代和多功能的第三方库)。
  • 作业控制让你可以中断作业、将其放入后台、将其返回到前台等。基本的 Unix 信号可以停止和继续进程,Python 也可以做到。但作业是 shell 中的一个更高层次的抽象,涉及进程组等,如果你想从 Python 中做这样的事情,你需要理解这些。
  • 在 shell 中,引号的使用可能会让人困惑,直到你理解 一切 基本上都是字符串。所以 ls -l / 等同于 'ls' '-l' '/',但字面量周围的引号是完全可选的。未加引号的字符串如果包含 shell 元字符,会经历参数扩展、空格分割和通配符扩展;双引号防止空格分割和通配符扩展,但允许参数扩展(变量替换、命令替换和反斜杠处理)。理论上这很简单,但在有多个解释层次时(例如远程 shell 命令),可能会变得令人困惑。

理解 sh 和 Bash 之间的区别

subprocess 默认使用 /bin/sh 来运行你的 shell 命令,除非你特别要求使用其他的(当然在 Windows 上,它使用 COMSPEC 变量的值)。这意味着 各种仅限 Bash 的功能,如数组、[[ 是不可用的。

如果你需要使用仅限 Bash 的语法,可以通过 executable='/bin/bash' 传入 shell 的路径(当然,如果你的 Bash 安装在其他地方,你需要调整路径)。

subprocess.run('''
    # This for loop syntax is Bash only
    for((i=1;i<=$#;i++)); do
        # Arrays are Bash-only
        array[i]+=123
    done''',
    shell=True, check=True,
    executable='/bin/bash')

子进程与其父进程是分开的,不能改变父进程

一个常见的错误是做类似这样的事情:

subprocess.run('cd /tmp', shell=True)
subprocess.run('pwd', shell=True)  # Oops, doesn't print /tmp

如果第一个子进程尝试设置环境变量,结果也是一样的,因为当你运行另一个子进程时,这个变量当然会消失。

子进程与 Python 完全分开运行,当它完成时,Python 对它做了什么一无所知(除了它可以从退出状态和子进程的输出中推断出的模糊指示)。子进程通常不能改变父进程的环境;它不能设置变量、改变工作目录,或者用简单的话说,不能在没有父进程合作的情况下与父进程通信。

在这个特定情况下,直接在一个子进程中运行两个命令是立即的解决方案;

subprocess.run('cd /tmp; pwd', shell=True)

不过显然这个特定的用例并不太有用;相反,可以在运行子进程之前使用 cwd 关键字参数,或者简单地使用 os.chdir()。同样,对于设置变量,你可以通过以下方式操作当前进程(因此也包括它的子进程)的环境:

os.environ['foo'] = 'bar'

或者通过以下方式将环境设置传递给子进程:

subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})

(更不用说明显的重构 subprocess.run(['echo', 'bar']);但 echo 本身就是一个在子进程中运行的糟糕示例。)

不要从 Python 中运行 Python

这条建议有点可疑;确实有一些情况,在 Python 脚本中将 Python 解释器作为子进程运行是有意义的,甚至是绝对必要的。但在很多情况下,正确的方法是简单地 import 另一个 Python 模块到你的调用脚本中,并直接调用它的函数。

如果另一个 Python 脚本在你的控制之下,并且它不是模块,考虑 将其转换为模块。(这个回答已经太长了,所以我不打算在这里深入细节。)

如果你需要并行处理,可以使用 multiprocessing 模块 在子进程中运行 Python 函数。还有 threading,它在单个进程中运行多个任务(这更轻量级,给你更多控制,但也更受限,因为同一进程中的线程是紧密耦合的,并且受限于单个 全局解释器锁)。

撰写回答