指南内含 1 场可玩狩猎

正则修饰符详解:i、g、m、s、u、y 和 d

每个 JavaScript 正则修饰符会改变什么,附可交互示例:忽略大小写 i、全局 g(以及 lastIndex 陷阱)、多行 m、dotAll s、Unicode u、粘性 y 和索引 d,还有 Python 中对应的写法。

Regex Hunter 标志

· 阅读约 2 分钟

/^colou?r$/i

抓住

  • color
  • Colour
  • COLOR
  • colour

放过

  • colr
  • colours
  • discolor
  • kolor

修饰符会改变整个正则的行为。在 JavaScript 中,修饰符写在结尾的斜杠后面:/cat/gi。在 Python 中,它们是参数,比如 re.IGNORECASE,或者以内联形式写在正则开头,比如 (?i)。

JavaScript 共有七个修饰符。其中两个你会经常用到,有几个偶尔才需要,还有一个藏着几乎坑过所有人的陷阱。每个示例都可以实时编辑,修改正则就能看到各行的变化。

01i:忽略大小写

字母匹配时不区分大小写:

匹配5/5 符合预期
/^colou?r$/i

应当匹配3/3

  • color
  • Colour
  • COLOR

不应匹配2/2

  • colours
  • discolor
在沙盒里试试
加上 i 后,正则能匹配任意大小写组合。

它省得你写 [Cc][Oo][Ll]…,而且在字符类里同样生效:加上 i 后,[a-z] 也接受大写字母。

Python:re.IGNORECASE、re.I 或 (?i)。

02g:全局

没有 g 时,正则找到第一个匹配就停下。加上它,match、matchAll 和 replace 等方法会作用于每一个匹配:

"a1b22c333".match(/\d+/);    // ["1"], plus index and input
"a1b22c333".match(/\d+/g);   // ["1", "22", "333"]
"a-b-c".replace(/-/, "+");   // "a+b-c"
"a-b-c".replace(/-/g, "+");  // "a+b+c"

Python 没有 g,而是通过选用不同的函数来实现:re.search 只找一个匹配,re.findall 和 re.finditer 找出全部匹配,而 re.sub 默认替换全部。

lastIndex 陷阱

带 g(或 y)的正则会在 lastIndex 中记住上一次匹配结束的位置,下一次 test() 就从那里开始。用同一个这样的正则去测试多个字符串,结果就会开始交替变化:

const hasDigit = /\d/g;

hasDigit.test("a1");  // true, lastIndex is now 2
hasDigit.test("b2");  // false: the search starts at index 2, past the "2"
hasDigit.test("b2");  // true again, lastIndex had reset to 0

解决方法很简单:只用于 test() 的正则不要加 g。检查是否匹配并不需要它。

03m:多行

^ 和 $ 匹配每一行的开头和结尾,而不是整段文本的开头和结尾。锚点与多行模式一文有完整介绍。

"one\ntwo".match(/^\w+$/gm);  // ["one", "two"]

Python:re.MULTILINE、re.M 或 (?m)。

04s:dotAll

通常 . 匹配除换行符以外的任意字符。加上 s 后,它也能匹配换行符:

/start.*end/.test("start\nend");   // false
/start.*end/s.test("start\nend");  // true

在 s 出现之前,人们用 [\s\S] 表示“任意字符”,在旧代码里你仍然会看到它。

Python:re.DOTALL、re.S 或 (?s)。

05u:Unicode

u 让正则以完整的 Unicode 字符为单位工作,而不是 UTF-16 片段,并且会启用 \p{L}(任何文字中的任意字母)这样的属性转义:

匹配6/6 符合预期
/^\p{L}+$/u

应当匹配4/4

  • hello
  • café
  • Zürich
  • Москва

不应匹配2/2

  • R2D2
  • two words
在沙盒里试试
\p{L} 表示任何字母表中的字母,包括带重音的字母。[a-z] 只会接受第一行。

它还会让 . 匹配一整个表情符号,而不是半个;并让解析器变得严格:在字符类外写 \- 这样不必要的转义会变成语法错误,而不是被悄悄忽略。JavaScript 较新的 v 修饰符是 u 的更严格的超集,支持在字符类内进行集合运算。

Python 的 re 默认支持 Unicode,但没有 \p{…}。第三方的 regex 模块提供了它。

06y:粘性

加上 y 后,匹配必须恰好从 lastIndex 开始,而不是从它之后的任意位置开始。它主要用于逐段遍历字符串的分词器:

const word = /\w+/y;

word.lastIndex = 0;
word.exec("hi there");  // ["hi"], lastIndex is now 2
word.exec("hi there");  // null: index 2 is a space, and y won't skip ahead

07d:索引

d 会给每个匹配加上一个 indices 数组,包含整个匹配以及每个分组的起止位置:

const m = /(\d+)-(\d+)/d.exec("pages 12-30");

m.indices[0];  // [6, 11], the whole match
m.indices[1];  // [6, 8], the first group, "12"

在编辑器中高亮匹配时很方便。Python 内置了这个功能:m.span(1)。

08试一试

用 i 修饰符让这个正则保持简短:

可玩狩猎标准杆 5

抓住任意大小写的 yes,其他都不要。

目标 · 抓住 0/5

  • yes目标
  • Yes目标
  • YES目标
  • yEs目标
  • yeS目标

无辜词 · 误抓 0

  • yesterday无辜
  • eyes无辜
  • no无辜
  • ye无辜
  • y e s无辜

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

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

09检查清单

  • i 忽略大小写。m 让 ^ 和 $ 按行匹配。s 让 . 匹配换行符。
  • g 会找出每一个匹配,但不要在重复使用的正则上配合 test() 使用它。
  • u 完整处理 Unicode,并启用 \p{L}。
  • y 只在 lastIndex 处匹配。d 添加匹配位置。
  • Python:re.I、re.M、re.S,或内联的 (?i)、(?m)、(?s)。没有 g:用 findall 或 sub。

10继续练习