2024-08-24

正则表达式

正则表达式可用于搜索、替换和验证字符串,它使用字符模式匹配来查找文本。字符模式匹配是一种在字符串中查找特定模式的技术,正则表达式就是一种用于字符模式匹配的语法,但不同的编程语言和工具对它的实现和使用有所不同。

正则表达式区分大小写,使用普通文本、元字符等来匹配一系列其他字符,术语规则中的正则表达式被正斜线包围:/regex/[flags],表达式两旁的正斜线表示分隔符,在不同的编程语言中分隔符也有可能不同,也有可能没有分隔符。

使用 (?#注释内容) 语法为正则表达式添加注释。

元字符

元字符说明
.匹配单个非换行符的任意字符。
{n}前一个字符或模式重复 n 次。
{n,}前一个字符或模式重复 n 次或更多次。
{n,m}前一个字符或模式重复 n 过 m 次。
*前一个字符或模式重复零次或多次,等价于 {0,}
+前一个字符或模式重复一次或多次,等价于 {1,}
?前一个字符或模式重复零次或一次,等价于 {0,1}

方括号 []

使用方括号定义一个字符集合,匹配一个字符集合中定义的字符。字符集合由字符或字符区间组成,字符区间用连字符 (-) 定义。一些常用的字符区间有:A-Z、a-z、0-9 和 A-z,其中 A-z 匹配从 ASCII 字符 A 过字符 z 的所有字符。其中包含了 [\]_^ 等一些在 Z 和 a 之间的字符,所以 A-z 并不常用。

连字符 -

一个特殊的元字符,仅在方括号之间且定义字符集合时才是元字符,在其他地方仅仅是个普通字符。定义字符区间时区间的首字符不能大于区间的尾字符,这种区间会使整个正则表达式失效。

乘方符号 ^

当定义字符集合使用时,作用是排除整个字符集合。仅在定义字符集合且紧跟左方括号时才表示排除整个字符集合。

例如 [^0-9] 匹配单个非数字字符;[a^0-9] 匹配“a”和“^”两个字符中的单个字符或单个数字,其中 ^ 符号表示其本身,而不是排除整个字符集合。

反斜线 \

反斜线有两个含义,一是转义元字符,转义后匹配元字符本身。二是定义元字符,与某些字符组合可定义元字符,例如 \d,匹配单个数字字符。

特定字符类型

元字符说明
\d一个数字字符。等价于 [0-9]
\D一个非数字字符。等价于 [^0-9][^\d]
\w一个字母、数字或下划线字符。等价于 [a-zA-Z0-9_]
\W一个非字母、非数字或非下划线字符。等价于 [^a-zA-Z0-9_][^\w]
\s一个空白字符,包括空格或制表符。等价于 [\r\n\t\f\v ],注意最后有个空格。
\S一个非空白字符。等价于 [^\r\n\t\f\v ][^\s]

非打印空白字符

元字符说明
\r回车符
\n换行符
\t制表符
\f换页符
\v垂直制表符

Flag

并非所有 Flag 都被广泛支持。

Flag说明
iignore case
gglobal search
mmultiline
uunicode
ysticky
sdotAll

i,忽略大小写。

g,全局搜索,返回所有匹配项,非全局搜索仅返回第一个匹配项。

m,多行模式,^$ 可以分别匹配行首和行尾,非多行模式时 ^$ 仅能分别匹配整个字符串开头和结尾。

s,dotAll 模式,允许 . 匹配换行符。

重复匹配

例如,以下字符如果要匹配所有引号及其内容:

/".+"/g:A "witch" and her "broom" is one.

一些重复次数没有上限的重复匹配量词默认是贪婪的,例如 +*{n,},会一次性地读入整个字符串,如果不匹配就吐掉最右边的一个字符再匹配,直到找到匹配的字符串或字符串的长度为 0 为止。

使用懒惰模式:+?*?{n,}?,懒惰模式从字符串的左边开始匹配,试图不读入字符串中的字符进行匹配,匹配失败则多读一个字符,再尝试匹配,如此循环。当找到一个匹配时会返回该匹配的字符串,然后再次重新匹配直到字符串结束。

所以上述例子正确的正则表达式应该是:/".+?"/g

位置匹配

元字符说明
\b匹配一个边界位置,一边是可被 \w 匹配的字符,另一边是其他内容 (可被 \W 匹配的字符)。匹配的是字符之间的一个位置,而不是实际字符。
\B非边界,与 \b 相反。
^使用在字符集合之外且是模式的开头时匹配字符串的起始位置。
$匹配字符串的结束位置。

分支

使用管道符号 | 定义分支,分支表示匹配其中之一,匹配分支时是从左往右匹配的,可以同时定义多个分支,如果满足了左边的条件,右边的条件会被忽略。

子表达式和反向引用

子表达式是更长的表达式的一部分,划分子表达式的目的是为了将其视为单一实体使用。子表达式使用 () 定义,可将内容分为一组,允许嵌套。子表达式可用于重复一组被匹配的字符或在表达式的其他部分引用被匹配的字符。默认情况下子表达式会自动有一个组号,组号的规则是从左至右,以子表达式的左括号为标志,从 1 开始递增,第一个子表达式的组号是 1。

反向引用 (backreference) 指的是在表达式其他位置引用先前子表达式所匹配的内容,可以使用组号或组名引用子表达式,使用 \1 引用第一个子表达式。在一些正则表达式实现里,组号 0 代表整个正则表达式。

反向引用基于子表达式位置,一些较新的正则表达式实现还支持命名捕获:给子表达式起一个唯一名称,随后用名称引用子表达式。

在不同的正则表达式实现中,反向引用有语法差异。例如 JavaScript 使用 \ 来标识反向引用,在替换操作中用的是 $

替换

正则表达式不仅可以用于查找,也可以用于替换操作。因为反向引用可以跨模式使用,查找模式里子表达式匹配的字符可以用在替换模式里。

例如,把以下字符串中的邮件替换为 HTML 中可点击的链接,假设使用的是 JavaScript:

html
Hello, [email protected] is my email address.

查找正则表达式:(\w+[\w\.]*@[\w\.]+\.\w+)

替换正则表达式:<a href="mailto:$1">$1</a>

替换后的字符串:

html
Hello, <a href="mailto:[email protected]">[email protected]</a> is my email address.

如前所述,JavaScript 在替换操作中使用 $ 来标识反向引用,因此替换正则表达式中引用子表达式使用的是 $,而不是 \

一些正则表达式实现还支持大小写转换。

元字符说明
\E结束 \L\U 转换。
\L\L\E 之间的字符全部转换为小写。
\U\U\E 之间的字符全部转换为大写。
\l把下一个字符转换为小写。
\u把下一个字符转换为大写。

\l\u 可以放置在字符 (或子表达式) 之前,转换下一个字符的大小写。

例如,把下列代码中的一级标题内容转换为大写,假设使用的是 JavaScript:

html
<body> <h1>Welcome to my Homepage</h1> Content is divided into two sections:<br /> <h2>SQL</h2> Information about SQL. </body>

查找正则表达式:(<[Hh]1>)(.*?)(<\/[Hh]1>)

替换正则表达式:$1\U$2\E$3

替换后的结果是:

html
<body> <h1>WELCOME TO MY HOMEPAGE</h1> Content is divided into two sections:<br /> <h2>SQL</h2> Information about SQL. </body>

环视 (lookaround)

环视 (lookaround),目标是确保要匹配的内容的前面或后面匹配或不匹配特定的内容。环视的语法是以特殊符号开头的子表达式。

环视返回结果永远都是零长度字符串,因此环视有时也被称为零宽度断言。

种类说明
(?=)positive lookahead,mainExp(?=ABC),匹配主表达式之后的组,而不将其包含在结果中。
(?!)negative lookahead,mainExp(?!ABC),指定主表达式之后不能匹配的组,如果匹配则丢弃结果。
(?<=)positive lookbehind,(?<=ABC)mainExp,匹配主表达式之前的组,而不将其包含在结果中。
(?<!)negative lookbehind,(?<!ABC)mainExp,指定主表达式之前不能匹配的组,如果匹配则丢弃结果。

positive lookahead

匹配单位是 px 的具体值:

/\d(?=px)/g:1pt 2px 3em 4px

negative lookahead

匹配单位不是 px 的具体值:

/\d(?!px)/g1pt 2px 3em 4px

positive lookbehind

匹配具体值是 1 或具体值是 3 的单位:

/(?<=1|3)[a-z]{2}/g:1pt 2px 3em 4px

negative lookbehind

匹配具体值不是 1 也不是 3 的单位:

/(?<!1|3)[a-z]{2}/g:1pt 2px 3em 4px

条件 (conditional)

根据条件是否成立匹配两个选项之一,条件有反向引用条件和环视条件两种。只有一部分正则表达式实现支持条件。

反向引用条件的语法:(?(backref)true|false),其中 backref 是反向引用,反向引用的子表达式匹配成功则匹配选项 true 的内容,否则匹配选项 false 的内容。

例如,匹配合法的北美电话号码 (只有前两个是合法的):

plaintext
123-456-7890 (123)456-7890 (123)-456-7890 123)456-7890 1234567890 123 456 7890

