python读取文件utf-8解码问题

时间:2018-07-02 19:03:24

标签: python python-3.x

我在读取具有UTF8和ASCII字符的文件时遇到问题。问题是我正在使用搜寻仅读取部分数据,但是我不知道我是否在UTF8的“中间”中“读取”。

  • osx
  • python 3.6.6

简单来说,我的问题可以通过以下代码进行演示。

# write some utf-8 to a file
open('/tmp/test.txt', 'w').write(chr(12345)+chr(23456)+chr(34567)+'\n')
data = open('/tmp/test.txt')
data.read() # this works fine. to just demo I can read the file as whole
data.seek(1)
data.read(1) # UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte
# I can read by seek 3 by 3
data.seek(3)
data.read(1) # this works fine. 

我知道我可以打开二进制文件,然后通过寻找任何位置来读取文件而不会出现问题,但是,我需要处理字符串,因此在解码为字符串时会遇到同样的问题。

data = open('/tmp/test.txt', 'rb')
data.seek(1)
z = data.seek(3)
z.decode() # will hit same error 

不使用搜索,即使调用read(1)也可以正确读取它。

data = open('/tmp/test.txt')
data.tell() # 0
data.read(1) 
data.tell() # shows 3 even calling read(1)

我能想到的是寻找到一个位置之后,尝试读取UnicodeDecodeError上的position = position -1,seek(position),直到我可以正确读取为止。

是否有更好(正确)的方法来处理它?<​​/ p>

1 个答案:

答案 0 :(得分:2)

如文档所述,当您seek使用文本文件时:

  

偏移量必须为TextIOBase.tell()返回的数字,或者为零。任何其他偏移值都会产生不确定的行为。

实际上,seek(1)的实际作用是在文件中寻找1个字节,这将其放在字符的中间。因此,最终发生的事情与此类似:

>>> s = chr(12345)+chr(23456)+chr(34567)+'\n'
>>> b = s.encode()
>>> b
b'\xe3\x80\xb9\xe5\xae\xa0\xe8\x9c\x87\n'
>>> b[1:]
b'x80\xb9\xe5\xae\xa0\xe8\x9c\x87\n'
>>> b[1:].decode()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb9 in position 3: invalid start byte

因此,seek(3)确实可以使用,尽管它不合法,因为您碰巧正在寻找字符的开头。等效于此:

>>> b[3:].decode()
'宠蜇\n'

如果您要依靠这种未记录的行为来尝试随机查找UTF-8文本文件的中间部分,通常可以按照建议进行操作,以摆脱它。例如:

def readchar(f, pos):
    for i in range(pos:pos+5):
        try:
            f.seek(i)
            return f.read(1)
        except UnicodeDecodeError:
            pass
    raise UnicodeDecodeError('Unable to find a UTF-8 start byte')

或者您可以使用the UTF-8 encoding的知识来手动扫描二进制文件中的有效起始字节:

def readchar(f, pos):
    f.seek(pos)
    for _ in range(5):
        byte = f.read(1)
        if byte in range(0, 0x80) or byte in range(0xC0, 0x100):
            return byte
    raise UnicodeDecodeError('Unable to find a UTF-8 start byte')

但是,如果您实际上只是在某个任意点之前或之后寻找下一个完整的行,那会容易得多。

在UTF-8中,换行符被编码为单个字节,并且与ASCII中的字节相同(即'\n'编码为b'\n'。 (如果您具有Windows风格的结尾,则返回也是如此,因此'\r\n'也编码为b'\r\n'。)这是设计使然,可以更轻松地处理此类问题。

因此,如果以二进制模式打开文件,则可以向前或向后搜索,直到找到换行符为止。然后,您可以使用(二进制文件)readline方法从那里开始读取,直到下一个换行符为止。

确切的细节取决于您要在此处使用什么规则。另外,我将展示一个愚蠢的,完全未优化的版本,该版本一次读取一个字符。在现实生活中,您可能想要一次备份,读取和扫描(例如,使用rfind),例如一次80个字符,但这希望更容易理解:

def getline(f, pos, maxpos):
    for start in range(pos-1, -1, -1):
        f.seek(start)
        if f.read(1) == b'\n':
            break
    else:
        f.seek(0)
    return f.readline().decode()

这里正在起作用:

>>> s = ''.join(f'{i}:\u3039\u5ba0\u8707\n' for i in range(5))
>>> b = s.encode()
>>> f = io.BytesIO(b)
>>> maxlen = len(b)
>>> print(getline(f, 0, maxlen))
0:〹宠蜇
>>> print(getline(f, 1, maxlen))
0:〹宠蜇
>>> print(getline(f, 10, maxlen))
0:〹宠蜇
>>> print(getline(f, 11, maxlen))
0:〹宠蜇
>>> print(getline(f, 12, maxlen))
1:〹宠蜇
>>> print(getline(f, 59, maxlen))
4:〹宠蜇