字符编码
字符串是种特殊的数据类型,因为它涉及到编码问题。
计算机只认识数字,处理文本就得先把文字转成数字。早期计算机设计时,8 个比特(bit)组成一个字节(byte),一个字节能表示的最大整数是 255(二进制 11111111 = 十进制 255)。
要表示更大的数,就得用更多字节。比如两个字节最大能表示 65535,四个字节最大能表示 4294967295。
计算机是美国人发明的,最早只把 127 个字符编进去:大小写英文字母、数字和一些符号。这套编码表叫 ASCII,比如大写字母 A 的编码是 65,小写字母 z 是 122。
处理中文显然一个字节不够用,至少要两个字节,而且不能跟 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 就能节省空间:
| 字符 | ASCII | Unicode | UTF-8 |
|---|---|---|---|
| A | 01000001 | 00000000 01000001 | 01000001 |
| 中 | 01001110 00101101 | 11100100 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 个字节。
操作字符串时,经常遇到 str 和 bytes 互相转换。为了避免乱码,应该始终用 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,直接支持多语言。
str 和 bytes 互相转换时要指定编码,最常用的是 UTF-8。Python 也支持其他编码,比如把 Unicode 编码成 GB2312:
>>> '中文'.encode('gb2312')
b'\xd6\xd0\xce\xc4'
但这纯属自找麻烦,没有特殊业务要求就只用 UTF-8。
格式化字符串时,可以在 Python 交互式环境里测试,方便快捷。