正则表达式简单入门

正则表达式(Regular Expression),是由一些特定字符及其组合所组成的字符串表达式,用来对目标字符串进行匹配、查找操作。

对于一些有规律的字符串匹配操作需求,例如特定网址、手机号码、生物信息等符合一定规律的字符串,无法用简单的判断表达式涵盖,而用正则表达式可以简洁、准确地表达其组成规律,从而高效地进行匹配操作。

基本匹配语法

在一个正则表达式中,一些普通文本用途就是代表实际需要匹配的字符。但不同于编程语言常见的字符串查找,正则表达式提供了特殊字符与结构,可以表达更抽象的字符串结构。

基本特殊字符

使用 \d 可以匹配一个数字:

"12\d"

  • 可以匹配 "124"
  • 不能匹配 "12a" "12G" "12>" "12我"

使用 \w 可以匹配一个字母或数字,但不匹配符号和汉字:

"ca\w"

  • 可以匹配 "cat" "ca4"
  • 不能匹配 "ca?" "ca字"

使用点号 . 可以匹配除换行符外的任意一个字符(包括数字、符号、中文):

".txt"

  • 可以匹配 ".txt" "xtxt" "1txt" ">txt" "这txt"
  • 不能匹配单独的 "txt"

限定匹配

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。

使用星号 * 可以匹配前一个字符 0 次、1 次或多次:

"lon*g"

  • 可以匹配 "log" "long" "lonng" "lonnnnnng"

使用加号 + 可以匹配前一个字符 1 次或多次:

"hi+"

  • 可以匹配 "hi" "hii" "hiiiiii"
  • 不能匹配 "h"

使用问号 ? 可以匹配前一个字符 0 次或 1 次:

"colou?r"

  • 可以匹配 "color" "colour"
  • 不能匹配 "colouur"

还可以进一步指定匹配的次数:

使用 {n} 可以将前一个字符匹配 n 次:

"hel{2}o"

  • 只能匹配 "hello"

使用 {n,} 可以将前一个字符匹配 n 次,或任意比 n 多的次数:

"no{3,}"

  • 可以匹配 "nooo" "noooo" "nooooo" ,或 n 后面跟上更多的 o
  • 不能匹配 "noo" "no"

使用 {n,m} 可以将前一个字符匹配 n ~ m 次,包含 nm
"o{2,6}h"

  • 可以匹配 "ooh" "ooooh" "ooooooh"
  • 不能匹配 "oh"
  • 当 o 数量超过6个时,只能匹配最后的 "ooooooh"

除了指定次数,还可以指定字符范围:

方括号对 [] 用来指定一个字符,方括号内为该字符的限定条件:

使用 […] 表示字符范围,可以匹配方括号内的所有字符。一对方括号只能表示一个字符:

"b[aeiou]d"

  • 只能匹配 "bad" "bed" "bid" "bod" "bud"

使用 [^…] 表示字符范围,可以匹配除方括号内字符的其余字符。一对方括号只能表示一个字符:
"n[^ot]r"

  • 可以匹配 "nnr" "nar" "n1r" "n我r" "n<r"
  • 只不能匹配 "nr" "nor" "ntr"
  • 如果要匹配 "n^r" ,请不要加上方括号,或将 ^ 号移到方括号的其它位置

特别地,字符 . 在方括号对内只代表 . 字符,相当于 \. ,而非匹配任意字符。

类似 [0-9][a-z][A-Z] 表示字符范围,可以匹配某个区间的所有数字或字母,包含区间两端。

减号 - 两端必须同时是数字、小写字母或大写字母,并且是按从小到大或字母表顺序排列(小数字或靠前的字母在前,大数字或靠后的字母在后),否则它们不会匹配到任何结果。

"[U-Z][0-3][a-g]"

可以匹配 "U3a" "Z2f" "Y0c"
不能匹配 "A0c" "G3k" "u0g" "C9f"

实际上 \d 等价于 [0-9]\w 等价于 [a-zA-Z0-9_]

定位符号

某些符号可以用来限定位置,用于匹配特殊位置上的字符,而不能匹配一般位置上的同一字符。这样的符号一般称为定位符锚点

