5.2. 字符串和编码

深入学习Python字符串和编码,包括字符编码基础、Unicode与UTF-8的区别、Python字符串处理、str与bytes转换、字符串格式化方法,掌握Python中的字符编码处理技巧。

字符编码

字符串是种特殊的数据类型,因为它涉及到编码问题。

计算机只认识数字,处理文本就得先把文字转成数字。早期计算机设计时,8 个比特(bit)组成一个字节(byte),一个字节能表示的最大整数是 255(二进制 11111111 = 十进制 255)。

要表示更大的数,就得用更多字节。比如两个字节最大能表示 65535,四个字节最大能表示 4294967295

计算机是美国人发明的,最早只把 127 个字符编进去:大小写英文字母、数字和一些符号。这套编码表叫 ASCII,比如大写字母 A 的编码是 65,小写字母 z122

处理中文显然一个字节不够用,至少要两个字节,而且不能跟 ASCII 冲突。中国制定了 GB2312 编码来表示中文。

全世界有上百种语言,日本把日文编进 Shift_JIS,韩国把韩文编进 Euc-kr。各国有各国的标准,冲突在所难免。多语言混合的文本就会出现乱码。

 

Unicode 字符集就是为了解决这个问题。它把所有语言统一到一套编码里,乱码问题就不存在了。

Unicode 标准一直在发展,最常用的是 UCS-16 编码,用两个字节表示一个字符(特别生僻的字符需要 4 个字节)。现代操作系统和大多数编程语言都直接支持 Unicode。

ASCII 和 Unicode 的区别:ASCII 用 1 个字节,Unicode 通常用 2 个字节。

字母 A 用 ASCII 编码是十进制 65,二进制 01000001; 字符 0 用 ASCII 编码是十进制 48,二进制 00110000(注意字符 '0' 和整数 0 是两回事); 汉字 超出了 ASCII 范围,用 Unicode 编码是十进制 20013,二进制 01001110 00101101

如果把 ASCII 编码的 A 转成 Unicode,只需要在前面补 0,所以 A 的 Unicode 编码是 00000000 01000001

新问题又来了:统一用 Unicode,乱码是没了,但如果文本主要是英文,Unicode 比 ASCII 多占一倍空间,存储和传输都不划算。

本着节约的精神,UTF-8 编码出现了。它是"可变长编码",把 Unicode 字符根据数字大小编码成 1-6 个字节。常用英文字母编码成 1 个字节,汉字通常 3 个字节,只有很生僻的字符才会用 4-6 个字节。如果文本包含大量英文,UTF-8 就能节省空间:

字符ASCIIUnicodeUTF-8
A0100000100000000 0100000101000001
01001110 0010110111100100 10111000 10101101

从表格还能看出,UTF-8 有个额外好处:ASCII 可以看成 UTF-8 的一部分,大量只支持 ASCII 的老软件在 UTF-8 下也能正常工作。

弄清楚 ASCII、Unicode 和 UTF-8 的关系,就能理解现在计算机系统通用的字符编码方式了:

在计算机内存里,统一用 Unicode 编码。需要保存到硬盘或传输时,转成 UTF-8 编码。

用记事本编辑时,从文件读取的 UTF-8 字符会转成 Unicode 加载到内存,编辑完成后,保存时再把 Unicode 转成 UTF-8 写入文件:

 

浏览网页时,服务器会把动态生成的 Unicode 内容转成 UTF-8 传输到浏览器:

 

很多网页源码里会有 <meta charset="UTF-8" /> 这样的信息,表示网页用的是 UTF-8 编码。

Python的字符串

理解了字符编码,再来看 Python 的字符串就简单了。

Python 3 的字符串用 Unicode 编码,所以支持多语言:

>>> print('包含中文的str')
包含中文的str

对于单个字符的编码,Python 提供了 ord() 函数获取字符的整数表示,chr() 函数把编码转成字符:

>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'

知道字符的整数编码,还可以用十六进制写 str:

>>> '\u4e2d\u6587'
'中文'

两种写法完全等价。

Python 的字符串类型是 str,在内存里用 Unicode 表示,一个字符对应若干个字节。要在网络上传输或保存到磁盘,就得把 str 转成以字节为单位的 bytes

Python 的 bytes 类型用带 b 前缀的引号表示:

x = b'ABC'

注意区分 'ABC'b'ABC'。前者是 str,后者虽然显示内容一样,但 bytes 的每个字符只占一个字节。

