正则表达式入门

正则表达式

Posted by xv_rong on August 12, 2021

元字符

代码 说明
. 匹配 换行符 以外的任意字符
\w 匹配 字母 数字 下划线 汉字
\s 匹配 任意空白符
\d 匹配 数字
^ 匹配 字符串的开始
$ 匹配 字符串的结束

转义字符

匹配元字符本身时使用

重复匹配

代码 说明
+ 匹配 1次或多次
* 匹配 0次或多次
匹配 0次或1次
{n} 匹配 n次
{min / max} 匹配 最少min次, 最多max次
{min, } 匹配 最少min次

字符类

代码 说明
[] 匹配 任意方括号中的字符
[aeiou] 匹配 元音字母
[0-9] 相当于 \d
[a-z0-9A-Z_] 相当于\w

反义

代码 说明
\W 匹配 不是\w的字符
\S 匹配 不是空白符的字符
\D 匹配 非数字的字符
\B 匹配 不是单词开头或者结束的位置
[^x] 匹配 除了x以外的任意字符
[^aeiou] 匹配 除了aeiou这几个字母以外的任意字符

分支条件

代码 说明
| 将不同的匹配条件分隔 存在短路现象

分组

代码 说明
() 创建一个括号中的子表达式

后向引用

组号分配

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推

实际上

  • 分组0对应整个正则表达式
  • 实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号
  • 你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权

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

\b(\w+)\b\s+\1\b 可以用来匹配重复的单词,像go go, 或者kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?<Word>\w+)(或者把尖括号换成’也行:(?’Word’\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容,你可以使用\k<Word>,所以上一个例子也可以写成这样:\b(?<Word>\w+)\b\s+\k<Word>\b。

分类 代码 说明
捕获 (exp) 匹配exp, 并捕获文本到自动命名的组里
捕获 (?exp) 匹配exp, 并捕获文本到名称为name的组里,也可以写成(?’name’exp)
捕获 (?:exp) 匹配exp, 不捕获匹配的文本,也不给此组分配组号
零宽断言 (?=exp) 匹配exp前面的位置
零宽断言 (?<=exp) 匹配exp后面的位置
零宽断言 (?!exp) 匹配后面跟的不是exp的位置
零宽断言 (!<!exp) 匹配前面不是exp的位置
注释 (?#comment) 注释

零宽断言

代码 说明
\b 匹配位置 它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w。
^ 字符串的开始
$ 字符串的结束
(?=exp) 零宽度正预测先行断言, 断言自身出现的位置的后面能匹配表达式exp
(?<=exp) 零宽单正回顾后发断言, 断言自身出现的位置的前面能匹配表达式exp

负向零宽断言

代码 说明
(?!exp) 零宽度负预测先行断言, 匹配 后面跟的不是exp的位置
(!<!exp) 零宽度负回顾后发断言, 匹配 前面不是exp的位置

注释

小括号的另一种用途是通过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249) 250-5 [01]?\d\d?(?#0-199)。

要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:

      (?<=    # 断言要匹配的文本的前缀
      <(\w+)> # 查找尖括号括起来的内容
              # (即HTML/XML标签)
      )       # 前缀结束
      .*      # 匹配任意文本
      (?=     # 断言要匹配的文本的后缀
      <\/\1>  # 查找尖括号括起来的内容
              # 查找尖括号括起来的内容
      )       # 后缀结束

贪婪与懒惰

  • 默认贪婪

  • 在数量限定符之后加上? 开启懒惰

  • 比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

代码 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或多次, 但尽可能少重复
?? 重复0次或多次, 但尽可能少重复
{n, m}? 重复n到m次, 但尽可能少重复
{n, }? 重复n次以上, 但尽可能少重复

处理选项

名称 说明
IgnoreCase(忽略大小写) 匹配时不区分大小写。
Multiline(多行模式) 更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$​的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式) 更改.的含义,使它与每一个字符匹配(包括换行符\n)。
IgnorePatternWhitespace(忽略空白) 忽略表达式中的非转义空白并启用由#标记的注释。
ExplicitCapture(显式捕获) 仅捕获已被显式命名的组。

单行模式和多行模式并不冲突

平衡组/递归匹配

有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构,这时简单地使用(.+)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如( 5 / ( 3 + 2 ) ) ),那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的内容呢?

代码 说明
(?’group’exp) 将捕获的内容命名为group, 并压入堆栈
(?‘-group’exp) 从堆栈中弹出exp, 否则匹配失败
(?(group)yes|no) 如果堆栈中存在group的话, 继续匹配yes部分的表达是, 否则继续匹配no部分
(?!) 零宽负向先行断言,由于没有后缀表达式, 试图匹配总是失败

xx <aa <bbb> <bbb> aa> yy这样的字符串里,最长的配对的尖括号内的内容捕获出来

<                   #最外层的左括号
  [^<>]*            #它后面非括号的内容
  (
      (
        (?'Open'<)  #左括号,压入"Open"
        [^<>]*      #左括号后面的内容
      )+
      (
        (?'-Open'>) #右括号,弹出一个"Open"
        [^<>]*      #右括号后面的内容
      )+
  )*
  (?(Open)(?!))     #最外层的右括号前检查
                    #若还有未弹出的"Open"
                    #则匹配失败

>                #最外层的右括号

平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的<div>标签:

<div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div>

其他

代码 说明
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII代码中八进制代码为nn的字符
\xnn ASCII代码中十六进制代码为nn的字符
\unnnn Unicode代码中十六进制代码为nnnn的字符
\cN ASCII控制字符。比如\cC代表Ctrl+C
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode中命名为name的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?-exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes|no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes|no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
(?(name)yes) 同上,只是使用空表达式作为no