Python 3:揭开编码和解码方法的神秘面纱

Posted

技术标签:

【中文标题】Python 3:揭开编码和解码方法的神秘面纱【英文标题】:Python 3: Demystifying encode and decode methods 【发布时间】:2012-11-08 07:22:02 【问题描述】:

假设我在 Python 中有一个字符串:

>>> s = 'python'
>>> len(s)
6

现在我encode这个字符串是这样的:

>>> b = s.encode('utf-8')
>>> b16 = s.encode('utf-16')
>>> b32 = s.encode('utf-32')

我从上述操作得到的是一个字节数组——也就是说,bb16b32 只是字节数组(每个字节当然是 8 位长)。

但我们对字符串进行了编码。那么这是什么意思?我们如何在原始字节数组中附加“编码”的概念?

答案在于这些字节数组中的每一个都是以特定方式生成的。让我们看看这些数组:

>>> [hex(x) for x in b]
['0x70', '0x79', '0x74', '0x68', '0x6f', '0x6e']

>>> len(b)
6

这个数组表示对于每个字符我们都有一个字节(因为所有字符都低于 127)。因此,我们可以说将字符串“编码”为 'utf-8' 会收集每个字符对应的代码点并将其放入数组中。如果代码点不能容纳在一个字节中,则 utf-8 会占用两个字节。因此 utf-8 占用尽可能少的字节数。

>>> [hex(x) for x in b16]
['0xff', '0xfe', '0x70', '0x0', '0x79', '0x0', '0x74', '0x0', '0x68', '0x0', '0x6f', '0x0', '0x6e',  '0x0']

>>> len(b16)
14     # (2 + 6*2)

在这里我们可以看到“编码为 utf-16”首先将两个字节的 BOM (FF FE) 放入字节数组中,然后对于每个字符将两个字节放入数组中。 (在我们的例子中,第二个字节总是零)

>>> [hex(x) for x in b32]
['0xff', '0xfe', '0x0', '0x0', '0x70', '0x0', '0x0', '0x0', '0x79', '0x0', '0x0', '0x0', '0x74', '0x0', '0x0', '0x0', '0x68', '0x0', '0x0', '0x0', '0x6f', '0x0', '0x0', '0x0', '0x6e', '0x0', '0x0', '0x0']

>>> len(b32)
28     # (2+ 6*4 + 2)

在“以utf-32编码”的情况下,我们首先放入BOM,然后为每个字符放入四个字节,最后放入两个零字节到数组中。

因此,我们可以说“编码过程”为字符串中的每个字符收集 1 2 或 4 个字节(取决于编码名称),并在它们前面添加更多字节以创建最终的字节结果数组。

现在,我的问题:

我对编码过程的理解是正确的还是我遗漏了什么? 我们可以看到变量bb16b32的内存表示实际上是一个字节列表。字符串的内存表示是什么?究竟是什么存储在内存中的字符串? 我们知道,当我们做encode()时,会收集每个字符对应的码点(编码名对应的码点)并放入数组或字节中。当我们执行decode() 时究竟会发生什么? 我们可以看到在 utf-16 和 utf-32 中都附加了一个 BOM,但是为什么在 utf-32 编码中附加了两个零字节呢?

【问题讨论】:

两个零字节是 utf32 BOM 的一部分(0x0 0x0 0xff 0xfe 是 big-endian 而不是 little-endian utf82 的 BOM)。 更多信息可以在spec中找到。基本上,在 Python 中编码成一串字节(原始数据)。 【参考方案1】:

首先UTF-32是4字节编码,所以它的BOM也是4字节序列:

>>> import codecs
>>> codecs.BOM_UTF32
b'\xff\xfe\x00\x00'

而且由于不同的计算机体系结构对字节顺序的处理方式不同(称为Endianess),因此 BOM 有两种变体,小端和大端:

>>> codecs.BOM_UTF32_LE
b'\xff\xfe\x00\x00'
>>> codecs.BOM_UTF32_BE
b'\x00\x00\xfe\xff'

BOM 的目的是将该命令传达给解码器;阅读 BOM,您知道它是大端还是小端。因此,UTF-32 字符串中的最后两个空字节是最后一个编码字符的一部分。

UTF-16 BOM 与此类似,有两种变体:

>>> codecs.BOM_UTF16
b'\xff\xfe'
>>> codecs.BOM_UTF16_LE
b'\xff\xfe'
>>> codecs.BOM_UTF16_BE
b'\xfe\xff'

这取决于您的计算机架构,默认使用哪一种。

UTF-8 根本不需要 BOM; UTF-8 每个字符使用 1 个或更多字节(根据需要添加字节以编码更复杂的值),但这些字节的顺序在标准中定义。 Microsoft 认为无论如何都需要引入 UTF-8 BOM(因此其记事本应用程序可以检测到 UTF-8),但由于 BOM 的顺序从不改变,因此不鼓励使用。

至于 Python 为 unicode 字符串存储了什么;这实际上在 Python 3.3 中发生了变化。在 3.3 之前,在 C 级别内部,Python 存储 UTF16 或 UTF32 字节组合,这取决于 Python 编译时是否支持宽字符(请参阅How to find out if Python is compiled with UCS-2 or UCS-4?,UCS-2 本质上是 UTF- 16 和 UCS-4 是 UTF-32)。因此,每个字符要么占用 2 个字节或 4 个字节的内存。

