AI 技术博客
返回首页
Python 基础 22 分钟阅读

字符串方法大全:检索、判断与变换

Python字符串方法字符串文本处理

引言

第五章「字符串与正则」共十篇,本篇开篇,也是全章的地基:把 str 类型自带的方法按用途系统过一遍。前置知识你已具备——第二章讲过字符串字面量、转义与 f-string 的写法,第四章讲清楚了函数与参数;本篇则回答「拿到一个字符串之后,能对它做什么」。正则表达式与格式化深度分别留给本批第四至六篇与第七篇,这里不越界,只用一个个现成方法把检索、判断、变换、大小写、填充五类操作全部跑通。

为什么要专门用一篇讲方法?因为在真实工程里,字符串的处理九成是「组合调用现成方法」而不是自己写循环:从日志行里找关键字、校验用户输入是不是数字、清洗数据两端的空白、把列表拼成 CSV 行——每一件都是方法调用。方法背得越熟,写出的代码越短、越不容易出错,也越接近资深工程师的写法。读完本篇,你应该能不看文档说出 find 与 index 的返回值差异、strip 三件套各自的边界、split 的 maxsplit 到底截在哪里。

概念与原理

方法是什么

方法(method)本质是挂在对象上的函数:s.upper() 中的 upper 是 str 类的方法,调用时自动把 s 作为第一个参数传入,等价写法是 str.upper(s),动作完全一样。用 dir(str) 可以在 REPL 里列出 str 的全部方法(八十来个),本篇从中挑出工程上最高频的二十多个逐一演示;其余(encode、isprintable 等)会在后续篇目遇到时再展开——你不需要一次背完,先记住「有哪几类」即可。

不可变性决定返回值语义

第二章建立的底线「str 是不可变对象」必须贯穿本篇:任何变换类方法都不会修改原字符串,而是返回一个新字符串。line.strip() 单独一行执行后,结果会立刻被丢弃,原字符串纹丝不动——所以惯用法是 line = line.strip(),把返回值重新绑定回名字。这是新程序员最容易犯的第一类错误,易错点部分还会回到这个主题。

链式调用:一行清水线

方法返回新字符串,调用因此可以像流水线一样串联:s.strip().lower().startswith("user") 依次执行三次变换,中间结果稍纵即逝、没有名字。它的代价有两条:每过一个环节就产生一个新字符串(长文本的高频场景要留意内存占用),以及某一段结果与预期不符时很难一眼定位是哪一环出了问题。工程经验是:链条短(两三个环节)且语义直白时直接串联;环节多或需要调试时,拆成带名字的中间变量,每行一步,随时可以打印检查。

两种失败哲学:哨兵值与异常

检索类方法的差异最能体现 Python 的错误处理设计。find 找不到时返回 -1(哨兵值,sentinel value),程序继续跑;index 找不到时直接抛 ValueError。前者适合「找不找得到无所谓」的宽松检查,后者适合「找不到就是出错」的严格场景。一个方法族里同时提供宽松与严格两个版本,是标准库的常见设计——下面的检索代码里就有 "abc".index("x") 的报错现场(注释掉的第 9 行),而列表的 remove 也是抛 ValueError 的严格派,你会在第六章与它重逢。

操作与实现

检索:find / rfind / index / count

find(sub, start, end) 返回子串首次出现的下标,可限定检索区间;rfind 从右往左找返回最靠右的下标;count 统计出现次数,注意它按「不重叠」计数。

msg = "Python 编程入门教程"
print(msg.find("Python"))       # 0
print(msg.find("编程"))          # 7:子串起始下标
print(msg.find("Java"))         # -1:未找到返回哨兵值 -1
print(msg.index("编程"))         # 7:与 find 一致
# print(msg.index("Java"))      # ValueError: substring not found
print(msg.rfind("程"))           # 12:从右往左,返回最靠右的下标
print(msg.count("程"))           # 2:统计出现次数
print(msg.find("程", 11))        # 12:限定检索起点
print(msg.find("程", 13))        # -1:起点之后不再出现
print("banana".count("ana"))     # 1:重叠出现也只计一次
print("aaaa".count("aa"))        # 2
print("banana".count("a", 2, 5)) # 1:区间 2 到 4 内只有 1 个 a
0
7
-1
7
12
2
12
-1
1
2
1

