首页 前端知识 第4章 Unicode 文本和字节序列

第4章 Unicode 文本和字节序列

2025-02-26 11:02:25 前端知识 前端哥 474 739 我要收藏

前言

  1. 对书中案例有补充性的修改,方便理解。
  2. 每个引用,无论中英文,均是来自于原文/原文翻译。同时也是对上一个话题的结束。
  3. 从“How to Discover the Encoding of a Byte Sequence”这个小节后,大部分(甚至说是全部)内容已经与我在工程上的开发出现了较大的偏离,所以均用gpt来进行总结,然后手动完善。后续如果碰到会对对应的章节一一进行改进。这个小节之前的部分还是很重要的。

Humans use text. Computers speak bytes.

Python 3 introduced a sharp distinction between strings of human text and sequences of raw bytes. Implicit conversion of byte sequences to Unicode text is a thing of the past. This chapter deals with Unicode strings, binary sequences, and the encodings used to convert between them.


Python 3 中字符串与字节序列的区分与转换

这句话的信息量很大,再此处作出解释:
这句话解释了 Python 3 处理文本和二进制数据的核心改变。

核心概念解析:

  1. 严格区分类型:

    • str 类型表示 Unicode 文本(人类可读的字符串)
    • bytes 类型表示原始字节序列(机器存储/传输的二进制数据)
  2. 禁止隐式转换:
    Python 3 不再允许自动在字符串和字节之间转换,必须显式指定编码方式

  3. 编码/解码的必要性:
    文本和字节的转换必须通过 .encode().decode() 方法明确处理

示例说明:

  1. 类型区别:
text = "你好世界"   # str 类型,Unicode 文本
binary = b"Hello"  # bytes 类型,原始字节
print(type(text))   # <class 'str'>
print(type(binary)) # <class 'bytes'>
  1. 编码过程(文本 → 字节):
# 将 Unicode 字符串编码为 UTF-8 字节
encoded = text.encode('utf-8')
print(encoded)  # b'\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c'
  1. 解码过程(字节 → 文本):
# 将字节解码为 UTF-8 字符串
decoded = encoded.decode('utf-8')
print(decoded)  # 输出:你好世界
  1. 禁止隐式转换(Python 3 会报错):
try:
    mixed = "文字" + b"bytes"  # 尝试混合 str 和 bytes
except TypeError as e:
    print(f"错误:{e}")  # 输出:can only concatenate str (not "bytes") to str
  1. 文件操作时的区别:
# 文本模式(自动解码)
with open("text.txt", "w", encoding='utf-8') as f:
    f.write("Python之禅")

# 二进制模式(直接操作字节)
with open("text.txt", "rb") as f:
    content = f.read()  # 获取的是 bytes 对象
    print(content)  # b'Python\xe4\xb9\x8b\xe7\xa6\x85'

常见编码方式对比:

text = "中文"

# UTF-8 编码(通用网页标准)
print(text.encode('utf-8'))    # b'\xe4\xb8\xad\xe6\x96\x87'

# GBK 编码(中文系统传统编码)
print(text.encode('gbk'))      # b'\xd6\xd0\xce\xc4'

# 错误解码演示
wrong_decoded = text.encode('utf-8').decode('gbk')
print(wrong_decoded)
# UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 2: illegal multibyte sequence
"""
GBK 是一种用于简体中文的字符编码集,它是 GB2312 的扩展,增加了对更多汉字和其他字符的支持。GBK 使用单字节和双字节编码方式来表示字符:
单字节编码:用于表示与 ASCII 兼容的字符,范围为 0x00 到 0x7F。
双字节编码:用于表示中文字符和其他特殊字符,范围通常在 0x8140 到 0xFEFE。

为什么 0xAD 无法解码?
在 GBK 编码中,0xAD 无法被单独解码,原因如下:
单字节范围:0xAD 超出了 ASCII 范围(0x00 到 0x7F),因此不能作为单字节字符解码。
双字节要求:对于双字节字符,GBK 需要两个字节的组合(第一个字节通常在 0x81 到 0xFE 之间,第二个字节通常在 0x40 到 0xFE 之间,且不包括 0x7F)才能表示一个合法的字符。0xAD 不能单独作为双字节字符的起始或结束字节。
因此,0xAD 在 GBK 中既不属于可解码的单字节字符,也不能作为双字节字符的一部分进行解码,这就导致了解码错误。
"""

关键要点总结:

  • 文本操作始终使用 str 类型
  • 处理文件/网络数据时使用 bytes 类型
  • 转换时显式指定编码(推荐 UTF-8)
  • 不同编码互操作会导致乱码或错误
  • 这种严格区分解决了 Python 2 中大量编码混乱问题

这种设计强制开发者明确处理编码问题,从根本上避免了因隐式转换导致的乱码和安全漏洞,使得程序在处理国际化文本时更加健壮可靠。


In this chapter, we will visit the following topics:
• Characters, code points, and byte representations
• Unique features of binary sequences: bytes, bytearray, and memoryview
• Encodings for full Unicode and legacy character sets
• Avoiding and dealing with encoding errors
• Best practices when handling text files
• The default encoding trap and standard I/O issues
• Safe Unicode text comparisons with normalization
• Utility functions for normalization, case folding, and brute-force diacritic removal
• Proper sorting of Unicode text with locale and the pyuca library
• Character metadata in the Unicode database
• Dual-mode APIs that handle str and bytes

4.1 Character Issues

The concept of “string” is simple enough: a string is a sequence of characters. The problem lies in the definition of “character.”

In 2021, the best definition of “character” we have is a Unicode character. Accordingly, the items we get out of a Python 3 str are Unicode characters, just like the items of a unicode object in Python 2—and not the raw bytes we got from a Python 2 str.

The Unicode standard explicitly separates the identity of characters from specific byte representations:
• The identity of a character—its code point—is a number from 0 to 1,114,111 (base 10), shown in the Unicode standard as 4 to 6 hex digits with a “U+” prefix, from U+0000 to U+10FFFF. For example, the code point for the letter A is U +0041, the Euro sign is U+20AC, and the musical symbol G clef is assigned to code point U+1D11E. About 13% of the valid code points have characters assigned to them in Unicode 13.0.0, the standard used in Python 3.10.0b4.

什么是“代码点”(code point)?

在计算机中, 字符需要一种数字方式来表示,这就是代码点的作用。 可以把代码点理解为每个字符的身份证号。在Unicode标准中,这个身份证号是一个十六进制的数字,并以“U+”开头。

举几个例子:

字母“A”:

代码点是U+0041。这里的“0041”是一个十六进制数字,转换成十进制就是65。
这意味着在Unicode中,“A”这个字符的唯一标识就是U+0041。

欧元符号“€”:

代码点是U+20AC。十六进制的“20AC”转换成十进制是8364。
所以,当我们需要在计算机中表示欧元符号时,我们实际上是使用这个代码点。

音乐符号“G音符”:

代码点是U+1D11E。这个较长的代码点表示一个较为复杂的符号。
它的十进制值是119070,表示这个字符在Unicode中占据了一个特定的位置。

通过代码点,不同的计算机系统可以识别和显示相同的字符,而不管底层的硬件和软件如何。这种标准化使得全球范围内的文本处理和显示成为可能,无论是西方字母、亚洲字符还是特殊符号。

• The actual bytes that represent a character depend on the encoding in use. An encoding is an algorithm that converts code points to byte sequences and vice versa. The code point for the letter A (U+0041) is encoded as the single byte \x41 in the UTF-8 encoding, or as the bytes \x41\x00 in UTF-16LE encoding. As another example, UTF-8 requires three bytes—\xe2\x82\xac—to encode the Euro sign (U+20AC), but in UTF-16LE the same code point is encoded as two bytes: \xac\x20.

字符编码是如何将代码点转换为字节序列的

字符编码就像是一种翻译算法,它帮助计算机将抽象的代码点(字符的唯一标识)转换为具体的字节序列,这样计算机才能存储和处理这些字符。

让我们用例子来说明:

  1. 字母“A”

    • 代码点:U+0041
    • UTF-8编码:在UTF-8编码中,字母“A”被表示为单个字节\x41。这是一个直接且简单的转换,因为字母"A"是基本拉丁字符,UTF-8对这些字符使用一个字节。
    • UTF-16LE编码:在UTF-16LE编码中,字母“A”被表示为两个字节\x41\x00。UTF-16通常使用两个字节来表示字符,所以这里你看到的是两个字节的表示形式。
  2. 欧元符号“€”

    • 代码点:U+20AC
    • UTF-8编码:在UTF-8中,欧元符号被编码为三个字节\xe2\x82\xac。UTF-8是可变长度的编码方式,对于更复杂的符号,它可能需要多字节表示。
    • UTF-16LE编码:在UTF-16LE中,欧元符号被编码为两个字节\xac\x20。这显示了UTF-16的一种固定长度的优势,即对许多常用字符和符号只需要两个字节。

通过不同的编码方式,同一个字符的代码点可以映射到不同的字节序列。选择哪种编码方式通常取决于具体应用的需求,比如文件的大小、处理速度以及对各种字符的支持等。UTF-8是目前最常用的编码方式,因为它兼容ASCII,并且能有效处理多种语言字符。

Converting from code points to bytes is encoding; converting from bytes to code
points is decoding

s = 'café'
print(len(s))
b = s.encode('utf8')
print(b)
print(len(b))
print(b.decode('utf8'))  
4
b'caf\xc3\xa9'
5
café

The str café has four Unicode characters.
Encode str to bytes using UTF-8 encoding.
bytes literals have a b prefix.
bytes b has five bytes (the code point for “é” is encoded as two bytes in UTF-8).
Decode bytes to str using UTF-8 encoding.

If you need a memory aid to help distinguish .decode() from .encode(), convince yourself that byte sequences can be cryptic machine core dumps, while Unicode str objects are “human” text. Therefore, it makes sense that we decode bytes to str to get human-readable text, and we encode str to bytes for storage or transmission.

Although the Python 3 str is pretty much the Python 2 unicode type with a new name, the Python 3 bytes is not simply the old str renamed, and there is also the closely related bytearray type. So it is worthwhile to take a look at the binary sequence types before advancing to encoding/decoding issues.

4.2 Byte Essentials

The new binary sequence types are unlike the Python 2 str in many regards. The first thing to know is that there are two basic built-in types for binary sequences: the immutable bytes type introduced in Python 3 and the mutable bytearray, added way back in Python 2.6.2 The Python documentation sometimes uses the generic term “byte string” to refer to both bytes and bytearray. I avoid that confusing term.

Each item in bytes or bytearray is an integer from 0 to 255, and not a one-character string like in the Python 2 str. However, a slice of a binary sequence always produces a binary sequence of the same type—including slices of length 1. See Example 4-2.

