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

时间:2012-11-20 08:57:26

标签: python unicode encoding python-3.x

让我们说我在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)放入bytes数组中,然后,对于每个字符,它将两个字节放入数组中。 (在我们的例子中,第二个字节始终为零)

>>> [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编码中附加了两个零字节?

3 个答案:

答案 0 :(得分:18)

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

>>> 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(因此其Notepad应用程序可以检测到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开始,内部表示使用表示字符串中所有字符所需的 minimal 字节数。对于纯ASCII和Latin1可编码文本,使用1个字节,因为使用了BMP 2个字节的其余部分,并且使用包含超过该4个字节的字符的文本。 Python根据需要在格式之间切换。因此,对于大多数情况,存储变得更加有效。有关详细信息,请参阅What's New in Python 3.3

强烈建议你阅读Unicode和Python:

答案 1 :(得分: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. 当你进行解码时,它基本上扫描字符串,寻找字节块。编码方案基本上提供“线索”,允许解码器查看一个字符何时结束而另一个字符何时开始。因此解码器扫描并使用这些线索来找到字符之间的边界,然后查找每个片段以查看它在该编码中表示的字符。如果您想查看每个编码如何使用字节来回映射代码点的详细信息,您可以在维基百科等上查找各个编码。
  4. 两个零字节是UTF-32的字节顺序标记的一部分。因为UTF-32每个代码点总是使用4个字节,所以BOM也是4个字节。基本上,您在UTF-16中看到的FFFE标记是零填充,带有两个额外的零字节。这些字节顺序标记指示构成代码点的数字是从最大到最小还是从最小到最大。基本上它就像选择是否将数字“一千二百三十四”写成1234或4321.不同的计算机体系结构在这个问题上做出了不同的选择。

答案 2 :(得分:2)

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

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

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

两个零字节是utf32 BOM的一部分:big-endian UTF32将0x0 0x0 0xff 0xfe