Skip to content
This page has been auto-translated and may contain errors.View in English

字符串

文本几乎出现在你编写的每一个程序中。名称、消息、分数、标签。在 Python 中,任何一段文本都被称为字符串:任何用引号包裹的值。单引号或双引号都可以,效果完全相同。

字符串是 Python 主要的文本类型。它承载着从用户名到 URL 路径再到格式化输出的一切。单引号和双引号产生完全相同的结果;选择哪种纯属风格问题。

str 是 Python 的不可变 Unicode 序列类型。它位于所有系统边界:终端 I/O、文件内容、网络响应、序列化数据。两种引号风格生成相同的对象;词法分析器对它们一视同仁。

python
greeting = "Hello, world"
username = 'alice'

选择引号的唯一关键时刻是当你的文本中包含引号时。使用相反的引号风格,这样就不必转义它们:

社区惯例是使用双引号。切换风格的实际原因是当内容包含该字符时避免转义:

惯例是使用双引号。切换的唯一原因是当内容包含该定界符时避免反斜杠转义:

python
note    = "It's a great day"      # 内部有撇号,使用双引号
message = 'She said "hello"'      # 内部有双引号,使用单引号
escaped = "She said \"hello\""    # 或用反斜杠转义

不可变性

字符串是不可变的:一旦创建,就无法修改。可以把字符串想象成创建那一刻就永久固定下来了。任何看起来在修改字符串的操作实际上都在产生一个全新的字符串。原始的那个保持原样不变。

字符串是不可变的:没有任何方法会就地修改字符串。每个转换文本的操作都会返回一个新字符串,原始字符串保持不变。其实际后果是:如果一个方法调用你没有赋值给任何地方,它对任何事物都不会产生影响。

str 对象是不可变的:内部缓冲区在构造时固定,无法被写入。这赋予字符串三个有用的属性:它们可哈希(可作为字典键和集合成员),可在引用间安全共享而无需复制,并且符合 CPython 对短字面量的驻留优化条件。

python
name = "alice"
name = name.upper()   # "ALICE" 是一个新字符串;"alice" 保持不变

直接的后果:你无法修改特定位置的字符。如果你尝试,Python 会抛出错误。

python
name = "alice"
name[0] = "A"   # TypeError: 'str' object does not support item assignment

要获得修改后的字符串,需要使用切片或方法构建一个新字符串。两者下面都会介绍。

尝试字符赋值直接显示了这个约束:

python
name = "alice"
name[0] = "A"   # TypeError: 'str' object does not support item assignment

当你需要修改后的版本时,标准工具是:对位置编辑使用切片加拼接,对替换使用 replace()。两者都会生成新字符串,原始字符串保持不变。

str.__setitem__ 未实现;项赋值无条件抛出 TypeError。对于位置修改,使用切片:name[:1].upper() + name[1:]。对于替换,使用 replace()。对于组装大量片段,"".join(parts) 很重要:循环中重复使用 s += chunk 的复杂度为 O(n²),因为每次 + 都会分配一个合并长度的新缓冲区,并将两个操作数复制进去。join() 只分配一次。

索引与切片

字符串中的每个字符都有一个编号位置,从零开始。你可以通过在方括号中放入该位置编号来读取单个字符。负数从末尾向前数。

字符串是采用从零开始索引的序列。负索引从末尾计数。切片可以在一个表达式中提取任意连续范围,并且在超出范围的值上不会抛出错误。

str 实现了完整的序列协议。下标访问(s[i])通过 __getitem__ 处理整数输入,超出范围时抛出 IndexError。切片(s[start:stop:step])传递一个 slice 对象;索引会被静默地限制在有效范围内,因此切片不可能产生 IndexError

python
word = "Python"
#       012345

print(word[0])    # "P"
print(word[2])    # "t"
print(word[5])    # "n"
print(word[-1])   # "n"  (最后一个字符)
print(word[-2])   # "o"  (倒数第二个)