cafe = bytes('café', encoding='utf_8')
print(cafe)
print(cafe[0])
print(cafe[:1])
cafe_arr = bytearray(cafe)
print(cafe_arr)
print(cafe_arr[-1:])
print(cafe_arr[0])
b'caf\xc3\xa9'
99
b'c'
bytearray(b'caf\xc3\xa9')
bytearray(b'\xa9')
99

bytes can be built from a str, given an encoding.
Each item is an integer in range(256).
Slices of bytes are also bytes—even slices of a single byte.
There is no literal syntax for bytearray: they are shown as bytearray() with a bytes literal as argument.
A slice of bytearray is also a bytearray.

The fact that my_bytes[0] retrieves an int but my_bytes[:1] returns a bytes sequence of length 1 is only surprising because we are used to Python’s str type, where (s[\theta]==s[: 1]) . For all other sequence types in Python, 1 item is not the same as a slice of length 1.

Python 序列类型的索引与切片行为

在 Python 中,序列类型(例如 str、bytes、list、tuple 等)允许通过索引和切片来访问其元素。不同类型的序列在索引和切片操作上表现各异,特别是在单个元素访问和长度为 1 的切片访问之间。以下是对常见序列类型行为的总结。

1. 字符串(str)

索引访问: s[0]

返回类型:str
返回值:单个字符的字符串。例如,‘h’。
切片访问: s[:1]

返回类型:str
返回值:包含一个字符的字符串。例如,‘h’。
在字符串中,s[0] 和 s[:1] 的结果在类型和值上是相同的。

2. 字节序列(bytes)

索引访问: b[0]

返回类型:int
返回值:单个字节的整数表示。例如,104(‘h’ 的 ASCII 值)。
切片访问: b[:1]

返回类型:bytes
返回值:长度为 1 的字节序列。例如,b’h’。
在字节序列中,索引返回整数,切片返回字节序列。

3. 列表(list)

索引访问: lst[0]

返回类型:元素类型
返回值:列表中的单个元素。例如,10。
切片访问: lst[:1]

返回类型:list
返回值:长度为 1 的列表。例如,[10]。

4. 元组(tuple)

索引访问: tup[0]

返回类型:元素类型
返回值:元组中的单个元素。例如,10。
切片访问: tup[:1]

返回类型:tuple
返回值:包含一个元素的元组。例如,(10,)。

5. NumPy 数组(numpy.ndarray)

索引访问: arr[0]

返回类型:元素类型
返回值:数组中的单个元素。例如,10。
切片访问: arr[:1]

返回类型:numpy.ndarray
返回值:长度为 1 的数组。例如,array([10])。

6. 自定义序列类型

通过实现 getitem 方法,可以定义自定义序列类型的索引和切片行为。

索引访问: seq[0]

返回类型:由实现决定
返回值:序列中的单个元素。
切片访问: seq[:1]

返回类型:自定义序列类型
返回值:包含一个元素的自定义序列。

总结

str 类型是一个特例,单个字符和长度为 1 的切片都返回字符串。
bytes 类型索引返回整数,切片返回字节序列。
其他序列类型 (list, tuple, numpy.ndarray) 的索引返回单个元素,切片返回相同类型的序列。
理解这些差异对于正确处理数据和避免潜在错误至关重要。

总结一下之前的部分概念

当然,结合之前的 ASCII 图和详细说明,这里是关于 Unicode、UTF-8、Python 的 strbytes 之间关系的完整说明:

+-----------+         +-------+
|  Unicode  |<------->|  str  |  (Python 3)
+-----------+         +-------+
     ^                   |
     |                   | encode() / decode()
     |                   v
+-----------+         +-------+
|   UTF-8   |<------->| bytes |  (Python)
+-----------+         +-------+

详细说明

  1. Unicode

    • Unicode 是一种字符集标准,给每个字符分配一个唯一的编号(码位)。
    • 它是一个抽象标准,不涉及具体的存储或表示方式。
    • 在 Python 3 中,str 类型用于表示文本数据,这些数据本质上是 Unicode 字符串。
  2. Python str

    • str 类型在 Python 3 中用于处理文本,是对 Unicode 字符的直接表示。
    • str 对象可以通过 encode() 方法转换为 bytes,这会将文本数据编码为指定的字节格式(如 UTF-8)。
  3. UTF-8

    • UTF-8 是一种用于将 Unicode 编码为字节序列的格式。
    • 它使用可变长度编码方式,1 到 4 个字节表示一个 Unicode 字符。
    • 当你将 str 中的文本编码为 UTF-8 时,你得到的是一个 bytes 对象。
  4. Python bytes

    • bytes 类型用于表示二进制数据,是字节序列的集合。
    • bytes 对象可以通过 decode() 方法转换为 str,这会将字节序列解码为 Unicode 字符串。
    • 在 Python 中,bytes 类型通常用于 I/O 操作中处理二进制数据。

关系总结

  • Unicode 与 str:在 Python 3 中,str 是 Unicode 字符串。str 提供了对文本数据的高层抽象,直接支持 Unicode。
  • UTF-8 与 bytes:UTF-8 是一种编码标准,将 Unicode 字符表示为字节序列。bytes 则是用于存储和处理这些字节序列的数据类型。
  • 编码和解码:通过 str.encode() 方法可以将 Unicode 字符串编码为 UTF-8 字节序列(bytes);通过 bytes.decode() 方法可以将 UTF-8 字节序列解码为 Unicode 字符串(str)。

这样,你可以通过 encodedecode 方法在 strbytes 之间灵活转换,而这两个类型之间的转换基于 UTF-8 编码标准。希望这能清晰地展示这些概念之间的关系!

Although binary sequences are really sequences of integers, their literal notation reflects the fact that ASCII text is often embedded in them. Therefore, four different displays are used, depending on each byte value:

• For bytes with decimal codes 32 to 126—from space to ~ (tilde)—the ASCII char‐
acter itself is used.
• For bytes corresponding to tab, newline, carriage return, and \, the escape
sequences\t, \n, \r, and \\ are used.
• If both string delimiters ’ and " appear in the byte sequence, the whole sequence
is delimited by ‘, and any ’ inside are escaped as \'.3
• For other byte values, a hexadecimal escape sequence is used (e.g., \x00 is the
null byte).
That is why in Example 4-2 you see b’caf\xc3\xa9’: the first three bytes b’caf’ are in the printable ASCII range, the last two are not

对例子的解释

当然,让我们更详细地解释一下这个例子:b'caf\xc3\xa9'

这个字节序列是用Python的字节字符串语法表示的,其中前缀 b 表示这是一个字节序列(而不是普通的字符串)。

  1. 前三个字节 b'caf'

    • 这些字节对应于 ASCII 字符 ‘c’、‘a’ 和 ‘f’。
    • ASCII 码中,‘c’、‘a’ 和 ‘f’ 的十进制值分别是 99、97 和 102,这些都在可打印字符的范围内(32 到 126)。
    • 因此,这些字节直接显示为它们对应的字符 ‘c’、‘a’ 和 ‘f’。
  2. 后两个字节 \xc3\xa9

    • 这两个字节不在可打印的 ASCII 范围内。
    • 它们实际上是字符 ‘é’ 的 UTF-8 编码。‘é’ 在 UTF-8 中编码为两个字节:\xC3 和 \xA9。
    • 由于这些字节不在可打印的范围内,使用十六进制转义序列来表示,因此显示为 \xc3\xa9

通过这种方式,整个字节序列 b'caf\xc3\xa9' 表示的是字符串 “café”,其中 ‘é’ 通过 UTF-8 编码的字节来表示。这样处理可以确保字节序列在显示时既能正确表达文本内容,又能处理非ASCII字符。

Both bytes and bytearray support every str method except those that do formatting (format, format_map) and those that depend on Unicode data, including casefold, isdecimal, isidentifier, isnumeric, isprintable, and encode. This means that you can use familiar string methods like endswith, replace, strip, translate, upper, and dozens of others with binary sequences—only using bytes and not str arguments. In addition, the regular expression functions in the re module also work on binary sequences, if the regex is compiled from a binary sequence instead of a str. Since Python 3.5, the % operator works with binary sequences again.4

Binary sequences have a class method that str doesn’t have, called fromhex, which builds a binary sequence by parsing pairs of hex digits optionally separated by spaces:

>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

例子解释

好的,以下是对 bytes.fromhex('31 4B CE A9') 以及其输出 b'1K\xce\xa9' 的详细解释:

  1. bytes.fromhex() 方法

    • bytes.fromhex()bytes 类型的一个类方法,它的作用是通过解析十六进制数字字符串来构建一个 bytes 对象。该方法会将输入的十六进制数字字符串解析成对应的字节序列。十六进制数字对(每个字节由两个十六进制数字表示)可以用空格分隔,也可以不用空格分隔。
  2. 解析输入字符串 '31 4B CE A9'

    • 31:十六进制的 31 转换为十进制是 49,在 ASCII 码表中,十进制 49 对应的字符是 '1'
    • 4B:十六进制的 4B 转换为十进制是 75,在 ASCII 码表中,十进制 75 对应的字符是 'K'
    • CEA9:这两个十六进制数组合在一起表示一个 Unicode 字符(这里可能是一个希腊字母或其他非 ASCII 字符)。在 UTF-8 或其他编码中,多个字节组合来表示一个非 ASCII 字符。在这种情况下,\xce\xa9 通常表示希腊字母 ω(在某些编码环境下)。
  3. 输出结果 b'1K\xce\xa9'

    • 最终构建的 bytes 对象 b'1K\xce\xa9' 表示了一个字节序列。b 前缀表示这是一个 bytes 对象,其中 '1''K' 是 ASCII 字符对应的字节表示,\xce\xa9 是表示非 ASCII 字符(这里是希腊字母 ω 相关的字节表示)的十六进制转义形式。

The other ways of building bytes or bytearray instances are calling their constructors with:

  • A str and an encoding keyword argument
  • An iterable providing items with values from 0 to 255
  • An object that implements the buffer protocol (e.g., bytes, bytearray, memoryview, array.array) that copies the bytes from the source object to the newly created binary sequence

Until Python 3.5, it was also possible to call bytes or bytearray with a single integer to create a binary sequence of that size initialized with null bytes. This signature was deprecated in Python 3.5 and removed in Python 3.6. See PEP 467—Minor API improvements for binary sequences.

Building a binary sequence from a buffer - like object is a low - level operation that may involve type casting. See a demonstration in Example 4 - 3.

Example 4 - 3. Initializing bytes from the raw data of an array

# 导入array模块,array模块提供了一种高效的存储同类型数据的数组类型
import array

