来自 Steven D'Aprano 的 comp.lang.python 的另一个非常全面的答案(我尝试将其格式化为 stackoverflow):
直接写一个原始的 UTF-8 编码字符串比如'Jalape\xc3\xb1o'
简单地产生一个九字符的字符串 U+004A, U+0061, U+006C, U+0061, U+0070, U+0065, U+00C3, U+00B1, U+006F,这可能不是你想要的。这是因为在 UTF-8 中,多字节序列
\xc3\xb1
应该表示单个字符 U+00F1,而不是两个字符 U+00C3 和 U+00B1。
这表明基本概念的混乱,同时仍然不小心绊倒了基本事实。难怪它让你困惑,它也让我困惑!:-)
编码不生成字符串,它生成字节。因此,您引用的人在谈论
“编码字符串”时会引起混淆,他应该明确表示他的意思是一串字节,或者根本不提及字符串一词。这些中的任何一个都可以工作:
对于较旧版本的 Python(2.5 或更早版本),不幸的是,该b''
符号不起作用,您必须省略b
.
如果 Python 不将ASCII字符与字节混为一谈,并且强迫您像这样编写字节字符串,那就更好了:
- 一个 UTF-8 编码的字节串
b'\x4a\x61\x6c\x61\x70\x65\xc3\xb1\x6f'
从而保持 ASCII 字符和字节之间的区别清晰。但这会过多地破坏向后兼容性,因此 Python 继续将 ASCII 字符与字节混为一谈,即使在 Python 中也是如此。
这里重要的是字节b'Jalape\xc3\xb1o'
由九个十六进制值组成,如上所示。其中七个代表 ASCII 字符Jalape
,o
其中两个不是 ASCII。它们的含义取决于您使用的编码。
(确切地说,即使是其他七个字节的含义也取决于编码。幸运的是,或者不幸的是,视情况而定,大多数但并非所有编码都使用与 ASCII 本身相同的 ASCII 字符的十六进制值,所以我将停止提到这一点,只是假装字符J
总是等于十六进制字节4A
。但现在你知道真相了。)
由于我们使用的是 UTF-8 编码,所以这两个字节\xc3\xb1
代表字符ñ
,也称为LATIN SMALL LETTER N WITH TILDE
. 在其他编码中,这两个字节将代表不同的东西。
所以,我推测原人的意图是得到一个 Unicode 文本字符串'Jalapeño'
。如果他们在 Unicode 方面很聪明,他们会写以下之一:
'Jalape\N{LATIN SMALL LETTER N WITH TILDE}o'
'Jalape\u00F1o'
'Jalape\U000000F1o'
'Jalape\xF1o' # hex
'Jalape\361o' # octal
而且要快乐。(在 Python 2 中,他们需要在所有这些前面加上
u
, 以使用 Unicode 字符串而不是字节字符串。)
但可惜他们被那些在互联网上传播关于 Unicode 的神话、误解和误解的人误导了,所以他们在某个地方查找,发现它具有UTF-8 中ñ
的双字节十六进制值,并认为他们可以写这个c3b1
:
'Jalape\xc3\xb1o'
这并不像他们认为的那样。它创建一个文本字符串,一个 Unicode 字符串,包含九个字符:
J a l a p e à ± o
为什么?因为字符Ã
的序数值是 195,它是c3
十六进制的,所以
\xc3
是字符Ã
; 同样\xb1
是±
具有序数值 177(b1
十六进制)的字符。于是他们发现了mojibake的邪恶之处。
相反,如果他们以byte-string开头,并将其显式解码为 UTF-8,他们会没事的:
# I manually encoded 'Jalapeño' to get the bytes below:
bytes = b'Jalape\xc3\xb1o'
print(bytes.decode('utf-8'))
我最初的问题是:这不应该是 8 个字符而不是 9 个字符吗?他说:\xc3\xb1
应该代表单个字符。然而,在与 Pythonistas 同行进行了一些互动之后,我更加困惑了。
取决于上下文。\xc3\xb1
可能表示 Unicode 字符串
'\xc3\xb1'
(在 Python 2 中,写成u'\xc3\xb1'
),也可能表示字节字符串b'\xc3\xb1'
(在 Python 2.5 或更早版本中,写成没有b
)。
作为字符串,\xc3\xb1
表示两个字符,具有序数值0xC3
(或十进制 195)和0xB1
(或十进制 177),即'Ã'
和'±'
。
作为字节,\xc3\xb1
代表两个字节(嗯,呃),这几乎可以意味着任何东西:
等等。在不了解上下文的情况下,无法判断这两个字节代表什么,或者它们是否需要作为一对或两个不同的东西放在一起。
参考上面的段落:“写一个原始的 UTF-8 编码字符串”是什么意思?
他的意思是他很困惑。您不会通过编码获得文本字符串,而是获得字节(我将接受“字节字符串”)。在这种情况下,形容词“原始”并没有任何意义。您有已编码的字节,或者您有一个包含字符的字符串。除了“嘿,注意,这是低级的东西”(对于“低级”的一些定义)之外,Raw 并没有真正的意思。
在 Python2 中,曾经可以做 'Jalape funny-n o'。
对于说西班牙语的人来说,这没什么好笑的。
就个人而言,我一直认为“o”很有趣。大声说“女人”和“女人”——第一个听起来像“w-oo-man”,第二个听起来像“wi-men”。现在这很有趣。但我离题了。
如果您输入'Jalapeño'
Python 2(带或不带b
前缀),您获得的结果将取决于您的终端设置,但终端内部将字符串表示为 UTF-8 的可能性很高,它为您提供字节
b'Jalape\xc3\xb1o'
这是九个字节。打印时,您的终端将尝试分别打印每个字节,给出:
- 字节
\x4a
打印为J
- 字节
\x61
打印为a
- 字节
\x6c
打印为l
- ...
等等。如果你运气不好,你的终端甚至可能足够聪明,可以将两个字节打印\xc3\xb1
为一个字符,从而为你提供ñ
你所希望的。为什么倒霉?因为你偶然得到了正确的结果。下次你做同样的事情,在不同的终端上,或者同一个终端设置不同的编码,你会得到完全不同的结果,并认为 Unicode 太乱了,无法使用。
使用 Python 2.5,我在这里连续打印 3 次相同的字符串,每次都更改终端的编码:
py> print 'Jalape\xc3\xb1o' # terminal set to UTF-8
Jalapeño
py> print 'Jalape\xc3\xb1o' # and ISO-8859-6 (Arabic)
Jalapeأ�o
py> print 'Jalape\xc3\xb1o' # and ISO-8859-5 (Cyrillic)
JalapeУБo
哪一个是“对的”?答:没有。甚至不是第一个,这恰好是我们所希望的。
真的,不要因为你感到困惑而感到难过。在 Python 2 和终端非常努力地做正确的事情之间,很容易混淆,因为正确的事情发生了,有时却没有。
这是一个“字节”字符串,其中每个字形为 1 个字节长
没有。它是一串字符。字形不会进入它。字形是您在屏幕上看到或打印在纸上的字母的小图片。它们可以是位图,也可以是精美的矢量图形。根据非常粗略的计算1 ,它们不太可能每个为一个字节 - 每个字形更可能为 200 个字节,但取决于它是位图、Postscript 字体、OpenType 字体还是其他字体。
当存储在内部时,因此每个字形都与根据字符集 ASCII 或 Latin-1 的整数相关联。如果这些字符集有一个有趣的 n 字形,那么是的!否则不行!这里没有UTF-8!!或 UTF-16!!这些是纯字节(8 位)。
你越来越近了。但你是对的:Python 2“字符串”是字节字符串,这意味着 UTF-8 不会出现在其中。但是您的终端可能会将这些字节视为 UTF-8,因此不小心做了“正确”(错误)的事情。
Unicode 是一个非常大的字形和整数之间的映射表,并且
不是字形。在抽象“字符”和整数之间,称为代码点。Unicode 包含:
- 不同的字母、数字、字符
- 重音字母
- 自己的口音
- 符号、表情
- 连字和字符的变体形式
- 仅用于向后兼容旧编码所需的字符
- 空白
- 控制字符
- 保留供私人使用的代码点,可以表示您喜欢的任何内容
- 保留为“永远不会使用”的代码点
- 明确标记为“非字符”的代码点
可能还有我忘记的其他人。
表示为Uxxxx
或Uxxxx-xxxx
。
官方的 Unicode 表示法是:
U+xxxx
U+xxxxx
U+xxxxxx
U+
紧接着是四个、五个或六个十六进制数字。U
总是大写的。不幸的是,Python 不支持这种表示法,您必须使用四位或八位十六进制数字,例如:
\uFFFF
\U0010FFFF
对于高达 255 的代码点(序数),您还可以使用十六进制或八进制转义符,例如\xFF
\3FF
UTF-8 UTF-16 是以有效方式存储这些大整数的编码。
几乎正确。它们不一定有效。
Unicode 代码点只是我们赋予某些含义的抽象数字。代码点 65 ( U+0041
,因为 hex 41 == decimal 65) 表示字母A
,依此类推。想象一下这些抽象代码点漂浮在您的脑海中。如何在计算机上将代码点的抽象概念转化为具体形式?就像所有东西都放在计算机中一样:作为字节,所以我们必须将每个抽象代码点(一个数字)转换为一系列字节。
Unicode 代码点的范围从U+0000
到U+10FFFF
,这意味着我们可以只使用三个字节,它们的值从 000000 到 10FFFF 以十六进制表示。超出此范围的值(例如 110000)将是错误的。出于效率的原因,使用四个字节更快更好,即使四个字节之一的值总是为零。
简而言之,这就是 UTF-32 编码:每个字符都恰好使用四个字节。例如,代码点U+0041
(字符A
)是十六进制字节00000041
,或者可能41000000
,取决于您的计算机是 Big Endian 还是 Little Endian。
由于大多数文本使用非常低的序数值,这非常浪费内存。所以 UTF-16 每个字符只使用两个字节,一个奇怪的方案使用所谓的“代理对”来处理不适合两个字节的所有内容。对于“作品”的某些定义,它有效,但很复杂,如果您需要上面的代码点,您真的想避免使用 UTF-16 U+FFFF
。
UTF-8 使用一种简洁的变量编码,其中具有低序数值的字符被编码为单个字节(更好的是:它与 ASCII 使用的字节相同,这意味着假设世界上一切都是 ASCII 的旧软件将继续工作,好吧主要工作)。更高的序数被编码为两个、三个或四个字节2。最重要的是,与大多数历史上的可变宽度编码不同,UTF-8 是自同步的。在传统编码中,如果单个字节被损坏,它可能会从那时
起破坏所有内容。使用 UTF-8,单个损坏的字节只会破坏包含它的单个代码点,接下来的一切都会好起来的。
因此,当 DB 说“编写原始 UTF-8 编码字符串”时,唯一的方法是使用 Python3,其中默认字符串文字以 Unicode 格式存储,然后将在内部使用 UTF-8 UTF-16 来存储各自结构中的字节;或者,可以使用u'Jalape'
两种语言中的 unicode (注意前导u
)。
Python 在内部从不使用 UTF-8 将字符串存储在内存中。因为它是一种可变宽度编码,如果字符串使用 UTF-8 进行存储,您将无法有效地索引字符串。
相反,Python 使用三种不同系统之一:
在 Python 3.3 之前,您可以选择。当您编译 Python 解释器时,您可以选择它应该使用 UTF-16 还是 UTF-32 进行内存存储。这种选择称为“窄”或“宽”构建。狭窄的构建使用较少的内存,但不能很好地处理上面的代码点U+FFFF
。广泛的构建使用更多的内存,但可以完美地处理完整的代码点范围。
从 Python 3.3 开始,在构建 Python 解释器时不再预先决定如何将字符串存储在内存中。相反,Python 会自动为每个单独的字符串选择最有效的内部表示。仅使用 ASCII 或 Latin-1 字符的字符串每个字符使用一个字节;使用代码点的字符串每个字符最多U+FFFF
使用两个字节;并且只有使用上述代码点的字符串,每个字符使用四个字节。
因此,假设这是 Python 3:('Jalape \xYY \xZZ o'
可读性空格)DB 的意思是,愚蠢的用户会期望 Jalapeno 带有 squiggly-n 但他得到的是: Jalape funny1 funny2 o(可读性空格)-9 字形或9 个 Unicode 点或 9-UTF8 字符。正确的?
有点儿。看上面。
这让我想知道他的意思是:“这是因为在 UTF-8 中,多字节序列\xc3\xb1
应该代表单个字符U+00F1
,而不是两个字符U+00C3
和U+00B1
”
他的意思是,如果您使用 UTF-8 对其进行编码,则单个代码点U+00F1
(字符ñ
,带波浪号的 n)将存储为两个字节(十六进制)。c3b1
但是如果你将字符填充\xc3
\xb1
到一个 Unicode 字符串(而不是字节)中,那么你会得到两个 Unicode 字符U+00C3
和U+00B1
.
换句话说,在字符串内部,Python 将十六进制转义\xC3
视为编写 Unicode 代码点\u00C3
或
\U000000C3
.
但是,如果您创建一个字节字符串:
b'Jalape\xc3\xb1o'
通过查找一个 UTF-8 编码表,大概是原始海报所做的,然后将这些字节解码为一个字符串,你会得到你所期望的。使用b
不需要前缀的 Python 2.5:
py> tasty = 'Jalape\xc3\xb1o' # actually bytes
py> tasty.decode('utf-8')
u'Jalape\xf1o'
py> print tasty.decode('utf-8') # oops I forgot to reset my terminal
JalapeУБo
py> print tasty.decode('utf-8') # terminal now set to UTF-8
Jalapeño
1假设字体文件的大小为 100K,它有 256 个字符的字形。每个字形最多 195 个字节。
2从技术上讲,UTF-8 方案可以处理 31 位代码点,直到(假设的)代码点 U+7FFFFFFF,每个代码点最多使用六个字节。但是 Unicode 官方永远不会超过 U+10FFFF,因此 UTF-8 也永远不会超过每个代码点的四个字节。