在Python 3.7.3中正确地对字节进行解码/编码

6

我遇到了困难:

b'"\xc2\xb7\xed\xa0\x81\xed\xb1\x96\xed\xa0\x81\xed\xb1\xb1\xed\xa0\x81\xed\xb1\x9d\xed\xa0\x81\xed\xb1\xbe\xed\xa0\x81\xed\xb1\xaf \xed\xa0\x81\xed\xb1\xa9\xed\xa0\x81\xed\xb1\xa4\xed\xa0\x81\xed\xb1\x93\xed\xa0\x81\xed\xb1\xa9\xed\xa0\x81\xed\xb1\x9a\xed\xa0\x81\xed\xb1\xa7\xed\xa0\x81\xed\xb1\x91"@en'

来自HDT压缩版本(https://github.com/rdfhdt/hdt-cpp)的二进制格式,来源于(dbpedia 3.5.1 (http://dbpedia.org/page/Shavian_alphabet)),并且可以通过此网站(https://mothereff.in/utf-8)以utf8格式很好地解码。
意思是:"·" @en
但在Python 3.7.3中,当尝试mystring.decode('utf8')时,遇到了众所周知的错误:UnicodeDecodeError:'utf-8'编解码器无法解码第3个位置上的0xed字节:无效的连续字节
如果我试图做相反的事情:'"· "@en'.encode('utf8),我得到以下表示:b'"\xf0\x90\x91\x96\xf0\x90\x91\xb1\xf0\x90\x91\x9d\xf0\x90\x91\xbe\xf0\x90\x91\xaf \xf0\x90\x91\xa8\xf0\x90\x91\xa4\xf0\x90\x91\x93\xf0\x90\x91\xa9\xf0\x90\x91\x9a\xf0\x90\x91\xa7\xf0\x90\x91\x91"@en',它不是完全相同的字符串,但是经过解码repr.decode('utf8')可以正确地得到相同的结果。 有人能帮我理解为什么解码第一个字节字符串不起作用吗? 我知道第一个字节字符串由于错误而不是有效的UTF-8字符串。 但是,为什么它可以被我链接的网站正确解码,而Python无法做到呢? 提前致谢!
最终编辑 在接受答案后,我对此进行了一些额外的研究,并发现该字符串使用CESU-8编解码。这种编码方式已经过时了,但仍有人在使用...因此,我找到了一个软件包,它编写了utf-8编解码的变体,可以解码此字符串。我认为这将帮助许多像我一样遇到同样问题的人。 Python库:https://github.com/LuminosoInsight/python-ftfy 添加的编解码是“utf-8-variants”。我希望这能帮助有同样需求的人。

你是在问“为什么它在那个网站上可用?”还是在问“我该如何将这个字节串转换为'xyz...'?” - wwii
我认为两个问题都需要解决。如何将这个字节字符串转换,并且它在网站上是如何工作的? - Folkvir
2
@wwii 在这种情况下,它并不是一个无效的连续字节,而是一个在下一级别上的问题,正如我在我的回答中所解释的那样。 - zvone
1个回答

7

看起来 Python 不想接受一些字节序列作为有效的 UTF-8,而一些网站(https://mothereff.in/utf-8)接受它。它们中的一个肯定是错误的,对吧?让我们来看看。

Python 接受前两个字节 (b'\xc2\xb7')。Python 不喜欢的第一件事是这个:\xed\xa0\x81\xed\xb1\x96,在那个网站上被解释为“?”。

让我们看一下二进制格式的\xed\xa0\x81\xed\xb1\x96

11101101
10100000
10000001
11101101
10110001
10010110

RFC3629说UTF-8被解释为:

   Char. number range  |        UTF-8 octet sequence
      (hexadecimal)    |              (binary)
   --------------------+---------------------------------------------
   0000 0000-0000 007F | 0xxxxxxx
   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
因此,有两个三字节字符:
11101101 10100000 10000001 ⇒ 1101100000000001,即 D801
11101101 10110001 10010110 ⇒ 1101110001010110,即 DC56
字符D801高代理项之一DC56低代理项之一
您可以在这里看到如何组合代理项:
引用:

代理对表示代码点0x10000 + (H − 0xD800) × 0x400

  • (L − 0xDC00),其中H和L分别是高代理项和低代理项的数字值。
如果将它们组合起来,您将得到:0x10000 + (0xD801 - 0xD800) * 0x400 + (0xDC56 - 0xDC00) = 0x10456,这是
然而,高代理项和低代理项是为UTF-16字符表示设计的,这些字符不适合16位,unicode.org关于在UTF-8中使用这样的代理对的说法如下:

问:如何将UTF-16代理对(例如<D800 DC00>)转换为UTF-8?作为一个4字节序列还是两个独立的3字节序列?

答:UTF-8的定义要求辅助字符(在UTF-16中使用代理对的字符)应该使用单个4字节序列进行编码。然而,在旧软件中,特别是在UTF-16引入之前或在特定约束条件下与UTF-16环境进行交互的软件中,普遍存在生成一对3字节序列的做法。这样的编码不符合UTF-8的定义。请参阅UTR #26:UTF-16的兼容性编码方案:8位(CESU),了解此类非UTF-8数据格式的正式描述。使用CESU-8时,必须非常小心,以防数据因格式相似而被意外视为UTF-8。[AF]

重点在于“这种编码不符合UTF-8的定义”。因此,您的输入实际上是无效的UTF-8序列,并且Python将其拒绝。
回答问题:
- https://mothereff.in/utf-8 忽略了unicode.org的指令,将其视为有效。 - Python将其视为无效。 - 如果您想解码它,即使它无效,您可以编写一个函数来执行我手动完成的操作。

顺便问一下,你有没有听说过一个库可以做到这个(避免重复造轮子)? 如果没有的话,我会根据你的指示创建自己的转换器。 - Folkvir
@Folkvir,恐怕我不知道有这样的库。它可能存在。你可以决定是花时间寻找它还是重新发明轮子 ;) - zvone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接