
在Python日常开发里判断一个字符串里有没有包含另一个子串可能是程序员最高频的字符串操作之一。不管是写爬虫解析网页、处理日志文本还是做表单校验几乎每天都在跟这个需求打交道。我见过不少新手上来就直接用if 某个词 in 文本: ...这当然没错但偶尔也会遇到in解决不了或不太好用的情况——比如你要忽略大小写、要拿到子串位置、要匹配的是日期格式而不是固定文本这时就需要其他方法登场。这篇文章我整理了7种判断字符串子串的方法从最简单的in操作符到str.find、str.index再到count、startswith、endswith和正则的re.search每种方法都会把用法、适用场景、性能和容易踩的坑点讲清楚。无论你是刚接触 Python 的新手还是已经写了两三年代码的老手这份清单都值得收藏备查。先说结论日常业务里in操作符能覆盖绝大多数需求但剩下那一小部分 固执 的场景恰恰需要后面几种方法来兜底。1. 先认识7种方法一次性看完1.1 7种方法速查表聊技术之前先把货摆出来。我常给团队新人发一份速查表让他们贴在工位旁边。今天也分享给你后面每个方法我们再逐个拆。序号方法典型写法返回值找不到子串时1in操作符子串 in 目标字符串布尔值返回 False2str.__contains__()目标字符串.__contains__(子串)布尔值返回 False3str.find()目标字符串.find(子串)整数索引返回 -14str.index()目标字符串.index(子串)整数索引抛 ValueError5str.count()目标字符串.count(子串) 0非负整数返回 06str.startswith()/str.endswith()目标字符串.startswith(子串)布尔值返回 False7re.search()re.search(r子串, 目标字符串)匹配对象或 None返回 None这里有个关键认知虽然都是判断是否包含但每种方法返回的数据类型不同。有的给你布尔值有的给你索引有的直接抛异常。选哪个取决于你拿到结果之后想干什么。1.2 为什么 Python 会有这么多种判断方式不少读者疑惑一个简单的包含判断Python 为啥折腾出这么多玩法这个问题的核心在于字符串底层是字符数组判断包含在不同的业务语境下语义完全不同。in解决的是存在性find解决的是位置count解决的是次数startswith解决的是边界正则解决的是模式。你看同样是包含真实需求其实分成了五个维度。Python 把每种维度都给你提供了原生工具而不是让你拿到结果后自己再做一层解析。这也是我觉得 Python 最舒服的地方语言设计者把常见业务场景都替你想到了。理解了这层接下来每个小节我们都可以带着问题去看这个方法返回什么它适合解决哪类包含问题以及它有什么副作用。2. 入门首选in 操作符和它的底层逻辑2.1in操作符的用法与特性in是绝大多数 Python 开发者第一个学会的字符串包含判断方式因为它在语法层面读起来就像一句英文text Python is one of the most popular languages if Python in text: print(包含 Python)如果只是想快速知道有没有in是效率最高、代码最整洁的选择。它的底层其实调用了字符串对象的__contains__方法而在 CPython 内部这个步骤会用专业的子串搜索算法处理对普通短字符串和中等长度字符串来说速度非常理想。in还有一个容易被忽略的优点它天然返回布尔值所以可以直接嵌进if、while、列表推导式里不需要额外比较。比如你要过滤一批日志中所有带ERROR的行logs [INFO: start, ERROR: timeout, WARN: slow] error_lines [line for line in logs if ERROR in line]这段代码里in直接把字符串的存在性翻译成了布尔表达式配合列表推导式一行搞定。如果你换成find还得写if line.find(ERROR) ! -1虽然也能跑但可读性就差了一截。2.2 手动调用__contains__和operator.contains可能有些读者不知道in操作符真正的执行路径是走__contains__这个魔法方法。你可以手动调用它但我不推荐在业务代码里这么干因为可读性真的不行而且显得很不 Pythonictext hello world print(text.__contains__(hello)) # True不过理解这个底层关系有一个实际好处当你自己写一个类想让这个类的对象也支持in判断时只需在类里实现__contains__方法即可。比如我写过一个小工具类用来管理一批关键词判断某个词是否在关键词集合里class KeywordSet: def __init__(self, keywords): self.keywords keywords def __contains__(self, item): return any(item in keyword for keyword in self.keywords) keywords KeywordSet([baidu, alibaba]) print(baidu in keywords) # True另外还有一个operator.contains函数作用跟in一样只是以函数式接口暴露出来。它在用functools.partial做高阶函数传参时可能有点用日常几乎用不到import operator print(operator.contains(hello, ell)) # True说实话__contains__和operator.contains这俩更多是帮我们理解in的底层原理而不是真的要大家去用。但面试的时候如果被问到in为什么能判断包含你能说出__contains__这层关系会显得功底扎实不少。3. 面向索引用 find 和 index3.1find方法找不到返回 -1in能告诉我们有没有但如果你还想知道在哪个位置那就该上find了。它的完整签名是str.find(sub[, start[, end]])在字符串中查找子串首次出现的位置如果找到了返回起始索引找不到返回-1text hello python world idx text.find(python) print(idx) # 输出 6 idx_not text.find(java) print(idx_not) # 输出 -1拿find做包含判断的惯用写法是判断返回值是否不等于-1if text.find(python) ! -1: print(包含 python 子串)这种写法并不是最优雅的但它有一个独特优势你同时拿到的索引可以直接用来切片、做字符串替换定位或者记录错误位置。比如我做配置文件解析的时候经常需要先定位某个标记符号再截取后面的内容这时候find一步到位。3.2index方法找不到直接抛异常index和find的参数、返回值逻辑几乎一样唯一的差别是子串不存在时find返回-1而index会抛出ValueErrortext hello python world try: idx text.index(python) print(idx) # 输出 6 except ValueError: print(不包含)有人觉得index抛异常太麻烦我倒认为它在特定场景下反而是优点。比如你有一段逻辑如果子串不存在程序本来就应该中断报错那index就能让错误更早暴露不用你再手动if idx -1去处理异常流程。我通常在写一些语法解析或模板引擎时用index因为那些场景下找不到标记本身就是一种不应该出现的异常状态。不过如果你只是做普通的包含判断我不建议用index毕竟每次都要 try-except 不仅啰嗦性能上也会因为异常处理的开销打折扣。3.3find和index的冷门参数start 和 end这两个方法其实都能指定搜索范围写法是find(sub, start, end)这个细节很多人不知道。比如你只想判断字符串的前半段是否包含某个子串就不需要先做切片直接限制搜索区间text prefix-123456-suffix # 只看前 10 个字符里是否包含 prefix if text.find(prefix, 0, 10) ! -1: print(前 10 个字符里包含 prefix)这里start是起始下标end是结束下标遵循左闭右开规则。这个功能在做文本局部判断时特别省事省掉了一次切片操作也避免了切片产生的额外内存。另外要注意index同样支持这两个参数用法一致。4. 用 count 次数来判断是否包含4.1count方法的基础用法count的本意是统计子串在字符串中出现的次数签名是str.count(sub[, start[, end]])。既然要统计次数那次数大于 0天然就等于包含text python is great, and python is everywhere count text.count(python) print(count) # 2 if text.count(python) 0: print(包含 python)这种方法在语义上多了一层统计但如果你的业务本身就关心子串出现了几次用count可以一步拿到次数同时完成包含判断和频率统计。比如检测一篇文章里某个错别字出现的次数超过阈值就要人工复核这时候count就是最自然的写法。4.2count的局限与注意点count做包含判断有个隐藏坑它是非重叠计数。也就是说如果子串是aa目标字符串是aaacount返回的是 1 而不是 2print(aaa.count(aa)) # 1不是 2因为count在找到第一个aa后下一个查找起点是从索引 2 开始的剩下的单个a不够组成aa。如果业务上需要统计重叠出现的次数就不能用count得用循环加find自己实现或者用正则的positive lookahead——这个我们后面提到正则时会再说。另一个注意点count需要完整遍历字符串并计数即使你已经找到了一次它也会继续数完。所以在只关心是否包含的场景下用count其实有点浪费性能in往往会在找到后提前返回而count不会。5. startswith 和 endswith专门处理前缀后缀5.1 基础用法与元组参数startswith判断字符串是否以指定子串开头endswith判断是否以指定子串结尾。严格意义上它们不是全串包含判断但它们能非常高效地解决一类包含问题——边界包含filename report_2026.pdf if filename.endswith(.pdf): print(这是 PDF 文件) if filename.startswith(report_): print(这是报告文件)这两个方法在文件处理、URL 路由、协议解析里非常常见。比如判断一个链接是不是 HTTPS 协议url https://example.com if url.startswith(https://): print(安全连接)这里有个特别实用的技巧startswith和endswith都支持传入元组多项匹配只用一次调用就完成if filename.endswith((.pdf, .docx, .txt)): print(支持处理的文档格式) if filename.startswith((draft_, final_)): print(文档状态符合规范)这个技巧能让代码少写好几个or可读性也更好。我自己在写文件类型过滤时基本都会用元组参数避免写一长串判断条件。5.2 参数 start 和 end 的进阶技巧和find一样startswith/endswith也支持start和end参数用于限定判断区间text 2026-01-15: ERROR occur # 判断从索引 11 开始到结尾的这一段是否以 ERROR 开头 if text.startswith(ERROR, 11): print(第 11 个字符后是 ERROR)这个签名非常方便比如日志解析时每行日志有固定长度的日期前缀之后才是日志级别。想判断某个时间段之后的记录是不是错误直接指定start就不需要切片了。不过要记住一点startswith和endswith判断的是边界而不是任意位置包含。如果需求是判断子串在字符串中间某个位置出现它们就无能为力了这时还得回到in或find。6. re.search正则表达式家族的包含判断6.1 re.search 与 re.match 的区别当子串不是一个固定的文本而是一段模式时正则表达式是唯一的选择。比如你想判断字符串里是否包含一个手机号、一个日期、一个 IP 地址用普通子串判断就没戏了必须用re.searchimport re text 会议时间定在 2026-03-18 下午三点 if re.search(r\d{4}-\d{2}-\d{2}, text): print(文本中包含日期格式)这里re.search会扫描整个字符串找到第一个能够匹配正则表达式的部分。还有一个re.match函数它从字符串开头尝试匹配注意这和包含是两个概念。很多初学者把match当成包含结果踩坑re.match(r\d{4}, 年份2026)返回 None因为字符串开头是年份两个字不是数字。如果你要表达包含语义用re.search只有明确要求从开头开始匹配时才用re.match。这是正则子串判断里最容易搞混的一点。6.2 正则做包含判断的实战从匹配对象到布尔值re.search返回的是匹配对象如果没有匹配则返回 None。所以在if里可以直接判断match re.search(r\d{4}-\d{2}-\d{2}, text) if match: print(包含日期位置从, match.start(), 开始)match对象还提供start()、end()、group()等方法相当于正则版的find。万一你需要拿到具体匹配到的内容比如要从一串话里提取日期m re.search(r(\d{4})-(\d{2})-(\d{2}), 2026-03-18 是会议日) if m: year, month, day m.groups() print(f{year}年{month}月{day}日)正则的缺点也很明显性能比in慢写复杂模式容易把自己绕晕。我的建议是能用固定字符串判断解决的问题就老老实实用in或find。只有真正面对模式匹配需求时再上正则别为了炫技把简单问题搞复杂。回到前面说到的重叠计数场景正则配合lookahead可以解决count无法处理的重叠问题。比如想统计aaa中所有重叠的aatext aaa matches re.findall(r(?(aa)), text) print(len(matches)) # 2这里用(?...)这种前向断言让匹配位置不消耗字符从而实现重叠匹配。这种写法偏进阶但如果遇到了类似的业务需求你应该能想起这个方案。7. 性能实测与选型思路7.1 7种方法的性能对比我们平时写业务代码性能坑往往不在这些基础操作上但既然聊到选型我还是用timeit做了一个简单对比供大家心里有数import timeit setup text hello world, python is great, python everywhere * 100; sub python print(in:, timeit.timeit(sub in text, setupsetup, number1000000)) print(find:, timeit.timeit(text.find(sub) ! -1, setupsetup, number1000000)) print(count:, timeit.timeit(text.count(sub) 0, setupsetup, number1000000)) print(__contains__:, timeit.timeit(text.__contains__(sub), setupsetup, number1000000))不同机器、不同字符串长度跑出来的数据会有差异但结论基本稳定in速度最快其次是find和__contains__三者差距很小。index和find性能接近但如果每次都不存在并抛异常性能会明显变差。count因为要统计所有出现位置比in慢不少字符串越长、子串出现越频繁差距越大。startswith/endswith因为它们只比较开头或结尾大部分情况下不做全串扫描速度非常快但使用范围有限。re.search性能最不稳定正则表达式越复杂越慢即使简单模式也要经过正则引擎编译和匹配开销远大于in。7.2 不同场景的选型建议基于上面的性能数据和语义差异我总结了一套自己的选型标准可以直接抄作业业务需求推荐方法原因只关心是否存在in最快、最可读需要子串出现的索引find不抛异常判断容易期望子串不存在时程序报错index用异常传递错误状态需要统计子串出现次数count专门做频率统计判断文件后缀 / URL 前缀endswith/startswith语义清晰支持元组按模式匹配日期、电话、IPre.search只有它能做模糊模式想知道某些类是否支持in__contains__大多用于理解底层或设计类你要记住的核心原则是方法不是越多越好而是越合适越好。简单问题用简单工具性能自然差不到哪去。8. 实际开发中的坑这些场景我踩过8.1 大小写敏感问题默认情况下Python 的字符串比较是大小写敏感的所以Python in text只会匹配大写 P 开头的Python小写python就会漏掉。要忽略大小写做包含判断最稳妥的做法是统一转换后比较text python is easy if PYTHON in text.upper(): print(忽略大小写后仍然匹配)每次调用upper()会产生新字符串性能有一点损耗但在绝大多数业务场景下可以忽略。如果你做的是极高频的调用可以提前把文本统一转好而不是在循环里反复转换。这里我建议用casefold()而不是lower()。对于英文来说两者差异不大但casefold()对德语、土耳其语等场景的大小写折叠处理更彻底。当然如果项目只处理英文内容用lower()也够用。8.2 空白字符和换行干扰文本从文件或接口读进来时常常带着换行符、制表符和首尾空格。比如判断一行日志是否包含某个关键词但关键词后面还跟着\r\n这时候直接in可能返回 False。我习惯在读取文本后先做一次标准化raw ERROR line \n cleaned raw.strip() if ERROR in cleaned: print(干净数据匹配)但如果空白字符在字符串中间strip()就无能为力了。比如文本里把user_name写成了user name判断user_name in text会失败。这种属于文本归一化问题通常需要根据具体场景做替换text.replace( , )或使用正则把连续空白压成一个空格。8.3 空字符串的隐藏行为这是我最想强调的一个坑。很多人不知道所有字符串都包含空字符串print( in abc) # True print(abc.find()) # 0 print(abc.index()) # 0 print(abc.startswith()) # True print(abc.endswith()) # True import re print(re.search(, abc)) # 能匹配到空位置 print(abc.count()) # 4也就是 len(s) 1如果你的变量可能是空字符串又用count做包含判断很容易得到异常结果。比如用户输入搜索关键词时没有做非空校验if text.count(keyword) 0就会对任何文本返回 True因为count()恒大于 0。处理办法很简单判空放在前面keyword keyword.strip() if keyword and keyword in text: print(执行搜索)不管用哪种方法凡是要把子串当作变量传入的场景我都建议先做一次非空判断。8.4 Type 相关和其他隐蔽坑字符串的判断方法只在字符串类型上有效如果你拿in去判断列表那就是成员包含语义完全不同print(a in [ab, cd]) # False列表里没有字符串 a print(a in ab) # True字符串子串包含这个坑经常发生在从数据库取数据、从 JSON 解析内容时——你以为是字符串其实拿到的是列表。另外bytes类型也支持这些方法但要求参数也是bytesdata bhello if bell in data: print(bytes 也支持 in 判断)还有个小细节find和index在子串为空时都返回 0上面第 3 点已经提到过。而startswith/endswith是区分大小写的同样需要忽略大小写时可以先把两个参数都转成统一格式。9. 实战演练写一个日志关键字筛选工具方法讲了这么多不看一个完整案例总觉得不够踏实。我带你写一个简单但实用的日志筛选工具把in、endswith、正则和大小写处理结合起来用。需求场景手头有一批日志文件需要筛选出同时满足以下条件的行包含关键字ERROR或Exception忽略大小写行尾以时间戳样式[2026-xx-xx]结尾如果提供正则模式pattern则按正则额外过滤。import re from typing import List, Optional def filter_log_lines( lines: List[str], keywords: Optional[List[str]] None, pattern: Optional[str] None, ignore_case: bool True, ) - List[str]: 筛选日志行满足关键字包含和行尾时间戳格式。 if keywords is None: keywords [ERROR, Exception] result [] ts_pattern re.compile(r\[\d{4}-\d{2}-\d{2}\]$) for line in lines: line_clean line.strip() if not line_clean: continue # 1. 关键字包含判断忽略大小写 check_text line_clean.casefold() if ignore_case else line_clean if not any(key.casefold() in check_text for key in keywords): continue # 2. 行尾时间戳判断用 endswith 的元组思路 if not ts_pattern.search(line_clean): continue # 3. 可选正则过滤 if pattern and not re.search(pattern, line_clean): continue result.append(line_clean) return result这个函数里我刻意把三种判断都用了不同工具关键字包含用in时间戳边界用正则的re.search整体思路里也沿用了endswith的边界语义。实际跑一下sample_logs [ 2026-01-15 12:00:01 [INFO] service started, 2026-01-15 12:00:02 [ERROR] database timeout [2026-01-15], 2026-01-15 12:00:03 [WARN] result is slow [2026-01-15], 2026-01-15 12:00:04 [ERROR] connection failed [2026-01-15], ] filtered filter_log_lines(sample_logs) for line in filtered: print(line)输出结果是那两条包含ERROR且以[2026-01-15]结尾的日志行。这个小工具可以直接扩展成命令行脚本用它处理几百 MB 的日志也不会有压力——因为in和正则搜索在底层都是优化的 C 实现循环开销可以接受。10. 我自己的选型心得每次给团队做分享总会有人问我你天天写代码真会每次都纠结用哪种方法吗说实话不会。我在实际开发里已经形成了一套肌肉记忆90% 的场景直接用in加casefold()处理大小写只有需要拿索引时才用find遇到模式匹配需求才上re.search文件后缀判断就用endswith至于count、__contains__、index这些更多是遇到特定业务语义时才轮得到它们出场。字符串这种底层操作没有所谓绝对最佳的方法只有当前场景最合适的方法。你在团队里写代码可读性往往比那毫秒级的性能差异更重要。in之所以成为主流不单是因为快还因为它几乎不需要注释看代码的人一眼就能懂你在做什么。所以我最后再分享一个小技巧当你拿不准用哪种方法时先问自己一句——这段代码三个月后别人读起来会不会需要想半天如果会那就换更直白的方式。技术选型的本质是在机器效率和人的理解成本之间找平衡点。