Unicode 表示的 str 可以通过 encode() 方法编码成 bytes:

>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

纯英文的 str 可以用 ASCII 编码成 bytes,内容一样。含中文的 str 可以用 UTF-8 编码成 bytes。含中文的 str 无法用 ASCII 编码,因为中文超出了 ASCII 范围,Python 会报错。

在 bytes 中,无法显示为 ASCII 字符的字节用 \x## 表示。

反过来,从网络或磁盘读取的数据是 bytes,要转成 str 就用 decode() 方法:

>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

如果 bytes 包含无法解码的字节,decode() 会报错:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
Traceback (most recent call last):
  ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

如果只有一小部分字节无效,可以传入 errors='ignore' 忽略错误:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'

计算 str 包含多少字符,用 len() 函数:

>>> len('ABC')
3
>>> len('中文')
2

len() 计算的是 str 的字符数。换成 bytes,len() 计算的就是字节数:

>>> len(b'ABC')
3
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
6
>>> len('中文'.encode('utf-8'))
6

1 个中文字符经过 UTF-8 编码后通常占 3 个字节,1 个英文字符只占 1 个字节。

操作字符串时,经常遇到 strbytes 互相转换。为了避免乱码,应该始终用 UTF-8 编码进行转换。

Python 源代码也是文本文件,源代码里包含中文时,保存时要指定 UTF-8 编码。Python 解释器读取源代码时,为了让它按 UTF-8 读取,通常在文件开头写这两行:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

第一行是告诉 Linux/OS X 系统这是 Python 可执行程序,Windows 会忽略; 第二行是告诉 Python 解释器按 UTF-8 编码读取源代码,否则源代码里的中文输出可能乱码。

申明了 UTF-8 编码不代表 .py 文件就是 UTF-8 编码的,还要确保文本编辑器用的是 UTF-8 编码。

如果 .py 文件用 UTF-8 编码保存,并且申明了 # -*- coding: utf-8 -*-,在命令提示符测试就能正常显示中文:

 

格式化

如何输出格式化的字符串?经常要输出类似 '亲爱的xxx你好!你xx月的话费是xx,余额是xx' 这样的内容,xxx 的部分根据变量变化,需要一种简便的格式化方式。

 

Python 的格式化方式跟 C 语言一样,用 % 实现:

>>> 'Hello, %s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

% 运算符用来格式化字符串。字符串内部 %s 表示用字符串替换,%d 表示用整数替换。有几个 %? 占位符,后面就跟几个变量或值,顺序要对应好。只有一个 %? 时括号可以省略。

常见占位符:

占位符替换内容
%d整数
%f浮点数
%s字符串
%x十六进制整数

格式化整数和浮点数还能指定是否补 0 和小数位数:

print('%2d-%02d' % (3, 1))
print('%.2f' % 3.1415926)

不确定用什么时,%s 永远管用,它会把任何数据类型转成字符串:

>>> 'Age: %s. Gender: %s' % (25, True)
'Age: 25. Gender: True'

有时候字符串里的 % 就是普通字符怎么办?用 %% 转义,表示一个 %:

>>> 'growth rate: %d %%' % 7
'growth rate: 7 %'

format()

另一种格式化方法是用字符串的 format() 方法,它用传入的参数依次替换占位符 {0}{1}…,不过这种方式比 % 麻烦:

>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'

f-string

还有一种格式化方法是用 f 开头的字符串,叫 f-string。它和普通字符串的区别是,包含 {xxx} 的部分会用对应变量替换:

>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

上面代码里,{r} 被变量 r 的值替换,{s:.2f} 被变量 s 的值替换,: 后面的 .2f 指定了格式(保留两位小数),所以 {s:.2f} 的结果是 19.62

练习

小明的成绩从去年的 72 分提升到今年的 85 分,计算成绩提升的百分点,用字符串格式化显示成 'xx.x%',只保留小数点后 1 位:

s1 = 72
s2 = 85
r = ???
print('???')

小结

Python 3 的字符串用 Unicode,直接支持多语言。

strbytes 互相转换时要指定编码,最常用的是 UTF-8。Python 也支持其他编码,比如把 Unicode 编码成 GB2312:

>>> '中文'.encode('gb2312')
b'\xd6\xd0\xce\xc4'

但这纯属自找麻烦,没有特殊业务要求就只用 UTF-8

格式化字符串时,可以在 Python 交互式环境里测试,方便快捷。