# 创建一个类型码为'h'的array对象,'h'表示有符号短整数(16位)
# 初始化数组,包含元素 [-2, -1, 0, 1, 2]
numbers = array.array('h', [-2, -1, 0, 1, 2])

# 使用bytes()函数,将array对象numbers转换为bytes对象
# 这会创建一个新的bytes对象,其中包含了构成numbers的字节的副本
octets = bytes(numbers)

# 打印转换后的bytes对象
print(octets)
# 输出结果会是 b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00',这10个字节表示了5个短整数

Creating a bytes or bytearray object from any buffer-like source will always copy the bytes. In contrast, memoryview objects let you share memory between binary data structures, as we saw in “Memory Views” on page 62.

解释

import array

# 创建一个数组,它支持缓冲区接口
arr = array.array('b', [1, 2, 3, 4, 5])

# 从数组创建一个 bytes 对象
b = bytes(arr)
print("Bytes:", b)

# 修改原始数组
arr[0] = 10

# 打印原始数组和 bytes 对象
print("Modified array:", arr)
print("Bytes after modifying array:", b)

# 从数组创建一个 memoryview 对象
mv = memoryview(arr)

# 修改 memoryview,将第三个元素改为 20
mv[2] = 20

# 打印 memoryview 和原始数组
print("Memoryview:", bytes(mv))
print("Array after modifying memoryview:", arr)
Bytes: b'\x01\x02\x03\x04\x05'
Modified array: array('b', [10, 2, 3, 4, 5])
Bytes after modifying array: b'\x01\x02\x03\x04\x05'
Memoryview: b'\n\x02\x14\x04\x05'
Array after modifying memoryview: array('b', [10, 2, 20, 4, 5])

After this basic exploration of binary sequence types in Python, let’s see how they are converted to/from strings.

Basic Encoders/Decoders

Python 发行版附带了 100 多种编解码器(编码器/解码器),用于文本到字节的转换,反之亦然。每种编解码器都有一个名称,比如“utf_8”,并且通常还有别名,例如“utf8”、“utf-8”和“U8”,你可以在诸如 open()str.encode()bytes.decode() 等函数中,将这些名称用作 encoding 参数。示例 4-4 展示了将相同的文本编码为三种不同字节序列的情况。

# 定义一个列表,包含了三种不同的编码格式
codecs = ['latin_1', 'utf_8', 'utf_16']
# 使用for循环遍历codecs列表
for codec in codecs:
    # 使用指定的编码格式(codec)对字符串 'El Niño' 进行编码
    # 并将编码后的字节序列打印出来,同时打印出对应的编码格式名称
    # 使用sep='\t'来设置打印时编码格式名称和字节序列之间的分隔符为制表符
    print(codec, 'El Niño'.encode(codec), sep='\t')
latin_1	b'El Ni\xf1o'
utf_8	b'El Ni\xc3\xb1o'
utf_16	b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

Figure 4-1 demonstrates a variety of codecs generating bytes from characters like the letter “A” through the G-clef musical symbol. Note that the last three encodings are variable-length, multibyte encodings.
在这里插入图片描述Figure 4-1. Twelve characters, their code points, and their byte representation (in hex) in 7 different encodings (asterisks indicate that the character cannot be represented in that encoding).

图4-1中展示的编码是作为具有代表性的样本而选取的:

  • latin1,也称为iso8859_1

    很重要,因为它是其他编码的基础,比如cp1252编码以及Unicode编码本身(注意latin1的字节值是如何出现在cp1252的字节中,甚至出现在码位中的)。

  • cp1252

    由微软创建的一种实用的latin1超集,添加了一些实用的符号,比如花括号引号和€(欧元符号);一些Windows应用程序称它为“ANSI”,但它从来都不是真正的ANSI标准。

  • cp437

    IBM个人电脑的原始字符集,包含绘制框线的字符。与后来出现的latin1不兼容。

  • gb2312

    用于对中国大陆使用的简体中文汉字进行编码的旧标准;是广泛应用于亚洲语言的多种多字节编码之一。

  • utf-8

    到目前为止,它是网络上最常见的8位编码方式。截至2021年7月,“W3Techs:网站字符编码使用情况统计”显示,97%的网站使用UTF-8编码,而在2014年9月我撰写本书第一版中这一段内容时,这一比例仅为81.4% 。

  • utf-16le

    UTF 16位编码方案的一种形式;所有的UTF-16编码都通过称为“代理对”的转义序列来支持高于U+FFFF的码位。UTF-16早在1996年就取代了最初的16位Unicode 1.0编码——通用字符集2(UCS-2)。尽管自上世纪起UCS-2就已被弃用,但它仍在许多系统中使用,因为它仅支持最高到U+FFFF的码位。截至2021年,已分配的码位中超过57%都高于U+FFFF,其中包括极为重要的表情符号。

现在对常见编码的概述已经完成,接下来我们将着手处理编码和解码操作中出现的问题。

Understanding Encode/Decode Problems

虽然存在一个通用的UnicodeError异常,但Python报告的错误通常会更具体:要么是UnicodeEncodeError(在将str转换为二进制序列时),要么是UnicodeDecodeError(在将二进制序列读取为str时)。当源编码与预期不符时,加载Python模块也可能会引发SyntaxError。我们将在接下来的部分中展示如何处理所有这些错误。

Coping with UnicodeEncodeError

大多数非UTF编码格式只能处理Unicode字符中的一小部分。在将文本转换为字节时,如果某个字符在目标编码中未被定义,就会引发UnicodeEncodeError(Unicode编码错误),除非通过向编码方法或函数传递errors参数来提供特殊处理。错误处理程序的行为如示例4-5所示。

# 定义一个字符串变量city,值为 'São Paulo'
city = 'São Paulo'

# 使用utf_8编码对city字符串进行编码,并打印编码后的字节序列
# utf_8编码通常能够处理任何字符串
print(city.encode('utf_8'))
# 输出:b'S\xc3\xa3o Paulo'

# 使用utf_16编码对city字符串进行编码,并打印编码后的字节序列
print(city.encode('utf_16'))
# 输出:b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'

# 使用iso8859_1编码对city字符串进行编码,并打印编码后的字节序列
# iso8859_1编码也能处理 'São Paulo' 这个字符串
print(city.encode('iso8859_1'))
# 输出:b'S\xe3o Paulo'

# 尝试使用cp437编码对city字符串进行编码,由于cp437不能编码 'ã' 字符,会抛出UnicodeEncodeError异常
try:
    print(city.encode('cp437'))
except UnicodeEncodeError as e:
    print(f"发生UnicodeEncodeError异常: {e}")

# 使用cp437编码对city字符串进行编码,指定错误处理方式为 'ignore',即忽略不能编码的字符
# 这种方式通常是个很糟糕的主意,会导致数据悄无声息地丢失
print(city.encode('cp437', errors='ignore'))
# 输出:b'So Paulo'

# 使用cp437编码对city字符串进行编码,指定错误处理方式为'replace',
# 用 '?' 替换不能编码的字符,数据也会丢失,但用户能察觉到有问题
print(city.encode('cp437', errors='replace'))
# 输出:b'S?o Paulo'

# 使用cp437编码对city字符串进行编码,指定错误处理方式为 'xmlcharrefreplace',
# 用XML实体替换不能编码的字符,如果不能使用UTF编码且不能承受数据丢失,这是唯一的选择
print(city.encode('cp437', errors='xmlcharrefreplace'))
# 输出:b'S&#227;o Paulo'

编解码器的错误处理是可扩展的。你可以通过将一个名称和一个错误处理函数传递给 codecs.register_error 函数,为 errors 参数注册额外的字符串值。请参阅 codecs.register_error 的文档说明。

说明

import codecs

# 自定义错误处理函数
def custom_error_handler(exception):
    # exception 是一个 UnicodeEncodeError 或 UnicodeDecodeError 实例
    if isinstance(exception, UnicodeEncodeError):
        # 返回一个元组,第一个元素是替换的字符串,第二个元素是新的位置索引
        return ("<error>", exception.start + 1)
    else:
        raise exception  # 其他错误类型不处理

# 注册自定义的错误处理函数
codecs.register_error('custom_replace', custom_error_handler)

# 示例字符串
city = 'São Paulo'

# 使用cp437编码对city字符串进行编码,指定错误处理方式为 'custom_replace'
encoded_city = city.encode('cp437', errors='custom_replace')
print(encoded_city)
# 输出:b'S<error>o Paulo'

ASCII 是我所知道的所有编码的一个通用子集,因此,如果文本完全由 ASCII 字符组成,那么编码操作应该总是可行的。Python 3.7 添加了一个新的布尔方法 str.isascii(),用于检查你的 Unicode 文本是否 100% 是纯 ASCII 字符。如果是,你应该能够使用任何编码将其编码为字节形式,而不会引发 UnicodeEncodeError(Unicode 编码错误)。

Coping with UnicodeDecodeError

并非每个字节都包含有效的 ASCII 字符,而且并非每个字节序列都是有效的 UTF-8 或 UTF-16 编码;因此,当你在将二进制序列转换为文本时假定使用了这些编码中的某一种,一旦发现意外的字节,就会得到一个 UnicodeDecodeError(Unicode 解码错误)。

另一方面,许多旧的 8 位编码,如“cp1252”、“iso8859_1”和“koi8_r”,能够解码任何字节流,甚至包括随机的乱码字节,而且不会报告错误。因此,如果你的程序采用了错误的 8 位编码,它会在毫无提示的情况下将乱码数据解码出来。

# 示例 4-6. 从字节(bytes)解码为字符串(str):成功情况和错误处理

# 定义一个字节对象 octets,其值为 'Montréal' 按某种方式编码后的字节序列
# 这里的 b'Montr\xe9al' 中,\xe9 是字母 “é” 编码为 latin1 后的字节表示
octets = b'Montr\xe9al'

# 使用 cp1252 编码对 octets 进行解码
# 因为 cp1252 是 latin1 的超集,所以可以正确解码,能得到正确的字符串 'Montréal'
print(octets.decode('cp1252'))

# 使用 iso8859_7 编码对 octets 进行解码
# iso8859_7 主要用于希腊语,所以会错误解释字节 \xe9,但不会发出错误
print(octets.decode('iso8859_7'))

# 使用 koi8_r 编码对 octets 进行解码
# koi8_r 是用于俄语的编码,此时字节 \xe9 会被解释为西里尔字母 “И”
print(octets.decode('koi8_r'))

# 尝试使用 utf_8 编码对 octets 进行解码
# 由于 octets 不是有效的 UTF-8 字节序列,所以会抛出 UnicodeDecodeError 异常
try:
    print(octets.decode('utf_8'))
