你能安全地读取带有朴素的 try-except 块的 utf8 和 latin1 文件吗?



我相信任何有效的latin1字符要么会被Python的utf8编码器正确解释,要么会引发错误。I、 因此,声称如果您只使用utf8文件或latin1文件,您可以安全地编写以下代码来读取这些文件,而不会以Mojibake告终:

from pathlib import Path
def read_utf8_or_latin1_text(path: Path, args, kwargs):
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="latin1")

我在这个大字符数据集上测试了这个假设,发现它经得起推敲。总是这样吗?

输入:

import requests
insanely_many_characters = requests.get(
"https://github.com/bits/UTF-8-Unicode-Test-Documents/raw/master/UTF-8_sequence_unseparated/utf8_sequence_0-0x10ffff_including-unassigned_including-unprintable-asis_unseparated.txt"
).text

print(
f"n=== test {len(insanely_many_characters)} utf-8 characters for same-same misinterpretations ==="
)
for char in insanely_many_characters:
if (x := char.encode("utf-8").decode("utf-8")) != char:
print(char, x)

print(
f"n=== test {len(insanely_many_characters)} latin1 characters for same-same misinterpretations ==="
)
latinable = []
nr = 0
for char in insanely_many_characters:
try:
if (x := char.encode("latin1").decode("latin1")) != char:
print(char, x)
latinable.append(char)
except UnicodeEncodeError:
nr += 1
if nr:
print(f"{nr} characters not in latin1 set")
print('found the following valid latin1 characters: """n' + "".join(latinable) + 'n"""')
print(
f"n=== test {len(latinable)} latin1 characters for utf-8 Mojibake ==="
)
for char in latinable:
try:
if (x := char.encode("latin1").decode("utf-8")) != char:
print(char, x)
except UnicodeDecodeError:
pass

输出:


=== test 1111998 utf-8 characters for same-same misinterpretations ===
=== test 1111998 latin1 characters for same-same misinterpretations ===
1111742 characters not in latin1 set
found the following latin1 characters: """

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
"""
=== test 256 latin1 characters for utf-8 Mojibake ===

附录:

我发现我完全忘记了测试拉丁1字符的序列,只测试了单个字符。通过添加此测试:

print(
f"n=== test {len(latinable)} latin1 sequences wrongly interoperable by utf-8 ==="
)
for char1 in latinable:
for char2 in latinable:
try:
if (x := (char1 + char2).encode("latin1").decode("utf-8")) != char1 + char2:
print(char1 + char2, x)
except UnicodeDecodeError:
pass

我最终生成了许多utf-8 Mojibake(总共1920个实例(,这是我假设的反例!:

=== test 256 latin1 sequences wrongly interoperable by utf-8 ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
   
¡ ¡
⋮

您不正确。latin-1编码是针对每个字节的1-1映射。有效编码的latin-1完全有可能同时包含解码为不同UTF-8字符的字节。数据中出现的非ASCII字符越多,这种情况发生的几率就会降低,但也可能发生。

您的测试没有发现这些情况,因为根据定义,至少需要两个编码为latin-1的字符才能以UTF-8对单个字符进行有效但不匹配的编码(非ASCII编码为UTF-8的长度总是2-4字节,而不是只有一个字节;ASCII在latin-1和UTF-8中编码相同(。由于您只测试了对latin-1中的单个字符进行编码,因此它不可能产生合法的UTF-8表示,但ASCII范围以上的许多对(或三元组或四元组(latin-1字符将同时产生合法的UTF-8字节。它们可能会造成完全的垃圾,但它们会有效地解码。

您最初的假设是正确的:如果一个只能是latin1编码的utf-8的文件不能被读取为utf-8,那么它就是latin1。事实上,任何字节序列都可以被解码为latin1,因为latin1编码是256个可能的字节和码点在[0;256[范围.的unicode字符之间的双射

但你的测试可能大不相同。您将一个有效的utf-8编码文件加载到unicode字符中,然后测试latin1中存在哪些unicode字符,发现只有最先的256个字符。

换句话说,这个问题和代码只是松散地联系在一起。。。

理论上,存在有效的Latin-1序列,这些序列也是有效的UTF-8序列。在实践中,这些在任何现实世界的文本数据中都是不可能的。

您可以很容易地创建一个可能的组合表;这是品尝的样品。

>>> for x in "u00a0u00a1u00a2u00ffu1234U00012345":
...   print(x.encode('utf-8').decode('latin-1'))
... 
 
¡
¢
ÿ
á´
ð

(ð后面有一些空白字符。(

一个重要的角落案例可能是包含mojibake的文件;几乎不可能设计出一种启发式方法,在人类愚蠢的所有方面都能正确工作。如果您将Latin-1与UTF-8中偶尔出现的mé¶jibéké©混合在一起,那么天真的算法会得出结论,整个文件必须是Latin-1。(另一方面,如果整个文件都受到影响,它实际上会为您解决问题。(

最新更新