第四章 Unicode 文本和字节序列

第四章 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:强制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值