如何将最长的子字节解码为 str?

How to decode longest sub-bytes into str?

假设我从某个地方读取了一个很长的 bytes 对象,知道它是 utf-8 编码的。但是读取可能不会完全消耗可用内容,因此流中的最后一个字符可能不完整。在此对象上调用 bytes.decode() 可能会导致解码错误。但真正失败的只是最后几个字节。在这种情况下是否有一个函数可以工作,return最长的解码字符串和剩余的字节?

utf-8 最多将一个字符编码为 4 个字节,因此尝试对截断的字节进行解码应该可行,但绝大多数计算将被浪费,我不太喜欢这种解决方案。

举个简单但具体的例子:

>>> b0 = b'\xc3\x84\xc3\x96\xc3'
>>> b1 = b'\x9c\xc3\x84\xc3\x96\xc3\x9c'
>>> (b0 + b1).decode()
>>> 'ÄÖÜÄÖÜ'

(b0 + b1).decode() 没问题,但 b0.decode() 会加注。该解决方案应该能够尽可能多地解码 b0 和 return 无法解码的字节。

您描述的是 io.TextIOWrapper 的基本用法:二进制流上的缓冲文本流。

>>> import io 
>>> txt = 'before\N{PILE OF POO}after' 
>>> b = io.BytesIO(txt.encode('utf-8'))
>>> t = io.TextIOWrapper(b) 
>>> t.read(5) 
'befor'
>>> t.read(1) 
'e'
>>> t.read(1)
''
>>> t.read(1) 
'a'

与直接读取字节流对比,后者可以读取编码的一堆便便的一半:

>>> b.seek(0) 
0
>>> b.read(5)
b'befor'
>>> b.read(1)
b'e'
>>> b.read(1)
b'\xf0'
>>> b.read(1)
b'\x9f'
>>> b.read(1)
b'\x92'
>>> b.read(1)
b'\xa9'
>>> b.read(1)
b'a'

如果要明确,请指定 encoding="utf-8"。默认编码,即 locale.getpreferredencoding(False),通常是 utf-8。

正如我在@wim 的回答下的评论中提到的,我认为您可以使用 codecs.iterdecode() 增量解码器来执行此操作。由于它是一个生成器函数,因此无需在迭代调用之间手动保存和恢复其状态。

以下是如何使用它来处理您描述的情况:

import codecs
from random import randint


def reader(sequence):
    """ Yield random length chunks of sequence until exhausted. """

    plural = lambda word, n, ending='s': (word+ending) if n > 1 else word

    i = 0
    while i < len(sequence):
        size = randint(1, 4)
        chunk = sequence[i: i+size]

        hexrepr = '0x' + ''.join('%02X' % b for b in chunk)
        print('read {} {}: {}'.format(size, plural('byte', len(chunk)), hexrepr))

        yield chunk
        i += size


bytes_obj = b'\xc3\x84\xc3\x96\xc3\x9c\xc3\x84\xc3\x96\xc3\x9c'  # 'ÄÖÜÄÖÜ'

for decoded in codecs.iterdecode(reader(bytes_obj), 'utf-8'):
    print(decoded)

示例输出:

read 3 bytes: 0xC384C3
Ä
read 1 byte: 0x96
Ö
read 1 byte: 0xC3
read 3 bytes: 0x9CC384
ÜÄ
read 2 bytes: 0xC396
Ö
read 4 bytes: 0xC39C
Ü