Python运行Shell命令的正确方法
Python是自动化任何事情的流行选择,包括自动化系统管理任务或需要运行其他程序或与操作系统交互的任务。然而,在Python中有很多方法可以实现这一点,其中大部分可以说是不好的。
原生工具
一般的经验法则应该是使用内置函数,而不是直接调用其他程序或操作系统命令。所以,首先让我们看看原生Python选项:
pathlib
如果你需要创建或删除文件/目录;检查文件是否存在;更改权限;等等。绝对没有理由运行系统命令,只需使用pathlib
,它就有你需要的一切。当你开始使用pathlib
时,你也会意识到你可以忘记其他Python模块,例如grob
或os. path
。
tempfile
同样,如果您需要一个临时文件,只需使用temfile
模块,不要手动使用/tmp
。
shutil
pathlib
应该可以满足您在Python中与文件相关的大部分需求,但如果您需要例如复制、移动、‘chown’、'that’或创建存档,那么您应该转向shutil
。
signal
以防您需要使用信号处理程序。
syslog
用于Unixsyslog
的接口。如果上述内置选项都不能满足您的需求,只有这样开始直接与操作系统或其他程序交互才有意义……
OS模块
从最糟糕的选项开始—os
模块—它提供了与操作系统交互的低级功能—其中许多已被其他模块中的功能所取代。
如果你只是想调用其他程序,你可以使用os. system
函数,但你不应该。我甚至不想给你举个例子,因为你根本不应该使用它。
虽然os
不应该是您的首选,但您可能会发现有几个功能很有用:
import os
print(os.getenv('PATH'))
# /home/martin/.local/bin:/usr/local/sbin:/usr/local/bin:...
print(os.uname())
# posix.uname_result(sysname='Linux', nodename='...', release='...', version='...', machine='x86_64')
print(os.times())
# posix.times_result(user=0.01, system=0.0, children_user=0.0, children_system=0.0, elapsed=1740.63)
print(os.cpu_count())
# 16
print(os.getloadavg())
# (2.021484375, 2.35595703125, 2.04052734375)
old_umask = os.umask(0o022)
# Do stuff with files...
os.umask(old_umask) # restore old umask
# Only if you need better random numbers than pseudo-random numbers from 'random' module:
# 仅当您需要比来自“随机”模块的伪随机数更好的随机数时:
from base64 import b64encode
random_bytes = os.urandom(64)
print(b64encode(random_bytes).decode('utf-8'))
# C2F3kHjdzxcP7461ETRj/YZredUf+NH...hxz9MXXHJNfo5nXVH7e5olqLwhahqFCe/mzLQ==
除了上面显示的函数,还有用于创建fd
(文件描述符)、管道、打开PTY
、chroot
、chmod
、mkdir
、kill
、stat
的函数,但我不鼓励你使用它们,因为还有更好的选择。文档中甚至有[部分](https://docs.python.org/3/library/subprocess.html#subprocess-replacements)展示了如何用“子进程”模块替换os
,所以不要考虑使用os. popen
、os.spawn
或os.system
。
os
模块中的大多数剩余函数都是与操作系统(或C语言)API的直接接口,例如os.dup
、os.splice
、os.mkafo
、os.exv
、os.fork
等。如果你需要使用所有这些,那么我不确定Python是否是适合这项任务的语言……
subprocess模块
我们在Python中的第二个稍微好一点的选项是subprocess
模块:
import subprocess
p = subprocess.run('ls -l', shell=True, check=True, capture_output=True, encoding='utf-8')
# 'p' is instance of 'CompletedProcess(args='ls -la', returncode=0)'
print(f'Command {p.args} exited with {p.returncode} code, output: \n{p.stdout}')
# Command ls -la exited with 0 code
# total 36
# drwxrwxr-x 2 martin martin 4096 apr 22 12:53 .
# drwxrwxr-x 42 martin martin 20480 apr 22 11:01 ..
# ...
如文档所述:调用子进程的推荐方法是对它可以处理的所有用例使用run()
函数。
在大多数情况下,您使用subprocess.run
,传入kwargs
来改变它的行为就足够了,例如shell=True
允许您将命令作为单个字符串传递,check=True
会在退出代码不是0时引发异常,而capture_output=True
会填充stdout
属性。
虽然subprocess.run()
是调用进程的推荐方法,但此模块中还有其他(不必要的、已弃用的)选项:call
、check_call
、check_output
、getstatusout
、getout
。通常,您应该只使用run和Popen:
with subprocess.Popen(['ls', '-la'], stdout=subprocess.PIPE, encoding='utf-8') as process:
# process.wait(timeout=5) # Returns only code: 0
outs, errs = process.communicate(timeout=5)
print(f'Command {process.args} exited with {process.returncode} code, output: \n{outs}')
# Pipes
import shlex
ls = shlex.split('ls -la')
awk = shlex.split("awk '{print $9}'")
ls_process = subprocess.Popen(ls, stdout=subprocess.PIPE)
awk_process = subprocess.Popen(awk, stdin=ls_process.stdout, stdout=subprocess.PIPE, encoding='utf-8')
for line in awk_process.stdout:
print(line.strip())
# .
# ..
# examples.py
# ...
上面的第一个例子显示了Popen
等同于前面显示的subprocess.run
。但是,您应该只在需要比run
提供的更大灵活性时使用Popen
,例如在第二个例子中,您可以看到如何将一个命令的输出通过管道传输到另一个命令中,有效地运行ls-la|awk '{print 9美元}'
。您还可以看到我们使用了shlex. split
,这是一个方便的函数,它将字符串拆分为可以传递给Popen
或run
的标记数组,而无需使用shell=True
。
当使用Popen
时,您还可以使用 terminate()
, kill()
和send_signal()
与进程进行更多交互。
在前面的例子中,我们没有真正做任何错误处理,但是在运行其他进程时可能会出错。对于简单的脚本,check=True
可能就足够了,因为它会导致CalledProcessError
在子进程运行到非零返回代码时立即引发,所以你的程序会快速而响亮地失败,这很好。如果你还设置了timeout
参数,那么你也可以得到TimeoutExpired
异常,但是一般来说,subProccess
模块中的所有异常都继承自Subprocess Error
,所以如果你想捕捉异常,那么你可以简单地观察Subprocess Error
。
linux系统支持的sh库
使用pip命令安装sh:
linux系统pip install sh
windows系统(win32api)pip install pywin32
# https://pypi.org/project/sh/
# pip install sh
import sh
# Run any command in $PATH...
print(sh.ls('-la'))
ls_cmd = sh.Command('ls')
print(ls_cmd('-la')) # Explicit
# total 36
# drwxrwxr-x 2 martin martin 4096 apr 8 14:18 .
# drwxrwxr-x 41 martin martin 20480 apr 7 15:23 ..
# -rw-rw-r-- 1 martin martin 30 apr 8 14:18 examples.py
# If command is not in PATH:
custom_cmd = sh.Command('/path/to/my/cmd')
custom_cmd('some', 'args')
with sh.contrib.sudo:
# Do stuff using 'sudo'...
...
当我们调用sh.some_command
时,sh
库会尝试查找内置的shell
命令或具有该名称的$PATH
中的二进制文件。如果它找到这样的命令,它会简单地为您执行它。如果命令不在$PATH
中,那么您可以创建Command
的实例并以这种方式调用它。如果您需要使用sudo
,您可以使用contrib
模块中的sudo
上下文管理器。如此简单明了,对吧?
要将命令的输出写入文件,您只需向函数提供“_out”参数:
sh.ip.address(_out='/tmp/ipaddr')
# Same as 'ip address > /tmp/ipaddr'
上面还展示了如何调用子命令——只需使用点。
最后,您还可以使用_in参数 (|
) 管道:
print(sh.awk('{print $9}', _in=sh.ls('-la')))
# Same as "ls -la | awk '{print $9}'"
print(sh.wc('-l', _in=sh.ls('.', '-1')))
# Same as "ls -1 | wc -l"
至于错误处理,您可以简单地观察“错误返回代码”或TimeoutException
异常:
try:
sh.cat('/tmp/doesnt/exist')
except sh.ErrorReturnCode as e:
print(f'Command {e.full_cmd} exited with {e.exit_code}')
# Command /usr/bin/cat /tmp/doesnt/exist exited with 1
curl = sh.curl('https://httpbin.org/delay/5', _bg=True)
try:
curl.wait(timeout=3)
except sh.TimeoutException:
print("Command timed out...")
curl.kill()
可选地,如果您的进程从您将收到的信号SignalException
终止,您可以使用例如SignalException_SIGKILL
(或_SIGTERM
、_SIGSTOP
)来检查特定信号。
该库还具有内置的日志记录支持,您所要做的就是打开它:
import logging
# Turn on default logging:
logging.basicConfig(level=logging.INFO)
sh.ls('-la')
# INFO:sh.command:<Command '/usr/bin/ls -la', pid 1631463>: process started
# Change log level:
logging.getLogger('sh').setLevel(logging.DEBUG)
sh.ls('-la')
# INFO:sh.command:<Command '/usr/bin/ls -la', pid 1631661>: process started
# DEBUG:sh.command:<Command '/usr/bin/ls -la'>: starting process
# DEBUG:sh.command.process:<Command '/usr/bin/ls -la'>.<Process 1631666 ['/usr/bin/ls', '-la']>: started process
# ...
【我之前自学Python的时候整理了很多Python学习资料,现在我也用不上了,我上传到CSDN官方了,有需要的朋友可以扫描下方二维码进行获取】
一、学习大纲
二、开发工具
三、Python基础材料
四、实战资料