except UnicodeDecodeError as e:
    print(f"发生 UnicodeDecodeError 异常: {e}")

# 使用 utf_8 编码对 octets 进行解码,并指定错误处理方式为'replace'
# 这种情况下,字节 \xe9 会被替换为 “�”(码位为 U+FFFD),这是官方的 Unicode 替换字符,用于表示未知字符
print(octets.decode('utf_8', errors='replace'))

# 以下是对上述操作的进一步解释注释
# “Montréal” 这个词编码为 latin1 时,'\xe9' 是字母“é”的字节。
# 使用 Windows 1252(即 cp1252)解码有效,因为它是 latin1 的超集。
# ISO-8859-7 是为希腊语设计的,所以 '\xe9' 字节被错误解释,且不发出错误。
# KOI8-R 是为俄语设计的。现在 '\xe9' 代表西里尔字母 “И”。
# 'utf_8' 编解码器检测到 octets 不是有效的 UTF-8,所以引发 UnicodeDecodeError。
# 使用'replace' 错误处理时,\xe9 被替换为 “�”(码位 U+FFFD),
# 这是官方的 Unicode 替换字符,用于表示未知字符。
Montréal
Montrιal
MontrИal
发生 UnicodeDecodeError 异常: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
Montr�al

SyntaxError When Loading Modules with Unexpected Encoding

UTF-8是Python 3的默认源编码,就如同ASCII是Python 2的默认编码一样。如果你加载一个包含非UTF-8数据且没有编码声明的.py模块,你会得到类似这样的消息:

语法错误:文件ola.py的第1行出现以“\xe1”开头的非UTF-8代码,但未声明编码;详情请见https://python.org/dev/peps/pep-0263/

由于UTF-8在GNU/Linux和macOS系统中被广泛应用,一种可能出现的情况是打开一个在Windows系统上使用cp1252编码创建的.py文件。请注意,即使在Windows版的Python中也会出现这个错误,因为Python 3源代码在所有平台上的默认编码都是UTF-8。
要解决这个问题,在文件顶部添加一个神奇的编码注释,如示例4-7所示。

示例4-7. ola.py:用葡萄牙语写的“Hello, World!”

# coding: cp1252
print('Olá, Mundo!')
# Olá, Mundo!

既然Python 3的源代码不再局限于ASCII,并且默认采用优秀的UTF-8编码,对于像“cp1252”这样的旧编码的源代码,最好的“修复”方法是将它们已经转换为UTF-8编码,而无需费心添加编码注释。如果你的编辑器不支持UTF-8,那么是时候更换编辑器了。

为什么还经常看到许多python代码文件开头是# -*- coding: UTF-8 -*-

在 Python 文件开头添加 # -*- coding: UTF-8 -*- 是为了:

  1. 兼容性:确保在 Python 2 中也能正确处理 UTF-8。
  2. 明确性:让编码方式一目了然,方便维护。
  3. 编辑器支持:帮助某些编辑器正确识别文件编码。
  4. 良好习惯:保持代码一致性和可读性。

即使在 Python 3 中默认是 UTF-8,这种声明仍然是有益的惯例。

假设你有一个文本文件,无论是源代码还是诗歌,但你不知道它的编码。你要如何检测其实际编码呢?下一部分将给出答案。

How to Discover the Encoding of a Byte Sequence

你要如何找到一个字节序列的编码呢?简短的回答是:你无法做到。你必须被告知(该字节序列的编码是什么)。

一些通信协议和文件格式,比如超文本传输协议(HTTP)和可扩展标记语言(XML),包含明确告知我们内容是如何编码的头部信息。你可以确定某些字节流不是美国信息交换标准代码(ASCII)编码,因为它们包含值大于127的字节,而且UTF-8和UTF-16的编码构成方式也限制了可能出现的字节序列。

例子

1. HTTP 和 XML
  • HTTP:在 HTTP 响应中,Content-Type 头可能包含 charset=utf-8,明确指出响应数据是用 UTF-8 编码的。

    Content-Type: text/html; charset=utf-8
    
  • XML:在 XML 文件的头部,常有类似 <?xml version="1.0" encoding="UTF-8"?> 的声明,指示文件编码。

2. 排除法示例
  • ASCII 排除:一个字节序列 [72, 101, 108, 108, 111, 200] 明显不是纯 ASCII,因为 200 超过了 ASCII 范围(0-127)。

  • UTF-8 特征:UTF-8 编码中,某些字节模式是无效的,比如单独的字节 0xC0。如果一个字节序列包含这些无效模式,它就不可能是合法的 UTF-8。

这些方法帮助我们在某些情况下推断编码,但除非有明确的声明,否则不能保证100%识别正确。


然后文章唧唧挂啦说了很多没那么重要的内容,总结下:

主要观点

  1. UTF-8 的设计:UTF-8 的编码方式使得随机或非 UTF-8 的字节序列很难被误解为合法的 UTF-8 序列,因为 UTF-8 的编码模式非常严格。

  2. 实用经验:在处理带有遗留系统的服务时,可以尝试先用 UTF-8 解码,如果失败(引发 UnicodeDecodeError),则尝试使用其他编码(如 cp1252)。这种方法虽然不完美,但在实践中有效。

  3. 启发式和统计方法:通过观察字节模式,可以猜测编码类型。例如,频繁出现的 \x00 可能表明使用 16 位或 32 位编码,而不是 8 位编码。

  4. Chardet 库:这是一个 Python 库,可以通过统计方法猜测文本的编码。它支持超过 30 种编码。

  5. 字节顺序标记(BOM):某些 UTF 编码(如 UTF-16 和 UTF-32)可能在文本开头有一个 BOM,用来指示字节顺序。

示例

UTF-8 解码策略
  • 尝试解码:假设你有一个字节序列 b'\xe2\x82\xac\x61',这可以解码为 UTF-8,因为它代表了字符 “€a”(欧元符号和字母 ‘a’)。

  • 错误处理:如果你有一个字节序列 b'\xe2\x28\xa1',尝试用 UTF-8 解码时会引发 UnicodeDecodeError,因为这个序列不是合法的 UTF-8。你可以在异常处理中尝试其他编码。

Chardet 使用
  • 命令行:假设你有一个文件 example.txt,可以用 chardetect example.txt 来检测其编码。输出可能会是:

    example.txt: utf-8 with confidence 0.99
    

    表示 Chardet 认为这个文件是 UTF-8 编码的,置信度为 99%。

字符串中的 BOM
  • UTF-16:假设你开始处理一个文件,其内容以 b'\xff\xfe' 开头,这个字节序列是一个 UTF-16 的 BOM,指示文本是以小端字节序存储的。

通过这些方法和工具,你可以在没有明确编码信息的情况下,合理地猜测文本的编码。

尽管经过编码的文本的二进制序列通常不会携带其编码方式的明确提示,但UTF格式可能会在文本内容前添加一个字节序标记。接下来将对此进行解释。

BOM: A Useful Gremlin

在示例4-4中,你可能已经注意到,在UTF-16编码序列的开头有几个额外的字节。下面再来看一下:

>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

这些字节是b'\xff\xfe'。这是一个字节序标记(BOM),表示执行编码操作所在的英特尔CPU采用的是“小端序”字节序。
在小端序机器上,对于每个码位,最低有效字节排在前面:字母'E',其码位为U+0045(十进制数69),在字节偏移量2和3处被编码为69和0,如下所示:

>>> list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]

在大端序CPU上,编码顺序会相反;'E'将被编码为0和69。
为了避免混淆,UTF-16编码会在要编码的文本前面加上特殊的不可见字符“零宽度不换行空格”(U+FEFF)。在小端序系统上,该字符被编码为b'\xff\xfe'(十进制数255、254)。由于根据设计,在Unicode中不存在U+FFFE这个字符,所以字节序列b'\xff\xfe'在小端序编码中必然表示“零宽度不换行空格”,这样编解码器就知道该使用哪种字节序了。
UTF-16有一种变体——UTF-16LE,它明确表示采用小端序,还有另一种明确表示采用大端序的变体,即UTF-16BE。如果你使用它们,就不会生成字节序标记:

>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

如果存在字节序标记,UTF-16编解码器应该会将其过滤掉,这样你得到的文件内容就只有实际的文本,而没有开头的“零宽度不换行空格”。Unicode标准规定,如果一个文件是UTF-16格式且没有字节序标记,那么应该假定它是UTF-16BE(大端序)格式。然而,英特尔x86架构是小端序的,所以实际上存在大量没有字节序标记的小端序UTF-16格式数据。
字节序这个问题只影响那些使用多个字节来表示字符的编码,比如UTF-16和UTF-32。UTF-8的一个很大优势是,无论机器采用何种字节序,它生成的字节序列都是相同的,所以不需要字节序标记。

尽管如此,一些Windows应用程序(特别是记事本)还是会在UTF-8文件中添加字节序标记;而且Excel依赖字节序标记来检测一个文件是否为UTF-8文件,否则它会假定文件内容是使用Windows代码页进行编码的。在Python的编解码器注册表中,这种带有字节序标记的UTF-8编码被称为UTF-8-SIG。字符U+FEFF在UTF-8-SIG编码下是三字节序列b'\xef\xbb\xbf'。所以,如果一个文件以这三个字节开头,那么它很可能是一个带有字节序标记的UTF-8文件。
关于UTF-8-SIG的凯莱布提示
凯莱布·哈廷(Caleb Hattingh)——本书的技术审校人员之一——建议在读取UTF-8文件时始终使用UTF-8-SIG编解码器。这没有什么坏处,因为UTF-8-SIG可以正确读取带有或不带有字节序标记的文件,而且不会返回字节序标记本身。在写入文件时,为了保证通用性和互操作性,我建议使用UTF-8编码。例如,如果Python脚本以注释#!/usr/bin/env python3开头,那么它就可以在Unix系统上执行。为了使这个操作生效,文件的前两个字节必须是b'#!',但是字节序标记会破坏这个约定。如果你有特定的需求,要将数据导出到需要字节序标记的应用程序中,可以使用UTF-8-SIG,但要注意Python的编解码器文档中提到:“在UTF-8中,不鼓励使用字节序标记,并且通常应该避免使用。”

例子讲解

