Python學(xué)習(xí):執(zhí)行系統(tǒng)shell命令

1.問題

python可以作為shell替代,代碼比較直觀,易于維護(hù)。 python支持調(diào)用外部shell命令。不過,這個問題沒有看上去簡單,要完美的解決此問題,比較復(fù)雜,就連標(biāo)準(zhǔn)庫也不見得處理得很好。

2.方案

2.1.方案一

首先最簡單的方法就是調(diào)用system方法,直接執(zhí)行系統(tǒng)shell命令,代碼如下:

import os

os.system('ls -l')

system主要問題,就是無法獲取shell命令的輸出,無法進(jìn)行輸入;也沒有超時設(shè)置,如果外部命令掛死,會直接導(dǎo)致當(dāng)前進(jìn)程掛死。

2.2.方案二

python3subprocess提供了check_output方法,可以直接獲取進(jìn)程的輸出,也支持輸入,同時關(guān)鍵的是支持超時設(shè)置。這就防止了shell命令掛死的問題。

def __exec_command(cmd: str, input: str = None, timeout=10) -> str:
    try:
        output_bytes = subprocess.check_output(cmd, input=input, stderr=subprocess.STDOUT, shell=True, timeout=timeout)
    except subprocess.CalledProcessError as err:
        output = err.output.decode('utf-8')
        logger.debug(output)
        raise err
    result = output_bytes.decode('utf-8')
    return result

print(__exec_command('ls -l'))

現(xiàn)在可以成功獲取系統(tǒng)命令的結(jié)果,并且很好的支持超時功能,防止命令掛死。不過,我們看看下面這個例子:

print(__exec_command('echo begin;sleep 10; echo end; sleep 3'), timeout=30)

上述代碼中,要想獲取shell命令的結(jié)果,實際測試的結(jié)果,只能等到子進(jìn)程結(jié)束才可以獲取,父進(jìn)程只能傻傻得等,對子進(jìn)程的執(zhí)行過程一無所知。

2.3.方案三

上述的問題,看上容易解決,實際上比較復(fù)雜。我們先看下,使用更低層的subprocess.Popen能否解決:

process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )

while True:
    if process.poll() is None and timeout > 0:
        output_bs = process.stdout.read()
        if not output_bs:
              ....
        time.sleep(0.5)
        timeout = timeout - 0.5

if process.poll() is None or timeout <= 0:
      process.kill()

上述問題是無法解決我們的問題,因為process.stdout.read()是阻塞的,如果子進(jìn)程沒有輸出,就掛住了。

我們使用有超時功能communicate方法再試試:

def exec_command(cmd: str, input: str = None, encoding='utf-8', shell=True, timeout=5) -> str:
    output_bytes = b''
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
    while process.poll() is None and timeout > 0:
        try:
            output_bytes, output_err_bytes = process.communicate(timeout=0.5)
        except subprocess.TimeoutExpired as err:
            if err.stdout:
                output = err.output.decode(encoding)
                print(output)
            timeout -= 0.5
            continue
    if process.poll() is None or timeout <= 0:
        process.kill()
        raise ValueError(f'exec command: {cmd} timeout')

    result = output_bytes.decode(encoding)
    return result

communicate超時時拋出異常subprocess.TimeoutExpired,這個異常對象的stdout帶有子進(jìn)程已經(jīng)輸出的內(nèi)容。

目前還不錯,可以滿足開頭提的問題。不過,不能算完美,因為輸出是有點奇怪,如下所示:

hello
hello
hello
hello
hello
hello
hello-end

每次TimeoutExpired超時,stdout所帶的內(nèi)容,是子進(jìn)程已經(jīng)輸出的內(nèi)容,而不是新增加的內(nèi)容。

2.4.方案四

要想實時獲取子進(jìn)程是否有內(nèi)容輸出,我們可以使用文件進(jìn)行重定下,代碼如下:

def exec_command(cmd: str, input: str = None, encoding='utf-8', shell=True, timeout=10, wait=0.5) -> str:
    _opener = lambda name, flag, mode=0o7770: os.open(name, flag | os.O_RDWR, mode)
    output_bytes = bytearray()
    with tempfile.NamedTemporaryFile('w+b') as writer, open(writer.name, 'rb', opener=_opener) as reader:
        try:
            process = subprocess.Popen(cmd, stdout=writer, stderr=writer, stdin=subprocess.PIPE, shell=shell)
            if input:
                process.stdin.write(input.encode(encoding))
                process.stdin.close()
            while process.poll() is None and timeout > 0:
                new_bytes = reader.read()
                if new_bytes or new_bytes != b'':
                    logger.debug(f'{new_bytes}')
                    output_bytes = output_bytes + bytearray(new_bytes)
                else:
                    logger.debug('waiting sub process output......')
                time.sleep(wait)
                timeout -= wait
        except Exception as err:
            process.kill()
            raise err

        if process.poll() is None:
            process.kill()
            raise ValueError(f'exec cmd:{cmd} timeout')
        new_bytes = reader.read()
        if new_bytes:
            output_bytes = output_bytes + bytearray(new_bytes)

    result = output_bytes.decode(encoding)
    return result

這里,我們試用了臨時文件對子進(jìn)程的輸入輸出進(jìn)行重定向,對于文件的讀取reader.read()實際上并不是阻塞的?;就昝缹崿F(xiàn)了本文的問題。

3.討論

windows系統(tǒng)中,python創(chuàng)建子進(jìn)程的時候,可以使用管道作為子進(jìn)程的輸入?yún)?shù)startupinfo,從而完美實現(xiàn)子進(jìn)程輸入輸出重定向。但在linux確不行,不支持參數(shù)startupinfo。

process=subprocess.Popen參數(shù)subprocess.PIPE字面上是返回管道,但子進(jìn)程process.stdout實際是文件句柄,讀操作完全是阻塞,沒有非阻塞得讀,這是問題的關(guān)鍵所在。

方案二和方案四不妨結(jié)合起來使用,對于長時間執(zhí)行任務(wù),選擇方案四,對于一般的任務(wù)執(zhí)行直接使用方案二。

python中執(zhí)行系統(tǒng)shell命令,也可以創(chuàng)建一個線程進(jìn)行子進(jìn)程輸出讀取,超時就殺掉線程;或者使用協(xié)程版本的subprocess,但是實現(xiàn)起來更加復(fù)雜,效率顯得更差。有興趣的同學(xué),可以自己實現(xiàn)試試。

enjoy~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容