指南内含 1 场可玩狩猎

正则锚点:^、$ 与多行模式详解

^ 和 $ 匹配的不是字符,而是位置。为什么没有锚点的正则检查得比你想的少,m 修饰符改变了什么,Python 的 $ 如何对待末尾的换行符,以及什么时候该改用 \A 和 \z。

Regex Hunter 标志

· 阅读约 4 分钟

/^\d{5}$/

抓住

  • 90210
  • 10001
  • 02139
  • 60614

放过

  • 9021
  • 902100
  • ZIP 90210
  • 90210-1234

大多数正则记号匹配的是一个字符。锚点匹配的是一个位置:文本的开头、结尾,或两个字符之间的空隙。它们完全不占宽度。

它们也是校验用的正则里最常被漏掉的东西。本文讲解 ^ 和 $、m 修饰符改变了什么,以及各语言之间不一样的边界情况。每个示例都可以直接编辑:改改正则,看看各行如何变化。

01没有锚点的正则,检查得比你想的少

正则引擎做的是搜索:只要正则能在文本的任意位置匹配上,就报告匹配。所以一个没有锚点的“五位数 ZIP 邮政编码”正则,实际意思是“包含连续五个数字”:

匹配4/4 符合预期
/\d{5}/
  • 90210
  • 902100
  • ZIP 90210
  • 90210-1234
在沙盒里试试
每一行都匹配。每一行都在某处包含五个数字。

02^ 和 $

^ 匹配文本的开头,$ 匹配文本的结尾。把它们放在正则两边,整个输入就必须完全符合:

匹配6/6 符合预期
/^\d{5}$/

应当匹配2/2

  • 90210
  • 02139

不应匹配4/4

  • 9021
  • 902100
  • ZIP 90210
  • 90210-1234
在沙盒里试试
现在五个数字前后都没有容纳其他内容的余地了。

也可以只用其中一个。^# 表示“以 # 开头”,\.pdf$ 表示“以 .pdf 结尾”:

匹配4/4 符合预期
/\.pdf$/

应当匹配2/2

  • report.pdf
  • 2026 taxes.pdf

不应匹配2/2

  • report.pdf.exe
  • pdf-guide.txt
在沙盒里试试
只固定了文本的结尾,其余部分可以是任何内容。

03分支两边的锚点

| 是正则里优先级最低的,比锚点还低。所以 ^cat|dog$ 的意思不是“恰好是 cat 或 dog”,而是“以 cat 开头,或者以 dog 结尾”:

匹配4/4 符合预期
/^cat|dog$/
  • cat
  • dog
  • catalog
  • hotdog
在沙盒里试试
最后两行也匹配了。^ 只作用于 cat,$ 只作用于 dog。

把各个分支放进一个分组里,两个锚点就都作用于所有分支:

匹配4/4 符合预期
/^(cat|dog)$/

应当匹配2/2

  • cat
  • dog

不应匹配2/2

  • catalog
  • hotdog
在沙盒里试试
分组让锚点作用于每一个分支。

这在真实代码里会坑人,比如 ^admin|editor$ 这样的白名单,会悄悄放行 admin-old 或 not-an-editor。

04多行模式

默认情况下,即使文本里有换行符,^ 和 $ 也只匹配整段文本的最开头和最结尾。m 修饰符让它们改为匹配每一行的开头和结尾:

const text = "apple\nbanana\ncherry";

text.match(/^\w+$/g);   // null: "apple\nbanana\ncherry" isn't one word
text.match(/^\w+$/gm);  // ["apple", "banana", "cherry"]

当你要从文件里逐行提取内容时,这正是你想要的。在 Python 里是 re.MULTILINE,或者在正则开头写 (?m):

import re

re.findall(r"^#+ .*$", markdown, re.MULTILINE)  # every heading line

这个修饰符只改变 ^ 和 $。点号仍然不会跨越换行符。那是另一个修饰符 s 的事,在正则修饰符一文中有讲解。

05末尾换行符陷阱

在这里各语言出现了分歧。在 JavaScript 里,不加 m 时,$ 只匹配最末尾。在 Python 里,$ 还能匹配最末尾那个换行符之前的位置:

import re

re.match(r"^\d{5}$", "90210\n")    # matches in Python
re.fullmatch(r"\d{5}", "90210\n")  # None
/^\d{5}$/.test("90210\n");  // false in JavaScript

所以在 Python 里,^\d{5}$ 会接受末尾粘着一个换行符的 ZIP 邮政编码。如果这个值要写进数据库或用作文件名,这就可能出问题。两种修复:

  • 使用 re.fullmatch(),它必须吃掉整个字符串。
  • 使用 \A 和 \Z:在 Python 里,\A 是最开头,\Z 是最末尾,没有换行符例外。(Ruby、PCRE 和 Java 把严格的结尾写作 \z。)

JavaScript 没有 \A 和 \z,但不加 m 修饰符时,它的 ^ 和 $ 本来就是这样的行为。

06单词边界也是锚点

\b 是另一个零宽锚点:单词字符和非单词字符之间的位置。在文本里匹配一个完整的单词,就靠它:

匹配5/5 符合预期
/\bcat\b/

应当匹配3/3

  • the cat sat
  • cat
  • a cat.

不应匹配2/2

  • concatenate
  • cats
在沙盒里试试
单独出现的 cat 匹配,但在别的单词里面的不匹配。

07试一试

抓住只有一个话题标签、别无其他内容的行:

可玩狩猎标准杆 6

抓住话题标签:一个 # 后面跟字母、数字或下划线,别无其他。

目标 · 抓住 0/5

  • #regex目标
  • #100DaysOfCode目标
  • #a目标
  • #snake_case目标
  • #2026目标

无辜词 · 误抓 0

  • regex无辜
  • ##double无辜
  • #two words无辜
  • #无辜
  • #dash-ed无辜
  • a#tag无辜

输入正则——棋盘会实时更新。

在沙盒里试试
喜欢吗?每天都有新的狩猎。玩今天的狩猎

08检查清单

  • 没有锚点时,正则只检查文本是否包含一个匹配。
  • ^ 是开头,$ 是结尾。做校验时两个都要用。
  • 把分支放进锚点之间的分组里:写 ^(cat|dog)$,不要写 ^cat|dog$。
  • m 修饰符让 ^ 和 $ 按行生效。
  • 在 Python 里,$ 允许末尾有一个换行符。用 re.fullmatch() 或 \Z。
  • \b 锚定一个完整的单词。

09继续练习