我在读取具有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.
我知道我可以打开二进制文件,然后通过寻找任何位置来读取文件而不会出现问题,但是,我需要处理字符串,因此在解码为字符串时会遇到同样的问题。
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>
答案 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:〹宠蜇