编码与字节序标记(BOM)
  1. UTF-16编码与BOM

    • 在UTF-16编码中,字符通常用两个字节表示,但字节序会因机器架构不同而有所不同:

      • 小端序(Little-Endian):最低有效字节在前(例如,字符’E’编码为69, 0)。
      • 大端序(Big-Endian):最高有效字节在前(例如,字符’E’编码为0, 69)。
    • **字节序标记(BOM)**用于指示文本的字节序:

      • 小端序的BOM是b'\xff\xfe'
      • 大端序的BOM是b'\xfe\xff'
    • 编码示例:

      text = 'El Niño'
      
      # UTF-16默认编码(带BOM)
      utf16 = text.encode('utf_16')
      print(list(utf16))  # 输出: [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
      
      # UTF-16LE编码(小端序,无BOM)
      utf16le = text.encode('utf_16le')
      print(list(utf16le))  # 输出: [69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
      
      # UTF-16BE编码(大端序,无BOM)
      utf16be = text.encode('utf_16be')
      print(list(utf16be))  # 输出: [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
      
  2. UTF-8编码与BOM

    • UTF-8编码是字节序无关的,因此通常不需要BOM。然而,一些应用程序(如Windows的记事本)可能在UTF-8文件中添加BOM。

    • UTF-8-SIG是一种在文件开头加上BOM的UTF-8编码,BOM被表示为b'\xef\xbb\xbf'

    • 示例:假设我们有一个UTF-8-SIG编码的文件,内容是“Hello, 世界”。

      # 写入一个带BOM的UTF-8文件
      with open('example.txt', 'w', encoding='utf-8-sig') as f:
          f.write('Hello, 世界')
      
      # 读取文件时使用utf-8-sig来自动处理BOM
      with open('example.txt', 'r', encoding='utf-8-sig') as f:
          content = f.read()
          print(content)  # 输出: Hello, 世界
      
      # 直接使用utf-8读取文件将会包含BOM
      with open('example.txt', 'rb') as f:
          raw_data = f.read()
          print(raw_data)  # 输出: b'\xef\xbb\xbfHello, \xe4\xb8\x96\xe7\x95\x8c'
      

实际应用建议

  • 当读取UTF-8文件时,建议使用utf-8-sig编码,这样即使文件有BOM,BOM也会被自动忽略。
  • 当写入文件时,建议使用标准utf-8编码,以避免添加BOM,从而保证文件的兼容性。

通过这些示例,我们涵盖了上文讨论的UTF-16和UTF-8编码的关键点,包括字节序标记的作用以及如何在编程中处理这些编码问题。

现在我们继续讨论在Python 3中处理文本文件的问题。

Handling Text Files

处理文本输入输出(I/O)的最佳实践是“Unicode三明治”(图4-2)。这意味着在输入时(例如,在打开文件进行读取时),字节应该尽早解码为字符串(str)。这个“三明治”的“夹心”部分就是你程序的业务逻辑,在这部分中,文本处理完全是针对字符串对象进行的。在其他处理过程中,你绝不应该进行编码或解码操作。在输出时,字符串应尽可能晚地编码为字节。大多数Web框架都是这样工作的,并且我们在使用它们时很少会直接处理字节。例如,在Django中,你的视图函数应该输出Unicode字符串;Django自身会负责将响应编码为字节,默认使用UTF-8编码。
在这里插入图片描述
图 4-2. Unicode 三明治:当前文本处理的最佳实践。

Python 3使得遵循“Unicode三明治”的建议变得更加容易,因为内置的open()函数在以文本模式读取文件时会进行必要的解码操作,在写入文件时会进行编码操作,所以从my_file.read()获取的内容以及传递给my_file.write(text)的内容都是字符串对象。

因此,使用文本文件看似很简单。但是,如果你依赖默认编码,就会遇到麻烦。

考虑一下示例4-8中的控制台会话。你能发现其中的错误吗?
示例4-8. 一个平台编码问题(如果你在自己的机器上尝试这个操作,可能会看到问题,也可能看不到)

>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'

错误在于:我在写入文件时指定了UTF-8编码,但在读取文件时却没有指定,所以Python假定使用的是Windows的默认文件编码——代码页1252,并且文件中的尾部字节被解码成了字符'é',而不是'é'

我在Windows 10(内部版本18363)系统上的64位Python 3.8.1中运行了示例4-8。在最新的GNU/Linux或macOS系统上运行相同的语句则一切正常,因为它们的默认编码是UTF-8,这就给人一种一切都没问题的错觉。如果在打开文件进行写入时省略了encoding参数,那么就会使用区域设置的默认编码,并且我们使用相同的编码也能正确读取该文件。

但这样一来,这个脚本生成的文件字节内容会因平台而异,甚至在同一平台上也会因区域设置不同而不同,从而产生兼容性问题。

必须在多台机器上或多次运行的代码,绝不应依赖默认编码。在打开文本文件时,始终要显式地传入encoding=参数,因为默认编码可能在不同机器之间有所不同,甚至在同一天的不同时刻也可能会改变。

示例4-8中一个值得注意的细节是,第一条语句中的write函数报告写入了4个字符,但在下一行读取时却得到了5个字符。示例4-9是示例4-8的扩展版本,对这一情况以及其他细节进行了解释。
示例4-9. 仔细检查在Windows上运行的示例4-8,能发现其中的错误以及如何修复它

# 打开一个名为 'cafe.txt' 的文件,使用写入模式 ('w') 并指定编码为 UTF-8
# 默认情况下,open 函数使用文本模式,并返回一个具有特定编码的 _io.TextIOWrapper 对象
fp = open('cafe.txt', 'w', encoding='utf_8')
# 打印文件对象,显示其名称、模式和编码信息
# 输出:<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
print(fp)

# 向文件中写入字符串 'café',write 方法返回写入的 Unicode 字符的数量
# 这里写入了 4 个 Unicode 字符,所以返回值为 4
# 输出:4
num_chars_written = fp.write('café')
print(num_chars_written)

# 关闭文件,确保数据被正确写入磁盘
fp.close()

# 导入 os 模块,用于获取文件的状态信息
import os
# 使用 os.stat 函数获取文件的状态,其中 st_size 属性表示文件的大小(以字节为单位)
# UTF-8 将 'é' 编码为 2 个字节,即 0xc3 和 0xa9,所以文件大小为 5 字节
# 输出:5
file_size = os.stat('cafe.txt').st_size
print(file_size)

# 打开 'cafe.txt' 文件进行读取,显式指定编码为 UTF - 8
# 这种情况下,返回的 TextIOWrapper 对象的编码会是指定的 UTF - 8
fp2 = open('cafe.txt', encoding='UTF-8')
# 打印文件对象,查看其编码信息
# 输出:<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='UTF-8'>
print(fp2)

# 查看文件对象的 encoding 属性,确认其使用的编码为 UTF - 8
# 输出:UTF-8
print(fp2.encoding)

# 尝试读取文件内容,由于使用了正确的编码(UTF - 8)进行解码
# 所以能正确显示内容为 'café'
# 输出:café
print(fp2.read())
# 关闭文件
fp2.close()

# 重新打开 'cafe.txt' 文件进行读取,并显式指定编码为 UTF - 8
# 这是正确的读取方式,使用与写入时相同的编码
fp3 = open('cafe.txt', encoding='utf_8')
# 打印文件对象,查看其编码信息
# 输出:<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
print(fp3)

# 读取文件内容,由于使用了正确的编码(UTF - 8)进行解码
# 所以能得到预期的结果:“café” 的 4 个 Unicode 字符
# 输出:café
print(fp3.read())
# 关闭文件
fp3.close()

# 以二进制模式 ('rb') 打开 'cafe.txt' 文件进行读取
# 'rb' 标志表示以二进制模式打开文件,返回的对象是一个 BufferedReader,而不是 TextIOWrapper
fp4 = open('cafe.txt', 'rb')
# 打印文件对象,确认其为 BufferedReader 类型
# 输出:<_io.BufferedReader name='cafe.txt'>
print(fp4)

# 读取文件内容,以二进制模式读取会返回字节对象
# 这里读取到的字节序列为 b'caf\xc3\xa9',符合 UTF - 8 对 'café' 的编码
# 输出:b'caf\xc3\xa9'
print(fp4.read())
# 关闭文件
fp4.close()

# 总结说明:
# 除非你需要分析文件内容以确定编码,否则不要以二进制模式打开文本文件
# 即便需要确定编码,也应该使用 Chardet 库,而不是自己编写复杂的逻辑
# 普通代码应该只使用二进制模式打开二进制文件,比如光栅图像文件
# 示例中的问题主要是因为打开文本文件时依赖了默认设置,不同环境下默认编码可能不同,从而导致编码问题

示例 4-9 中的问题与在打开文本文件时依赖默认设置有关。正如接下来的部分所展示的,这些默认设置有多种来源。

Beware of Encoding Defaults

有几种设置会影响Python中输入输出(I/O)的默认编码。请查看示例4-10中的default_encodings.py脚本。
示例4-10. 探究默认编码

import locale
import sys
expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""
my_file = open('dummy', 'w')
for expression in expressions.split():
    value = eval(expression)
    print(f'{expression:>30} -> {value!r}')

示例4-10在GNU/Linux(Ubuntu 14.04到19.10版本)和macOS(10.9到10.14版本)系统上的输出是相同的,这表明在这些系统中处处都使用了UTF-8编码:

$ python3 default_encodings.py
locale.getpreferredencoding() -> 'UTF-8'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'

然而,在Windows系统上,输出如示例4-11所示。
示例4-11. Windows 10 PowerShell中的默认编码(在cmd.exe中的输出相同)

# 执行 chcp 命令,该命令用于显示或设置活动代码页
# 输出结果显示当前控制台的活动代码页为 437
# 代码页 437 是一种早期用于 IBM PC 的字符编码
> chcp
# 输出
Active code page: 437

# 运行 default_encodings.py 脚本,并将脚本的输出打印到控制台
> python default_encodings.py

# 以下是 default_encodings.py 脚本的输出及对应解释

# 调用 locale.getpreferredencoding() 函数,获取系统首选的编码
# 此设置是最重要的,文本文件默认使用该函数返回的编码
# 在 Windows 系统上,这里返回 'cp1252',说明系统默认使用的是 cp1252 编码
locale.getpreferredencoding() -> 'cp1252'

# 查看变量 my_file 的类型
# my_file 是通过 open('dummy', 'w') 创建的文件对象,其类型为 _io.TextIOWrapper
type(my_file) -> <class '_io.TextIOWrapper'>

# 查看 my_file 文件对象的编码
# 由于文本文件默认使用 locale.getpreferredencoding() 返回的编码,所以这里为 'cp1252'
my_file.encoding -> 'cp1252'

# 检查标准输出流(sys.stdout)是否连接到终端设备
# 因为输出是发送到控制台的,所以返回 True
sys.stdout.isatty() -> True

# 查看标准输出流(sys.stdout)的编码
# 这里显示为 'utf-8',这与 chcp 命令报告的控制台代码页 437 不同
sys.stdout.encoding -> 'utf-8'

