跳转到内容
新建笔记

Python 字符串:Unicode、转换与格式化

Python 的 str 是不可变的 Unicode 文本序列,bytes 是字节序列。字符串方法通常返回结果,不会原地改写原字符串。本文按 Python 3.11 说明,并把原笔记的查阅表、转义说明和交互记录整理为可独立运行的程序。

字面量、访问、拼接与转义

跳转到“字面量、访问、拼接与转义”

单引号和双引号都可以定义字符串。索引从零开始,负索引从结尾计数;切片包含起点、排除终点。len(s) 计算 Unicode 码点数,不保证等于用户眼中的字符个数或屏幕宽度。例如字母与组合重音可以由两个码点组成。

var1 = 'Hello World!'
var2 = "Runoob"
assert var1[0] == "H" and var1[-1] == "!"
assert var1[:6] + var2 + "!" == "Hello Runoob!"
assert var1[0:2] == "He" and "World" in var1
assert var1[::-1] == "!dlroW olleH"
assert "a" * 5 == "aaaaa" and 2 * "ab" == "abab"
assert len("e\u0301") == 2
assert len("é") == 1
continued = "line1 \
line2 \
line3"
assert continued == "line1 line2 line3"
assert "\a\b\000\n\r" == "\x07\x08\x00\x0a\x0d"
assert "\000" != "" and len("\000") == 1
assert r'\n' == R'\n' == "\\n"
assert len(r'\n') == 2 and len("\n") == 1
print(repr(continued), repr("\a\b\000\n\r"))

反斜杠接源代码换行会忽略这次换行;它不同于字符串内的 \n。原稿中把自然语言、>>> 提示符和输出放在 Python 围栏内的片段,不能整体作为脚本执行。

写法字符含义或词法作用使用边界
\a响铃控制字符终端是否发声取决于宿主与设置
\b退格控制字符不等于从 Python 字符串中删除前一个字符
\000、\0NUL,码点为零是一个字符,不是空字符串
\n换行文件和终端的显示行为还与环境有关
\r回车某些终端会移到行首并覆盖显示,日志界面可能显示控制标记
r'...'、R'...'原始字符串字面量反斜杠通常保留,但引号和结尾反斜杠仍受词法规则限制

因此 'google runoob taobao\r123456' 的内容仍含有完整文字和一个回车,不能把某个终端上显示的 123456 runoob taobao 当作字符串本身。原始字符串也不能以奇数个反斜杠结束;r 不会把一切非法字面量变为合法。上面的程序只打印控制字符的 repr,避免依赖声音或光标效果。字符串字面量规则

大小写、清理、替换与字符映射

跳转到“大小写、清理、替换与字符映射”
方法用途需要注意
upper、lower、swapcase大写、小写、互换大小写Unicode 映射不保证一字符对应一字符,也不保证两次互换总能恢复
capitalize首字符标题化,其余小写不是寻找第一个字母再大写
title、casefold简单标题化、适合无大小写比较的折叠标题化不懂人名或自然语言语法;casefold 不替代 Unicode 规范化
strip、lstrip、rstrip移除两侧、左侧、右侧的字符默认按空白字符;参数是字符集合,不是一个完整前后缀
removeprefix、removesuffix移除一个精确前缀或后缀Python 3.9 起提供;不匹配时保留原内容
replace替换子串,可限制次数返回结果,不原地修改
maketrans、translate构建映射并逐码点转换可替换成多个字符,映射为 None 可删除字符
text = "abcd12efg"
assert text.upper() == "ABCD12EFG"
assert text.lower() == "abcd12efg"
assert text.upper().title() == text.title() == "Abcd12Efg"
assert text.capitalize() == "Abcd12efg"
assert "12abc".capitalize() == "12abc"
assert "Hello".swapcase() == "hELLO"
assert "Straße".casefold() == "STRASSE".casefold() == "strasse"
assert text == "abcd12efg"
assert " \thello\n".strip() == "hello"
assert " hello ".lstrip() == "hello "
assert " hello ".rstrip() == " hello"
assert "000020".strip("0") == "2"
assert "000020".lstrip("0") == "20"
assert "abc/def".removeprefix("abc/") == "def"
assert "edit#text".removesuffix("text") == "edit#"
assert "abcabcX".lstrip("abc") == "X"
assert "abcabcX".removeprefix("abc") == "abcX"
assert "Hello World".replace("World", "Python") == "Hello Python"
assert "one one".replace("one", "two", 1) == "two one"
vowels = str.maketrans("aeiou", "12345")
assert "hello".translate(vowels) == "h2ll4"
mapping = str.maketrans({"a": "xy", "!": None})
assert "a!b".translate(mapping) == "xyb"
print("text transformation checks passed")

查找、分割、连接与行

