在Python中运行Bash命令
在我的本地电脑上,我运行了一个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 个回答
用子进程来调用它
import subprocess
subprocess.Popen("cwm --rdf test.rdf --ntriples > test.nt")
你遇到的错误看起来是因为服务器上没有安装 swap 模块,你需要在服务器上安装 swap,然后再运行这个脚本。
不要使用 os.system
。这个方法已经被淘汰了,推荐使用 subprocess。根据 官方文档 的说法:“这个模块的目的是替代几个旧的模块和函数: os.system
和 os.spawn
。”
就像你提到的情况:
import subprocess
bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
为了更详细地解释之前的回答,这里有一些常被忽视的细节。
- 优先使用
subprocess.run()
,而不是subprocess.check_call()
,再往下是subprocess.call()
,然后是subprocess.Popen()
,最后是os.system()
和os.popen()
- 理解并通常使用
text=True
,也就是universal_newlines=True
。 - 理解
shell=True
和shell=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=True
或 subprocess.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
字符串作为 stdout
和 stderr
的内容。也许在稍后的某个时刻,你确实知道它们是文本字符串,并且知道它们的编码。然后,你可以对它们进行解码。
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=True
与 shell=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
,它在单个进程中运行多个任务(这更轻量级,给你更多控制,但也更受限,因为同一进程中的线程是紧密耦合的,并且受限于单个 全局解释器锁)。