# 检查标准输入流(sys.stdin)是否连接到终端设备
# 由于输入通常来自控制台,所以返回 True
sys.stdin.isatty() -> True

# 查看标准输入流(sys.stdin)的编码
# 显示为 'utf-8'
sys.stdin.encoding -> 'utf-8'

# 检查标准错误流(sys.stderr)是否连接到终端设备
# 因为错误信息通常也输出到控制台,所以返回 True
sys.stderr.isatty() -> True

# 查看标准错误流(sys.stderr)的编码
# 显示为 'utf-8'
sys.stderr.encoding -> 'utf-8'

# 获取 Python 解释器内部使用的默认编码
# 这里为 'utf-8'
sys.getdefaultencoding() -> 'utf-8'

# 获取文件系统使用的编码
# 显示为 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'

剩下的本小节内容不在展示,有兴趣可以去读原文。更多是在论证。下面只展示本小节的总结:

这一章节讨论了在 Python 中处理文本和字节时默认编码可能带来的问题。默认编码设置在不同的操作系统中可能不同,尤其是在处理文件输入/输出(I/O)时,这可能导致意外的编码错误。下面我用简单的例子来解释这个问题。

示例解释

假设你有一个简单的 Python 脚本,它打开一个文件并写入一些文本:

# example.py
with open('example.txt', 'w') as file:
    file.write('Hello, world!')

在这个例子中,Python 会默认使用你的系统的“首选编码”来写入文件。在大多数现代的 GNU/Linux 和 macOS 系统中,这个默认编码是 UTF-8。这意味着你可以安全地写入包括非 ASCII 字符在内的任何 Unicode 字符。

然而,在 Windows 系统上,默认编码可能是 cp1252 或其他不同的编码。这种编码只支持有限的字符集,因此如果你尝试写入不在这个字符集中的字符,就可能会遇到编码错误。

更复杂的例子

假设你想写入一些特殊的 Unicode 字符,比如:

# example_unicode.py
with open('example_unicode.txt', 'w') as file:
    file.write('Hello, world! … ∞ ㊷')

在 Windows 上运行这个脚本时,如果不指定编码,你可能会遇到问题,因为 cp1252 不支持 这两个字符。这会导致 UnicodeEncodeError

解决方案

为了避免这种问题,最好的做法是显式地指定编码。例如:

# example_utf8.py
with open('example_utf8.txt', 'w', encoding='utf-8') as file:
    file.write('Hello, world! … ∞ ㊷')

通过指定 encoding='utf-8',你可以确保 Python 使用 UTF-8 编码来处理文件,这样可以避免绝大多数的编码问题。

结论

这个章节提醒我们,不要依赖系统的默认编码,因为它们可能会在不同的环境中有所不同。特别是当你编写需要跨平台运行的程序时,显式地指定编码可以帮助你避免许多潜在的问题。

Normalizing Unicode for Reliable Comparisons

这一章节比较抽象,离现实应用较远。只展示大概。

文中例子的讲解

  1. Ω (OHM SIGN) 和 Ω (GREEK CAPITAL LETTER OMEGA)

    >>> from unicodedata import normalize, name
    >>> ohm = '\u2126'
    >>> name(ohm)
    'OHM SIGN'
    >>> ohm_c = normalize('NFC', ohm)
    >>> name(ohm_c)
    'GREEK CAPITAL LETTER OMEGA'
    >>> ohm == ohm_c
    False
    >>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
    True
    
    • 解释:在这个例子中,‘OHM SIGN’ 和 ‘GREEK CAPITAL LETTER OMEGA’ 是两个不同的Unicode字符。通过NFC规范化,‘OHM SIGN’ 被转换为 ‘GREEK CAPITAL LETTER OMEGA’。虽然它们在视觉上看起来一样,但它们的Unicode码点不同,因此直接比较时结果为False。通过规范化后,再比较它们,结果为True。

    • 易错点:有人可能会以为因为它们看起来一样就应该在比较时相等,但实际不做规范化时,它们是不相等的。

  2. VULGAR FRACTION ONE HALF (½)

    >>> half = '\N{VULGAR FRACTION ONE HALF}'
    >>> print(half)
    ½
    >>> normalize('NFKC', half)
    '1⁄2'
    >>> for char in normalize('NFKC', half):
    ...     print(char, name(char), sep='\t')
    ...
    1 DIGIT ONE
    ⁄ FRACTION SLASH
    2 DIGIT TWO
    
    • 解释:‘VULGAR FRACTION ONE HALF’(½)在NFKC规范化后被分解为三个字符:‘1’(DIGIT ONE)、‘⁄’(FRACTION SLASH)和’2’(DIGIT TWO)。这说明NFKC会将某些字符分解为多个字符的组合。

    • 易错点:有人可能会误以为规范化后的字符中使用的是普通的斜杠’/‘(SOLIDUS),而实际上使用的是’FRACTION SLASH’,它们是不同的字符。

  3. MICRO SIGN (μ) 和 GREEK SMALL LETTER MU (μ)

    >>> micro = 'μ'
    >>> micro_kc = normalize('NFKC', micro)
    >>> micro, micro_kc
    ('μ', 'μ')
    >>> ord(micro), ord(micro_kc)
    (181, 956)
    >>> name(micro), name(micro_kc)
    ('MICRO SIGN', 'GREEK SMALL LETTER MU')
    
    • 解释:虽然’MICRO SIGN’(μ, U+00B5)和’GREEK SMALL LETTER MU’(μ, U+03BC)看起来是一样的字符,但它们有不同的Unicode码点。NFKC规范化将’MICRO SIGN’转换为’GREEK SMALL LETTER MU’。

    • 易错点:看到两者视觉上相同,可能会忽略它们的不同码点。在某些应用场景中,这种差异可能导致意外的行为。

总结

这些例子展示了在处理Unicode字符时,规范化的重要性。不同的规范化形式(NFC、NFD、NFKC、NFKD)有不同的应用场景和效果,尤其是在字符比较和数据处理时,需要特别注意字符的实际码点和规范化后的变化。正确地理解这些规范化的效果,可以帮助避免数据处理中的潜在错误。

Case Folding

Case folding是将所有文本转换为小写,同时进行一些额外的转换。Python通过str.casefold()方法支持这一功能。Case folding的主要用途是为了在不区分大小写的情况下进行字符串比较。

对于只包含latin1字符的字符串,s.casefold()通常会产生与s.lower()相同的结果,但有两个例外:

  1. 微符号(μ)

    • ‘MICRO SIGN’(μ, U+00B5)会被转换为’GREEK SMALL LETTER MU’(μ, U+03BC)。这两者在大多数字体中看起来是相同的,但它们的Unicode码点不同。
  2. 德语的Eszett或“锐s”(ß)

    • ‘LATIN SMALL LETTER SHARP S’(ß)会被转换为"ss"。

这两种情况是Case Folding中特殊的转换规则。

文中例子的讲解

  1. 微符号(μ)

    >>> micro = 'μ'
    >>> name(micro)
    'MICRO SIGN'
    >>> micro_cf = micro.casefold()
    >>> name(micro_cf)
    'GREEK SMALL LETTER MU'
    >>> micro, micro_cf
    ('μ', 'μ')
    
    • 解释:尽管’MICRO SIGN’和’GREEK SMALL LETTER MU’在视觉上是相同的字符,但它们有不同的Unicode码点。在进行Case Folding时,‘MICRO SIGN’被转换为’GREEK SMALL LETTER MU’,这符合Case Folding的规则。

    • 易错点:可能会忽视这两个字符的差异,因为它们在视觉上相同,但在编码上不同。

  2. 德语的Eszett(ß)

    >>> eszett = 'ß'
    >>> name(eszett)
    'LATIN SMALL LETTER SHARP S'
    >>> eszett_cf = eszett.casefold()
    >>> eszett, eszett_cf
    ('ß', 'ss')
    
    • 解释:在Case Folding中,‘LATIN SMALL LETTER SHARP S’(ß)被转换为"ss"。这是因为在德语中,Eszett在某些情况下可以被替换为"ss"。

    • 易错点:可能有人会认为ß在小写转换中保持不变,但在Case Folding中它会被转换为"ss"。

总结

Case Folding在处理不区分大小写的字符串比较时非常有用,但也有一些特例需要注意。Python的实现考虑了大多数用户的需求,虽然Unicode中的大小写处理存在许多语言学上的特殊情况。理解这些转换规则有助于在文本处理中避免一些常见的错误。

Utility Functions for Normalized Text Matching

当然,以下是对文中内容和例子的详细讲解:

NFC和NFD的应用

NFC(Normalization Form C)和NFD是安全的Unicode规范化形式,允许我们在进行字符串比较时得到合理的结果。通常,NFC是大多数应用程序中使用的最佳规范化形式。

  • NFC(Normalization Form C):将组合字符合并成单个字符。
  • NFD(Normalization Form D):将组合字符分解为基本字符和附加符号。

str.casefold()的作用

str.casefold()是进行不区分大小写比较的推荐方式,因为它不仅仅是将字符转换为小写字符,还考虑了一些语言特例(例如德语的Eszett)。

工具函数示例

代码示例提供了两个函数:nfc_equalfold_equal,用于进行规范化的Unicode字符串比较。

例子详解
  1. 使用NFC进行比较

    >>> s1 = 'café'
    >>> s2 = 'cafe\u0301'
    >>> s1 == s2
    False
    >>> nfc_equal(s1, s2)
    True
    >>> nfc_equal('A', 'a')
    False
    
    • 解释s1s2在视觉上是相同的,因为’s2’中使用了组合字符,但由于它们的编码不同,直接比较结果为False。使用nfc_equal进行比较后,结果为True,因为它们被规范化为相同的形式。
    • 易错点:可能会误以为视觉相同的字符总是比较相等,但编码不同的字符需要规范化才能比较。
  2. 使用NFC和Case Folding进行比较

    >>> s3 = 'Straße'
    >>> s4 = 'strasse'
    >>> s3 == s4
    False
    >>> nfc_equal(s3, s4)
    False
    >>> fold_equal(s3, s4)
    True
    >>> fold_equal(s1, s2)
    True
    >>> fold_equal('A', 'a')
    True
    
    • 解释:‘Straße’ 和 'strasse’在直接比较和NFC比较中都是不同的字符串,但通过fold_equal函数进行Case Folding后,可以得到相等的结果。这是因为Case Folding将’ß’转换为"ss"。
    • 易错点:忽视Case Folding的效果,特别是在处理包含语言特例的字符串时。

函数实现

from unicodedata import normalize

def nfc_equal(str1, str2):
    return normalize('NFC', str1) == normalize('NFC', str2)

