3

根据David Beazley的 Python Essential Reference (4th Edition) 第 29 页:

直接写一个原始的 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。

这不应该是 8 个字符而不是 9 个字符吗?他说:\xc3\xb1应该代表单个字符。

4

3 回答 3

10

来自 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。

这表明基本概念的混乱,同时仍然不小心绊倒了基本事实。难怪它让你困惑,它也让我困惑!:-)

编码不生成字符串,它生成字节。因此,您引用的人在谈论 “编码字符串”时会引起混淆,他应该明确表示他的意思是一串字节,或者根本不提及字符串一词。这些中的任何一个都可以工作:

  • 一个 UTF-8 编码的字节串b'Jalape\xc3\xb1o'

  • UTF-8 编码字节b'Jalape\xc3\xb1o'

对于较旧版本的 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 字符Jalapeo其中两个不是 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代表两个字节(嗯,呃),这几乎可以意味着任何东西:

  • 16 位 Big Endian 整数 50097

  • 16 位小尾数整数 45507

  • 4x4 黑白位图

  • Big5 编码字节中的字符'簽'(CJK UNIFIED IDEOGRAPH-7C3D)

  • '뇃'(HANGUL SYLLABLE NWAES) UTF-16 (Big Endian) 编码字节

  • 'ñ'UTF-8 编码字节

  • 'ñ'Latin-1 编码字节中的两个字符

  • '√±'在 MacRoman 编码字节中

  • 'Γ±'ISO-8859-7 编码字节

等等。在不了解上下文的情况下,无法判断这两个字节代表什么,或者它们是否需要作为一对或两个不同的东西放在一起。

参考上面的段落:“写一个原始的 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 包含:

  • 不同的字母、数字、字符
  • 重音字母
  • 自己的口音
  • 符号、表情
  • 连字和字符的变体形式
  • 仅用于向后兼容旧编码所需的字符
  • 空白
  • 控制字符
  • 保留供私人使用的代码点,可以表示您喜欢的任何内容
  • 保留为“永远不会使用”的代码点
  • 明确标记为“非字符”的代码点

可能还有我忘记的其他人。

表示为UxxxxUxxxx-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+0000U+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+00C3U+00B1

他的意思是,如果您使用 UTF-8 对其进行编码,则单个代码点U+00F1(字符ñ,带波浪号的 n)将存储为两个字节(十六进制)。c3b1但是如果你将字符填充\xc3 \xb1到一个 Unicode 字符串(而不是字节)中,那么你会得到两个 Unicode 字符U+00C3U+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 也永远不会超过每个代码点的四个字节。

于 2013-07-14T09:12:24.273 回答
3

不,这个说法是正确的。

在 UTF-8\xc3\xb1应该表示单个字符。也就是说,如果你从 UTF-8 解码字符串,你会得到一个字符,因此是 8 个字符。

但是,在特定示例中,字符串被视为原始字符序列,而不是UTF-8。因此,两个八位字节产生两个字符。

我可能会向前一点,但看到 ipython 的以下输出:

In [1]: b'Jalape\xc3\xb1o'
Out[1]: b'Jalape\xc3\xb1o'

In [2]: len(b'Jalape\xc3\xb1o')
Out[2]: 9

In [3]: b'Jalape\xc3\xb1o'.decode('utf8')
Out[3]: 'Jalapeño'

In [4]: len(b'Jalape\xc3\xb1o'.decode('utf8'))
Out[4]: 8

In [5]: 'Jalape\xf1o'
Out[5]: 'Jalapeño'

上面的代码适用于 Python 3。对于 Python 2,字节字符串 ( b'Jalape\xc3\xb1o') 将替换为常规字符串 ( 'Jalape\xc3\xb1o'),常规字符串将替换为 unicode 字符串 ( u'Jalape\xf1o')。

于 2013-07-13T17:00:25.737 回答
1

https://groups.google.com/forum/#!topic/comp.lang.python/1boxbYjhClg