-1 始终是最后一个字符,-2 是倒数第二个,以此类推。当你想要获取字符串末尾而不知道其确切长度时,它们很有用。

负索引会进行环绕:-1len(s) - 1,-2len(s) - 2。当你不想手动计算长度时,它最适合用于末端定位访问。超出范围的负索引同样会抛出 IndexError,与正索引一样。

负索引在边界检查之前被规范化为 len(s) + i。解释器中没有特殊处理;只是算术运算。无论正负,超出范围都会抛出 IndexError

切片提取一个片段。[start:stop] 包含 start,不包含 stop:

python
word = "Python"

print(word[0:2])   # "Py"     (位置 0 和 1)
print(word[2:])    # "thon"   (位置 2 到末尾)
print(word[:3])    # "Pyt"    (开头到位置 2)
print(word[:])     # "Python" (整个字符串的副本)
print(word[::2])   # "Pto"    (每隔一个字符)
print(word[::-1])  # "nohtyP" (反转)

最常用的三种模式:word[:n] 获取前 n 个字符,word[n:] 获取从位置 n 开始的所有内容,word[-n:] 获取最后 n 个字符。word[::-1] 反转字符串。第一次看起来很奇怪,但它是 Python 的惯用法,你会经常见到。

与直接索引不同,切片永远不会抛出 IndexError。Python 会静默地限制超出范围的索引,所以在短字符串上 word[100:] 会返回 "" 而不是崩溃。步长参数控制跨度:word[::2] 取每隔一个字符,word[::-1] 反向遍历。

s[start:stop:step]slice(start, stop, step) 传递给 __getitem__。三个参数默认都是 None,而非 0len()。当步长为负时,默认值会反转:start 默认为 len - 1,stop 默认为 -(len + 1)。正是这一点使得 [::-1] 无需任何显式边界就能反向遍历整个字符串。

基本字符串方法

字符串自带一套内置方法:你可以直接对任何字符串值调用的操作。先写字符串(或保存字符串的变量),然后是点,然后是方法名。每个方法都返回一个新字符串。原始字符串永远不会改变。

字符串方法是附加到 str 类型上的函数。因为字符串是不可变的,每个方法都返回一个新字符串而非修改原始字符串。一个你没有赋值或传递到某处的方法调用不会有持久的影响。

str 的方法定义在类型对象上,以 C 实现。所有转换方法都遵循不可变性契约:它们返回新的 str 对象。CPython 的实现全程支持 Unicode;方法在代码点上操作,而非字节。

大小写

python
text = "Hello, World"

text.lower()       # "hello, world"
text.upper()       # "HELLO, WORLD"
text.title()       # "Hello, World"  (每个单词首字母大写)
text.capitalize()  # "Hello, world"  (仅首词)

lower()upper() 是你最常用的两个。lower() 在比较文本时特别有用:一旦你在两边都调用 .lower(),"Alice""alice" 就变成相同的东西了。

lower() 是比较或存储前的标准规范化步骤。title() 使用一个简单规则将每个单词的首字母大写,但在缩写上会失误:"it's" 会变成 "It'S"。把它当作仅用于显示的格式化处理。

lower() 应用 Unicode 完整大小写转换。对于不区分大小写的比较,casefold() 更正确:它应用了 lower() 跳过的额外转换(例如德语 ß 变成 ss)。title() 在任何非字母数字字符之后大写,这会错误处理缩写和带连字符的名字。要正确实现标题大小写,需要手动实现逻辑。

空白字符

python
text = "  hello  "

text.strip()    # "hello"    (两侧)
text.lstrip()   # "hello  "  (仅左侧)
text.rstrip()   # "  hello"  (仅右侧)

strip() 移除字符串两端的空格。每当处理用户输入或来自文件的文本时,你几乎都会用到它,因为多余的空格会导致悄无声息的失败:"alice" != "alice "

strip() 移除所有前导和尾随的空白字符:空格、制表符和换行符。方向变体让你可以只清理一侧,适合在不影响缩进的情况下剥离尾部换行符。这三个方法都接受一个可选的字符参数,用于剥离特定字符。