跳转到“查找、分割、连接与行”
方法返回结果失败或特殊情况
find、rfind首次、最后一次匹配的起始索引未找到返回 -1
index、rindex相应匹配的起始索引未找到抛出 ValueError,不能直接当作 find 替换
count不重叠的出现次数不是统计所有重叠窗口
startswith、endswith是否匹配前缀、后缀可接受字符串元组,并用 start、end 限制检查范围
split、rsplit分割后的列表maxsplit 限制次数;有分隔符与默认空白规则不同
partition、rpartition(前段, 分隔符, 后段)总是三元组;找不到时左右版本的空段位置不同
splitlines按行边界分割可保留行结束符;不只识别单独的 \n
join以当前字符串连接各项接受可迭代对象,但各项必须是字符串
s = "Hello World"
assert s.find("World") == 6 and s.rfind("l") == 9
assert s.index("World") == 6 and s.rindex("l") == 9
assert s.find("missing") == -1
try:
s.index("missing")
except ValueError:
pass
else:
raise AssertionError("index must reject a missing substring")
assert "aaaa".count("aa") == 2
assert s.startswith("He") and s.endswith(("ld", "!"))
assert s.endswith("World", 6, 11)
assert s.startswith("World", 6)
assert " a b\t".split() == ["a", "b"]
assert " a b ".split(" ") == ["", "a", "", "b", ""]
assert "one,two,three".split(",") == ["one", "two", "three"]
assert "one#two#three".split("#", 1) == ["one", "two#three"]
assert "one#two#three".rsplit("#", 1) == ["one#two", "three"]
assert "part#a#partition".partition("#") == ("part", "#", "a#partition")
assert "part#a#partition".rpartition("#") == ("part#a", "#", "partition")
assert "abc".partition("#") == ("abc", "", "")
assert "abc".rpartition("#") == ("", "", "abc")
assert "Line 1\r\nLine 2\n".splitlines() == ["Line 1", "Line 2"]
assert "a\nb\n".splitlines(keepends=True) == ["a\n", "b\n"]
assert " ".join(["Hello", "World"]) == s
assert ",".join(str(n) for n in [1, 2]) == "1,2"
print("search and split checks passed")

find() 返回 -1 后直接拿它索引,会读到最后一个字符,应先判断。空分隔符不能传给 split 或 partition;"".split() 得到空列表,而 "".split(",") 得到包含一个空字符串的列表。

格式化、对齐、零填充与制表符

跳转到“格式化、对齐、零填充与制表符”

str.format 和 format_map 在模板中替换字段;format(value, spec) 与 f-string 则把格式说明交给该值类型的 __format__。这几项有关联,但不是同一个方法。% 是另一套字符串格式化协议 __mod__,不能把 C 的所有 printf 转换都直接搬过来。

% 转换Python 字符串格式化中的含义
%c一个字符或有效 Unicode 码点整数,不限 ASCII
%s取字符串表示
%d、%u十进制整数表示;%u 是保留的旧别名,不建立无符号整数类型
%o、%x、%X八进制、十六进制小写/大写;负数可以带负号
%f定点格式,可指定小数位
%e、%E科学计数法,指数标记大小写不同
%g、%G按精度和数值选择常规表示,精度通常指有效数字
%pPython str 的 % 不支持;不能据此格式化对象地址
%%输出字面百分号
assert "我叫 %s 今年 %d 岁!" % ("小明", 10) == "我叫 小明 今年 10 岁!"
assert "%c" % ord("中") == "中"
assert "%s" % 20 == "20"
assert "%u" % -2 == "-2"
assert "%o" % -8 == "-10"
assert "%x %X" % (255, 255) == "ff FF"
assert "%.2f" % 3.14159 == "3.14"
assert "%.2e %.2E" % (123, 123) == "1.23e+02 1.23E+02"
assert "%.3g %.3G" % (1e8, 1e8) == "1e+08 1E+08"
assert "%d%%" % 50 == "50%"
try:
"%p" % object()
except ValueError:
pass
else:
raise AssertionError("%p is unsupported")
assert "{0} {1}".format("Hello", "World") == "Hello World"
assert "{name} {greeting}".format_map({"name": "Alice", "greeting": "Hello"}) == "Alice Hello"
assert format("Hi", ">5") == "Hi".__format__(">5") == " Hi"
assert f"{3.14159:.2f}" == "3.14"
assert "Hi".center(6, "-") == "--Hi--"
assert "Hi".ljust(5, ".") == "Hi..."
assert "Hi".rjust(5, ".") == "...Hi"
assert "5".zfill(3) == "005" and "-5".zfill(3) == "-05"
assert "1234".zfill(3) == "1234"
assert "Hello\tWorld".expandtabs(4) == "Hello World"
print("formatting checks passed")

center、ljust、rjust 和 zfill 的宽度是最低要求,不会截断过长内容;填充字符需要是单个字符。zfill 会照顾前导正负号。expandtabs(4) 按当前列到下一个四列制表位补空格,不是把每个制表符机械替换为四个空格。