正则表达式 /\(?\d{3}(-|\))\d{3}-\d{4}/g 可以匹配到前两个,但是第四个不合法的也会被匹配。这个问题的关键在于开头匹配到左括号时,第三个数字后应该匹配右括号而不是连字符。

正则表达式 /(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}/g 可以符合预期只匹配前两个。其中 (?(1)\)|-) 就是条件匹配,?(1) 检查反向引用的子表达式是否匹配,反向引用编号在条件中不需要转义,所以正确写法是 (1) 而不是 (\1)。如果子表达式匹配,那么此处匹配 \),否则匹配 -

环视条件,允许根据环视操作是否成功来决定执行的分支。语法与反向引用条件类似,只需要把反向引用改为环视表达式即可:(?(?=...)true|false)

例如,匹配合法的美国邮政编码 (只有第三个不合法):

plaintext
11111 22222 33333- 44444-4444

正则表达式:/\d{5}(?(?=-)-\d{4})/g(?(?=-)-\d{4}) 是环视条件匹配,表示满足条件“主表达式后是 -”则匹配 -\d{4},这里没有 else 内容,因为 else 内容是可选的。

实例

IPv4 地址

regex
(((25[0-5])|(2[0-4]\d)|(1\d{2})|(\d{1,2}))\.){3}((25[0-5])|(2[0-4]\d)|(1\d{2})|(\d{1,2}))

参考

  1. 《正则表达式必知必会 (修订版)》
  2. 正则表达式三十分钟入门教程
  3. regex101: build, test, and debug regex
  4. RegExr: Learn, Build, & Test RegEx
  5. Regulex:JavaScript Regular Expression Visualizer
  6. 正则表达式 - 现代 JavaScript 教程
正则表达式
作者
Yaowei Zou
发布日期
2024-08-24
许可协议
转载或引用本文时请遵守许可协议,注明出处,不得用于商业用途!