我在读取具有UTF8和ASCII字符的文件时遇到了问题。问题是,我使用的是寻求只读取部分数据,但我不知道我是否“读”在一个UTF8的“中间”。
简单地说,我的问题可以用下面的代码演示。
# 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. 我知道我可以以二进制方式打开文件,然后通过查找到任何位置来读取它,但是,我需要处理字符串,所以在解码成string时,我将以相同的问题结束。
data = open('/tmp/test.txt', 'rb')
data.seek(1)
z = data.seek(3)
z.decode() # will hit same error 不使用using,即使只调用read(1),我也可以正确地读取它。
data = open('/tmp/test.txt')
data.tell() # 0
data.read(1)
data.tell() # shows 3 even calling read(1)我能想到的一件事是,在寻找一个位置之后,尝试阅读,在UnicodeDecodeError上,位置=位置-1,寻找(位置),直到我能够正确地阅读它。
有更好(对)的方法来处理它吗?
发布于 2018-07-02 19:12:55
正如文档所解释的,当您对文本文件进行seek时:
偏移量必须是
TextIOBase.tell()返回的数字,或者是零。任何其他偏移值都会产生未定义的行为。
实际上,seek(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')或者,您可以使用UTF-8编码知识手动扫描二进制文件中的有效开始字节:
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:〹宠蜇https://stackoverflow.com/questions/51142314
复制相似问题