【问题标题】:How to prevent "UnicodeDecodeError" when reading piped input from sys.stdin?从 sys.stdin 读取管道输入时如何防止“UnicodeDecodeError”?
【发布时间】:2018-10-27 20:44:28
【问题描述】:

我正在将一些主要的十六进制输入读入 Python3 脚本。然而,系统 设置为使用 UTF-8 并且当从 Bash shell 管道到脚本时,我保留 得到以下UnicodeDecodeErrorerror:

UnicodeDecodeError: ('utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte)

根据其他 SO 答案,我在 Python3 中使用sys.stdin.read() 读取管道输入,如下所示:

import sys
...
isPipe = 0
if not sys.stdin.isatty() :
    isPipe = 1
    try:
        inpipe = sys.stdin.read().strip()
    except UnicodeDecodeError as e:
        err_unicode(e)
...

使用这种方式配管时有效:

# echo "\xed\xff\xff\x0b\x04\x00\xa0\xe1" | some.py
<output all ok!>

但是,使用原始格式不会:

# echo -en "\xed\xff\xff\x0b\x04\x00\xa0\xe1"

    ▒▒▒
   ▒▒

# echo -en "\xed\xff\xff\x0b\x04\x00\xa0\xe1" | some.py
UnicodeDecodeError: ('utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte)

还尝试了其他有希望的 SO 答案:

# echo -en "\xed\xff\xff\x0b\x04\x00\xa0\xe1" | python3 -c "open(1,'w').write(open(0).read())"
# echo -en "\xed\xff\xff\x0b\x04\x00\xa0\xe1" | python3 -c "from io import open; open(1,'w').write(open(0).read())"

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib/python3.6/codecs.py", line 321, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte

从我目前了解到的情况来看,当您的终端遇到UTF-8 序列时,它是expecting 它后面跟着1-3 个其他字节,如下所示:

UTF-8 是一种可变宽度字符编码,能够使用 一到四个 8 位字节 对 Unicode 中的所有有效代码点进行编码。 因此,在 前导字节0xC2 - 0xF4 范围内的第一个 UTF-8 字符)之后的任何内容都将跟随 1-3 连续字节,在里面 范围0x80 - 0xBF

但是,我不能总是确定我的输入流来自哪里,它很可能是原始数据,而不是上面的 ASCII HEX 版本。所以我需要以某种方式处理这个原始输入。

我查看了一些替代方案,例如:

  • 使用codecs.decode

  • open("myfile.jpg", "rb", buffering=0)raw i/o 一起使用

  • 使用来自bytesbytes.decode(encoding="utf-8", errors="ignore")

  • 或者只使用open(...)

但我不知道他们是否或如何像我想要的那样读取管道输入流。

如何让我的脚本也处理原始字节流?

PS。是的,我已经阅读了大量类似的 SO 问题,但没有一个能够充分处理这个 UTF-8 输入错误。最好的是this one

这不是重复的。

【问题讨论】:

  • 您的输入是否(某些)恰好是十六进制数字并不重要。但是“原始”是指任意 二进制 输入,对吧?
  • @DavisHerring 是的,二进制。但是,我不同意我的问题是重复的,只是因为其中可能有一个与我的远程相关的嵌入式答案。这个问题(你链接的)与我的完全不同,当遇到我的问题或错误时,任何人都不太可能搜索这些词。
  • 它几乎没有“远程相关”:这个问题涉及读取写入二进制数据,但是一个答案的前三句话完全回答了这个问题。我通过搜索与这个问题相关的术语找到了它,尽管我同意它的标题有点缺乏“规范的buffer 问题”。

标签: python python-3.x character-encoding pipe stdin


【解决方案1】:

我终于设法通过使用sys.stdin来解决这个问题!

我使用了with open(0, 'rb')。其中:

  • 0 是等同于 stdin 的文件指针。
  • 'rb' 正在使用 binary 模式进行阅读

这似乎绕过了 system 试图解释管道中的 locale 字符的问题。在看到以下内容后我得到了这个想法,并返回了正确的(不可打印的)字符:

echo -en "\xed\xff\xff\x0b\x04\x00\xa0\xe1" | python3 -c "with open(0, 'rb') as f: x=f.read(); import sys; sys.stdout.buffer.write(x);"

▒▒▒
   ▒▒

所以为了正确读取任何管道数据,我使用了:

if not sys.stdin.isatty() :
    try:
        with open(0, 'rb') as f: 
            inpipe = f.read()

    except Exception as e:
        err_unknown(e)        
    # This can't happen in binary mode:
    #except UnicodeDecodeError as e:
    #    err_unicode(e)
...

这会将您的管道数据读入python 字节字符串

下一个问题是确定管道数据是来自字符串(如echo "BADDATA0")还是来自二进制流。后者可以由echo -ne "\xBA\xDD\xAT\xA0" 模拟,如 OP 所示。就我而言,我只是使用 RegEx 来查找超出范围的非 ASCII 字符。

if inpipe :
    rx = re.compile(b'[^0-9a-fA-F ]+') 
    r = rx.findall(inpipe.strip())
    if r == [] :
        print("is probably a HEX ASCII string")
    else:
        print("is something else, possibly binary")

当然,这可以做得更好、更聪明。 (欢迎评论!)


附录:(来自here

mode 是一个可选字符串,用于指定打开文件的模式。它默认为r,这意味着以文本模式打开以供阅读。在文本模式中,如果未指定编码,则使用的编码取决于平台:调用locale.getpreferredencoding(False) 以获取当前的语言环境编码。 (对于读取和写入原始字节,请使用二进制模式,并且未指定编码。)默认模式是“r”(打开以读取文本,“rt”的同义词)。对于 binary 读写访问,w+b 模式打开文件并将文件截断为 0 字节。 r+b 打开文件而不截断。

... Python 区分二进制和文本 I/O。以二进制模式打开的文件(包括 mode 参数中的 b)将内容作为 bytes 对象 返回,无需任何解码。在文本模式下(默认或当t 包含在模式参数中时),文件的内容以 str 的形式返回,这些字节首先使用平台相关的编码解码或如果给定,则使用指定的编码。

如果 closefdFalse 并且给出了文件描述符而不是文件名,则当文件关闭时,底层文件描述符将保持打开状态。如果给定文件名,closefd 必须是 True(默认),否则会引发错误。

【讨论】:

  • 您应该将 closefd=False 传递给 open 以便 with 语句在完成时不会关闭标准输入。同样打开和读取二进制文件也不会引发 UnicodeDecodeError。当一个字节被解码为一个字符串时会抛出这个问题,当您将文件作为文本读取(使用不带 'b' 的打开并读取文件)或使用 bytes.decode 函数时会发生这种情况。
  • @daz 是的,我现在看到我的第一个open() 试验没有使用b 标志。此外,使用closefd=False 似乎没有任何区别。那你为什么认为这很重要?再说一次,我没有尝试从输入中断流。
  • 无论您使用什么脚本,它都可能无关紧要。但是如果没有 closefd,stdin 会被 with 语句关闭,因此之后您将无法使用它。标准流总是会被打开。
【解决方案2】:

这是一种像文件一样读取二进制标准输入的 hacky 方法:

import sys

with open(sys.stdin.fileno(), mode='rb', closefd=False) as stdin_binary:
    raw_input = stdin_binary.read()
try:
    # text is the string formed by decoding raw_input as unicode
    text = raw_input.decode('utf-8')
except UnicodeDecodeError:
    # raw_input is not valid unicode, do something else with it

【讨论】:

    【解决方案3】:

    使用sys.stdin.buffer.raw 而不是sys.stdin

    【讨论】:

    • 在您的答案中添加一些信息,例如为什么最好使用sys.stdin.buffer.raw 而不是sys.stdin 会使这个答案更好。 From review
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多