方法判断规则概要
isupper、islower至少有一个有大小写的字符,且这些字符都符合要求;数字等无大小写字符不直接使结果为假
istitle是否符合字符串方法采用的标题化规则
isalpha、isalnumUnicode 字母,或字母与数字;不是只检查英文字母
isdecimal、isdigit、isnumeric分别覆盖十进制数字、更广的数字、再更广的数值字符
isascii每个码点是否属于 ASCII;空字符串也为真
isprintable是否全部为 Python 定义的可打印字符;空字符串也为真
isspace非空且全部是空白字符
isidentifier是否符合标识符词法规则;还要单独排除关键字
import keyword
assert "ABC123".isupper() and "abc123".islower()
assert not "123".isupper() and not "123".islower()
assert "Hello World".istitle()
assert "中文".isalpha() and "abc123".isalnum()
assert "١".isdecimal() and int("١") == 1
assert "²".isdigit() and not "²".isdecimal()
assert "⅕".isnumeric() and not "⅕".isdigit()
assert not "-2".isnumeric() and not "1.5".isnumeric()
assert "ASCII".isascii() and not "中".isascii()
assert "".isascii() and "".isprintable()
assert not "".isalpha() and not "".isspace()
assert " \t\n".isspace() and not "\n".isprintable()
assert "name_1".isidentifier()
assert "for".isidentifier() and keyword.iskeyword("for")
assert not "2name".isidentifier()
print("classification checks passed")

isdigit() 或 isnumeric() 为真,不保证 int() 接受该字符串;若实际任务是解析数值,直接调用对应转换并处理异常通常更准确。判断可作为名字还可能涉及特定语法上下文中的软关键字,不能把 isidentifier() 当成完整 Python 程序验证器。

编码、对象协议与原表查阅入口

跳转到“编码、对象协议与原表查阅入口”

encode 把文本按指定编码转换为字节;字节的 decode 执行逆向转换。长度不同很常见,编码名称与错误处理策略应写清楚。

text = "中文"
encoded = text.encode("utf-8")
assert isinstance(encoded, bytes)
assert len(text) == 2 and len(encoded) == 6
assert encoded.decode("utf-8") == text
try:
text.encode("ascii")
except UnicodeEncodeError:
pass
else:
raise AssertionError("ASCII cannot represent this text")
try:
b"\xff".decode("utf-8")
except UnicodeDecodeError:
pass
else:
raise AssertionError("invalid UTF-8 must be rejected in strict mode")
print("encoding checks passed")

普通使用优先写 len(s)、s[index]、hash(s) 等公开操作。以下完整保留原表中的特殊方法名称,同时区分协议和常用方法;拼错的 _sizeof__ 在这里明确改为 __sizeof__。

名称推荐操作或说明
__class__、__doc__、__dir__、__getattribute__类型、文档、dir(s)、属性获取;可用 type、getattr 等常规入口
__len__、__getitem__、__iter__、__contains__len、下标/切片、迭代、成员检测
__repr__、__str__repr(s) 展示引号/转义,str(s) 取得字符串表示
__add__、__mul__、__rmul__字符串拼接、重复及反射乘法
__eq__、__ne__、__lt__、__le__、__gt__、__ge__相等与按码点进行的词典序比较;不是自然语言排序
__format__、__mod__单个字符串值的格式说明与 % 格式化;另见上文模板 format、format_map
__hash__可哈希字符串用于键或集合;哈希值不应当作跨进程稳定指纹
__new__创建字符串实例的类型入口,普通代码用 str(...)
__reduce_ex__序列化协议使用的重建信息;普通持久化应选择合适格式
_sizeof__(原拼写)、__sizeof__正确方法是双下划线版本;sys.getsizeof 的数值依实现,只描述对象浅层开销
import pickle
import sys
s = "Hello World"
assert s.__class__ is str and isinstance(s.__doc__, str)
assert "lower" in s.__dir__()
assert getattr(s, "lower")() == "hello world"
assert s.__getattribute__("lower")() == "hello world"
assert len(s) == 11 and list(iter(s)) == list(s)
assert repr("a\nb") == "'a\\nb'"
assert str(s) == s and str.__new__(str, "abc") == "abc"
assert "A" < "a" and "10" < "2"
assert "abc" <= "abc" and "abc" >= "abc"
assert "b" > "a" and "b" != "a"
assert hash(s) == hash(" ".join(["Hello", "World"]))
assert isinstance(s.__sizeof__(), int) and sys.getsizeof(s) > 0
assert not hasattr(s, "_sizeof__")
assert isinstance(s.__reduce_ex__(4), tuple)
assert pickle.loads(pickle.dumps(s, protocol=4)) == s
print("string protocol checks passed")

序列化示例只处理本程序生成的数据;不能把从外部拿到的 pickle 当成普通文本载入。需要交换文本时,应优先考虑明确编码的文本文件或 JSON。字符串与其他对象的通用协议见特殊方法。方法规则参见 Python 字符串方法、% 格式化转换和 Unicode 文本说明。