如何捕获Python subprocess.check_output()中的异常输出?

130 投票
10 回答
266522 浏览
提问于 2025-04-18 14:01

我想在Python中进行比特币支付。在命令行中,我通常会这样做:

bitcoin sendtoaddress <bitcoin address> <amount>

比如说:

bitcoin sendtoaddress 1HoCUcbK9RbVnuaGQwiyaJGGAG6xrTPC9y 1.4214

如果支付成功,我会得到一个交易ID作为输出,但如果我尝试转账的金额超过了我的比特币余额,我会得到以下输出:

error: {"code":-4,"message":"Insufficient funds"}

现在在我的Python程序中,我尝试这样进行支付:

import subprocess

try:
    output = subprocess.check_output(['bitcoin', 'sendtoaddress', address, str(amount)])
except:
    print "Unexpected error:", sys.exc_info()

如果余额足够,支付就能顺利进行,但如果余额不足,sys.exc_info() 会输出这个:

(<class 'subprocess.CalledProcessError'>, CalledProcessError(), <traceback object at 0x7f339599ac68>)

不过它没有包含我在命令行上看到的错误信息。所以我想问的是;我怎么才能在Python中获取到这个错误信息({"code":-4,"message":"Insufficient funds"})呢?

10 个回答

11

从Python 3.5开始,subprocess.run()这个函数增加了一个叫做check的参数:

如果check设置为真,而程序的退出状态码不是零,那么就会抛出一个叫CalledProcessError的异常。这个异常里面会包含一些信息,比如传入的参数、退出状态码,以及如果有的话,标准输出和错误输出。

下面是一个简单的例子,这个例子会抛出并打印出CalledProcessError

import subprocess
try:
    subprocess.run("exit 1", shell=True, check=True, timeout=15, capture_output=True)
except subprocess.CalledProcessError as e:
    print(e)  # Output: Command 'exit 1' returned non-zero exit status 1.
12

尝试“转账一个超过我比特币余额的金额”并不是一个意外的错误。你可以直接使用 Popen.communicate(),而不是 check_output(),这样就可以避免不必要地抛出异常:

from subprocess import Popen, PIPE

p = Popen(['bitcoin', 'sendtoaddress', ..], stdout=PIPE)
output = p.communicate()[0]
if p.returncode != 0: 
   print("bitcoin failed %d %s" % (p.returncode, output))
19

正如@Sebastian提到的,默认的解决方案应该使用 run() 方法:https://docs.python.org/3/library/subprocess.html#subprocess.run

这里有一个方便的实现(你可以把日志类换成打印语句,或者用你正在使用的其他日志功能):

import subprocess

def _run_command(command):
    log.debug("Command: {}".format(command))
    result = subprocess.run(command, shell=True, capture_output=True)
    if result.stderr:
        raise subprocess.CalledProcessError(
                returncode = result.returncode,
                cmd = result.args,
                stderr = result.stderr
                )
    if result.stdout:
        log.debug("Command Result: {}".format(result.stdout.decode('utf-8')))
    return result

还有一个示例用法(这段代码虽然不相关,但我觉得它可以作为一个例子,展示这个简单实现的错误处理是多么易读和好用):

try:
    # Unlock PIN Card
    _run_command(
        "sudo qmicli --device=/dev/cdc-wdm0 -p --uim-verify-pin=PIN1,{}"
        .format(pin)
    )

except subprocess.CalledProcessError as error:
    if "couldn't verify PIN" in error.stderr.decode("utf-8"):
        log.error(
                "SIM card could not be unlocked. "
                "Either the PIN is wrong or the card is not properly connected. "
                "Resetting module..."
                )
        _reset_4g_hat()
        return
62

我觉得被接受的解决方案没有处理错误信息在标准错误输出(stderr)上的情况。根据我的测试,异常的输出属性没有包含来自标准错误输出的结果,而且文档也提醒不要在check_output()中使用stderr=PIPE。相反,我建议对J.F Sebastian的解决方案做一个小改进,增加对标准错误输出的支持。毕竟,我们是在处理错误,而错误通常是在标准错误输出中报告的。

from subprocess import Popen, PIPE

p = Popen(['bitcoin', 'sendtoaddress', ..], stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if p.returncode != 0: 
   print("bitcoin failed %d %s %s" % (p.returncode, output, error))
182

根据 subprocess.check_output() 的文档,当出现错误时,会抛出一个异常,这个异常有一个 output 属性,你可以用它来获取错误的详细信息:

try:
    subprocess.check_output(...)
except subprocess.CalledProcessError as e:
    print(e.output)

然后你可以分析这个字符串,并使用 json 模块来解析错误的具体信息:

if e.output.startswith('error: {'):
    error = json.loads(e.output[7:]) # Skip "error: "
    print(error['code'])
    print(error['message'])

撰写回答