12

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

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

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

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

我从上面的操作中得到的是一个字节数组——即b,b16并且b32只是字节数组(每个字节当然是 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 个字节(取决于编码名称),并在它们前面添加和附加更多字节以创建最终的字节结果数组。

现在,我的问题:

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

3 回答 3

19

首先,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 是否使用宽字符支持进行编译(请参阅如何确定 Python 是使用 UCS-2 还是 UCS-4 编译?, UCS-2本质上是UTF-16,而 UCS-4 是 UTF-32)。因此,每个字符要么占用 2 个字节或 4 个字节的内存。

从 Python 3.3 开始,内部表示使用表示字符串中所有字符所需的最少字节数。对于纯 ASCII 和 Latin1 可编码文本,使用 1 个字节,对于BMP的其余部分,使用2 个字节,并且使用包含超过 4 个字节的字符的文本。Python 根据需要在格式之间切换。因此,在大多数情况下,存储变得更加高效。有关更多详细信息,请参阅Python 3.3 中的新增功能

强烈建议您阅读 Unicode 和 Python:

于 2012-11-20T09:17:07.933 回答
5
  1. 就目前而言,您的理解基本上是正确的,尽管它并不是真正的“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。
  2. 我想你可以看看内部结构,但是字符串类型(或 Python 2 中的 unicode 类型)的目的是保护你免受这些信息的影响,就像 Python 列表的目的是保护你不必操纵原始数据一样该列表的内存结构。存在字符串数据类型,因此您可以使用 unicode 代码点而不必担心内存表示。如果要使用原始字节,请对字符串进行编码。
  3. 当您进行解码时,它基本上会扫描字符串,寻找字节块。编码方案本质上提供了“线索”,使解码器可以看到一个字符何时结束而另一个字符何时开始。所以解码器扫描并使用这些线索来找到字符之间的边界,然后查找每个片段以查看它在该编码中代表什么字符。如果您想了解每种编码如何将代码点与字节来回映射的详细信息,您可以在 Wikipedia 等上查找各个编码。
  4. 两个零字节是 UTF-32 字节顺序标记的一部分。因为 UTF-32 始终使用每个代码点 4 个字节,所以 BOM 也是 4 个字节。基本上,您在 UTF-16 中看到的 FFFE 标记是用两个额外的零字节补零的。这些字节顺序标记指示组成代码点的数字是按从大到小还是从小到大的顺序排列的。基本上就像选择将“一千二百三十四”这个数字写成1234还是4321一样。不同的计算机体系结构在这件事上做出了不同的选择。
于 2012-11-20T09:14:48.337 回答
2

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

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

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

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

于 2012-11-20T09:11:47.650 回答