Joshua Landau (answering my question wrote)

"directly writing a raw UTF-8 encoded string such as 'Jalape\xc3\xb1o' simply produces a nine-character string U+004A, U+0061, U+006C, U+0061, U+0070, U+0065, U+00C3, U+00B1, U+006F, which is probably not what you intended.This is because in UTF-8, the multi- byte sequence \xc3\xb1 is supposed to represent the single character U+00F1, not the two characters U+00C3 and U+00B1."

Correct.

My original question was: Shouldn't this be 8 characters - not 9?

No, Python tends to be right on these things.

He says: \xc3\xb1 is supposed to represent the single character. However after some interaction with fellow Pythonistas i'm even more confused.

You would be, given the way he said it.

With reference to the above para: 1. What does he mean by "writing a raw UTF-8 encoded string"??

Well, that doesn't really mean much with no context like he gave it.

In Python2, once can do 'Jalape funny-n o'. This is a 'bytes' string where each glyph is 1 byte long when stored internally so each glyph is associated with an integer as per charset ASCII or Latin-1. If these charsets have a funny-n glyph then yay! else nay! There is no UTF-8 here!! or UTF-16!! These are plain bytes (8 bits).

Unicode is a really big mapping table between glyphs and integers and are denoted as Uxxxx or Uxxxx-xxxx.

Waits for our resident unicode experts to explain why you're actually wrong

UTF-8 UTF-16 are encodings to store those big integers in an efficient manner. So when DB says "writing a raw UTF-8 encoded string" - well the only way to do this is to use Python3 where the default string literals are stored in Unicode which then will use a UTF-8 UTF-16 internally to store the bytes in their respective structures; or, one could use u'Jalape' which is unicode in both languages (note the leading 'u').

Correct.

  1. So assuming this is Python 3: 'Jalape \xYY \xZZ o' (spaces for readability) what DB is saying is that, the stupid-user would expect Jalapeno with a squiggly-n but instead he gets is: Jalape funny1 funny2 o (spaces for readability) -9 glyphs or 9 Unicode-points or 9-UTF8 characters. Correct?

I think so.

  1. Which leaves me wondering what he means by: "This is because in UTF-8, the multi- byte sequence \xc3\xb1 is supposed to represent the single character U+00F1, not the two characters U+00C3 and U+00B1"

He's mixed some things up, AFAICT.

Could someone take the time to read carefully and clarify what DB is saying??

Here's a simple explanation: you're both wrong (or you're both almost right):

As of Python 3:

>>> "\xc3\xb1"
'ñ'
>>> b"\xc3\xb1".decode()
'ñ'

"WHAT?!" you scream, "THAT'S WRONG!" But it's not. Let me explain.

Python 3's strings want you to give each character separately (*winces in case I'm wrong*). Python is interpreting the "\xc3" as "\N{LATIN CAPITAL LETTER A WITH TILDE}" and "\xb1" as "\N{PLUS-MINUS SIGN}"¹. This means that Python is given two characters. Python is basically doing this:

number = int("c3", 16) # Convert from base16
chr(number) # Turn to the character from the Unicode mapping

When you give Python raw bytes, you are saying that this is what the string looks like when encoded -- you are not giving Python Unicode, but encoded Unicode. This means that when you decode it (.decode()) it is free to convert multibyte sections to their relevant characters.

To see how an encoded string is not the same as the string itself, see:

>>> "Jalepeño".encode("ASCII", errors="xmlcharrefreplace")
b'Jalepeño'

Those represent the same thing, but the first (according to Python) is the thing, the second needs to be decoded.

Now, bringing this back to the original:

>>> "\xc3\xb1".encode()
b'\xc3\x83\xc2\xb1'

You can see that the encoded bytes represent the two characters; the string you see above is not the encoded one. The encoding is internal to Python.

I hope that helps; good luck.

¹ Note that I find the "\N{...}" form much easier to read, and recommend it.

于 2013-07-14T08:06:08.753 回答