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


72

我正在尝试从Python内部进行比特币付款。在bash中,我通常会这样做:

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>)

它不包括我在命令行中得到的错误。所以我的问题是;如何{"code":-4,"message":"Insufficient funds"}从Python中获取输出的错误()?

欢迎所有提示!

Answers:


106

根据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'])

我正在调用一个程序,该程序将输出内容输出到stdout,然后返回1,但check_output无法捕获它
JorgeeFG 2016年

@JorgeeFG然后我猜你的程序出了点问题。请注意,评论部分不是提出新问题的正确位置。如果您需要特定问题的帮助,请单击页面右上方的大“询问问题”按钮。
Ferdinand Beyer

36

我认为接受的解决方案无法处理在stderr上报告错误文本的情况。根据我的测试,异常的输出属性不包含stderr的结果,并且文档警告不要在check_output()中使用stderr = PIPE。相反,我建议通过添加stderr支持对JF Sebastian的解决方案进行一点小改进。毕竟,我们正在尝试处理错误,而stderr是经常被报告的地方。

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))

我同意stderr这里的输出非常重要。一种替代解决方案是改用该run()函数(请参见check_output文档如何替换)。因为这样您可以e.stderr在错误报告中使用异常。
塞巴斯蒂安

这应该在顶部。
奥列格

为了保留输出的明显顺序,您可以使用stderr=STDOUT(合并两个流)。
jfs

我认为有必要指出,如果您不拨打电话,.communicate.returncode输出将为空(None
lupodellasleppa

if p.returncode != 0:在这里没有工作,因为我获得None了成功。必须使用if p.returncode:
Giulio

10

尝试“转账大于我的比特币余额的金额”不是意外的错误。您可以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))

Python鼓励使用EAFP编程风格(比请求权限更容易获得宽恕),在此类情况下更喜欢使用异常处理而不是“ if”检查。
Ferdinand Beyer

8
@FerdinandBeyer:EAFP在这种情况下不适用:您不会拨打任何您不会拨打的电话。该代码不具有LBYL结构:if check(): do()您可能已将其替换为EAFP try: do() except Error: handle_error()。答案内联check_output()函数中的代码起作用,并避免在if p.returncode分支中引发异常以仅在同一级别捕获该异常。想一想,避免使用繁琐的编程方式
-jfs

2
我们还可以执行以下操作:p = Popen(['bitcoin', 'sendtoaddress', ..], stdout=PIPE, stderr=PIPE)并将错误消息捕获为:output, error = p.communicate()
alper

对于使用管道的命令,我也该怎么做?@jfs
alper

@alper将命令作为字符串传递并添加shell=True参数:p = Popen("a | b", shell=True, ..)
jfs

3

这里有很好的答案,但是在这些答案中,没有来自堆栈跟踪输出文本的答案,这是异常的默认行为。

如果您希望使用格式化的回溯信息,则可能希望:

import traceback

try:
    check_call( args )
except CalledProcessError:
    tb = traceback.format_exc()
    tb = tb.replace(passwd, "******")
    print(tb)
    exit(1)

如您所知,如果您希望防止在check_call(args)中输入密码,则上述内容很有用。


3

这对我有用。它捕获子进程的所有stdout输出(对于python 3.8):

from subprocess import check_output, STDOUT
cmd = "Your Command goes here"
try:
    cmd_stdout = check_output(cmd, stderr=STDOUT, shell=True).decode()
except Exception as e:
    print(e.output.decode()) # print out the stdout messages up to the exception
    print(e) # To print out the exception message

2

如@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

0

基于@macetw的答案,我将异常直接打印到装饰器中的stderr。

Python 3

from functools import wraps
from sys import stderr
from traceback import format_exc
from typing import Callable, Collection, Any, Mapping


def force_error_output(func: Callable):
    @wraps(func)
    def forced_error_output(*args: Collection[Any], **kwargs: Mapping[str, Any]):
        nonlocal func

        try:
            func(*args, **kwargs)
        except Exception as exception:
            stderr.write(format_exc())
            stderr.write("\n")
            stderr.flush()

            raise exception

    return forced_error_output

Python 2

from functools import wraps
from sys import stderr
from traceback import format_exc


def force_error_output(func):
    @wraps(func)
    def forced_error_output(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except Exception as exception:
            stderr.write(format_exc())
            stderr.write("\n")
            stderr.flush()

            raise exception

    return forced_error_output

然后在您的工人中使用装饰器

@force_error_output
def da_worker(arg1: int, arg2: str):
    pass
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.