前言
文章就是这篇文章中对自己有用信息的概括。因此可能不是很全,而且理解可能有些偏差,请自行决定看不看{.is-info}
这东西主要的需求就是通过python执行命令行命令,获取返回值进行处理和交互。
比如调用专用的下载工具IDM
进行下载,而python虽然会有类似的库,但是可能有两个情况
- 这个库上手有点困难,而我又不想麻烦。
- 这个库功能没那么强大,我想要更强大更省心的
同时,你选择的那个更高级的工具,你很难对其进行更改并融合进你的项目,比如他是闭源的,或者虽然开源但是源码你看不懂,无从下手。
这种情况下,用这个是一个很好的选择,当然还有些别的情况,比如需要命令行命令才能获取的信息。
不同于原文,我这大概介绍下最基本的前置知识,就直接从两个方法及参数介绍下手了,OK现在开始。
前置知识
CLI与GUI
CLI:Command-Line Interface
,是命令行接口
GUI:Graphic User Interface
,图形用户接口
说白了就是一个没有界面,在黑框里运行,例如在cmd.exe
输入git int
来创建一个仓库,就是启动了一CLI
一个有界面,例如你在vscode中用它的git插件,点击commit
按钮完成一次提交,这就是与GUI
交互
一个是基于文本的输入输出的操作
一个是鼠标点点点,可视觉交互的操作
terminal和shell
具体可以看这个视频。下面是原文章中的解释,也是差不多。
- The interpreter, which is typically thought of as the whole CLI. Common interpreters are Bash on Linux, Zsh on macOS, or PowerShell on Windows. In this tutorial, the interpreter will be referred to as the shell.
- The interface, which displays the output of the interpreter in a window and sends user keystrokes to the interpreter. The interface is a separate process from the shell, sometimes called a terminal emulator.
这里面的interpreter
就是指shell
,而interface
就是terminal
而subprocess提供的两个最重要的方法,run
和Popen
,都不与两者中的任意一个交互,不与terminal交互很自然,不与shell交互的细节会在run
方法的shell
参数中解释
return code/exit status
当你写c语言的时候,应该就已经接触到了这东西
int main(){
return 0;
}
这个0
就是要说的return code
,0代表正常,其他数字都是错误,即遍是python,你不退出,程序如果正常退出,也是会返回0的。
print('hello')
exit(1) # 即便不写这行,也会默认运行exit(0)
运行这个程序
import subprocess
res = subprocess.run('python your_program.py')
print(res.returncode)
输出了两行结果
hello
1
而不写exit
结果是0
通过return code可以判断程序是否正常运行,以便处理一些,错误情况,具体可以看subprocess.run
的check
参数的介绍
CLI的运行
命令行程序,绝大部分其实并不需要靠shell来运行,他们只是存储在计算机中一些文件。
以windows为例,当你运行python程序的时候用的python xxx.py
实际上,python是一个计算机上的exe文件,他有他自己的路径,而这个路径被你安装的时候添加进了环境变量,shell就是获取了这个环境变量给你找到了这个程序,再帮你把参数传进去,执行并拿到返回值,再传给终端给你看,仅此而已,而这个环境变量并不是shell专有的,是整个计算机共享的,因此说命令行程序只是一个输入文本,输出文本的的文件而已。
如果你有linux系统,并在上面运行
subprocess.run("ls")
你会发现你可以成功地拿到返回值,这是因为在类unix系统中的这些个命令,实际上都是文件,这也是linux的设计理念,万物皆文件。 但是在windows中则不同,cmd
中没有ls
命令,但有一个同样功能的dir
命令,但如果你同样运行subprocess.run("dir")
你会发现报错了FileNotFoundError: [WinError 2] 系统找不到指定的文件。
。 这是因为系统中并不存在一个叫dir.exe
的文件,subprocess
找不到对应的文件去运行。原因是dir
是cmd
中内置的命令,必须要cmd
的shell
才能运行,如果加上参数shell=True
就能成功运行了。{.is-warning}
重要的方法
subprocess.run
run(*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs):
Run command with arguments and return a CompletedProcess instance.
input
和原生的input
类似,获取一个输入,然后敲回车,输入进去让程序获得你的输入。
# prog1.py
user_input = input()
print("user says: '%s'" % user_input)
# 运行这行代码来运行上面的程序
subprocess.run('python prog1.py', input='hello world', encoding='utf-8')
# 也可以直接传入btyes类型的input,这样就不需要input,二者是等价的
# subprocess.run('python prog1.py', input=b'hello world')
这样你就可以看到prog1.py
中的input
捕获了我们run
方法中传入的input
参数值。
capture_output
有时候我们的需求是获取程序输出的内容进行处理,而从上面的例子来看,程序的运行其他的程序,它的输出是默认输入向终端的,因此我们的程序并不能获得这个输出的结果。
但如果设置capture_output=True
我们就可以得到程序的输出,还是用上面的例子:
result = run('python prog1.py', input=b'hello world', capture_output=True)
print(result.stdout) # "user says: 'hello world'\n"
可以看到终端中并没有输出'hello world'
,与此同时我们可以从运行结果的stdout
变量中获得所有的输出。
timeout
字面意思,超出timeout
传入的时间之后就会报一个subprocess.TimeoutExpired
错误。
check
一般情况下如果你要运行的程序出错了,你的应用不能捕获到这个错误,但有的时候我们就需要根据这个程序运行是否出错来决定后面要做什么。这时候你就可以加入check
参数。
# prog2.py
raise 'error'
# 我们运行
run('python prog2.py', check=True)
我们得到以下错误信息
CalledProcessError: Command 'python prog2.py' returned non-zero exit status 1.
因此你也明白了,可以直接通过returncode
来判断程序运行有没有出错,因此这个方法实际上只是个语法糖罢了。
shell
是否使用shell
来运行我们的程序,我们在CLI的运行这一节的基础上继续讨论。
在windows中,即便你设置shell=True
,也就是多了些cmd
中有的功能,这个默认的shell
可以更改但是不建议,因为它是通过更改环境变量,而这个环境变量还有很多程序也在使用,如果你改成了其他的shell
如powershell
,其他程序依赖于这个环境变量的程序的运行可能会出错。
因此这个命令是很鸡肋的,但如果你一定要用shell呢? 解决办法也不难,就是通过命令行执行:
cmd /k "dir"
pwsh -Command "ls"
这表示使用cmd/powershell
来运行引号中的命令
如果你使用的是
powershell
而且还配置了复杂的$PROFILE
,这将导致你启动执行pwsh
命令的速度变得奇慢无比,使用-nop
参数可以跳过profile
,执行速度更快。 {.is-info}
subprocess.Popen
很多情况下我们运行的程序是很快就会出结果的,但也有些情况,我们运行的程序需要执行很长的时间,而如果只用run
方法,在程序完全结束并返回之前,我们的应用是处于一个阻塞的状态的,因此subprocess
提供了一个非阻塞的方法,也就是Popen
Popen(
args,
bufsize=-1,
executable=None,
stdin=None,
stdout=None,
stderr=None,
preexec_fn=None,
close_fds=True,
shell=False,
cwd=None,
env=None,
universal_newlines=None,
startupinfo=None,
creationflags=0,
restore_signals=True,
start_new_session=False,
pass_fds=(),
*,
user=None,
group=None,
extra_groups=None,
encoding=None,
errors=None,
text=None,
umask=-1,
)
其中有用的基本上就是前面的那几个了,值得一说的是,subprocess
的所有方法,其底层使用的都是Popen
这个对象,所以这个方法其实挺全能的。
stdin/stdout/stderr
分别表示标准的输入,输出,和错误输出。一般情况下输入都是从终端输入,输出和错误也是在终端输出,之前也提到了,run
方法中通过capture_output
可以获取到程序的返回。我们也可以用Popen
来复刻这个实现
# prog3.py
import time
for i in range(5):
print("hello world", i, flush=True)
time.sleep(1)
# run.py
from subprocess import Popen, PIPE
result = Popen('python prog3.py', stdout=PIPE)
print(result.stdout.read())
你会发现程序等了五秒,才一次性把所有结果全部打印出来了,那说好的非阻塞呢?实际上是写法出问题,想要非阻塞可使用如下写法
from subprocess import Popen, PIPE
import time
with Popen('python prog3.py', stdout=PIPE) as process:
while(process.poll() is None):
print(process.stdout.read1().decode(), end="")
time.sleep(0.5)
process.poll
可以用来检测程序是否运行结束,如果结束了会返回return code
反之返回None
。意思就是在程序未结束时,每次都读取stdout里的所有内容有多少读多少,然后再print出来,然后每0.5秒重复相同操作。
注意到在prog3.py中print方法添加了
flush=True
的参数,这表示print的输出结果不会缓存,有多少输出多少,去掉这个则程序不会如期运行。你也可以修改启动的命令为python -u prog3.py
来达到同样的效果,其中u
指的就是unbuffered
,不缓冲。 {.is-warning}
然后是stdin,用法差不多,直接看例子
# prog4.py
key = 6174
num = input('please input 6174')
if int(num) == key:
print('success')
else:
print('error')
# run.py
from subprocess import Popen, PIPE
import re
with Popen('python -u prog4.py', stdout=PIPE, stdin=PIPE) as process:
text = process.stdout.read1().decode('utf-8')
key = re.search(r'\d+', text).group(0)
process.stdin.write((key + '\n').encode('utf-8'))
process.stdin.flush()
print('result:', process.stdout.read1().decode(), 'key:', key)
注意想要让input
获取到输入还需要process.flush()
,否则会卡死不动。
看到这里是不是觉得这些操作有点熟悉,其实这些很想文件的io操作,而事实上,你也确实可以用文件io来替换里面的PIPE
,方法很类似,就不举具体例子了。
结束语
subprocess
主要是写一些自动化的脚本有点用,说实话吧没必要了解那么深,不过也无所谓了,就当好玩吧。
最后修改于 2024-10-07