msg.find("程", 11) 返回 12,直观说明 start 参数是「从哪个下标开始找」;banana 里 “ana” 只出现一次的原因是它出现在 1 号位,下一个重叠的 “ana” 被 count 跳过——与正则的 findall 不同(那是第四至六篇的事),方法版的 count 永远不重叠。

判断:startswith / endswith 与 is* 家族

前缀后缀判断支持传入元组做「多选一」,这在 URL 白名单、文件类型过滤里非常常用;isalpha、isdigit、isalnum、isspace 等是「全体字符是否满足条件」的全称判断——只要有一个字符不满足就返回 False。

url = "https://example.com/index.html"
print(url.startswith("https://"))          # True
print(url.startswith(("http://", "https://")))  # True
print(url.endswith(".html"))               # True
print(url.endswith((".html", ".htm")))     # True
print("中文".isalpha())                     # True:汉字也属于字母类
print("a b".isalpha())                     # False:空格不是字母
print("202691".isdigit())                  # True
print("中文123".isalnum())                  # True
print("user_123".isalnum())                # False:下划线非字母数字
print(" ".isspace(), "\t".isspace())       # True True
print("HELLO".isupper(), "hello".islower())  # True True
True
True
True
True
True
False
True
True
False
True True
True True

注意 "中文".isalpha() 为 True:isalpha 依据的是 Unicode 字符类别——汉字的类别是字母类(Ll/Lu/Lm/Lo 之一),所以它不是「只认英语」的 ASCII 判断。需要纯 ASCII 判断时用 isascii(),第 3 篇(编码篇)会实际演示。空字符串调用任何 is* 方法都返回 False("".isdigit() 为 False),这是全称判断的边界语义,别写出 if s.isdigit() and s: 这种冗余条件——s.isdigit() 已经把空串排除了。

变换:strip 家族与 removeprefix

strip 是清洗数据的第一利器:不传参数时去除两端的空白字符(空格、制表符、换行等);传入字符串参数时,把它当作字符集合,从两端逐个剥掉属于该集合的字符,直到遇到不属于它的字符为止。下面用 repr 打印,便于看清引号与空白。

raw = "  \t 待清洗  \n"
print(repr(raw.strip()))        # '待清洗':两端空白全去掉
print(repr(raw.lstrip()))       # '待清洗  \n':只去左端
print(repr(raw.rstrip()))       # '  \t 待清洗':只去右端
token = "aaabXXbaaa"
print(repr(token.strip("ab")))        # 'XX':把两端属于集合 {a, b} 的字符都剥掉
print(repr(token.lstrip("ab")))       # 'XXbaaa'
print(repr(token.rstrip("ab")))       # 'aaabXX'
print(repr("report_v3.csv".removeprefix("report_")))  # 'v3.csv'
print(repr("report_v3.csv".removeprefix("version")))  # 'report_v3.csv':不匹配则原样返回
print(repr("report_v3.csv".removesuffix(".csv")))     # 'report_v3'
'待清洗'
'待清洗  \n'
'  \t 待清洗'
'XX'
'XXbaaa'
'aaabXX'
'v3.csv'
'report_v3.csv'
'report_v3'

removeprefix / removesuffix 是 Python 3.9 加入的:它们是「精确子串」语义,只去掉一次前缀或后缀,与 strip 的「字符集合」语义完全不同——这是易错点二,务必分清。

切分与拼接:split / rsplit / splitlines / join

split 有两个行为模式,初学者常混:不传参数时按「任意空白」切分,且自动合并连续空白、丢弃空串;传入分隔符时严格按它切分,连续分隔符会产生空字符串。maxsplit 参数表示最多切几刀,剩余部分整体收进最后一个元素。

record = "2026,09,01,发布"
print(record.split(","))                 # ['2026', '09', '01', '发布']
print(record.split(",", 2))              # ['2026', '09', '01,发布']
print(record.rsplit(",", 1))             # ['2026,09,01', '发布']:从右切一刀
print("  a  b   c  ".split())            # ['a', 'b', 'c']:按空白,合并连续空白
print("a,,b".split(","))                 # ['a', '', 'b']:传参时空串保留
print("line1\nline2\nline3".splitlines())  # ['line1', 'line2', 'line3']
print("a=b=c".partition("="))            # ('a', '=', 'b=c')
print("a=b=c".rpartition("="))           # ('a=b', '=', 'c')
print("abc".partition("="))              # ('abc', '', '')
print("-".join(["2026", "09", "01"]))    # 2026-09-01
print("".join(["Py", "thon"]))           # Python
['2026', '09', '01', '发布']
['2026', '09', '01,发布']
['2026,09,01', '发布']
['a', 'b', 'c']
['a', '', 'b']
['line1', 'line2', 'line3']
('a', '=', 'b=c')
('a=b', '=', 'c')
('abc', '', '')
2026-09-01
Python