def fold_equal(str1, str2):
    return (normalize('NFC', str1).casefold() ==
            normalize('NFC', str2).casefold())
  • nfc_equal:将两个字符串规范化为NFC形式后进行比较。
  • fold_equal:在NFC规范化后,还对字符串进行Case Folding,然后进行比较。

总结

这些工具函数能够帮助在处理多语言文本时,进行更可靠的字符串比较。它们利用了Unicode的规范化和Case Folding来确保在不同编码和大小写情况下的字符串一致性。理解这些函数的应用场景和方法,对于文本处理中的准确性至关重要。

Extreme “Normalization”: Taking Out Diacritics

去除变音符号的应用

在某些情况下,例如在搜索引擎中,去除变音符号(例如重音符、变音符等)是有帮助的。这是因为用户在输入时可能会忽略这些符号,或者语言的拼写规则可能会随时间而改变。然而,去除变音符号并不是一种规范的标准化形式,因为它可能改变单词的含义,并在搜索时产生误报。

去除变音符号在某些情况下也有助于生成更易读的URL。例如:

  • https://en.wikipedia.org/wiki/São_PauloSão Paulo 的URL编码形式,其中 ã 是字符“ã”的UTF-8编码。
  • https://en.wikipedia.org/wiki/Sao_Paulo 更易于识别,尽管不是正确的拼写。

示例讲解

示例 4-14:去除所有变音符号
import unicodedata
import string

def shave_marks(txt):
    """Remove all diacritic marks"""
    norm_txt = unicodedata.normalize('NFD', txt)
    shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c))
    return unicodedata.normalize('NFC', shaved)
  • 步骤

    1. 将文本规范化为NFD形式,将组合字符分解为基本字符和附加符号。
    2. 过滤掉所有的附加符号。
    3. 将文本重新规范化为NFC形式。
  • 例子

    >>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
    >>> shave_marks(order)
    '“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”'
    >>> Greek = 'Ζέφυρος, Zéfiro'
    >>> shave_marks(Greek)
    'Ζεφυρος, Zefiro'
    
    • 只有字母“è”、“ç”和“í”被替换。
    • “έ”和“é”都被替换。
示例 4-16:仅从拉丁字母中移除变音符号
def shave_marks_latin(txt):
    """Remove all diacritic marks from Latin base characters"""
    norm_txt = unicodedata.normalize('NFD', txt)
    latin_base = False
    preserve = []
    for c in norm_txt:
        if unicodedata.combining(c) and latin_base:
            continue  # 忽略拉丁基字符上的变音符号
        preserve.append(c)
        if not unicodedata.combining(c):
            latin_base = c in string.ascii_letters
    shaved = ''.join(preserve)
    return unicodedata.normalize('NFC', shaved)
  • 步骤
    1. 对字符进行分解。
    2. 当基字符为拉丁字母时,跳过组合字符。
    3. 重新组合所有字符。
示例 4-17:将一些西方排版符号转换为ASCII
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""", """'f"^<''""---~>""")
multi_map = str.maketrans({
    '€': 'EUR',
    '…': '...',
    'Æ': 'AE',
    'æ': 'ae',
    'Œ': 'OE',
    'œ': 'oe',
    '™': '(TM)',
    '‰': '<per mille>',
    '†': '**',
    '‡': '***',
})
multi_map.update(single_map)

def dewinize(txt):
    """Replace Win1252 symbols with ASCII chars or sequences"""
    return txt.translate(multi_map)

def asciize(txt):
    no_marks = shave_marks_latin(dewinize(txt))
    no_marks = no_marks.replace('ß', 'ss')
    return unicodedata.normalize('NFKC', no_marks)
  • 步骤

    1. 构建字符到字符的映射表。
    2. 构建字符到字符串的映射表。
    3. 合并映射表。
    4. dewinize 用于将Win1252符号替换为ASCII字符或序列。
    5. asciize 应用dewinize,去除变音符号,并替换’ß’。
  • 例子

    >>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
    >>> dewinize(order)
    '"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."'
    >>> asciize(order)
    '"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."'
    

总结

simplify.py中的函数远远超出了标准的规范化,并对文本进行了深度处理,可能会改变其含义。是否使用这些函数,取决于您对目标语言、用户需求以及转换后文本使用场景的了解。了解这些方法的应用场景和潜在影响,对于文本处理是至关重要的。

Sorting Unicode Text

这个地方有点偏离我的工程代码,所以直接上总结。

上文讨论了在Python中如何对带有非ASCII字符的字符串进行排序的问题。默认情况下,Python会使用字符的Unicode编码点来排序,这对使用非ASCII字符的语言(如葡萄牙语)来说,可能会产生不符合语言习惯的排序结果。例如:

fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
print(sorted(fruits))
# 输出: ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

在葡萄牙语中,带有重音符号的字符排序时通常会被视为不带重音符号的字符。因此,‘cajá’应该被视为’caja’,并排在’caju’之前。正确的排序结果应该是:

['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

使用locale.strxfrm进行排序

为了实现这种语言习惯的排序,Python提供了locale.strxfrm函数。使用这个函数之前,需要设置合适的locale(语言环境),例如葡萄牙语的pt_BR.UTF-8。示例代码如下:

import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)
# 输出: ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

潜在的易错点和难以理解的地方

  1. Locale设置是全局的:调用setlocale会影响整个Python进程的locale设置,这意味着如果在库中使用它,可能会影响到整个应用程序的行为。因此,建议在应用程序启动时进行设置,而不是在库中调用。

  2. Locale必须安装在操作系统上:如果指定的locale没有安装,setlocale会抛出异常locale.Error: unsupported locale setting。这意味着在不同的操作系统上,可能需要预先安装相应的语言包。

  3. Locale名字拼写:必须知道正确的locale名字,比如pt_BR.UTF-8,否则设置可能会失败。

  4. 操作系统支持的局限性:不同的操作系统对locale的支持不同。在某些系统上,即使设置了locale,排序结果也可能不正确。例如,作者提到在macOS上,locale设置返回正常,但排序结果仍然不正确。

解决方案

由于上述问题,作者提到了一种替代方案,即使用pyuca库。这个库专门用于解决国际化排序问题,并且不依赖于操作系统的locale设置。

希望通过这些解释和示例,可以帮助你更好地理解在Python中进行国际化字符串排序时的挑战和解决方案。

Sorting with the Unicode Collation Algorithm

上文介绍了如何使用pyuca库来进行Unicode字符串排序,这是James Tauber创建的一个纯Python实现的Unicode Collation Algorithm (UCA)。这个库的目的就是解决在不同语言环境下的排序问题。以下是如何使用pyuca进行排序的示例:

使用pyuca进行排序

import pyuca

# 创建一个Collator对象
coll = pyuca.Collator()

# 定义要排序的水果列表
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']

# 使用coll.sort_key作为排序键
sorted_fruits = sorted(fruits, key=coll.sort_key)

# 输出排序结果
print(sorted_fruits)
# 输出: ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

从示例中可以看到,pyuca的使用非常简单,而且在GNU/Linux、macOS和Windows上都能正常工作。

pyuca的特点和局限

  1. 不依赖于操作系统的locale设置pyuca不需要操作系统支持特定的locale。这使得它在跨平台应用中非常有用,因为不用担心不同操作系统对locale支持的不一致性。

  2. 默认使用的排序表pyuca使用了allkeys.txt作为默认的排序表,这个文件是Unicode.org提供的Default Unicode Collation Element Table的一部分。如果需要自定义排序规则,可以提供自己的排序表路径给Collator构造器。

  3. 不考虑具体语言的排序规则:虽然pyuca提供了标准的Unicode排序规则,但它不考虑特定语言的排序习惯。例如,德语中,字母’Ä’应该排在’A’和’B’之间,而在瑞典语中,'Ä’应该排在’Z’之后。

PyICU库的推荐

技术审阅者Miroslav Šedivý推荐了另一个库PyICU,它能够像locale一样进行排序,但不改变进程的locale设置。PyICU更适合处理特定语言的排序需求,比如在土耳其语中处理’i’和’ı’的大小写转换。不过,PyICU需要编译一个扩展模块,因此在某些系统上可能比pyuca更难安装。

结论

对于一般的Unicode排序需求,pyuca是一个简单且跨平台的解决方案。但如果需要支持特定语言的排序规则,或者需要处理特定语言环境的细节问题(如土耳其语的大小写),则可以考虑使用PyICU。希望通过这些解释和示例,可以帮助你更好地理解如何在Python中进行国际化字符串排序。

The Unicode Database

核心概念

  1. Unicode标准:Unicode为每个字符分配了一个唯一的代码点,并为每个字符提供了丰富的元数据。例如,一个字符是否是字母、数字、可打印字符等。

  2. 字符串方法:Python的字符串方法如isalpha()isprintable()isdecimal()isnumeric()等,都是基于Unicode的元数据来工作的。例如:

    • isalpha():检查字符串中的每个字符是否都是字母。
    • isprintable():检查字符串中的每个字符是否都是可打印的。
  3. unicodedata模块unicodedata.category(char)可以返回一个字符的Unicode类别,两字母代码表示。例如,Lu表示大写字母,Ll表示小写字母。

例子和易错点

  • 例子1:使用isalpha()方法

    label1 = "Hello"
    label2 = "Hello123"
    print(label1.isalpha())  # 输出: True,因为所有字符都是字母
    print(label2.isalpha())  # 输出: False,因为包含数字
    
  • 易错点1isalpha()只检查字母,不检查空格或其他符号。

    label3 = "Hello World"
    print(label3.isalpha())  # 输出: False,因为空格不是字母
    
  • 例子2:使用unicodedata.category()

    import unicodedata
    
    char = 'A'
    print(unicodedata.category(char))  # 输出: 'Lu',表示大写字母
    
    char = '1'
    print(unicodedata.category(char))  # 输出: 'Nd',表示十进制数字
    
  • 易错点2:了解类别代码

    • Lu:大写字母
    • Ll:小写字母
    • Nd:十进制数字
    • Zs:空格

Finding Characters by Name

以上内容介绍了如何使用Python的unicodedata模块来获取字符的官方名称,并演示了一个名为cf.py的命令行工具,用于根据名称查找Unicode字符。以下是这些内容的详细解释和可能的易错点:

核心概念

  1. unicodedata.name()函数

    • 用于获取字符的Unicode标准官方名称。
    • 例如,name('A')返回'LATIN CAPITAL LETTER A'
  2. 字符查找工具cf.py

    • 允许用户通过输入名称的关键字来搜索Unicode字符。
    • 使用unicodedata.name()函数获取每个字符的名称,并检查名称中是否包含用户输入的关键字。

