Unicode

本速查表的主要目标是收集一些与 Unicode 相关的常见代码片段。在 Python 3 中,字符串由 Unicode 而不是字节表示。更多信息可以在 PEP 3100 中找到。

ASCII 码是最著名的标准,它为字符定义了数字代码。这些数值最初只定义了 128 个字符,因此 ASCII 只包含控制代码、数字、小写字母、大写字母等。然而,这不足以让我们表示世界各地存在的带重音符号的字符、汉字或表情符号。因此,开发了Unicode 来解决此问题。它定义了代码点来表示各种字符,就像 ASCII 一样,但字符数量高达 1,111,998 个。

字符串

在 Python 2 中,字符串以字节表示,而不是Unicode。Python 提供了不同类型的字符串,例如 Unicode 字符串、原始字符串等。在这种情况下,如果我们想声明一个 Unicode 字符串,我们为字符串字面量添加 u 前缀。

>>> s = 'Café'  # byte string
>>> s
'Caf\xc3\xa9'
>>> type(s)
<type 'str'>
>>> u = u'Café' # unicode string
>>> u
u'Caf\xe9'
>>> type(u)
<type 'unicode'>

在 Python 3 中,字符串以Unicode表示。如果我们想表示一个字节字符串,我们为字符串字面量添加 b 前缀。请注意,早期版本的 Python(3.0-3.2)不支持 u 前缀。为了减轻将支持 Unicode 的应用程序从 Python 2 迁移到 Python 3 的痛苦,Python 3.3 再次支持为字符串字面量添加 u 前缀。更多信息可以在 PEP 414 中找到。

>>> s = 'Café'
>>> type(s)
<class 'str'>
>>> s
'Café'
>>> s.encode('utf-8')
b'Caf\xc3\xa9'
>>> s.encode('utf-8').decode('utf-8')
'Café'

字符

Python 2 将所有字符串字符视为字节。在这种情况下,字符串的长度可能与字符数不等价。例如,Café 的长度为 5,而不是 4,因为 é 被编码为 2 个字节的字符。

>>> s= 'Café'
>>> print([_c for _c in s])
['C', 'a', 'f', '\xc3', '\xa9']
>>> len(s)
5
>>> s = u'Café'
>>> print([_c for _c in s])
[u'C', u'a', u'f', u'\xe9']
>>> len(s)
4

Python 3 将所有字符串字符视为 Unicode 代码点。字符串的长度始终等价于字符数。

>>> s = 'Café'
>>> print([_c for _c in s])
['C', 'a', 'f', 'é']
>>> len(s)
4
>>> bs = bytes(s, encoding='utf-8')
>>> print(bs)
b'Caf\xc3\xa9'
>>> len(bs)
5

移植 unicode(s, ‘utf-8’)

在 Python 3 中移除了 unicode() 内置函数,那么如何最好地转换表达式 unicode(s, 'utf-8') 以使其在 Python 2 和 3 中都能工作?

在 Python 2 中

>>> s = 'Café'
>>> unicode(s, 'utf-8')
u'Caf\xe9'
>>> s.decode('utf-8')
u'Caf\xe9'
>>> unicode(s, 'utf-8') == s.decode('utf-8')
True

在 Python 3 中

>>> s = 'Café'
>>> s.decode('utf-8')
AttributeError: 'str' object has no attribute 'decode'

所以,真正的答案是…

Unicode 代码点

ord 是一个强大的内置函数,用于从给定的字符获取 Unicode 代码点。因此,如果我们想检查字符的 Unicode 代码点,我们可以使用 ord

>>> s = u'Café'
>>> for _c in s: print('U+%04x' % ord(_c))
...
U+0043
U+0061
U+0066
U+00e9
>>> u = '中文'
>>> for _c in u: print('U+%04x' % ord(_c))
...
U+4e2d
U+6587

编码

Unicode 代码点转换为字节字符串称为编码。

>>> s = u'Café'
>>> type(s.encode('utf-8'))
<class 'bytes'>

解码

字节字符串转换为Unicode 代码点称为解码。

>>> s = bytes('Café', encoding='utf-8')
>>> s.decode('utf-8')
'Café'

Unicode 规范化

某些字符可以用两种类似的形式表示。例如,字符 é 可以写成 e ́(规范分解)或 é(规范合成)。在这种情况下,即使两个字符串看起来相同,我们在比较它们时也可能会得到意外的结果。因此,我们可以规范化 Unicode 形式来解决此问题。

# python 3
>>> u1 = 'Café'       # unicode string
>>> u2 = 'Cafe\u0301'
>>> u1, u2
('Café', 'Café')
>>> len(u1), len(u2)
(4, 5)
>>> u1 == u2
False
>>> u1.encode('utf-8') # get u1 byte string
b'Caf\xc3\xa9'
>>> u2.encode('utf-8') # get u2 byte string
b'Cafe\xcc\x81'
>>> from unicodedata import normalize
>>> s1 = normalize('NFC', u1)  # get u1 NFC format
>>> s2 = normalize('NFC', u2)  # get u2 NFC format
>>> s1 == s2
True
>>> s1.encode('utf-8'), s2.encode('utf-8')
(b'Caf\xc3\xa9', b'Caf\xc3\xa9')
>>> s1 = normalize('NFD', u1)  # get u1 NFD format
>>> s2 = normalize('NFD', u2)  # get u2 NFD format
>>> s1, s2
('Café', 'Café')
>>> s1 == s2
True
>>> s1.encode('utf-8'), s2.encode('utf-8')
(b'Cafe\xcc\x81', b'Cafe\xcc\x81')

避免 UnicodeDecodeError

当字节字符串无法解码为 Unicode 代码点时,Python 会引发 UnicodeDecodeError。如果我们想避免此异常,我们可以将replacebackslashreplaceignore 传递给 decode 中的 errors 参数。

>>> u = b"\xff"
>>> u.decode('utf-8', 'strict')
    Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte
>>> # use U+FFFD, REPLACEMENT CHARACTER
>>> u.decode('utf-8', "replace")
'\ufffd'
>>> # inserts a \xNN escape sequence
>>> u.decode('utf-8', "backslashreplace")
'\\xff'
>>> # leave the character out of the Unicode result
>>> u.decode('utf-8', "ignore")
''

长字符串

以下代码片段显示了在 Python 中声明多行字符串的常用方法。

# original long string
s = 'This is a very very very long python string'

# Single quote with an escaping backslash
s = "This is a very very very " \
    "long python string"

# Using brackets
s = (
    "This is a very very very "
    "long python string"
)

# Using ``+``
s = (
    "This is a very very very " +
    "long python string"
)

# Using triple-quote with an escaping backslash
s = '''This is a very very very \
long python string'''