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

tre*_*der 11 python unicode encoding python-3.x

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

>>> s = 'python'
>>> len(s)
6
Run Code Online (Sandbox Code Playgroud)

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

>>> b = s.encode('utf-8')
>>> b16 = s.encode('utf-16')
>>> b32 = s.encode('utf-32')
Run Code Online (Sandbox Code Playgroud)

我从上述操作得到的是一个字节数组-即b,b16b32只是字节阵列(每个字节是8位长的过程).

但我们编码了字符串.那么这是什么意思?我们如何将"编码"的概念与原始字节数组相连?

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

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

>>> len(b)
6
Run Code Online (Sandbox Code Playgroud)

这个数组表明,对于每个字符,我们有一个字节(因为所有字符都低于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)
Run Code Online (Sandbox Code Playgroud)

在这里我们可以看到"编码到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)
Run Code Online (Sandbox Code Playgroud)

在"utf-32编码"的情况下,我们首先放入BOM,然后为每个字符放置四个字节,最后我们将两个零字节放入数组中.

因此,我们可以说"编码过程"为字符串中的每个字符收集1 2或4个字节(取决于编码名称)并预先添加并向其附加更多字节以创建最终结果字节数组.

现在,我的问题:

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

Mar*_*ers 18

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

>>> import codecs
>>> codecs.BOM_UTF32
b'\xff\xfe\x00\x00'
Run Code Online (Sandbox Code Playgroud)

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

>>> codecs.BOM_UTF32_LE
b'\xff\xfe\x00\x00'
>>> codecs.BOM_UTF32_BE
b'\x00\x00\xfe\xff'
Run Code Online (Sandbox Code Playgroud)

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'
Run Code Online (Sandbox Code Playgroud)

这取决于您的计算机体系结构,默认情况下使用哪一个.

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是否使用宽字符支持进行编译(请参阅如何找出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:


Bre*_*arn 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一样.不同的计算机体系结构在这个问题上做出了不同的选择.