提问人:Simon Streicher 提问时间:11/1/2022 最后编辑:Simon Streicher 更新时间:11/2/2022 访问量:592
你能用一个朴素的 try-except 块安全地读取 utf8 和 latin1 文件吗?
Can you safely read utf8 and latin1 files with a naïve try-except block?
问:
我相信任何有效的 latin1 字符都会被 Python 的 utf8 编码器正确解释或抛出错误。因此,我声称,如果您只使用 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 ===
补遗:
我看到我完全忘记了测试 latin1 字符的序列,而只测试了单个字符。通过添加此测试:
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 序列,它们也是有效的 UTF-8 序列。在实践中,这些在任何真实世界的文本数据中都不太可能。
您可以轻松创建可能组合的表格;这是一个样品供品尝。
>>> for x in "\u00a0\u00a1\u00a2\u00ff\u1234\U00012345":
... print(x.encode('utf-8').decode('latin-1'))
...
Â
¡
¢
ÿ
á´
ð
(后面有一些空格字符。ð
一个重要的极端情况可能是包含mojibake的文件;几乎不可能设计出一种启发式方法,在面对人类愚蠢的聪明才智的所有方面都能正确工作。如果您将 Latin-1 与偶尔来自 UTF-8 的 möjibäké 混合在一起,则朴素的算法会得出结论,整个文件必须是 Latin-1。(另一方面,如果整个文件都受到影响,它实际上会为您解决问题。
评论
utf-8-sig
utf-8
utf-8
encoding="auto"
utf-8
latin1
utf-8
latin1
你错了。编码是每个字节的 1-1 映射。巧合的是,有效编码完全有可能包含解码为不同 UTF-8 字符的字节。数据中存在的非 ASCII 字符越多,发生此类事情的几率就越低,但它可能会发生。latin-1
latin-1
您的测试没有找到这些情况,因为根据定义,至少需要两个编码为 latin-1 的字符才能生成有效但不匹配的 UTF-8 中单个字符的编码(编码为 UTF-8 的非 ASCII 长度始终为 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[ 范围。
但是你的测试如果完全不同。将有效的 utf-8 编码文件加载为 unicode 字符,然后测试 latin1 中存在哪些 unicode 字符,发现只有 256 个字符排在第一位。
换句话说,问题和代码只是松散地相关......
评论
The fact is that any sequence of bytes can be decoded as latin1
是我的假设的一个很好的理论反驳,谢谢。因为这意味着任何有效的 utf-8 文件理论上都可以是所需的 latin1 文件。
评论