使用 ^ 号用在方括号对 [] 以外的其它地方,用来表示一行的开头位置,将匹配开头位置:

"^12"

  • 只能匹配开头位置的 "12"
  • 不能匹配其余位置的 "12"

注意:要匹配开头位置,^ 号应该位于字符串最前面,否则自相矛盾。

特别地,如果还希望能匹配上一行的字符,请在 ^ 号前使用换行符 \n

其等价于转义字符 \A


使用 $ 号用来表示行末位置,将匹配一行的末尾:

"that$"

  • 只能匹配行末位置的 "that"
  • 不能匹配其余位置的 "that"

该符号同样应该位于字符串末尾。如果还希望能匹配下一行的字符,请在 $ 号后使用换行符 \n

其等价于转义字符 \Z

^\A$\Z 是有区别的,当它们在“多行模式”下的匹配方式不同。可以参考后续介绍的匹配模式

使用 \b 匹配一个单词的边界。如果它位于要匹配的字符串的开始,它在单词的开始处查找匹配项。如果它位于字符串的结尾,它在单词的结尾处查找匹配项:

"\bas"

  • 可以匹配以 as 开始的单词,例如 "assert" 中的 as
  • 不能匹配在单词中或单词末尾的 as ,例如 "Las Vegas" 中的两个 as

所谓“单词的边界”与空白符有一定区别:单词的边界可能是一行的开始,即便它前面没有任何空白或换行符。单词的边界还可能是一篇文章的末尾,尽管它后面没有任何空白、换行符、换页符等。除此之外,单词的边界还可以是一个符号:

"\bemphasis\b"

  • 可以匹配 "(emphasis)" "Warning:emphasis." 中的 emphasis
  • 不能匹配 "3emphasis" 中的 emphasis

捕获与引用

使用圆括号 () 可以指定一个子表达式,用来对表达式包含的内容进行区分。

使用单个竖线 | 代表“或”,代表该符号两侧的表达式都可以进行匹配:

"P(ython|HP)"

  • 只能匹配 "Python" "PHP"

在匹配过程中,为了不至于引起表意不清,请尽量使用圆括号 (…) 将子表达式括起来。

圆括号还有一个用途:默认情况下,圆括号会将表达式进行分组,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志(嵌套的表达式也是),第一个出现的分组的组号为 1 ,第二个为 2 ,以此类推。

特别地,第 0 组代表整个表达式捕获的内容本身。


后向引用使用转义的数字用于重复搜索前面某个分组匹配的文本。例如,\1 代表分组 1 匹配的文本。

"\b(\w+)\b\s+\1\b"

用来匹配重复的单词,其规则为:分组 1 必须要由至少一个字符组成,其组前和后必须为单词的边界(如空格、换行符等,或者什么也没有);在经历了若干空格后,应该出现一个分组 1 对应的单词,并且该单词后应为单词的边界。换句话说,它匹配两个重复的单词。

  • 可以匹配 "是 是" " go go"
  • 不能匹配 "age ago" "mere merely"

如果使用 \0 则引用整个被匹配的正则表达式本身。

分组引用一般用于匹配对称、重复但不确定的字符串,如 HTML 的开始和结束标签。

注意:

  1. 引用不能引用其自身,如 "([a-z]\1)" 是错误的,它匹配不到任何结果。同样,\0 代表正则表达式匹配本身,所以不能在正则表达式中引用,只能用于之后的其余操作中,如替换等。
  2. 引用不能用于方括号内的字符集内部,如 "[\1b]" 是不恰当的,其引用 \1 会被正则表达式解释为八进制转码。
  3. 由于圆括号匹配到的内容可能是不确定的,所以当圆括号没有匹配到任何内容时,其引用也无效,即引用的内容为空。
  4. 当对组使用重复操作符时,缓存里的引用内容会被不断刷新,只保留最近匹配的项目。例如 "([abc]+)=\1" 可能匹配 "cab=cab" ,但是 "([abc])+=\1" 则不匹配 "cab=cab" ,这是由于 ([abc]) 第一次匹配到 "c" 时,"\1" 代表 "c" ,然后 ([abc]) 会继续匹配 "a""b" ,最后缓存刷新的结果为 "\1" 代表 "b" ,所以它会匹配 "cab=b"

