在 Python 里,字符串的 find 和 index 方法都能定位子串,但它们默认只返回第一次出现的索引。如果目标字符在一段文本中反复出现,比如统计某个字母在长字符串中的所有位置,就需要自己编写逻辑来收集全部下标。这个问题在文本解析、日志检索、序列标注等场景中很常见。下面会从最直接的循环查找开始,逐步介绍几种不同实现,并对比它们的适用条件。

利用 str.find 的起始位置参数循环收集索引
str.find(sub, start, end) 允许指定搜索起点,找到后返回下标,找不到返回 -1。要获取全部索引,可以维护一个游标 start,每次从上一个命中位置的下一位开始继续查找,直到返回 -1 为止。这种方式的优点是逻辑直观,不需要额外导入模块,而且对单字符和子串都通用。
下面是一个完整函数实现,它接收原始字符串和目标字符,返回一个包含所有索引的列表:
def find_all_indexes(text, target):
indexes = []
start = 0
while True:
pos = text.find(target, start)
if pos == -1:
break
indexes.append(pos)
start = pos + 1
return indexes
# 示例
s = "python and java and rust"
print(find_all_indexes(s, "a"))
注意循环中 start 被设置为 pos + 1 而不是 pos + len(target),这样才能捕获重叠匹配。比如在字符串 aaaa 中查找 aa,如果每次跳过整个匹配长度,只能得到索引 0 和 2;而如果只移动一位,还能得到索引 1。不过对于单个字符,pos + 1 已经足够避免重复。另一个边界是目标字符串为空串,text.find('', start) 会返回 start 本身,导致死循环。因此生产代码应在一开始判断 target 是否为空,或者抛出 ValueError。
这个方法的时间复杂度接近 O(n*m),其中 n 是文本长度,m 是目标长度,因为每次 find 都会从新的起点扫描。在大多数普通文本处理中,这点开销可以接受。但当字符串非常长且命中次数很多时,可以用一次遍历的线性方案替代。
使用 enumerate 一次性遍历并记录所有匹配索引
如果只是查找单个字符,列表推导配合 enumerate 是最简洁的写法。enumerate 会在迭代字符串时同时给出当前字符和它在原字符串中的位置,这样只需要判断字符是否等于目标即可。代码可以压缩到一行:
text = "hello world and hello python" target = "o" indexes = [i for i, ch in enumerate(text) if ch == target] print(indexes) # [4, 7, 14, 19]
这种写法的时间复杂度是 O(n),因为整个字符串只遍历一次。对于单字符查找,它比多次调用 find 更高效,可读性也更高。但如果目标不是单个字符,而是一个子串,就不能直接比较 ch == target,因为 ch 只是一个字符。此时需要改用切片或 startswith 进行判断:
text = "abababa" target = "aba" indexes = [i for i in range(len(text)) if text.startswith(target, i)] print(indexes) # [0, 4]
上面的 startswith 版本会检查每个起点位置,复杂度同样是 O(n*m),但代码依然清晰。需要注意的是 range(len(text)) 会遍历到最后一个字符位置,如果目标子串比剩余部分长,startswith 会直接返回 False,不会报错。这种实现同样支持重叠匹配。例如 target 为 aba 时,abababa 中索引 0 和 4 都是有效起点,索引 2 虽然以 a 开头但不是 aba。
和 while 循环版相比,enumerate 方案更容易阅读,也方便加入额外过滤条件,比如只保留偶数索引、同时统计出现次数等。但当文本规模极大且目标出现次数很少时,逐字符遍历可能不如 find 跳转快,因为 find 在 C 层面可能会利用更底层的搜索算法。实际应用中,除非遇到明显性能瓶颈,否则优先选择这段代码即可。
借助 re.finditer 获取匹配对象的位置信息
正则表达式模块 re 提供了 finditer 函数,它会返回一个包含所有匹配对象的迭代器。每个匹配对象都有 start 和 end 方法,可以拿到匹配在字符串中的起止索引。对于需要忽略大小写、匹配数字或更复杂模式的需求,re.finditer 是比手写循环更灵活的方案。
import re text = "cat, dog, bird, cat, fish" pattern = "cat" indexes = [m.start() for m in re.finditer(pattern, text)] print(indexes) # [0, 15]
如果目标字符串中包含正则元字符,例如点号、星号、括号等,直接拼进 pattern 会被当成特殊语法处理,导致匹配结果不符合预期。正确做法是先调用 re.escape(target) 进行转义,再传给 re.finditer。下面的例子演示了查找字符串 a.c 的情况:
import re text = "a.c and abc and aXc" target = "a.c" indexes = [m.start() for m in re.finditer(re.escape(target), text)] print(indexes) # 只有第一个 a.c 被匹配
re.finditer 的优势在于可以处理比固定字符更复杂的需求,比如忽略大小写可以写成 re.finditer(target, text, re.IGNORECASE),查找所有数字可以用模式 \d+。不过正则默认不会重叠匹配,如果希望找到重叠子串,要使用前瞻断言,但复杂度较高,通常不推荐。对于简单固定字符或子串,re.finditer 的可读性不如前两种方案,而且正则解析有一定性能开销。但当文本解析需求复杂时,把匹配逻辑交给正则引擎可以大幅减少手写判断代码。
几种方法的性能对比与选择建议
为了更直观地比较不同方案,可以构造一个长度较大、目标字符出现频率不同的字符串,然后用 timeit 模块测试运行时间。大致结论是:单字符场景下 enumerate 列表推导最快;子串场景如果子串较短且命中少,while + find 和 re.finditer 性能接近;如果文本极大且需要复杂模式,正则虽然稍慢但表达能力最强。
下面给出一个简单的基准测试示例,主要观察不同实现在同一文本上的耗时差异:
import re
import timeit
text = "abc" * 10000
target = "bc"
def use_find():
indexes = []
start = 0
while True:
pos = text.find(target, start)
if pos == -1:
break
indexes.append(pos)
start = pos + 1
return indexes
def use_regex():
return [m.start() for m in re.finditer(target, text)]
print("find:", timeit.timeit(use_find, number=100))
print("regex:", timeit.timeit(use_regex, number=100))
结果会受到 Python 版本、字符串内容、目标长度和命中频率影响,因此不要只看绝对数值。更值得关注的是代码可维护性。对于绝大多数业务场景,如果目标只是单个字符,直接用 enumerate 方案;如果目标是子串且希望代码清晰,用 while + find;如果需求涉及忽略大小写、通配符或多种模式,则用 re.finditer 配合 re.escape 或正则表达式。
还有一点容易忽略:当目标字符不存在时,三种方案都应返回空列表,不能抛出未捕获异常。str.index 虽然也能找到位置,但找不到时会抛出 ValueError,不适合直接用于收集所有索引。所以本文的示例统一使用 find 而不是 index,这也是实际编码中推荐的做法。