partition(以及从右开始的 rpartition)是另一路切分法器:只切第一处(或最后一处),返回三元组(头、分隔符、尾)。它天然适合「键=值」「地址:端口」这类分隔符唯一的场景,结果可以直接解包成三个变量;而且分隔符一定在结果里出现——"abc".partition("=") 的中间元素是空串,正好用来判断「分隔符到底存不存在」,比 find 的 -1 更直观。

join 是 split 的逆操作,常用它把列表拼成 CSV 行或 SQL 的 IN 子句参数。注意 join 只接受「字符串的可迭代对象」——",".join([1, 2]) 会抛 TypeError(sequence item 0: expected str instance, int found),数字要先 str() 转换。第二章推荐过 "-".join([...]) 而不是循环累加拼接,理由(一次分配 vs 反复产生中间对象)到这里可以兑现了。

大小写:upper / lower / casefold / title / capitalize / swapcase

upper 与 lower 是最基础的;casefold(Python 3.3 引入)比 lower 更激进,专门为「忽略大小写的比较」设计;capitalize 只大写首字符并小写其余,title 把每个单词的首字母大写。它们全部返回新字符串。

print("hello world".upper())        # HELLO WORLD
print("ÄBC".lower())                # äbc
print("hello world".capitalize())   # Hello world:仅首字符大写
print("hello world".title())        # Hello World:每个单词首字母大写
print("HeLLo".swapcase())           # hEllO
print("Straße".upper())             # STRASSE:ß 大写后展开成 SS
print("Straße".lower())             # straße:lower 不会把 ß 变 ss
print("Straße".casefold())          # strasse:casefold 为忽略大小写比较而生
print("don't".title())              # Don'T:撇号后的 t 也被大写(常非所愿)
print("Hello World".istitle())      # True
HELLO WORLD
äbc
Hello world
Hello World
hEllO
STRASSE
straße
strasse
Don'T
True

"Straße".upper() 是 SS、casefold() 得到 strasse,这些边界都来自 Unicode 的大小写映射表:single code point 与多码点的映射同时存在。用 casefold 而不是 lower 做「忽略大小写」的比较,正是为了处理这类字符。

其他高频变换:replace 与填充对齐

print("a-b-c-d".replace("-", "/"))      # a/b/c/d
print("aaa".replace("a", "b", 2))       # bba:最多替换 2 次
print("2026/09/01".replace("/", "-", 1))  # 2026-09/01:从最左侧开始替换
print("2026".ljust(8, "·"))             # 2026····
print("2026".rjust(8, "·"))             # ····2026
print("2026".center(8, "·"))            # ··2026··
print("42".zfill(6))                    # 000042:数字左补零
print("-42".zfill(6))                   # -00042:负号留在最前
print("3.14".zfill(6))                  # 003.14
a/b/c/d
bba
2026-09/01
2026····
····2026
··2026··
000042
-00042
003.14

replace 不传次数时替换所有出现;传第三参数则限制次数。zfill 用于数字对齐特别方便:负号不被零覆盖,小数点在补零后仍居左;配合 ljust/rjust/center 可以做出对齐的报表列——第 7 篇讲完整格式化语法时,你会看到更强大的 spec 语法,但这一组方法在简单场景已经够用。

工程合成:清理日志行

把本篇的方法组合起来,处理一行真实的、带多余空白的日志:

raw = "  2026-09-01 10:30:05  INFO  line=42  msg=服务启动完成  "
parts = raw.split()                     # 按任意空白拆分,自动合并连续空白
print(len(parts))                       # 5
print(parts[0], parts[1], parts[2])     # 2026-09-01 10:30:05 INFO
print(parts[3])                         # line=42
print(parts[3].split("=", 1))           # ['line', '42']
print(parts[4].removeprefix("msg="))    # 服务启动完成
print(",".join(parts[:3]))              # 2026-09-01,10:30:05,INFO
5
2026-09-01 10:30:05 INFO
line=42
['line', '42']
服务启动完成
2026-09-01,10:30:05,INFO

如果当初用 split(" ") 按单空格切,连续空格会产生空字符串元素,整条日志就散架了——这正是「不传参数按空白切」的设计价值。第七章的文件读写与第八章的日志实战会继续放大这个场景。