也可以使用 (?P<name>exp) 自己指定子表达式的组名,这样就把表达式 exp 的组名指定为 name 了。要反向引用这个分组捕获的内容,可以使用 (?P=name)

所以上一个例子也可以写成这样:

"\b(?P<repeat>\w+)\b\s+(?P=repeat)\b"

但用圆括号会有一个副作用,使相关的匹配会被缓存,造成匹配速度偏慢。若不需要给捕获的表达式分组,可以使用 (?:exp) ,它会正常匹配表达式 exp ,但不会给此分组分配组号。

"C(?:12|34)"

  • 会匹配所有的 "C12""C34" ,并且不将 "12""34" 分组。

当拥有了一个组名或者组编号时,可以使用 (?(id/name)yes|no) 来表示一个 yes/no pattern

其规则为:如果 id/name 对应的组成功匹配,则继续匹配 yes 部分对应的正则表达式;如果 id/name 对应的组匹配失败,则会匹配 no 对应的表达式。

"(?Pa candy)?(?(group) is true| is false)"

  • 如果成功匹配到了 "a candy" ,则会继续匹配到 "a candy is true"
  • 如果没有匹配到 "a candy" ,由于组括号后存在一个 ? ,它会允许该组匹配的结果为 0 次,转而匹配 " is false"

"(a candy)?(?(1) (makes me) happy|unhappy)"

如果成功匹配到了 "a candy" ,则会继续匹配到 "a candy makes me happy" ,其中 "a candy" 是组 1,"makes me" 是组 2。
如果没有匹配到 "a candy""a candy makes me" ,则会直接匹配 "unhappy"

从上述例子可以看出,使用 yes/no pattern 有一个细节就是要在组括号后使用 ? 限定符来表示当没有匹配成功时可以忽略该组,从而正确地匹配到 no pattern 的表达式。

非捕获元

非捕获元会被匹配,但不会被分到实际捕获的组内(实际不占用捕获的字符),它主要用于定位。

正向零宽断言的格式为 exp1(?=exp2) ,用来匹配并捕获 exp2 前面的 exp1

"https?(?=://)"

只能匹配 "http://""https://" 中的 "http""https"
不能匹配单独的 "http" ,即便在其余某处又出现了不紧邻的 "://"

反向零宽断言的格式为 (?<=exp2)exp1 ,用来匹配并捕获 exp2 后面的 exp1

(?<=<a>).+(?=</a>)

  • 可以匹配一个无属性的HTML <a> 标签中的内容
  • 不会匹配到标签本身,但会匹配到里面嵌套标签的完整内容
  • 一个细节可能导致匹配会造成出乎意料的结果,详见 贪婪匹配与懒惰匹配 的相关内容

正向否定零宽断言可以使用 exp1(?!exp2) 来匹配并捕获后面不是 exp2exp1

"name(?!error)"

  • 只不匹配 "nameerror" 中的 "name"
  • 对于其它的 "name" 都可以匹配

反向否定零宽断言可以使用 (?<!exp2)exp1 来匹配并捕获前面不是 exp2exp1

"(?<!eco)system"

  • 只不匹配 "ecosystem" 中的 "system"
  • 对于其它的 "system" 都可以匹配

