小刘那天写了一个静态文件服务器的路由逻辑,需要根据请求路径判断该交给谁处理。规则很简单:路径以 /api/ 开头的,交给 API 处理器;以 .json 结尾的,返回 JSON 内容;其他的当静态文件处理。
他写了一段代码:
def route(path):
if path.endswith("/api/"):
return handle_api(path)
elif path.startswith(".json"):
return serve_json(path)
else:
return serve_static(path)
跑起来之后,所有请求都走到了 serve_static,API 和 JSON 服务完全失效。他盯着代码看了半天,嘴里念叨:“/api/users 明明是以 /api/ 开头的啊,为什么 endswith 返回 False?”
旁边的同事探过头来,看了一眼说:“你把 startswith 和 endswith 用反了。/api/ 是前缀,应该用 startswith;.json 是后缀,应该用 endswith。”
小刘愣了一下,然后拍了下脑门:“我把这俩搞混了。”
这个错误听起来很蠢,但真到写代码的时候,很多人会因为赶时间、变量名相似、或者纯粹的手滑,把这两个方法用反。更麻烦的是,用反了通常不会报错,程序会静默地走错分支,直到你发现某个功能完全没生效。
先看一个最直观的对比
s = "hello_world.py"
print(s.startswith("hello")) # True
print(s.endswith("hello")) # False
print(s.startswith(".py")) # False
print(s.endswith(".py")) # True
startswith 检查字符串是否以某个子串开头。endswith 检查字符串是否以某个子串结尾。一个看头,一个看尾,位置完全相反。
用一个生活场景来理解。你有一排人站在操场上,从左到右编号。
startswith 是问:“队伍最左边的人是不是叫张三?”
endswith 是问:“队伍最右边的人是不是叫李四?”
一个问题看队头,一个问题看队尾。问反了,答案自然就错了。
startswith:检查开头
startswith 的签名是 str.startswith(prefix[, start[, end]])。它检查字符串在指定范围内是否以 prefix 开头。
path = "/api/users"
print(path.startswith("/api/")) # True
print(path.startswith("/api/user")) # True
print(path.startswith("/admin")) # False
print(path.startswith("api")) # False,因为开头是 "/",不是 "a"
注意最后一个例子:path.startswith("api") 返回 False,因为字符串以 / 开头,不是以 a 开头。startswith 是严格的前缀匹配,从第一个字符开始比较。
startswith 可以接受一个元组作为参数,检查是否以其中任意一个开头:
filename = "photo.jpg"
print(filename.startswith((".jpg", ".png", ".gif"))) # True
print(filename.startswith((".txt", ".md"))) # False
这个元组参数的特性很多人不知道,导致写出这样的代码:
if filename.startswith(".jpg") or filename.startswith(".png") or filename.startswith(".gif"):
...
用元组可以写成一行:
if filename.startswith((".jpg", ".png", ".gif")):
...
endswith:检查结尾
endswith 的签名是 str.endswith(suffix[, start[, end]])。它检查字符串在指定范围内是否以 suffix 结尾。
filename = "report.pdf"
print(filename.endswith(".pdf")) # True
print(filename.endswith(".doc")) # False
print(filename.endswith("pdf")) # True,没有点也匹配
注意最后一个例子:filename.endswith("pdf") 返回 True,因为 "report.pdf" 确实以 "pdf" 结尾。endswith 不关心你有没有写点,它只做字符串匹配。
这可能会带来一个隐患:如果你写 filename.endswith("pdf"),那么 "notpdf" 也会匹配,因为它以 "pdf" 结尾。通常我们应该写 filename.endswith(".pdf"),确保匹配的是扩展名,而不是文件名中随便出现的 pdf。
endswith 同样支持元组参数:
filename = "archive.tar.gz"
print(filename.endswith((".gz", ".zip", ".rar"))) # True
print(filename.endswith((".txt", ".md"))) # False
坑一:把 startswith 和 endswith 用反
这就是小刘踩的坑。前缀检查用了 endswith,后缀检查用了 startswith。
# 错误写法
if path.endswith("/api/"): # 检查开头却用了 endswith
handle_api(path)
if path.startswith(".json"): # 检查结尾却用了 startswith
serve_json(path)
这种错误不会报错,因为 endswith 和 startswith 都返回布尔值,语法上完全合法。程序会继续跑,只是逻辑全错了。
正确的写法:
if path.startswith("/api/"):
handle_api(path)
if path.endswith(".json"):
serve_json(path)
防坑的方法很简单:记住 start 是“开始”,end 是“结束”。startswith 看开头,endswith 看结尾。名字本身就是提示。
坑二:以为 startswith 会匹配任意位置
有些人把 startswith 和 in 搞混,以为 startswith 是“包含”的意思。
s = "hello world"
print(s.startswith("world")) # False
print("world" in s) # True
startswith 只检查开头,不检查中间。如果你想检查子串是否出现在任意位置,用 in。
同样,endswith 只检查结尾:
s = "hello world"
print(s.endswith("hello")) # False
print("hello" in s) # True
坑三:start 和 end 参数用错
startswith 和 endswith 都支持可选的 start 和 end 参数,用来限定检查范围。
s = "hello world"
print(s.startswith("world", 6)) # True,从索引 6 开始,以 "world" 开头
print(s.endswith("hello", 0, 5)) # True,在 [0, 5) 范围内,以 "hello" 结尾
注意 end 参数是开区间,不包含 end 位置。
s = "hello"
print(s.endswith("o", 0, 4)) # False,因为 end=4 不包含索引 4
print(s.endswith("o", 0, 5)) # True
这个细节容易搞混,但和切片的行为一致。
坑四:用 or 连接多个 startswith 调用
前面提到过,startswith 和 endswith 都支持元组参数。但很多人不知道,写成多个 or:
if filename.startswith(".jpg") or filename.startswith(".png") or filename.startswith(".gif"):
...
这样写逻辑是对的,但很啰嗦,而且每次都要重复 filename.startswith。用元组更简洁:
if filename.startswith((".jpg", ".png", ".gif")):
...
元组参数不仅更短,而且性能更好。因为只需要调用一次方法,而不是三次。
坑五:大小写敏感
startswith 和 endswith 是大小写敏感的。
filename = "Photo.JPG"
print(filename.endswith(".jpg")) # False
print(filename.endswith(".JPG")) # True
如果要做大小写不敏感的匹配,需要先转换:
filename.lower().endswith(".jpg") # True
或者用正则表达式加 re.IGNORECASE。
坑六:路径分隔符在不同系统上不一样
如果你在用 startswith 或 endswith 判断路径,要小心不同操作系统的分隔符。
path = "C:\\Users\\Alice\\file.txt" # Windows
print(path.endswith(".txt")) # True
print(path.endswith("\\file.txt")) # True
print(path.endswith("/file.txt")) # False
Windows 用 \,Linux/macOS 用 /。如果你的代码需要跨平台,最好用 os.path 或 pathlib 来处理路径,而不是直接用字符串方法。
from pathlib import Path
p = Path("file.txt")
print(p.suffix == ".txt") # True
性能:startswith 和 endswith 非常快
startswith 和 endswith 都是 O(k) 操作,k 是前缀或后缀的长度。它们不遍历整个字符串,只比较开头或结尾的固定长度。所以无论字符串多长,只要前缀/后缀长度固定,速度都很快。
import timeit
long_str = "a" * 1000000 + ".txt"
t1 = timeit.timeit(lambda: long_str.endswith(".txt"), number=1000000)
t2 = timeit.timeit(lambda: long_str[-4:] == ".txt", number=1000000)
print(f"endswith: {t1:.4f}")
print(f"切片比较: {t2:.4f}")
在我的机器上,endswith 大约 0.05 秒,切片比较大约 0.08 秒。endswith 略快,因为它是 C 实现的,不需要创建切片对象。
什么时候用 startswith,什么时候用 endswith?
判断标准很清晰:
检查开头:用 startswith。比如 URL 协议(http://、https://)、路径前缀(/api/)、命令前缀(git )。
检查结尾:用 endswith。比如文件扩展名(.py、.json)、域名后缀(.com、.org)、字符串后缀(;、})。
检查是否包含:用 in。startswith 和 endswith 都不检查中间。
检查多个前缀/后缀:用元组参数,不要写多个 or。
大小写不敏感:先 lower() 或 upper(),或者用正则。
实际应用场景
场景一:文件类型过滤
def is_image(filename):
return filename.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))
场景二:URL 路由
def route(path):
if path.startswith("/api/"):
return handle_api(path)
elif path.startswith("/static/"):
return serve_static(path)
else:
return serve_index(path)
场景三:协议检查
def is_secure(url):
return url.startswith("https://")
场景四:日志级别解析
def is_error(line):
return line.startswith("ERROR") or line.startswith("CRITICAL")
场景五:字符串清理
def strip_quotes(s):
if s.startswith('"') and s.endswith('"'):
return s[1:-1]
return s
回到小刘的故事
小刘最终的代码是这样的:
def route(path):
if path.startswith("/api/"):
return handle_api(path)
elif path.endswith(".json"):
return serve_json(path)
else:
return serve_static(path)
/api/users 正确进入了 API 处理器,data.json 正确返回了 JSON 内容。他把两个方法换回来之后,一切正常。
他后来在团队文档里写了一条:“startswith 看开头,endswith 看结尾。名字就是提示:start 是开始,end 是结束。别搞反了,搞反了不报错,但逻辑全错。”
这个错误听起来很低级,但真到写代码的时候,特别是在连续写了很多行判断逻辑之后,手滑把两个方法写反是常有的事。更麻烦的是,它不会报错,只会让程序静默地走错分支。
记住那个操场上排队的比喻:startswith 问的是队头,endswith 问的是队尾。你想问哪一头,就用哪个方法。
字符串判断本身不难,难的是在快速编码时保持对方法名的敏感度。startswith 和 endswith 的区别,不在于功能有多复杂,而在于位置完全相反。用对了,代码清晰高效;用反了,程序静默出错。
小刘后来养成了一个习惯:写 startswith 或 endswith 的时候,嘴里默念一遍“开头还是结尾”,确认无误再敲回车。这个习惯花不了两秒钟,但能省下一下午的调试时间。