不带参数的 strip() 会移除 str.isspace() 返回 True 的字符,这是一个支持 Unicode 的集合,包括非 ASCII 空白字符。带字符参数时,它会从两端移除该集合中的任何字符(是字符成员关系检查,而非前缀匹配)。"xxhelloxx".strip("x") 返回 "hello"。多字符参数会单独剥离这些字符中的任何一个,这是细微 bug 的常见来源。

查找

python
text = "Hello, world"

text.find("world")         # 7
text.find("Python")        # -1  (未找到)
text.count("l")            # 3
text.startswith("Hello")   # True
text.endswith("world")     # True

find() 返回一段文本在你字符串中开始的位置。如果不存在,返回 -1。当你只关心字符串是否以特定内容开头或结尾时,使用 startswith()endswith()

find() 返回首次匹配的起始索引,或 -1-1 这一约定使你可以直接在切片或算术运算中使用结果而无需检查。startswith()endswith() 都接受字符串元组,使得在一次调用中测试多个前缀或后缀变得简单。

find() 是从左到右的线性扫描,最坏情况下为 O(n*m)。index() 完全相同,但在未匹配时抛出 ValueError:当缺失是编程错误时使用 index(),当预期可能缺失时使用 find()startswith()endswith() 在第一次不匹配时短路退出,比 find()in 检查更快地完成前缀/后缀测试。

替换

python
text = "Hello, world"

text.replace("world", "Python")   # "Hello, Python"
text.replace("l", "L")            # "HeLLo, worLd"  (所有出现)
text.replace("l", "L", 1)         # "HeLlo, world"  (仅第一次)

replace() 将一段文本的每次出现替换为另一段文本,并返回一个新字符串。原始字符串不会改变。如果只想替换第一次出现,传入第三个参数。

replace() 默认替换所有不重叠的出现。count 参数限制替换的次数。由于它返回新字符串,调用可以链式进行:text.replace("a", "A").replace("e", "E") 依次应用两个替换。

replace() 执行字面子串扫描,当未指定 count 时会在一次分配中构建结果;指定 count 时会提早停止。对于基于模式的替换,Python 的 re 模块是合适的工具。这将在"模块"章节中介绍。

拆分与连接

split() 在分隔符处将字符串切分成多个片段,并以列表形式返回。你告诉它在什么位置切分:

split() 在分隔符处分割,并以列表形式返回各段。不带参数调用时,它在任何空白字符串(任意连续空白)处分割,并丢弃由多个连续空格产生的空字符串:

split(sep) 从左到右扫描,在每次不重叠的 sep 出现处分割。不带参数时,它使用不同的算法:在任何连续的空白处分割,并从结果中剥离前导和尾随的空白。rsplit(sep, n) 从右侧分割,适用于分离点分路径或带命名空间的标识符的最后一段:

python
csv_row = "小明,28,北京"
parts = csv_row.split(",")     # ["小明", "28", "北京"]

"  hello   world  ".split()   # ["hello", "world"]

列表是什么?

列表是一个有序的值集合。上面的 ["小明", "28", "北京"] 就是一个。列表有自己的一章;现在,把它们看作是 split() 产出、join() 消费的项的序列即可。

join() 则相反:它将字符串列表合并成一个。.join() 前的字符串放在每项之间:

python
words = ["Hello", "world"]

" ".join(words)    # "Hello world"
", ".join(words)   # "Hello, world"
"".join(words)     # "Helloworld"

要记住的模式:separator.join(list_of_strings)。分隔符在左,列表在右。" ".join(words) 在每个单词之间放一个空格。"".join(words) 用空字符串粘合它们。

当你从多个片段组装单个字符串时,join() 是合适的工具。它执行单次分配,而非每步创建新字符串。对于两三个字符串,+ 完全没问题。一旦你有一个有相当大小的列表,就使用 join()