从 Python 3.3 开始,内部表示使用 最小 字节数来表示字符串中的所有字符。对于纯 ASCII 和 Latin1 编码文本,使用 1 个字节,对于其余的 BMP,使用 2 个字节,并且使用包含超过 4 个字节的字符的文本。 Python 根据需要在格式之间切换。因此,在大多数情况下,存储变得更加高效。更多详情请见What's New in Python 3.3。

我可以强烈建议您阅读 Unicode 和 Python:

The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) Python Unicode HOWTO

【讨论】:

微软没有“引入”UTF-8 BOM;它只是根据用于所有其他代码点的规则使用 UTF-8 编码对 BOM 的代码点进行编码的结果。 @KarlKnechtel:见en.wikipedia.org/wiki/UTF-8#Byte_order_mark,它主要是在 UTF-8 文本文件中使用 BOM 的 MS 软件。是的,我知道,BOM 只是编码为目标编码(UTF-8、-16 或 -32)的特定 Unicode 代码点,所以微软没有发明代码点,但该公司肯定引入了 usage UTF-8 文件的 BOM。 说 UCS-2 “本质上”是 UTF-16 有点牵强。一个狭窄的 Python 版本(谢天谢地在 3.3 中)创建了 UTF-16 使用的代理对,但它没有对它们进行特殊处理,因此这些字符串的长度(就代码点而言)报告不正确,必须小心在切片等时注意不要分解代理。 @eryksun:就本答案而言,将它们等同起来就足够了。我不想用一篇关于差异的论文来轰炸 OP。 :-)【参考方案2】:
    就目前而言,您的理解基本上是正确的,尽管它并不是真正的“1、2 或 4 字节”。对于 UTF-32,它将是 4 个字节。对于 UTF-16 和 UTF-8,字节数取决于被编码的字符。对于 UTF-16,它将是 2 或 4 个字节。对于 UTF-8,它可能是 1、2、3 或 4 个字节。但是,是的,基本上编码采用 unicode 代码点并将其映射到字节序列。这种映射是如何完成的取决于编码。对于 UTF-32,它只是代码点编号的直接十六进制表示。对于 UTF-16,通常是这样,但对于不寻常的字符(在基本多语言平面之外)会有所不同。对于 UTF-8,编码更复杂(请参阅Wikipedia。)至于开头的额外字节,这些是字节顺序标记,用于确定代码点的哪个顺序进入 UTF-16 或 UTF-32 . 我想你可以看看内部结构,但字符串类型(或 Python 2 中的 unicode 类型)的目的是保护你免受这些信息的影响,就像 Python 列表的目的是保护你不必操纵该列表的原始内存结构。存在字符串数据类型,因此您可以使用 unicode 代码点而不必担心内存表示。如果您想使用原始字节,请对字符串进行编码。 当您进行解码时,它基本上会扫描字符串,寻找字节块。编码方案本质上提供了“线索”,使解码器可以看到一个字符何时结束而另一个字符何时开始。所以解码器扫描并使用这些线索来找到字符之间的边界,然后查找每个片段以查看它在该编码中代表什么字符。如果您想了解每种编码如何将代码点与字节来回映射的详细信息,可以在 Wikipedia 或类似网站上查找各个编码。 两个零字节是 UTF-32 字节顺序标记的一部分。因为 UTF-32 始终使用每个代码点 4 个字节,所以 BOM 也是 4 个字节。基本上,您在 UTF-16 中看到的 FFFE 标记是用两个额外的零字节补零的。这些字节顺序标记指示组成代码点的数字是按从大到小还是从小到大的顺序排列的。基本上就像是选择将“一千二百三十四”这个数字写成 1234 还是 4321。不同的计算机架构在这件事上做出不同的选择。

【讨论】:

【参考方案3】:

我假设您使用的是 Python 3(在 Python 2 中,“字符串”实际上是一个字节数组,这会导致 Unicode 问题)。

(Unicode)字符串在概念上是一系列 Unicode 代码点,它们是对应于“字符”的抽象实体。您可以在Python repository. 中看到实际的 C++ 实现由于计算机没有代码点的固有概念,因此“编码”指定代码点和字节序列之间的部分双射。

设置了编码,因此在可变宽度编码中没有歧义——如果您看到一个字节,您总是知道它是否完成了当前代码点,或者您是否需要读取另一个代码点。从技术上讲,这被称为 prefix-free.。所以当您执行 .decode() 时,Python 会遍历字节数组,一次构建一个编码字符并输出它们。

两个零字节是 utf32 BOM 的一部分:大端 UTF32 将具有 0x0 0x0 0xff 0xfe

【讨论】:

一个小警告:一个代码点不一定是一个字符,因为一个字符可以分解成组合代码。 unciodedata.normalize 在这里提供帮助。

以上是关于Python 3:揭开编码和解码方法的神秘面纱的主要内容,如果未能解决你的问题,请参考以下文章

细说Java揭开Java的main方法神秘的面纱(转)

揭开SolidWorks二次开发的神秘面纱

初识5G——揭开5G的神秘面纱

揭开云原生数据管理的神秘面纱:操作层级

揭开智能配置上网(微信Airkiss)的神秘面纱

揭开智能配置上网(微信Airkiss)的神秘面纱