前言
「边界匹配」这个词在 Python 里其实指两类不同的东西:一类是字符串两端的匹配(这个字符串是不是以某段文字开头或结尾),另一类是正则表达式里的锚点与词边界(^、$、\A、\z、\b)。前者用str自带方法就能做,后者必须动用re模块。很多人把两者混着用,写出「用正则判断前缀」这种绕远路的代码。
另一个高频误解是「re.match就是全文匹配」。不是。re.match只在字符串开头尝试匹配,"hello".匹配"hello world"一样会成功;要「必须整段匹配」,得用re.fullmatch或自己在两端加锚点。这个差别不搞清楚,校验用户输入时就会放过一堆不该放过的值。
本文从str方法讲到正则锚点,把边界相关的工具按「够用就好」的顺序排开,并标注版本差异。
一、最基础:startswith / endswith
判断前缀后缀,最直接的是str自带方法:
# 适用于 Python 3.8+
s = "report-2026.csv"
print(s.startswith("report")) # True
print(s.endswith(".csv")) # True
print(s.startswith("data")) # Falsestr.startswith()和str.endswith()的完整形态支持元组和区间:
# 适用于 Python 3.8+
s = "report-2026.csv"
print(s.startswith(("data", "report"))) # True,任一前缀命中即可
print(s.endswith((".csv", ".tsv"))) # True
print(s.startswith("2026", 7)) # True,从下标 7 开始比较
print(s.startswith("report", 0, 6)) # True,只比较 s[0:6]要点:
- 参数可以是字符串或字符串元组。传元组时,任意一个匹配即返回
True。 - 可选的
start、end参数按切片语义解释,用来把「前缀」限制在某个区间内。 - 返回布尔值,找不到不会报错。
如果需求不是「判断」而是「去掉」前后缀,Python 3.9 起有removeprefix()/removesuffix():
# 适用于 Python 3.9+
print("report-2026.csv".removeprefix("report-")) # 2026.csv
print("report-2026.csv".removesuffix(".csv")) # report-2026
print("report-2026.csv".removeprefix("data-")) # 原样返回这两个方法要求整体匹配:只有字符串真的以该前缀开头,才会去掉;否则原样返回副本。
二、正则锚点:^、$、\A、\z
进入re的地界,先认清四个锚点:
| 锚点 | 含义 | 受 MULTILINE 影响 |
|---|
^ | 字符串开头(多行模式下也匹配每行行首) | 是 |
$ | 字符串结尾,或结尾换行符之前(多行模式下也匹配每行行尾) | 是 |
\A | 只匹配字符串开头 | 否 |
\z | 只匹配字符串结尾 | 否 |
注意$的一个特殊行为:它除了匹配字符串末尾,还匹配末尾换行符之前的位置。所以"foo\n"里搜$会找到两处(换行前一处、真正的末尾一处)。如果要求严格的「末尾」,用\z(或旧写法\Z)。
版本提示:\z是Python 3.14 新增的。在 3.14 之前,末尾锚点只有\Z,而且\Z在旧版本里的语义就等同于现在的\z。所以:
# 适用于 Python 3.8 ~ 3.13
import re
print(re.search(r"abc\Z", "abc")) # 匹配成功
# 适用于 Python 3.14+
import re
print(re.search(r"abc\z", "abc")) # \z 是标准写法
print(re.search(r"abc\Z", "abc")) # \Z 仍可用,是 \z 的兼容别名写跨版本的代码时,用\Z最稳妥;只有在明确要求 3.14+ 时才写\z。
^与\A的区别同理:在默认模式下两者都只匹配开头;一旦加上re.MULTILINE,^会变成「每行行首」,而\A依旧只管整串开头。
# 适用于 Python 3.8+
import re
text = "a1\nb2\nc3"
print(len(re.findall(r"^\w", text))) # 1,只匹配整串开头
print(len(re.findall(r"^\w", text, re.MULTILINE))) # 3,每行行首
print(len(re.findall(r"\A\w", text))) # 1,\A 不受 MULTILINE 影响三、match / search / fullmatch 的区别
这三个函数最容易搞混:
# 适用于 Python 3.8+
import re
s = "hello world"
print(bool(re.search(r"world", s))) # True,任意位置搜索
print(bool(re.match(r"world", s))) # False,match 只从开头试
print(bool(re.match(r"hello", s))) # True,开头命中即可,不要求整串
print(bool(re.fullmatch(r"hello", s))) # False,fullmatch 要求整串匹配
print(bool(re.fullmatch(r"hello world", s))) # True对照表:
| 函数 | 从哪里开始找 | 是否要求整串匹配 | 最低版本 |
|---|
re.search | 任意位置 | 否 | 3.0 |
re.match | 仅字符串开头 | 否 | 3.0 |
re.fullmatch | 仅字符串开头 | 是 | 3.4 |
re.findall | 任意位置,找全部 | 否 | 3.0 |
re.fullmatch是Python 3.4 起才有的。更早的版本要写re.match(r"(?:pattern)\Z", s)。今天直接用fullmatch即可,它是校验「整段格式」(比如邮箱、订单号)最贴切的工具。
四、\b 与 \B:词边界
\b匹配的不是字符,而是位置。官方定义是:\b匹配「\w与\W之间(或反之),以及\w与字符串开头/结尾之间」的空位置。默认(Unicode 模式)下\w指 Unicode 字母数字加下划线。
# 适用于 Python 3.8+
import re
print(re.findall(r"\bat\b", "at, at. (at) as at ay")) # ['at', 'at', 'at', 'at']
print(re.findall(r"\bat\b", "attempt atlas")) # []"\bat\b"能匹配"at"、"at."、"(at)",但不匹配"attempt"、"atlas"——因为后两者里at后面紧跟着词字符,不构成边界。
\B是\b的反面:匹配不是词边界的位置。
# 适用于 Python 3.8+
import re
# \b 要求「词字符与非词字符交界」,\B 正好相反:要求两侧都是词字符
print(re.findall(r"\bth\b", "python athena")) # [],th 两边都是字母,不构成边界
print(re.findall(r"\Bth\B", "python athena")) # ['th', 'th'],th 两侧都是词字符实际使用中\B出现得较少,但它适合「要求两侧都是词字符」的场景,比如匹配单词内部的片段。
要精确控制「什么算词字符」,可以配合re.ASCII让\w只认 ASCII:
# 适用于 Python 3.8+
import re
print(re.findall(r"\b\w+\b", "中文 abc 123")) # ['中文', 'abc', '123']
print(re.findall(r"\b\w+\b", "中文 abc 123", re.ASCII)) # ['abc', '123']五、实战:校验与提取
# 适用于 Python 3.9+(里面用到了 removesuffix)
import re
ORDER_RE = re.compile(r"[A-Z]{2}-\d{6}")
def is_valid_order(code):
"""订单号:两个大写字母 + 短横线 + 六位数字,且必须整串匹配。"""
return bool(ORDER_RE.fullmatch(code))
def extract_numbers(text):
"""提取文本中独立的数字(不粘在字母上)。"""
return re.findall(r"\b\d+\b", text)
def strip_extension(filename):
"""去掉已知后缀,3.9+ 一行搞定。"""
for ext in (".csv", ".tsv", ".txt"):
if filename.endswith(ext):
return filename.removesuffix(ext)
return filename
if __name__ == "__main__":
print(is_valid_order("AB-123456")) # True
print(is_valid_order("ab-123456")) # False
print(is_valid_order("AB-1234567")) # False
print(extract_numbers("a1 22 b333 c")) # ['22']
print(strip_extension("data.csv")) # dataextract_numbers的结果是['22']:1和333都紧贴着字母,所以\b不成立,被过滤掉了。
常见坑点
- 以为
re.match是整串匹配
❌re.match(r"\d{4}", "2026a")返回匹配对象,误以为输入的a也合法 ✅ 要整串匹配用re.fullmatch(r"\d{4}", s)(3.4+)
- 用
$当严格的末尾锚点
❌re.search(r"abc$", "abc\n")匹配成功,因为$也匹配末尾换行之前 ✅ 严格末尾用\Z(跨版本安全);3.14+ 可写\z
- 在 3.14 之前用
\z
❌ 3.13 上写re.search(r"abc\z", s),\z不是合法转义,行为不符预期 ✅ 3.14 起才有\z;旧版本一律用\Z
- 以为
\A会受re.MULTILINE影响
❌ 加re.MULTILINE后期待\A匹配每行开头 ✅ 只有^会变成「每行行首」;\A始终只匹配整串开头
- 用
startswith判断时传了非字符串
❌s.startswith(1)→TypeError: startswith first arg must be str or a tuple of str✅ 传str或str的元组;数字先str()化
- 把
startswith的start当成「第几个字符」
❌"abc".startswith("b", 1)记不清是从 1 开始还是跳过 1 个 ✅start就是切片起点,s.startswith(p, 1)等价于s[1:].startswith(p)
- 用
strip去前后缀而不是removeprefix
❌"report-2026".strip("report-")会把结尾的t、r等也啃掉 ✅"report-2026".removeprefix("report-")(3.9+)
\b在 Unicode 文本上误判
❌ 以为\b\w+\b只匹配英文,结果中文也被切出来 ✅ 明确需求:加re.ASCII限定\w为 ASCII 字母数字下划线
总结
| 需求 | 工具 | 说明 |
|---|
| 判断前缀 / 后缀 | s.startswith(p)/s.endswith(p) | 可用元组,可带start/end |
| 去掉固定前后缀 | removeprefix/removesuffix | 需 Python 3.9+ |
| 整串必须匹配 | re.fullmatch(pattern, s) | Python 3.4+ |
| 从开头匹配 | re.match | 不要求整串 |
| 任意位置查找 | re.search/re.findall | 默认行为 |
| 严格串首 / 串尾 | \A/\Z | \Z跨版本通用 |
| 串尾(3.14+) | \z | \Z成为其兼容别名 |
| 词边界 | \b/\B | 匹配位置,不匹配字符 |
| 每行行首行尾 | ^/$+re.MULTILINE | \A、\Z不受影响 |
边界匹配的核心是分清「位置」和「字符」:锚点与\b匹配的都是一处位置,长度为 0,这也是为什么"foo\n"里$能找到两处。写校验逻辑时优先用fullmatch,写提取逻辑时用search/findall;至于前后缀的增删,str自带的startswith、removeprefix往往比正则更短也更清楚。