例子和易错点

  • 例子1:使用unicodedata.name()

    import unicodedata
    
    print(unicodedata.name('A'))  # 输出: 'LATIN CAPITAL LETTER A'
    print(unicodedata.name('😀'))  # 输出可能是: 'GRINNING FACE'
    
  • 易错点1:字符名称大小写敏感

    • unicodedata.name()返回的名称是大写的,所以在比较时要注意大小写。
  • 例子2:使用cf.py脚本查找字符

#!/usr/bin/env python3
import sys
import unicodedata

START, END = ord(' '), sys.maxunicode + 1

def find(*query_words, start=START, end=END):
    query = {w.upper() for w in query_words}
    for code in range(start, end):
        char = chr(code)
        name = unicodedata.name(char, None)
        if name and query.issubset(name.split()):
            print(f'U+{code:04X}\t{char}\t{name}')

def main(words):
    if words:
        find(*words)
    else:
        print('Please provide words to find.')

if __name__ == '__main__':
    main(sys.argv[1:])
$ ./cf.py cat smiling
# 可能的输出:
# U+1F638 😸 GRINNING CAT FACE WITH SMILING EYES
# U+1F63A 😺 SMILING CAT FACE WITH OPEN MOUTH
# U+1F63B 😻 SMILING CAT FACE WITH HEART-SHAPED EYES
  • 易错点2:确保输入的关键字是正确的

    • cf.py中的find函数将查询词转换为大写,所以用户输入时不必担心大小写。
  • 代码解释

    • find函数读取所有Unicode字符的名称,并检查其中是否包含用户提供的关键字。
    • 使用issubset()方法来简化查询词在字符名称中的存在性检查。

Numeric Meaning of Characters

Unicode 数字字符的处理及分析

介绍

在处理 Unicode 字符时,Python 提供了 unicodedata 模块,可以用于检查一个 Unicode 字符是否代表一个数字,以及获取其对应的数值。除此之外,字符串对象的 .isdecimal().isnumeric() 方法也可以帮助判断字符的类型。

示例分析

下面的示例展示了如何使用 unicodedata 模块和字符串的方法来分析一组字符:

import unicodedata
import re

re_digit = re.compile(r'\d')
sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:
    print(f'U+{ord(char):04x}',
          char.center(6),
          're_dig' if re_digit.match(char) else '-',
          'isdig' if char.isdigit() else '-',
          'isnum' if char.isnumeric() else '-',
          f'{unicodedata.numeric(char):5.2f}',
          unicodedata.name(char),
          sep='\t')

输出解析

在这里插入图片描述

例如,图中显示了如下字符及其分析结果:

  • U+0031 对应字符 1,匹配正则表达式 r'\d'isdigitisnumeric 均为真,数值为 1.00,名称为 “DIGIT ONE”。
  • U+00bc 对应字符 ¼,未匹配正则表达式,isnumeric 为真,数值为 0.25,名称为 “VULGAR FRACTION ONE QUARTER”。

关键点解释

  1. 正则表达式与 Unicode

    • re.compile(r'\d') 用于匹配十进制数字,但它对 Unicode 的支持有限。例如,它无法识别 ¼ 这样的字符。
  2. .isdigit().isnumeric()

    • .isdigit() 方法只返回 True 对于十进制数字。
    • .isnumeric() 方法可以识别更多的数字字符,包括分数和罗马数字。
  3. unicodedata.numeric()

    • 返回字符的数值表示,即使是非传统数字字符,例如分数或罗马数字。

可能的易错点

  • 正则表达式的局限性:在处理 Unicode 数字时,re 模块可能不能完全满足需求,特别是对于非传统数字字符。
  • 方法选择:理解 .isdigit().isnumeric() 的区别对于正确识别不同类型的数字字符很重要。

结论

使用 unicodedata 模块可以更全面地处理和分析 Unicode 字符中的数字元素。通过结合不同的方法和工具,可以更准确地识别和处理多种类型的数字字符。对于需要更好 Unicode 支持的场景,考虑使用更先进的正则表达式库,如 regex(可通过 PyPI 获取)。

Dual-Mode str and bytes APIs

Python 的标准库中有一些函数,它们可以接受字符串(str)或字节(bytes)类型的参数,并且会根据参数的类型表现出不同的行为。在 re 模块和 os 模块中可以找到一些这样的例子。

str Versus bytes in Regular Expressions

正则表达式在字符串和字节上的不同表现

介绍

在 Python 中,正则表达式可以用于字符串 (str) 和字节 (bytes) 数据。它们在处理 Unicode 和 ASCII 字符时表现不同。特别是,\d\w 这样的模式在字符串中可以匹配 Unicode 字符,而在字节中仅匹配 ASCII 字符。

示例分析

以下是一个示例,比较字符串和字节模式如何匹配不同类型的字符:

import re

re_numbers_str = re.compile(r'\d+')
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+')
re_words_bytes = re.compile(rb'\w+')

text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"
            " as 1729 = 1³ + 12³ = 9³ + 10³.")
text_bytes = text_str.encode('utf_8')

print(f'Text\n {text_str!r}')
print('Numbers')
print(' str :', re_numbers_str.findall(text_str))
print(' bytes:', re_numbers_bytes.findall(text_bytes))
print('Words')
print(' str :', re_words_str.findall(text_str))
print(' bytes:', re_words_bytes.findall(text_bytes))

结果解析

在这里插入图片描述

图中的输出显示了不同模式的匹配结果:

  • 数字匹配

    • str 模式 r'\d+' 能够匹配泰米尔数字(例如 ௧௭௨௯)和 ASCII 数字。
    • bytes 模式 rb'\d+' 仅匹配 ASCII 数字字节。
  • 单词匹配

    • str 模式 r'\w+' 匹配字母、上标、泰米尔数字和 ASCII 数字。
    • bytes 模式 rb'\w+' 只匹配 ASCII 字母和数字字节。

关键点解释

  1. 字符串与字节的正则表达式

    • 字符串正则表达式在处理 Unicode 时更灵活,能识别多种字符。
    • 字节正则表达式局限于 ASCII 范围外的字符,非 ASCII 字节被视为非数字和非单词字符。
  2. 使用 re.ASCII 标志

    • 对于字符串正则表达式,可以使用 re.ASCII 标志强制执行 ASCII 字符匹配。

可能的易错点

  • 误用字节正则表达式:对于需要处理 Unicode 字符的场景,使用字节正则表达式可能导致匹配不完整。
  • 忽略编码差异:在处理字节数据时,确保正确的编码以避免误解字符。

结论

通过本例,我们可以看出正则表达式在字符串和字节上的不同表现。理解这些差异有助于在正确的场合使用合适的模式,确保文本处理的准确性。对于需要严格控制的场景,可以结合使用 re.ASCII 标志来限制匹配范围。

str Versus bytes in os Functions

处理文件名中的 Unicode 与字节序列

介绍

在 GNU/Linux 系统中,内核对 Unicode 的支持有限,因此可能会遇到由无效字节序列组成的文件名,这些序列无法被解码成字符串。这在有多种操作系统客户端的文件服务器上尤为常见。

文件名处理策略

为了处理这一问题,os 模块的函数可以接受字符串 (str) 或字节 (bytes) 作为文件名或路径名的参数:

  • 字符串参数:如果函数以字符串作为参数调用,参数会自动使用 sys.getfilesystemencoding() 指定的编码进行转换,操作系统的响应也会用相同的编码解码。这种方式通常符合 “Unicode 三明治” 的最佳实践。
  • 字节参数:如果需要处理无法通过上述方式处理的文件名,可以传递字节参数给 os 函数,以获取字节返回值。这允许处理任何文件或路径名,无论其内部字节序列如何。

示例分析

以下是一个示例,展示了如何使用字符串和字节参数调用 os.listdir

>>> os.listdir('.')
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.')
[b'abc.txt', b'digits-of-\xcf\x80.txt']
  • 第一个调用返回字符串列表,其中包括文件名 “digits-of-π.txt”。
  • 第二个调用返回字节列表,其中 b'digits-of-\xcf\x80.txt' 是希腊字母 π 的 UTF-8 编码。

重要函数

  • os.fsencode(name_or_path)os.fsdecode(name_or_path)
    • 这些函数帮助手动处理作为文件名或路径名的字符串或字节序列。
    • 它们接受 strbytes 或实现 os.PathLike 接口的对象作为参数。

结论

对于不能用常规方法处理的文件名,使用字节参数提供了灵活性。同时,os.fsencodeos.fsdecode 函数为手动处理文件名提供了便利工具。在处理文件名时,理解这些方法的区别和应用场景非常重要,以确保程序的鲁棒性和兼容性。

Chapter Summary

本章总结:Unicode 与文本处理

1. 字符与字节的区别

  • 字符不等于字节:随着 Unicode 的普及,我们需要将文本字符串与其在文件中表示的二进制序列区分开来。Python 3 强制执行了这种分离。

2. 二进制序列数据类型

  • bytes, bytearray, 和 memoryview:这些数据类型用于处理二进制数据。

3. 编码与解码

  • 重要编码:介绍了多种编码和解码方法。
  • 错误处理:讨论了如何防止或处理 UnicodeEncodeError、UnicodeDecodeError,以及因错误编码导致的 SyntaxError。

4. 编码检测

  • 理论与实践:理论上编码检测是困难的,但实践中 Chardet 包能很好地处理常见编码。
  • 字节顺序标记:UTF-16 和 UTF-32 文件中常见的编码提示,有时也出现在 UTF-8 文件中。

5. 打开文本文件

  • 编码关键字:打开文本文件时,encoding= 参数不是强制的,但应该使用,否则可能导致跨平台不兼容。
  • 默认编码:解释了 Python 使用的默认编码设置及其检测方法。Windows 用户可能面临不兼容问题,而 GNU/Linux 和 macOS 通常默认为 UTF-8。

6. 文本规范化

  • 规范化与大小写折叠:对于文本匹配是必要的。也介绍了一些实用函数,比如去除重音符号。
  • 排序:使用标准 locale 模块排序 Unicode 文本,并介绍了不依赖 locale 配置的替代方案:pyuca 包。

7. Unicode 数据库

  • 命令行工具:利用 Unicode 数据库编写工具以按名称搜索字符。
  • 其他元数据:概述了一些 Unicode 元数据。
  • 双模式 API:一些函数可以接受 strbytes 参数,产生不同结果。

结论

理解 Unicode 和文本处理的各个方面对于编写兼容性强、可靠的程序至关重要。通过本章的学习,我们掌握了处理编码、文本匹配和排序等常见任务的方法和工具。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/21568.html
标签
评论
发布的文章

库制作与原理

2025-02-26 11:02:28

仿12306项目(1)

2025-02-26 11:02:27

2.25 链表 2 新建链表 82

2025-02-26 11:02:26

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!