join() 是 O(n):它调用 __iter__ 一次,在一次遍历中计算所需总长度,执行单次分配,然后直接将每个片段和分隔符写入缓冲区。重复使用 + 是 O(n²):每次操作都分配一个合并长度的新缓冲区并复制两个操作数。CPython 对单个局部变量上的重复 += 有有限的优化,但它在重构间很脆弱,且不被保证。join() 始终正确且始终快速。

f-字符串

f-字符串将值直接嵌入文本中。在开始引号前放置 f,然后将任何变量或表达式包裹在花括号中。Python 在代码运行时填入它。你还可以在值后面加一个冒号来控制它的显示方式。

f-字符串在运行时计算 {} 中的任何表达式,并将结果转换为字符串。花括号内的冒号引入格式规范:一种用于控制小数位数、对齐方式和数字格式的紧凑语法。

f-字符串(PEP 498)将每个 {} 表达式编译为调用 format(value, spec) 的字节码,后者委托给 value.__format__(spec)。实现了 __format__ 的任何类都可以控制其在 f-字符串内的显示。转换标志 !r!s!a 在格式调用前应用 repr()str()ascii()

python
name  = "小明"
score = 94.5

print(f"Hello, {name}!")           # "Hello, 小明!"
print(f"Score: {score:.1f}%")      # "Score: 94.5%"
print(f"2 + 2 = {2 + 2}")          # "2 + 2 = 4"
print(f"Name: {name.upper()}")     # "Name: 小明"

: 之后的格式规范控制值的显示方式:

规范含义示例
.2f2 位小数f"{3.14159:.2f}""3.14"
.0%百分比,无小数f"{0.94:.0%}""94%"
,千位分隔符f"{1000000:,}""1,000,000"
>10在 10 个字符宽度内右对齐f"{'hi':>10}"" hi"

你最常用的是 .2f:每当显示小数并希望得到一个整洁的数字而不是一长串数字时使用。表中的其他内容在需要时再用。你可以在 {} 中放入任何变量、算术运算或方法调用。

.2f.0% 涵盖了大多数显示格式化。对齐符(><^)与宽度组合可以生成表格输出。一般模式是 {value:[align][width][.precision][type]}。一旦你认识各个部分,任何规范都不需要记忆所有组合就能读懂。

规范被逐字传递给 __format__;内置类型用 C 处理。!r 是最有用的转换标志:它在格式化之前调用 repr(),这会给字符串加上引号,并将不可见字符(制表符、尾随空格、换行符)显示为转义序列。自定义类可以实现 __format__ 来接受任意规范字符串并产生任何输出。

多行字符串

要编写跨越多行的字符串,使用三引号:开头三个 ",结尾三个 "。Python 会完全按照你写的样子保留所有换行符和间距。

三引号字符串按字面保留所有空白和换行符。它们是长文本块(如电子邮件模板和 SQL 查询)以及文档字符串的标准:即放在函数或类体开头的内联文档。

