如何在读取sys.stdin的管道输入时防止"UnicodeDecodeError"错误?

3
我正在将一些主要为十六进制的输入读入Python3脚本。然而,系统设置为使用UTF-8,当从Bash shell导入到脚本中时,我不断收到以下UnicodeDecodeErrorerror:

UnicodeDecodeError: ('utf-8'编解码器无法在位置0解码字节0xed:无效的连续字节)

我正在使用sys.stdin.read()在Python3中读取管道输入,根据其他SO答案,像这样:

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序列时,它期望其后跟随1至3个其他字节,就像这样:

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

然而,我不能总是确定我的输入流来自哪里,它很可能是原始数据,而不是像上面那样ASCII HEX字符串。因此,我需要以某种方式处理这些原始输入。

我看过了几种替代方案,比如:

  • 使用codecs.decode进行编码

  • 使用open("myfile.jpg", "rb", buffering=0)原始I/O一起使用

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

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

但我不知道它们是否能够读取像我想要的管道输入流。

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

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

这不是一个重复问题。


如果你的输入是十六进制数字,那也没关系。但是“原始”的意思是任意的二进制输入,对吧? - Davis Herring
@DavisHerring 是的,二进制。然而,我不同意我的问题是重复的,仅仅因为它可能包含一个与我的问题有关的嵌入式答案。这个问题(你链接的)的形式完全不同于我的,当遇到我的问题或错误时,很少有人会搜索那些单词。 - not2qubit
这几乎没有“远程关联”:该问题涉及读取和写入二进制数据,但一个答案的前三个句子完全回答了这个问题。我通过搜索与此问题相关的术语找到了它,尽管我同意它的标题有点不足以成为“规范的buffer问题”。 - Davis Herring
3个回答

7

我最终通过不使用sys.stdin来解决了这个问题!

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

  • 0是文件指针,等同于stdin
  • 'rb'是使用二进制模式进行读取

这似乎规避了系统尝试在管道中解释您的本地化字符的问题。我得到了这个想法,因为看到以下内容有效,并返回了正确的(不可打印)字符:

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")还是二进制流。后者可以通过像OP中显示的echo -ne "\xBA\xDD\xAT\xA0"这样的方法模拟。在我的情况下,我只使用了一个正则表达式来查找超出范围的非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")

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

补充:(来自此处

mode 是一个可选的字符串,用于指定文件打开的模式。默认为r,表示以文本模式读取文件。在文本模式下,如果未指定编码,则使用平台相关的编码:locale.getpreferredencoding(False)将被调用以获取当前的区域编码。(对于读写原始字节,请使用二进制模式并保留编码未指定。)默认模式为'r'(打开文本进行读取,与'rt'同义)。对于二进制读写访问,模式w+b会打开并将文件截断为0字节。r+b打开文件但不截断。

... Python区分二进制和文本I/O。以二进制模式打开的文件(包括在模式参数中加入b)将返回bytes对象而没有任何解码。在文本模式下(默认情况下或在模式参数中包括t时),文件的内容将作为str返回,首先使用平台相关的编码或使用指定的编码进行解码。

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


1
你应该在open函数中传入closefd=False,这样with语句在结束时不会关闭stdin。此外,以二进制方式打开和读取文件时不会引发UnicodeDecodeError。当将字节解码为字符串时(使用open而不带'b'读取文件或使用bytes.decode函数),才会抛出该异常。 - daz
@daz 是的,我现在看到我的第一个 open() 尝试没有使用 b 标志。此外,使用 closefd=False 似乎没有任何区别。那么你为什么认为这很重要呢?不过话说回来,我还没有尝试从输入中打断流程。 - not2qubit
对于您正在使用的任何脚本,这可能并不重要。但是如果没有closefd,stdin将被with语句关闭,因此您之后将无法使用它。标准流始终应该是打开的。 - daz

3
这是一种粗糙的读取标准输入流的二进制数据的方法,类似于文件操作:
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

2
请使用sys.stdin.buffer.raw代替sys.stdin

1
给你的答案添加一些信息,例如为什么最好使用sys.stdin.buffer.raw而不是sys.stdin,这将使您的答案更好。[来自评论] - Pranav Hosangadi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接