《Fluent Python》第四章 Unicode 文本和字节序列
第四章 Unicode 文本和字节序列
定义
Unicode
字符串是字符的序列,但字符的准确定义依赖于 Unicode 标准。在 Python 3 中,str 类型表示 Unicode 字符串,每个元素是一个 Unicode 字符。
Unicode 将字符的身份与存储形式分离,码位(Code Point) 是字符的唯一标识,用 U+XXXX(4–6 位十六进制)表示,范围为 U+0000 到 U+10FFFF(共 1,114,112 个可能值),如'A' → U+0041 、'€' → U+20AC '𝄞'(高音谱号)→ U+1D11E。
编码(Encoding) 是将码位转换为字节序列的规则。不同编码方式(如 UTF-8、UTF-16)对同一字符可能产生不同字节表示。 例如字符 'A'(U+0041),UTF-8 → \x41(1 字节);UTF-16LE → \x41\x00(2 字节)。
编码与解码
- 编码(encode):
str→bytes(用于存储或传输) - 解码(decode):
bytes→str(用于人类可读)
s = 'café' # 4 个 Unicode 字符
b = s.encode('utf8') # 转为 bytes,长度为 5('é' 占 2 字节)
decoded = b.decode('utf8') # 还原为原始字符串
字节基础
特性
Python 3 引入**bytes,不可变**。bytes中的每个元素是 0–255 的整数(不是字符)。对bytes进行切片操作返回同类型对象,即使长度为 1。
cafe = bytes('café', encoding='utf-8') # b'caf\xc3\xa9'
cafe # b'caf\xc3\xa9' 前三个字节 `b'caf'` 属于可打印 ASCII 范围,而后两个字节则不是 参考下文字面量显示规则
cafe[0] # 99 是一个整数
cafe[:1] # b'c' 仍是一个bytes对象
字面量显示规则
二进制序列的显示方式取决于字节值:
- 32–126(可打印 ASCII):直接显示字符(如
b'caf')。 - 特殊控制字符:用转义序列表示(
\t,\n,\r,\\)。 - 引号处理:若同时含
'和",整体用单引号,内部'转义为\'。 - 其他字节:用十六进制转义(如
\xc3,\xa9)。
编码/解码问题处理指南
同一字符串经不同编码会生成完全不同的字节表示:
for codec in ['latin_1', 'utf_8', 'utf_16']:
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'
ASCII、Latin-1等传统编码无法表示所有 Unicode 字符。 UTF 系列(UTF-8/UTF-16)是唯一能覆盖全部 Unicode 码位的编码方案。
Python 中 Unicode 相关错误主要有三类:
UnicodeEncodeError:str→bytes时,字符无法用目标编码表示。UnicodeDecodeError:bytes→str时,字节序列不符合目标编码规则。SyntaxError:加载.py源文件时,文件编码与 Python 默认(UTF-8)不符且未声明。
UnicodeEncodeError
非 UTF 编码(如 cp437)仅支持有限字符集。当字符无法编码时,默认抛出异常,但可通过 errors 参数指定策略:
city = 'São Paulo'
# 成功编码
print(city.encode('utf_8')) # b'S\xc3\xa3o Paulo'
print(city.encode('utf_16')) # b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
print(city.encode('iso8859_1')) # b'S\xe3o Paulo'
# 失败与错误处理
try:
city.encode('cp437') # 抛出 UnicodeEncodeError
except UnicodeEncodeError as e:
print("Error:", e)
# 跳过无法编码的字符 会导致静默的数据丢失
print(city.encode('cp437', errors='ignore')) # b'So Paulo'
# 用 ? 替换无法编码的字
print(city.encode('cp437', errors='replace')) # b'S?o Paulo'
# 用 XML 字符实体(如 ã)替换无法编码的字符
print(city.encode('cp437', errors='xmlcharrefreplace')) # b'São Paulo'
UnicodeDecodeError
UTF-8/UTF-16结构严格,非法字节会抛出 UnicodeDecodeError。传统 8 位编码(如 cp1252, koi8_r)可解码任意字节,静默生成乱码(mojibake)。
octets = b'Montr\xe9al' # 实际为 latin1 编码的 "Montréal"
# 使用错误的 8 位编码会无提示地产生错误文本
print(octets.decode('cp1252')) # 'Montréal'(正确)
print(octets.decode('iso8859_7')) # 'Montrιal'(希腊语乱码)
print(octets.decode('koi8_r')) # 'MontrИal'(俄语乱码)
try:
octets.decode('utf_8') # 抛出 UnicodeDecodeError
except UnicodeDecodeError as e:
print("UTF-8 decode error:", e)
# 使用 Unicode 官方的替换字符 码位 U+FFFD表示未知
print(octets.decode('utf_8', errors='replace')) # 'Montral'
源文件编码与 SyntaxError
Python 3 默认源码编码为 UTF-8(跨平台一致)。若文件含非 UTF-8 字节且未声明编码,会报 SyntaxError。
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line 1,
but no encoding declared; see https://python.org/dev/peps/pep-0263/ for details
最好的修复方式是将文件转为 UTF-8,也可以在文件顶部添加编码声明来进行临时处理。
# coding: cp1252
print('Olá, Mundo!')
检测未知编码
**未知编码无法 100% 确定,必须被告知。**但可借助线索:
- HTTP/XML 等协议常在头部声明编码;
- ASCII编码中不包含大于127的字节值;
- UTF-8 结构严谨,能成功解码通常就是 UTF-8;
- 频繁出现
b'\x00'→ 可能是 UTF-16/32; b'\x20\x00'→ 可能是 UTF-16LE 的空格
chardet 库(基于统计模型)可以用于编码检测
$ chardetect myfile.txt
myfile.txt: utf-8 with confidence 0.99
BOM
BOM(Byte Order Mark,字节顺序标记)是一个特殊的 Unicode 字符:U+FEFF(ZERO WIDTH NO-BREAK SPACE)。被用作字节序指示器,主要出现在 UTF-16 和 UTF-32 编码的文件开头。由于 Unicode 未分配 U+FFFE(即 BOM 的字节反转形式),编解码器可通过检测 b'\xff\xfe' 或 b'\xfe\xff' 来判断字节序。
u16 = 'El Niño'.encode('utf_16')
print(u16) # b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
print(list(u16)) # [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
开头的 b'\xff\xfe' 表示小端序(Little Endian),即低字节在前。字母 'E'(U+0045)被编码为 [69, 0],而非大端序的 [0, 69]。
为避免依赖 BOM,Unicode 定义了两种显式字节序的编码格式:
utf_16le:强制小端序,不生成 BOM。utf_16be:强制大端序,不生成 BOM。
# 小端序(无 BOM)
u16le = 'El Niño'.encode('utf_16le')
print(list(u16le)) # [69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
# 大端序(无 BOM)
u16be = 'El Niño'.encode('utf_16be')
print(list(u16be)) # [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
当使用 utf_16(无后缀)编码时,Python 自动在开头插入 BOM;而使用 utf_16le/utf_16be 时绝不插入 BOM。
解码时若检测到 BOM,标准 UTF-16/UTF-32 编解码器会自动跳过它,不会将其作为文本内容返回。Unicode 标准规定,无 BOM 的 UTF-16 文件应视为大端序(UTF-16BE)。但由于 x86/x64 架构均为小端序,大量 UTF-16 文件无 BOM 但实际为小端序,这可能导致跨平台解析错误。
UTF-8 本质无需 BOM:因其为 8 位编码,不受字节序影响,任何平台生成的 UTF-8 字节序列都一致。但 Windows 应用程序(如 Notepad、Excel)会添加 BOM,Notepad将带 BOM 的 UTF-8 视为“真正的 UTF-8”。Excel依赖 BOM 识别 UTF-8,否则按系统代码页(如 cp1252)解析,导致乱码。
Python 提供 utf-8-sig 编解码器(也写作 'utf_8_sig')
- 编码时:在输出开头添加
b'\xef\xbb\xbf'(U+FEFF 的 UTF-8 编码)。 - 解码时:若开头存在
b'\xef\xbb\xbf',则自动跳过,不返回该字符。
# 编码带 BOM
bom_utf8 = 'Hello'.encode('utf-8-sig')
print(bom_utf8) # b'\xef\xbb\xbfHello'
# 解码带 BOM 的文件(安全)
text = bom_utf8.decode('utf-8-sig') # → 'Hello'(BOM 被丢弃)
实用建议
读取文件时优先使用 utf-8-sig,因为能安全处理带或不带 BOM 的 UTF-8 文件。若文件有 BOM → 自动跳过;若文件无 BOM → 正常解码;永远不会返回 BOM 字符。
# 推荐:读取未知来源的 UTF-8 文件
with open('data.txt', encoding='utf-8-sig') as f:
content = f.read()
写入文件:默认使用 utf-8(无 BOM),这可以确保最大兼容性,尤其在 Unix/Linux 环境中,可执行脚本必须以 #!/usr/bin/env python3 开头,前两字节为 b'#!';若写入 BOM(b'\xef\xbb\xbf#!'),系统将无法识别 shebang,导致脚本执行失败。
三明治原则
- 输入端:尽早将字节(
bytes)解码为str(如读取文件时); - 中间层:程序逻辑只处理
str对象,绝不混用字节; - 输出端:尽可能晚地将
str编码为字节(如写入文件或网络)。
三明治原则隔离编码细节,避免“乱码”(mojibake)和 UnicodeError。 在现实中由Web 框架(如 Django)实现,用于自动处理边缘编码,开发者只需返回 str。
python3内置的open() 在文本模式下自动编解码,返回 str。但默认编码不可靠,依赖系统/区域设置,跨平台行为不一致。需要在多台机器或多场合运行的代码绝不应依赖默认编码。打开文本文件时,务必显式传入 encoding= 参数,因为默认值可能因机器、时间甚至用户环境而异。
# 写入 UTF-8 文件
with open('cafe.txt', 'w', encoding='utf_8') as f:
print("写入字符数:", f.write('café')) # 输出: 4
# 错误:读取时未指定编码 → 使用系统默认(如 Windows cp1252)
# 默认值可能因机器、时间甚至用户环境而异
with open('cafe.txt') as f:
print("错误读取:", f.read()) # 可能输出 'café'(乱码)
# 正确:显式指定 UTF-8
with open('cafe.txt', encoding='utf_8') as f:
print("正确读取:", f.read()) # 输出: 'café'
# 二进制模式:查看原始字节
with open('cafe.txt', 'rb') as f:
print("原始字节:", f.read()) # 输出: b'caf\xc3\xa9'
默认编码
import locale, sys, os
with open('dummy', 'w') as my_file:
print(f"{'locale.getpreferredencoding()':>30} -> {locale.getpreferredencoding()!r}")
print(f"{'my_file.encoding':>30} -> {my_file.encoding!r}")
print(f"{'sys.stdout.encoding':>30} -> {sys.stdout.encoding!r}")
print(f"{'sys.getdefaultencoding()':>30} -> {sys.getdefaultencoding()!r}")
print(f"{'sys.getfilesystemencoding()':>30} -> {sys.getfilesystemencoding()!r}")
os.remove('dummy')
| 设置 | Linux / macOS | Windows(Python ≥3.6) |
|---|---|---|
| 文本文件默认编码 | UTF-8 |
cp1252定) |
| 标准流(终端) | UTF-8 |
UTF-8 |
| 标准流(重定向) | UTF-8 |
cp1252 |
| 内部默认编码 | utf-8 |
utf-8 |
| 文件名编码 | utf-8 |
utf-8 |
由此可以发现不同平台的默认编码可能是不一样的,因此,关于默认编码的最佳建议是:不要依赖它们。
# 重定向输出示例
import sys
from unicodedata import name
test_chars = [
'\N{HORIZONTAL ELLIPSIS}', # …(cp1252 有,cp437 无)
'\N{INFINITY}', # ∞(cp437 有,cp1252 无)
'\N{CIRCLED NUMBER FORTY TWO}' # ㊷(两者均无)
]
for char in test_chars:
try:
print(f"输出 {name(char)}: {char}")
except UnicodeEncodeError as e:
print(f"编码错误: {e}")
- 直接运行:UTF-8 编码,前两字符正常显示;
- 重定向到文件:使用
cp1252,∞无法编码 → 抛出UnicodeEncodeError; - 乱码根源:同一字节在不同编码中含义不同(如
0x85在 cp1252 是…,在 cp437 是à)。
文本规范化
Unicode 字符串比较之所以复杂,是因为存在组合字符(combining characters):即附加在基础字符上的变音符号(如重音、波浪符等),它们在显示时与基础字符合成一个视觉单元,但内部由多个码位组成。
# 'café' 的两种表示方式
s1 = 'café'# 使用预组合字符 `'é'`
s2 = 'cafe\N{COMBINING ACUTE ACCENT}' # 使用基础字符 'e' + 组合锐音符
print(s1, s2) # ('café', 'café')
print(len(s1), len(s2)) # (4, 5)
print(s1 == s2) # False
Unicode 标准将此类序列定义为“规范等价”(canonical equivalents),但 Python 默认不做等价判断。
unicodedata.normalize()
标准化
通过标准化(Normalization)将不同形式的等价字符串转换为统一形式。
| 形式 | 全称 | 行为 |
|---|---|---|
| NFC | Normalization Form C | 组合:将基础字符 + 组合标记 → 预组合字符(最短形式) |
| NFD | Normalization Form D | 分解:将预组合字符 → 基础字符 + 独立组合标记 |
from unicodedata import normalize
s1 = 'café'
s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
# 长度统一
print(len(normalize('NFC', s1)), len(normalize('NFC', s2))) # (4, 4)
print(len(normalize('NFD', s1)), len(normalize('NFD', s2))) # (5, 5)
# 比较结果一致
print(normalize('NFC', s1) == normalize('NFC', s2)) # True
print(normalize('NFD', s1) == normalize('NFD', s2)) # True
用户输入通常为 NFC 形式,但为确保一致性,保存前应执行 normalize('NFC', text)。W3C 也推荐使用 NFC。
某些单个字符在 NFC 下会被标准化为另一个单个字符。例如,电阻单位“欧姆”符号(Ω,U+2126)会被标准化为希腊大写字母 Omega(Ω,U+03A9)。它们视觉上完全相同,但比较结果为不等
from unicodedata import normalize, name
ohm = '\u2126' # OHM SIGN
ohm_c = normalize('NFC', ohm)
print(name(ohm)) # 'OHM SIGN'
print(name(ohm_c)) # 'GREEK CAPITAL LETTER OMEGA'
print(ohm == ohm_c) # False
# 进行标准化后则转化成相同的符号
print(normalize('NFC', ohm) == normalize('NFC', ohm_c)) # True
兼容性标准化
兼容性标准化会将“兼容性字符”替换为语义等价但格式不同的“首选”形式,可能导致信息丢失或语义改变。在 NFKC 和 NFKD 形式中,每个兼容性字符都会被替换为一个或多个字符组成的“兼容性分解”(compatibility decomposition),这些字符被认为是“首选”表示形式,即使会损失一些格式信息。
from unicodedata import normalize, name
half = '\N{VULGAR FRACTION ONE HALF}'
print(normalize('NFKC', half)) # '1⁄2'
# 进行兼容分解后被分解成三个字符
for char in normalize('NFKC', half):
print(char, name(char), sep='\t')
# 1 DIGIT ONE
# ⁄ FRACTION SLASH 这并不是常见的 ASCII 斜杠(SOLIDUS,码位 47)
# 这导致搜索 ASCII 序列 '1/2' 将无法匹配标准化后的 Unicode 序列
# 2 DIGIT TWO
# 被兼容分解成42 和原始含义不同
print(normalize('NFKC', '4²')) # '42'
micro = 'µ'
# 看起来一样但其实不是两个相同的符号
micro_kc = normalize('NFKC', micro)
print(micro, micro_kc) # ('µ', 'μ')
print(ord(micro), ord(micro_kc)) # (181, 956)
大小写折叠
用于不区分大小写的比较,比 .lower() 更全面。对于仅包含 Latin-1 字符的字符串 s,s.casefold() 的结果通常与 s.lower() 相同,但存在一些例外,近 300 个码位在 .casefold() 与 .lower() 下结果不同。
micro = 'µ'
print(micro.casefold()) # 'μ'(转为希腊 mu)
eszett = 'ß'
print(eszett.casefold()) # 'ss'(德语 sharp s)
近 300 个码位在 .casefold() 与 .lower() 下结果不同
实用函数封装
from unicodedata import normalize
def nfc_equal(str1, str2):
"""使用 NFC 进行规范等价比较(区分大小写)"""
return normalize('NFC', str1) == normalize('NFC', str2)
def fold_equal(str1, str2):
"""使用 NFC + casefold 进行不区分大小写的规范等价比较"""
return (normalize('NFC', str1).casefold() ==
normalize('NFC', str2).casefold())
去除变音符号可以增加搜索容错(用户常省略重音),用于生成可读 URL(如 Sao_Paulo 代替 São_Paulo)。
# 移除所有组合标记
import unicodedata
def shave_marks(txt):
"""移除所有变音符号(包括非拉丁字符)"""
norm_txt = unicodedata.normalize('NFD', txt)
shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c))
return unicodedata.normalize('NFC', shaved)
# 仅移除拉丁字符上的变音符号
import string
def shave_marks_latin(txt):
"""仅移除拉丁基础字符上的变音符号"""
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
return unicodedata.normalize('NFC', ''.join(preserve))
# 全面 ASCII 化
single_map = str.maketrans('“”‘’•–—‹›', "''''---<>")
multi_map = str.maketrans({
'€': 'EUR', '…': '...', 'Æ': 'AE', 'æ': 'ae',
'Œ': 'OE', 'œ': 'oe', '™': '(TM)', '‰': '<per mille>',
'†': '**', '‡': '***'
})
multi_map.update(single_map)
def dewinize(txt):
"""替换 Win1252 特有符号为 ASCII"""
return txt.translate(multi_map)
def asciize(txt):
"""深度 ASCII 化:去符号 + 去变音 + 兼容标准化"""
no_marks = shave_marks_latin(dewinize(txt))
no_marks = no_marks.replace('ß', 'ss')
return unicodedata.normalize('NFKC', no_marks)
order = '"Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí."'
print(dewinize(order))
# "Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of acai."
print(asciize(order))
# "Herr Voss: - 1/2 cup of OEtker(TM) caffe latte - bowl of acai."
文本排序
Python 对字符串排序时,直接按 Unicode 码位逐字符比较,这在处理带变音符号的非 ASCII 文本时会产生不符合语言习惯的结果。
# 示例:巴西葡萄牙语水果列表
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
print(sorted(fruits))
# 输出:['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
# 但是在葡萄牙语中,正确的排序应该是
# ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
标准库 locale 模块
使用 locale.strxfrm 作为排序键,该函数将字符串转换为“本地化感知比较形式”,但是需先通过 locale.setlocale(locale.LC_COLLATE, locale) 设置区域。
import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale) # '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']
| 问题 | 说明 |
|---|---|
| 全局副作用 | setlocale 影响整个进程,禁止在库中调用 |
| 依赖系统安装 | 操作系统必须已安装对应区域(如 pt_BR.UTF-8),否则抛出 locale.Error |
| 平台支持不一 | GNU/Linux(如 Ubuntu)通常有效 macOS 常失效(即使返回成功,排序仍错误) Windows 支持不确定 |
| 需精确拼写 | 区域名称必须完全正确(如 pt_BR.UTF-8 而非 pt_BR) |
其具体效果因环境而异。
pyuca
pyuca(PyPI 包)是 Unicode 排序算法(UCA, Unicode Collation Algorithm)的纯 Python 实现,不依赖操作系统区域设置。
import pyuca
coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
print(sorted_fruits)
# 输出:['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
- 跨平台一致:在 GNU/Linux、macOS、Windows 上行为相同
- 无全局副作用:不修改进程区域设置
- 开箱即用:默认使用 Unicode 官方排序表
allkeys.txt
其局限于不支持语言特定规则:使用统一的 Unicode 默认排序,无法区分:
- 德语:
Ä视为A,排在 A-B 之间 - 瑞典语:
Ä是独立字母,排在 Z 之后
PyICU
- 真正的语言感知排序:支持德语、瑞典语、土耳其语等特殊规则
- 无全局副作用:区域设置仅作用于 ICU 实例
- 完整 Unicode 支持:包括土耳其语
i/İ大小写转换等边缘情况
缺点在于需编译 C++ 扩展,依赖系统 ICU 库,安装较复杂。
# Debian/Ubuntu
sudo apt-get install libicu-dev
pip install PyICU
# macOS (Homebrew)
brew install icu4c
export PKG_CONFIG_PATH="$(brew --prefix)/opt/icu4c/lib/pkgconfig"
pip install --no-binary=pyicu pyicu
Unicode 数据库
Unicode 标准通过 Unicode 字符数据库(UCD)为每个码位(code point)定义多种属性,这些属性用于文本处理、排版、双向文本显示等场景。Python 的 unicodedata 模块(基于 UCD 15.1.0)提供了访问这些元数据的接口,是 str 类型方法(如 isalpha()、isdecimal()、casefold() 等)的底层实现基础。
- 字符属性:包括通用类别(General Category)、双向类别(Bidi_Class)、数值类型(Numeric Type)、组合类(Combining Class)等。
- 属性级别:分为规范性(normative)、信息性(informative)、贡献性(contributory)和临时性(provisional)。
- 字符名称:每个已分配字符拥有唯一名称(如
LATIN CAPITAL LETTER A),符合命名规则(仅含 A–Z、0–9、连字符和空格,且无非法序列)。 - 特殊码位:控制字符(Cc)、私用区(Co)、代理项(Cs)、非字符(Cn)等无正式名称,使用元标签(如
<control>)引用。
# 通过名称查找字符
import unicodedata
# 查找字符
char = unicodedata.lookup('LEFT CURLY BRACKET')
assert char == '{'
# 获取字符名称
name = unicodedata.name('½')
assert name == 'VULGAR FRACTION ONE HALF'
# 处理无名称字符(如非字符)
fallback_name = unicodedata.name('\uFFFF', 'fallback')
assert fallback_name == 'fallback'
双模式 API
Python 标准库中部分模块(如 re 和 os)提供了“双模式”API,即函数或对象可接受 str 或 bytes 类型的参数,并根据输入类型自动调整行为。这种设计兼顾了 Unicode 文本处理与底层字节操作的灵活性。
re
re 模块允许使用 str 或 bytes 编译正则表达式。两者在语义上有重要区别:
-
str模式(默认):\d、\w、\s等特殊序列匹配 完整的 Unicode 字符集。\d匹配所有 Unicode 数字(如泰米尔数字௧);\w匹配 Unicode 字母、数字、下划线(包括非 ASCII 字符)。
-
bytes模式(前缀rb):- 所有特殊序列 仅匹配 ASCII 范围内的字符。
\d仅匹配b'0'–b'9';\w仅匹配 ASCII 字母、数字和下划线。
- 所有特殊序列 仅匹配 ASCII 范围内的字符。
str 与 bytes 模式不能混用。模式类型必须与待匹配字符串类型一致。
# 对比 str 与 bytes 正则表达式行为
import re
# str 模式的正则表达式
re_numbers_str = re.compile(r'\d+')
re_words_str = re.compile(r'\w+')
# bytes 模式的正则表达式
re_numbers_bytes = re.compile(rb'\d+')
re_words_bytes = re.compile(rb'\w+')
# 包含泰米尔数字(U+0BE7, U+0BED, U+0BE8, U+0BEF)的 Unicode 字符串
text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"
" as 1729 = 1^3 + 12^3 = 9^3 + 10^3.")
text_bytes = text_str.encode('utf-8') # 转换为 bytes 用于 bytes 模式匹配
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))
Text
'Ramanujan saw ௧௭௨௯ as 1729 = 1^3 + 12^3 = 9^3 + 10^3.'
Numbers
str : ['௧௭௨௯', '1729', '1', '12', '9', '10']
bytes: [b'1729', b'1', b'12', b'9', b'10']
Words
str : ['Ramanujan', 'saw', '௧௭௨௯', 'as', '1729', '1^3', '12^3', '9^3', '10^3']
bytes: [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']
若希望 str 模式也仅匹配 ASCII 字符,可使用 re.ASCII(或 re.A)标志:
re.compile(r'\w+', re.ASCII)
os
操作系统内核(如 Linux)以字节序列处理文件名,并不理解 Unicode。因此,文件名可能包含无法用任何编码正确解码的“无效”字节序列。为应对这一现实,os 模块中所有接受路径或文件名的函数(如 os.listdir()、os.open() 等)均支持 str 或 bytes 参数:
-
传入
str:- 自动使用
sys.getfilesystemencoding()编码为字节传给系统; - 系统返回的字节再自动解码为
str。 - 符合“Unicode 三明治”原则,适用于绝大多数正常场景。
- 自动使用
-
传入
bytes:- 直接将字节传给系统;
- 返回值也为
bytes,不进行任何编码/解码。 - 可处理任意“畸形”文件名(如包含无效 UTF-8 序列的名称)。
# os.listdir() 的双模式调用
import os
# 使用 str 参数:返回解码后的 Unicode 文件名
files_str = os.listdir('.')
print("str :", files_str)
# 使用 bytes 参数:返回原始字节形式的文件名
files_bytes = os.listdir(b'.')
print("bytes:", files_bytes)
假设目录中有一个文件名为 "digits-of-π.txt"(其中 π 是希腊字母 U+03C0):
# 输出可能是如下结果
str : ['abc.txt', 'digits-of-π.txt']
bytes: [b'abc.txt', b'digits-of-\xcf\x80.txt']
注:
b'\xcf\x80'是π的 UTF-8 编码。
为方便在 str 与 bytes 路径之间转换,os 模块提供:
os.fsencode(path):将str/PathLike转为文件系统字节序列;os.fsdecode(path):将bytes/PathLike转为 Unicode 字符串。
这两个函数自 Python 3.6 起支持 str、bytes 或 os.PathLike 对象作为输入。
更多推荐


所有评论(0)