三引号字面量逐字保留所有字符,包括每行的前导空白。当作为函数、类或模块体中的第一个语句使用时,Python 将该字符串存储为该对象的 __doc__ 属性。help() 等工具会显示它;前导空白通常由 textwrap.dedent() 剥离。三 '''""" 是等价的;""" 是惯例。

python
message = """
亲爱的小明,

感谢您的订单。

此致
团队
"""

转义序列

有些字符在字符串内很难直接输入。Python 使用转义序列:一个反斜杠后跟一个表示某种含义的字母。你会经常用到的两个是 \n 表示换行和 \t 表示制表符。

转义序列让你嵌入那些会破坏语法或无法直接输入的字符。你会用到的有:\n(换行符)、\t(制表符)、\\(字面反斜杠)、\"\'(与定界符相同的引号内嵌于字符串中)。Windows 路径需要反斜杠,这与转义处理冲突。在前面加 r 可禁用转义。

Python 支持 C 风格的转义集以及 Unicode 转义:\uXXXX(16 位代码点)、\UXXXXXXXX(32 位)、\xNN(十六进制字节值)、\N{name}(命名 Unicode 字符)。原始字符串字面量(r"...")抑制所有转义处理,将每个反斜杠原样传递给字符串。这对 Windows 路径和正则表达式至关重要,因为反斜杠的含义是给消费者而非 Python 词法分析器的。

序列字符
\n换行符
\t制表符
\\字面反斜杠
\"双引号
\'单引号
python
print("Line one\nLine two")        # 两行输出
print("Name:\tAlice")              # Name:   Alice
path = r"C:\Users\Alice\Documents" # 原始字符串,无转义处理

检查字符串内容

Python 有一些方法可以回答有关字符串内容的是/否问题。它们返回 TrueFalse。早期最有用的:isdigit() 让你在转换之前检查字符串是否全是数字,这样可以避免在意外输入上崩溃。

is* 系列方法各自测试整个字符串的特定属性,只有当每个字符都满足条件时才返回 True。它们的主要用途是输入验证:在转换前进行检查以避免在意外输入上崩溃。int() 前调用 isdigit() 是经典模式。

is* 方法使用 Unicode 类别检查,而非 ASCII 范围。isdigit() 对上标数字和其他超出 0-9 的数值型 Unicode 代码点返回 True。对于严格的 ASCII 数字检查,组合使用 s.isascii() and s.isdigit()isnumeric() 范围更广,涵盖分数和具有数值的 Unicode 字符。在使用之前要知道你实际需要哪一个。

python
"42".isdigit()       # True
"hello".isalpha()    # True
"hello42".isalnum()  # True
"   ".isspace()      # True
"Hello".islower()    # False
"HELLO".isupper()    # True

实践应用

剥离空白,规范化大小写,然后提取你需要的内容。这个序列几乎可以处理任何用户提供的文本:

python
raw_input = "  [email protected]  "
email     = raw_input.strip().lower()   # "[email protected]"

at_pos   = email.find("@")
username = email[:at_pos]
domain   = email[at_pos + 1:]

print(f"User:   {username}")    # "alice"
print(f"Domain: {domain}")      # "example.com"

从各部分构建 URL,并立即验证和解析它:

python
BASE_URL = "https://api.example.com"
version  = "v2"
resource = "users"
user_id  = 42

url      = f"{BASE_URL}/{version}/{resource}/{user_id}"
# "https://api.example.com/v2/users/42"

protocol = url.split("://")[0]                    # "https"
secured  = url.startswith("https")
domain   = url.split("://")[1].split("/")[0]      # "api.example.com"

print(f"Protocol : {protocol}")
print(f"Secure   : {secured}")
print(f"Domain   : {domain}")

使用 find()、切片和 f-字符串对齐解析一个结构化日志行:

python
log_entry = "[2024-01-15 09:42:11] ERROR: File not found: report.csv"

timestamp = log_entry[1:20]
rest      = log_entry[22:]                # "ERROR: File not found: report.csv"
colon_pos = rest.find(":")
level     = rest[:colon_pos]              # "ERROR"
message   = rest[colon_pos + 2:]          # "File not found: report.csv"

print(f"[{timestamp}] {level:>8}: {message}")
# [2024-01-15 09:42:11]    ERROR: File not found: report.csv

find() 定位边界,切片提取各部分,>8 格式规范将严重性标签右对齐,使得当级别名称长度不同时列保持一致。

方法参考

方法作用
.lower() / .upper()转换为全小写 / 全大写
.title() / .capitalize()每个单词首字母大写 / 仅首词
.strip() / .lstrip() / .rstrip()移除周围空白
.find(sub)首次匹配的索引,或 -1
.count(sub)sub 出现的次数
.startswith(s) / .endswith(s)前缀 / 后缀检查
.replace(old, new)替换出现
.split(sep)分割成列表
sep.join(iterable)将各项连接成字符串
.isdigit() / .isalpha() / .isalnum()字符类型检查