首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的字符串判断把我坑惨了,原来startswith和endswith的区别这么大

Python的字符串判断把我坑惨了,原来startswith和endswith的区别这么大

原创
作者头像
风一样的男子
发布于 2026-10-19 14:17:45
发布于 2026-10-19 14:17:45
440
举报
文章被收录于专栏:编程教程编程教程

小刘那天写了一个静态文件服务器的路由逻辑,需要根据请求路径判断该交给谁处理。规则很简单:路径以 /api/ 开头的,交给 API 处理器;以 .json 结尾的,返回 JSON 内容;其他的当静态文件处理。

他写了一段代码:

代码语言:javascript
复制
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。”

小刘愣了一下,然后拍了下脑门:“我把这俩搞混了。”

这个错误听起来很蠢,但真到写代码的时候,很多人会因为赶时间、变量名相似、或者纯粹的手滑,把这两个方法用反。更麻烦的是,用反了通常不会报错,程序会静默地走错分支,直到你发现某个功能完全没生效。

先看一个最直观的对比

代码语言:javascript
复制
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 开头。

代码语言:javascript
复制
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 可以接受一个元组作为参数,检查是否以其中任意一个开头:

代码语言:javascript
复制
filename = "photo.jpg"

print(filename.startswith((".jpg", ".png", ".gif")))  # True
print(filename.startswith((".txt", ".md")))            # False

这个元组参数的特性很多人不知道,导致写出这样的代码:

代码语言:javascript
复制
if filename.startswith(".jpg") or filename.startswith(".png") or filename.startswith(".gif"):
    ...

用元组可以写成一行:

代码语言:javascript
复制
if filename.startswith((".jpg", ".png", ".gif")):
    ...

endswith:检查结尾

endswith 的签名是 str.endswith(suffix[, start[, end]])。它检查字符串在指定范围内是否以 suffix 结尾。

代码语言:javascript
复制
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 同样支持元组参数:

代码语言:javascript
复制
filename = "archive.tar.gz"

print(filename.endswith((".gz", ".zip", ".rar")))  # True
print(filename.endswith((".txt", ".md")))           # False

坑一:把 startswith 和 endswith 用反

这就是小刘踩的坑。前缀检查用了 endswith,后缀检查用了 startswith。

代码语言:javascript
复制
# 错误写法
if path.endswith("/api/"):     # 检查开头却用了 endswith
    handle_api(path)

if path.startswith(".json"):   # 检查结尾却用了 startswith
    serve_json(path)

这种错误不会报错,因为 endswith 和 startswith 都返回布尔值,语法上完全合法。程序会继续跑,只是逻辑全错了。

正确的写法:

代码语言:javascript
复制
if path.startswith("/api/"):
    handle_api(path)

if path.endswith(".json"):
    serve_json(path)

防坑的方法很简单:记住 start 是“开始”,end 是“结束”。startswith 看开头,endswith 看结尾。名字本身就是提示。

坑二:以为 startswith 会匹配任意位置

有些人把 startswith 和 in 搞混,以为 startswith 是“包含”的意思。

代码语言:javascript
复制
s = "hello world"
print(s.startswith("world"))  # False
print("world" in s)           # True

startswith 只检查开头,不检查中间。如果你想检查子串是否出现在任意位置,用 in。

同样,endswith 只检查结尾:

代码语言:javascript
复制
s = "hello world"
print(s.endswith("hello"))  # False
print("hello" in s)         # True

坑三:start 和 end 参数用错

startswith 和 endswith 都支持可选的 start 和 end 参数,用来限定检查范围。

代码语言:javascript
复制
s = "hello world"

print(s.startswith("world", 6))     # True,从索引 6 开始,以 "world" 开头
print(s.endswith("hello", 0, 5))    # True,在 [0, 5) 范围内,以 "hello" 结尾

注意 end 参数是开区间,不包含 end 位置。

代码语言:javascript
复制
s = "hello"
print(s.endswith("o", 0, 4))  # False,因为 end=4 不包含索引 4
print(s.endswith("o", 0, 5))  # True

这个细节容易搞混,但和切片的行为一致。

坑四:用 or 连接多个 startswith 调用

前面提到过,startswith 和 endswith 都支持元组参数。但很多人不知道,写成多个 or:

代码语言:javascript
复制
if filename.startswith(".jpg") or filename.startswith(".png") or filename.startswith(".gif"):
    ...

这样写逻辑是对的,但很啰嗦,而且每次都要重复 filename.startswith。用元组更简洁:

代码语言:javascript
复制
if filename.startswith((".jpg", ".png", ".gif")):
    ...

元组参数不仅更短,而且性能更好。因为只需要调用一次方法,而不是三次。

坑五:大小写敏感

startswith 和 endswith 是大小写敏感的。

代码语言:javascript
复制
filename = "Photo.JPG"

print(filename.endswith(".jpg"))  # False
print(filename.endswith(".JPG"))  # True

如果要做大小写不敏感的匹配,需要先转换:

代码语言:javascript
复制
filename.lower().endswith(".jpg")  # True

或者用正则表达式加 re.IGNORECASE。

坑六:路径分隔符在不同系统上不一样

如果你在用 startswith 或 endswith 判断路径,要小心不同操作系统的分隔符。

代码语言:javascript
复制
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 来处理路径,而不是直接用字符串方法。

代码语言:javascript
复制
from pathlib import Path

p = Path("file.txt")
print(p.suffix == ".txt")  # True

性能:startswith 和 endswith 非常快

startswith 和 endswith 都是 O(k) 操作,k 是前缀或后缀的长度。它们不遍历整个字符串,只比较开头或结尾的固定长度。所以无论字符串多长,只要前缀/后缀长度固定,速度都很快。

代码语言:javascript
复制
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(),或者用正则。

实际应用场景

场景一:文件类型过滤

代码语言:javascript
复制
def is_image(filename):
    return filename.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))

场景二:URL 路由

代码语言:javascript
复制
def route(path):
    if path.startswith("/api/"):
        return handle_api(path)
    elif path.startswith("/static/"):
        return serve_static(path)
    else:
        return serve_index(path)

场景三:协议检查

代码语言:javascript
复制
def is_secure(url):
    return url.startswith("https://")

场景四:日志级别解析

代码语言:javascript
复制
def is_error(line):
    return line.startswith("ERROR") or line.startswith("CRITICAL")

场景五:字符串清理

代码语言:javascript
复制
def strip_quotes(s):
    if s.startswith('"') and s.endswith('"'):
        return s[1:-1]
    return s

回到小刘的故事

小刘最终的代码是这样的:

代码语言:javascript
复制
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 的时候,嘴里默念一遍“开头还是结尾”,确认无误再敲回车。这个习惯花不了两秒钟,但能省下一下午的调试时间。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先看一个最直观的对比
  • startswith:检查开头
  • endswith:检查结尾
  • 坑一:把 startswith 和 endswith 用反
  • 坑二:以为 startswith 会匹配任意位置
  • 坑三:start 和 end 参数用错
  • 坑四:用 or 连接多个 startswith 调用
  • 坑五:大小写敏感
  • 坑六:路径分隔符在不同系统上不一样
  • 性能:startswith 和 endswith 非常快
  • 什么时候用 startswith,什么时候用 endswith?
  • 实际应用场景
  • 回到小刘的故事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档