第四章 Unicode 文本和字节序列

定义

Unicode

字符串是字符的序列,但字符的准确定义依赖于 Unicode 标准。在 Python 3 中,str 类型表示 Unicode 字符串,每个元素是一个 Unicode 字符

Unicode 将字符的身份存储形式分离,码位(Code Point) 是字符的唯一标识,用 U+XXXX(4–6 位十六进制)表示,范围为 U+0000U+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)strbytes(用于存储或传输)
  • 解码(decode)bytesstr(用于人类可读)
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 相关错误主要有三类:

  • UnicodeEncodeErrorstrbytes 时,字符无法用目标编码表示。
  • UnicodeDecodeErrorbytesstr 时,字节序列不符合目标编码规则。
  • 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-16UTF-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 字符的字符串 ss.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 标准库中部分模块(如 reos)提供了“双模式”API,即函数或对象可接受 strbytes 类型的参数,并根据输入类型自动调整行为。这种设计兼顾了 Unicode 文本处理与底层字节操作的灵活性。

re

re 模块允许使用 strbytes 编译正则表达式。两者在语义上有重要区别:

  • str 模式(默认)

    • \d\w\s 等特殊序列匹配 完整的 Unicode 字符集
      • \d 匹配所有 Unicode 数字(如泰米尔数字 );
      • \w 匹配 Unicode 字母、数字、下划线(包括非 ASCII 字符)。
  • bytes 模式(前缀 rb

    • 所有特殊序列 仅匹配 ASCII 范围内的字符
      • \d 仅匹配 b'0'b'9'
      • \w 仅匹配 ASCII 字母、数字和下划线。

strbytes 模式不能混用。模式类型必须与待匹配字符串类型一致。

# 对比 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() 等)均支持 strbytes 参数:

  • 传入 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 编码。

为方便在 strbytes 路径之间转换,os 模块提供:

  • os.fsencode(path):将 str/PathLike 转为文件系统字节序列;
  • os.fsdecode(path):将 bytes/PathLike 转为 Unicode 字符串。

这两个函数自 Python 3.6 起支持 strbytesos.PathLike 对象作为输入。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