易错点与陷阱

陷阱一:把 find 的返回值当布尔值用

s.find("x") 找不到返回 -1,而 -1 是真值;找到且位置在 0 号位时返回 0,0 是假值——所以 if s.find("x"): 在「x 就在开头」时误判为没找到,行为完全反向。正解:用 "x" in s 判断存在,用 if s.find("x") != -1 判断位置;需要下标且接受「找不到即出错」时,直接用 index。

陷阱二:把 strip(“ab”) 当成「去掉子串 ab」

"aaabXXbaaa".strip("ab") 的结果是 “XX”——它按字符集合从两端剥,而不是删掉每一处出现的 “ab”(“XX” 两侧之外的 “ab” 子串一个都没少删的语义)。要精确去掉前缀/后缀子串,用 3.9+ 的 removeprefix / removesuffix;要做全文替换,用 replace。三者语义不同,场景不同,别混用。

陷阱三:split 传不传参数是两个世界

" a b c ".split() 是 ['a', 'b', 'c'](合并连续空白),而 "a,,b".split(",") 是 ['a', '', 'b'](空串保留)。写解析器时先问自己:输入的分隔符是否可能连续出现?连续出现时你想保留空字段还是跳过?答案决定用哪种调用。此外 maxsplit 是「最多切几刀」,切完剩余部分整体归入末元素——"a,b,c".split(",", 1) 是 ['a', 'b,c'],不是 ['a', 'b']。

陷阱四:isdigit 不等于「十进制数字」

"²".isdigit() 是 True(上标二是数字类字符),但 "²".isdecimal() 是 False;"三".isnumeric() 是 True 而 "三".isdigit() 是 False;阿拉伯-印度数字 "١٢٣".isdigit() 也是 True。三个方法严格程度不同:isdecimal 最严(只认十进制数字),isdigit 次之,isnumeric 最宽。校验手机号、金额、学号时,先确认业务允许哪些写法,再选对方法——本机实测的结果是:"²" 三个方法分别为 False/True/True,"三" 为 False/False/True。

陷阱五:title 的撇号与缩写坑

"don't".title() 得到 "Don'T"——撇号后的 t 也被大写了。自然语言里撇号后面往往是缩写尾巴,title 的「每个空白分隔的词的每个首字母」规则不区分这些。清洗英文标题时多用 capitalize + 白名单,而不是盲目 title。

陷阱六:find 返回 -1 与切片联手「倒着截」

s[s.find("x"):] 的本意是「从关键字截到末尾」,但 find 找不到时返回 -1,切片 s[-1:] 会安静地取出最后一个字符——程序不报错,结果却完全错误。处理「找不到」分支要先于切片:idx = s.find("x"); if idx != -1: ...,或者改用 partition 并检查中间元素是否为空串("abc".partition("=") 的中间元素是空串,一眼可见分隔符不存在)。

小结

本篇把 str 方法按五类过了一遍:检索(find/rindex/index/count,注意哨兵值 -1 与 ValueError 两种失败哲学)、判断(startswith/endswith/is* 家族,isalpha 认汉字、isdigit 与 isnumeric 严格度不同)、变换(strip 家族按字符集合剥离,removeprefix/removesuffix 按子串剥离;split 的默认空白合并与 maxsplit 截断;join 一次拼装)、大小写(casefold 才是忽略大小写比较的正解、title 有撇号坑)、填充(zfill 负数不覆盖负号)。所有方法基于不可变性返回新字符串,必须重新绑定。

练习与思考题

  1. 写一个函数:给定 " Host: 192.168.1.10:8080 ",用本篇方法清洗出元组 ("192.168.1.10", "8080"),要求只用 strip/split/removeprefix 组合,不引入正则。(答案导向:先整体 strip 两端空白,再 split(”:”, 1) 或按空格拆分后处理;f-string 与筛选留到格式化篇。)
  2. 用 REPL 验证:"²".isdigit()、"三".isnumeric()、"١٢٣".isdigit() 各自的返回值,并解释它们与 "123".isdecimal() 的差别。
  3. 思考题:为什么 find 选择返回 -1 而不是抛异常?请举一个「宽松检查」场景和一个「必须用 index」的场景。
  4. 动手题:对 "abxxab" 分别执行 strip("ab")、removeprefix("ab")、replace("ab", ""),比较三种结果,解释各自动作。