在编写正则表达式时,也可以用 (?#comment) 代表注释。这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读:

"[0-9][a-g](?#between 0a~9g)"

提示并匹配第一位 0 到 9 ,第二位 a 到 g 的所有字符串。注释内的内容不参与匹配,不造成分组,不产生任何影响。

使用细节

特殊字符

特殊字符为正则表达式中出现的,用来表示匹配规律的词。为了使在匹配过程中能将其当做一个字符而不是表达式进行匹配,必须要用反斜杠 \ 进行转义。

以下为常见的特殊字符:

(){}[]$^*+?.|\

除此之外,某些普通字符一旦被转义,含义与原先也截然不同。这点也很好理解,比如常见的字符 n 在大多数语言中的转义结果都是换行符 \n


非打印字符指的是肉眼不可视,却又因为排版原因而必要存在的字符。为了使用非打印字符,必须用\进行转义。

以下为常见的非打印字符:

\cX匹配由 X 指明的控制字符。例如,\cM 匹配一个 Control-M 或回车符。X 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\xNN匹配十六进制数字 NN 对应的字符
\n匹配一个换行符。等价于 \x0a\cJ
\f匹配一个换页符。等价于 \x0c\cL
\r匹配一个回车符。等价于 \x0d\cM
\s匹配任何空白字符,包括空格、制表符、换行、换页符等等。等价于表达式 [ \f\n\r\t\v](注意空格)。另外注意使用 Unicode 的正则表达式会匹配全角空格符。
\t匹配一个制表符。等价于 \x09\cI
\v匹配一个垂直制表符。等价于 \x0b\cK
\u匹配一个 Unicode 字符

有时需要查找不属于某个能简单定义的字符类的字符,这时需要用到转义字符的反义

\W匹配任意不是字母,数字,下划线,汉字的字符,等价于表达式 [^\w]
\D匹配任意非数字的字符,等价于表达式 [^\d]
\S匹配任何非空白字符,等价于表达式 [^\s]
\B匹配不是单词开头或结束的位置,等价于表达式 [^\b]

也就是说,将这些字母变成大写,就代表对应的不匹配。

另外,匹配汉字的正则表达式为:

"[\u4e00-\u9fa5]"

在支持 Unicode 编码的环境中,也可以直接使用汉字精确匹配。

运算符优先级

在正则表达式中,运算符的优先级别从最高到最低为:

  • 转义符 \ 的优先级别最高,即默认字符最先发生转义
  • 方括号 [] 和各种圆括号 ()(?:)(?=) 等。因此为了表意清晰,最好多使用括号。
  • 六种限定符,分别是 *+?{n}{n,}{n,m}
  • 任何字符和表达任意单个字符的匹配项,如 ^$ 和任意转义后的字符
  • | ,即“或”逻辑字符。任意用该字符隔开的表达式都是一个完整的匹配项

匹配模式:贪婪与懒惰

精确匹配

在正则表达式中,匹配普通字符、普通字符族 […] 和转义字符 \x 的方式都是精确匹配:正则表达式会按照给定的字符串逐个检索字符,每当检索到一个字符在要匹配的字符内,则将该字符标记为匹配成功,并继续向后检索。

正则表达式匹配字符串的方式也是精确匹配:正则表达式会先匹配第一个字符,如果第一个字符匹配成功,则会继续向后逐个检查字符是否匹配。如果每个字符都匹配成功,则记录为一个成功匹配的字符串,并继续向下检索第一个字符,以此类推。

贪婪匹配

正则表达式除了精确匹配外,默认使用贪婪匹配。所谓贪婪匹配,指的是一种尽可能多的匹配字符的匹配模式。

正则表达式的六种限定符 *+?{n}{n,}{n,m} ,它们会尽可能多的匹配前一个字符。例如:

"10+"

  • 对于 1 后面跟着无论多少个零(没有除外),它会将 1 和后面的 0 全部匹配,而不是只匹配确定个 0

"<.+>"

  • 给定一个字符串 "<book><title>Python Programming</title></book>" ,它会匹配这个字符串全部,而不仅仅是第一个起始标签,因为整个字符串本身满足这个规则

其贪婪匹配的原理为:正则表达式会先匹配精确字符 "<" ,然后贪婪匹配字符 "." ,直至换行符 "\n" 不能匹配该字符,然后正则表达式开始从后向前匹配字符 "">" ,直至匹配成功,则它会将该范围内的字符全部匹配。

"is{3,6}"

  • 也是遵循这样的匹配方式,如果 i 后面跟着足够多的 s ,正则表达式也会匹配 i 和尽可能多的 s ,直至匹配满 6 个 s 的 "issssss"

懒惰匹配

贪婪匹配可能会导致过多的内容被匹配到,例如:

"(?<=href=").+(?=")"

本意是想匹配 href 属性内的值,但贪婪匹配的规则会让符号 . 一路匹配下去,遇到引号 " 也不不会停下,直到遇到段尾,为了满足语法再回溯找到遇到的最后一个引号 " 停在之前。可以预料到,后面几个属性的内容也会被匹配进去。

这就是懒惰匹配的用途。懒惰匹配与贪婪匹配相反,它是一种尽可能少的匹配字符的匹配模式。

懒惰匹配只针对六种限定符 *+?{n}{n,}{n,m} ,要使用懒惰匹配非常简单,只要在限定符后面再加上一个问号 ? 即可。

也就是说,六种限定符对应的懒惰匹配模式分别为:*?+???{n}?{n,}?{n,m}?

例如:

"20*?"

  • 对于 2 后面跟着无论多少个零,只要 2 后面有跟着 0 ,它会忽略后面的 0 ,只匹配 "2"

对于上述例子 "<book><title>Python Programming</book></title>" ,使用懒惰匹配模式:

"<.+?>"

  • 则只会匹配最先出现的 "<book>"

类似地:

"py{3,}?"

不管 p 后面跟着多少个 y ,只要 y 的个数不低于3个,那么正则表达式便会且仅会匹配最开始的 "pyyy"

修饰符

修饰符(modifier)可以改变正则表达式的一些规则,来适应不同的使用场景。

下表列举了比较广泛支持的一些修饰符:

m(多行模式)在许多编程语言里,^$ 只会给定匹配字符串的开始和末尾。多行模式可以让这两个字符匹配一行的开始和末尾。
i(不区分大小写)匹配英文字符时将不区分大小写
x(冗长模式)冗长模式专门用于将复杂的正则表达式表达得美观。这种模式下,会忽略正则表达式内的空格并将一行中 # 号及以后后的部分视为注释(除非使用转义符强制匹配),这样可以将一条正则表达式分为多个部分展示
s(单行模式)该模式下点号 . 会匹配所有字符,包括换行符
u(Unicode模式)该模式下会强制启用并使用 Unicode 理解下的字符。例如,使用拉丁字母会同时匹配所有变音的字符类,\d 会匹配全角数字(注意,一些使用Unicode字符串的编程语言会默认开启该模式,这是一个很隐秘的坑)

正则表达式原生支持如下两种修饰符的用法:

内联修饰符:格式为 (?flags-flags) ,作用于整个正则表达式对象,会启用减号前所有的修饰符,并停用减号后所有的修饰符(如果不需要停用,则减号可以忽略)。

局部内联修饰符:格式为 (?flags-flags:expression) ,用法类似,仅作用于括号内的 expression


以上就是正则表达式的基本用法。对于不同的编程语言,其构造方式、语法细节(如分组引用格式)、支持的修饰符都有部分不同。例如PHP支持正则表达式的递归,DotNet(C#)支持类似栈的平衡组,甚至能用来平衡左右两侧的符号。不管如何,在使用正则表达式之前,请参阅相关语言对正则表达式支持的相关文档。

正则表达式只是一个类似于XPath的工具,它在许多要处理文本的情况下都会用到,例如格式修改、数据清洗,甚至用在编译器的词法分析工具。正则表达式也在不断发展,变得越来越强大。

参考资料

https://regexr.com/

一个在线正则表达式测试工具,包含可视化的词法分析、语法参考,提供了一些常用正则表达式

https://regex101.com/

一个非常强大的在线正则表达式测试工具,它提供了详细的可视化词法分析、匹配组信息、语法参考、错误分析,可以使用替换甚至单元测试工具。并且它能支持不同的编程语言,还能为常用的编程语言直接生成相应的代码

https://www.regular-expressions.info/

一个非常详细的正则表达式参考网站,提供了包括入门、实现细节、使用示例、缺陷说明的完整的介绍,并提供了大部分支持正则表达式编程语言的特性对比,堪称正则表达式的百科全书

https://alf.nu/RegexGolf

一个有趣的在线正则表达式闯关游戏,只有匹配了规定的字符串才算通过,并会根据使用正则表达式的长度打分。

https://docs.python.org/3/library/re.html

Python3正则表达式语言参考

https://docs.microsoft.com/dotnet/standard/base-types/regular-expressions

DotNet正则表达式语言参考

https://developer.mozilla.org/docs/Web/JavaScript/Guide/Regular_Expressions

JavaScript正则表达式语言参考

https://www.cplusplus.com/reference/regex/

C++正则表达式语言参考

https://www.php.net/manual/ref.pcre.php

PHP正则表达式语言参考

京ICP备2021034974号
contact me by hello@frozencandles.fun