小张那天写了一个日志解析工具,需要从每行日志里提取请求的URL。日志格式大概是这样的:
ini
2024-01-01 10:00:00 INFO request_url=/api/user/123 status=200
他需要找到 request_url= 这个标记,然后截取后面的内容。代码很简单:
ini
line = "2024-01-01 10:00:00 INFO request_url=/api/user/123 status=200"
start = line.index("request_url=") + len("request_url=")
end = line.index(" ", start)
url = line[start:end]
print(url)
跑起来没问题,提取出了 /api/user/123。但上线之后,有些日志行没有 request_url= 字段(比如错误日志),程序直接崩了:
makefile
ValueError: substring not found
小张很郁闷:找不到就返回 -1 不行吗?为什么非要抛异常?他同事看了一眼,说:"你这里用 find 就好了,找不到返回 -1,你自己判断一下。" 小张改成 find 之后,程序不再崩溃了。
"同样都是查找子串,怎么一个抛异常一个返回 -1?"
这个问题的答案,就是 find 和 index 的核心区别。
先看一个最直观的对比
bash
s = "hello world"
print(s.find("world")) # 6
print(s.index("world")) # 6
print(s.find("python")) # -1
print(s.index("python")) # ValueError
当子串存在时,find 和 index 都返回子串第一次出现的起始索引。这一点完全一样。
当子串不存在时,行为完全不同:
find返回-1,表示"没找到"。index抛出ValueError,直接中断程序。
用一个生活场景来理解。你去图书馆找一本书。
find 的做法是:管理员在系统里查一下,如果书在,告诉你书架号;如果不在,告诉你"没有这本书",你还可以继续问别的。
index 的做法是:管理员查一下,如果书在,告诉你书架号;如果不在,直接拍桌子大喊"这本书根本不存在!",把整个图书馆的秩序打乱。
区别一目了然:一个温和地告诉你结果,一个用异常表达"这是不应该发生的事"。
find:找不到是正常情况
find 的签名是 str.find(sub[, start[, end]])。它接受一个子串,以及可选的起始和结束位置。如果在范围内找到子串,返回第一个匹配的起始索引;如果没找到,返回 -1。
bash
s = "hello world"
print(s.find("o")) # 4
print(s.find("o", 5)) # 7
print(s.find("o", 8)) # -1
print(s.find("xyz")) # -1
find 从不抛异常。它把"找不到"当成一种正常结果,用 -1 来表示。
这种设计适合"子串可能存在,也可能不存在"的场景。比如解析日志时,某个字段是可选的;或者搜索关键词时,用户可能输入了不存在的词。
index:找不到是异常情况
index 的签名是 str.index(sub[, start[, end]])。参数和 find 完全一样,返回值也一样------找到时返回索引,没找到时抛出 ValueError。
bash
s = "hello world"
print(s.index("o")) # 4
print(s.index("o", 5)) # 7
print(s.index("o", 8)) # ValueError
index 把"找不到"当成一种错误。它认为:你既然调用 index,就说明你确信子串存在。如果不存在,那就是程序逻辑有问题,应该立刻报错。
这种设计适合"子串必须存在"的场景。比如解析固定格式的数据,某个标记一定会有;或者协议解析中,缺少某个字段就是非法数据。
为什么会有两个方法?
这不是 Python 设计失误,而是一种刻意的区分。
在很多编程场景中,"找不到"有两种截然不同的含义:
第一种:找不到是正常的。比如在一个文本里搜索关键词,关键词可能没有。这时候你希望得到一个明确的信号,然后继续处理其他逻辑。
第二种:找不到是异常的。比如你正在解析一个 JSON 字符串,你知道它一定以 { 开头。如果找不到 {,说明数据格式完全错了,这时候应该立刻报错,而不是继续往下跑。
find 服务于第一种场景,index 服务于第二种场景。它们用不同的方式表达"找不到",让你可以根据业务语义选择。
Python 在很多地方都有这种设计。比如字典取值,d[key] 找不到键会抛 KeyError,而 d.get(key) 找不到返回 None。列表删除,remove 找不到抛 ValueError,而 pop 索引越界抛 IndexError。每个方法都有自己的"失败语义"。
坑一:用 index 但不处理异常
小张踩的就是这个坑。
ini
line = "2024-01-01 10:00:00 ERROR something failed"
start = line.index("request_url=") # ValueError
如果日志行没有 request_url=,程序直接崩溃。正确的做法要么用 find,要么用 try/except 包住。
用 find:
sql
start = line.find("request_url=")
if start != -1:
start += len("request_url=")
end = line.find(" ", start)
url = line[start:end]
else:
url = None
用 try/except:
ini
try:
start = line.index("request_url=") + len("request_url=")
end = line.index(" ", start)
url = line[start:end]
except ValueError:
url = None
用 find 更简洁,但需要记住检查 -1。用 index 加异常处理更明确,但代码稍长。
坑二:用 find 但忘了检查 -1
反过来,有些人从 index 改成 find 之后,忘了判断返回值是不是 -1,直接拿去做切片或计算。
ini
start = line.find("request_url=")
url = line[start + 12 : ] # 如果 start 是 -1,start+12 = 11,从索引 11 开始切片
如果没找到,start 是 -1,start + 12 变成 11。你会从字符串第 11 个字符开始截取,得到一个完全错误的结果,而且不会报错。
这种 bug 比 index 抛异常更可怕,因为它静默地产生错误数据,可能很久之后才被发现。
所以用 find 时,永远要检查返回值是否为 -1。
坑三:以为 find 和 index 的返回值总是一样的
在找到的情况下,find 和 index 返回的索引确实一样。但有一种情况例外:你传了 start 和 end 参数。
bash
s = "hello world"
print(s.find("o", 5)) # 7
print(s.index("o", 5)) # 7
print(s.find("o", 8)) # -1
print(s.index("o", 8)) # ValueError
只要子串在指定范围内存在,返回值就一样。如果不存在,一个返回 -1,一个抛异常。
但要注意:find 和 index 的 end 参数是"开区间",不包含 end 位置。
lua
s = "hello"
print(s.find("o", 0, 4)) # -1,因为索引 4 是 'o',但 end=4 不包含 4
print(s.find("o", 0, 5)) # 4
这个细节容易搞混,但 find 和 index 行为一致。
坑四:在循环中反复查找
有些人会在循环里对同一个字符串反复调用 find 或 index,每次从头开始找。如果字符串很长,循环次数很多,效率会很低。
ini
text = "a" * 1000000 + "target" + "b" * 1000000
pos = 0
while True:
pos = text.find("target", pos)
if pos == -1:
break
# 处理
pos += 1
这种写法是对的,因为传了 start 参数,不会每次都从头找。但如果不传 start,每次都从头找,就会变成 O(n²)。
更好的方式是使用 re.finditer 或者一次性找到所有位置。但对于简单的子串查找,传 start 参数已经够用。
其他查找方式:in、startswith、endswith
除了 find 和 index,Python 还提供了其他字符串查找方法。
in 运算符:判断子串是否存在,返回布尔值。
bash
if "world" in s:
print("存在")
in 不关心位置,只关心是否存在。它的底层实现和 find 类似,但不返回索引。
startswith 和 endswith:判断字符串是否以某个子串开头或结尾。
bash
s = "hello world"
print(s.startswith("hello")) # True
print(s.endswith("world")) # True
它们也支持 start 和 end 参数,并且可以传入元组来匹配多个前缀或后缀。
perl
print(s.startswith(("hi", "hello"))) # True
rfind 和 rindex:从右边开始查找,返回最后一个匹配的位置。
bash
s = "hello world"
print(s.rfind("o")) # 7
print(s.rindex("o")) # 7
print(s.rfind("xyz")) # -1
print(s.rindex("xyz")) # ValueError
rfind 和 rindex 的关系,与 find 和 index 完全一样。一个返回 -1,一个抛异常。
性能差异:基本可以忽略
find 和 index 的底层实现几乎一样,都是遍历字符串查找子串。在找到的情况下,性能没有区别。在没找到的情况下,index 需要构造异常对象,稍微慢一点点,但差距可以忽略不计。
真正影响性能的是字符串长度和查找次数。对于长字符串,Python 使用高效的子串搜索算法(如 Two-Way 算法),速度很快。
所以选择 find 还是 index,不应该基于性能,而应该基于语义。
什么时候用 find,什么时候用 index?
判断标准很简单:你认为"找不到"是正常情况还是异常情况?
如果找不到是正常情况(比如搜索可选字段、用户输入的关键词),用 find,然后检查 -1。
如果找不到是异常情况(比如解析固定格式、协议必须有某个标记),用 index,让异常尽早暴露问题。
如果你不确定,或者不想写异常处理,用 find 更安全。但一定要检查 -1。
一个常见的折中:用 in 判断,再用 index
有些人喜欢先判断存在性,再用 index 取位置:
arduino
if "request_url=" in line:
start = line.index("request_url=")
这样虽然避免了一部分异常,但做了两次查找:in 一次,index 一次。效率不如直接用 find 一次查找。
更 Pythonic 的写法:
ini
start = line.find("request_url=")
if start != -1:
...
回到小张的故事
小张最终的代码是这样的:
sql
line = "2024-01-01 10:00:00 INFO request_url=/api/user/123 status=200"
start = line.find("request_url=")
if start != -1:
start += len("request_url=")
end = line.find(" ", start)
if end == -1:
end = len(line)
url = line[start:end]
else:
url = None
他用了 find,并且检查了 -1。即使日志行没有 request_url=,程序也不会崩溃,而是把 url 设为 None,后续逻辑再决定怎么处理。
他后来在团队文档里写了一条:"find 找不到返回 -1,index 找不到抛 ValueError。找不到是正常情况就用 find,记得检查 -1;找不到是异常情况就用 index,让程序早点报错。别用 index 去处理可选字段。"
这个区分看起来简单,但真到写代码的时候,很多人会随手写 index,然后在生产环境被 ValueError 炸。或者用了 find 却忘了检查 -1,结果得到一堆错误数据。
记住那个图书馆的比喻:find 是礼貌地问管理员,没有就告诉你没有;index 是命令管理员,没有就掀桌子。你要礼貌还是命令,取决于你面对的场景。
字符串查找本身不难,难的是想清楚"找不到的时候该怎么办"。想清楚了,find 和 index 就不再是坑,而是两个各司其职的工具。