正则表达式(一)

本文译自 制作正则引擎的作者 Jan Goyvaerts 为工具 RegexBuddy 写的教程
版权归原作者所有
注意:本页面存在bug,有的字符被过滤。
表达式测试网站 https://regex101.com/

正则表达式

正则表达式是一种用来描述一定数量文本的模式。
RegEx 代表 Regular Express

正则表达式引擎

是一种可以处理正则表达式的软件(通常引擎是更大的应用程序的一部分)

本教程集中讨论应用最广泛的 Perl 5 类型的引擎。对比其他引擎的区别。

近代引擎都很类似但不完全相同 如 .NET 正则库 JDK 正则包

文字符号

最简单的正则表达式由单个文字符号组成。
a将匹配字符串中第一次出现的字符a
匹配字符串Jack is a boy
结果:J后的a将被匹配。而第二个a将不会被 匹配。

正则表达式也可以匹配第二个a,这必须是你告诉正则表达式引擎:从第一次匹配的地方开始搜索。
(相当于文本编辑器中的查找下一个。也相当于编程中有一个函数从前一次匹配的位置开始继续向后搜索。)

cat会匹配About cats and dogs中的cat
等于是告诉正则表达式引擎:
找到一个c,紧跟一个a,再跟一个t

正则表达式引擎默认大小写敏感
除非你告诉引擎忽略大小写,否则cat 不会匹配Cat

元字符(metacharacter)

对于文字字符,有 12 个字符被保留作特殊用途,即 元字符:
[ ]\^ $. |? *+ ()

在正则表达式中,如果要将这些字符用作无特殊含义的文本字符
用反斜杠对其进行转义 (escape)

例如
表达式为1\+1=2
匹配1+1=2

注意正则表达式1+1=2是有效的 +表示重复 1 次到多次
它不会匹配文本1+1=2
会匹配文本123+111=234中的111=2

在编程语言中,一些特殊的字符会先被编译器处理,然后再传递给正则引擎。

#因此正则表达式
1\+2=2

#在C++中要写成
1\\+1=2
#为了匹配到文本 C:\temp
#正则表达式 C:\\temp
#C++中的正则表达式 C:\\\\temp

不可显示字符

某些不可显示字符 使用它对应的 特殊字符序列

 \t          Tab        0x09
 \r         回车符     0x0D
 \n         换行符      0x0A

#注意
Windows 中文本文件结束一行用\r\n
Unix 使用\n

正则表达式引擎的内部工作机制

正则表达式引擎是如何工作的有助于你很快理解为何某个正则表达式不像你期望的那样工作。

有2种类型的引擎:

  • 文本导向(text-directed)的引擎
  • 正则导向(regex-directed)的引擎 (目前最流行,本文谈的就是正则导向的引擎)

Jeffrey Friedl 把他们称作 DFA 和 NFA 引擎。

因为一些非常有用的特性只能在正则导向的引擎中实现:

惰性量词(lazy quantifiers)
反向引用(backreferences)

通过测试 判断 某引擎 文本导向/正则导向:
如果 反向引用 或 惰性量词,被实现则引擎必定是正则导向
正则表达式regex|regex not
对字符串regex not进行匹配,得到结果:
结果1 regex not 则 该引擎是文本导向的。
结果2 regex则 该引擎是正则导向的。正则导向的引擎总是很急切地报告它找到的第一个匹配

正则导向的引擎 -> 急切的返回第1个(即最左边)匹配结果

这是需要你理解的很重要的一点,即使以后有可能发现更好的匹配,正则导向的引擎也总是返回最左边的匹配。


正则表达式cat
文本He captured a catfish for his cat

详细匹配过程:
引擎先比较正则表达式符号cH,匹配失败;
于是引擎再比较正则表达式符号ce,匹配失败;
... 直到匹配第4个字符c,匹配成功;
正则表达式符号a匹配第5个字符a,匹配成功;
第六个字符t匹配p,匹配失败;
引擎继续从第5个字符重新检查匹配性
...直到第15个字符开始,cat匹配上了catfish 中的cat
就此结束。正则表达式引擎急切返回第1个匹配结果,而不会再继续向后查找(无论是否有其他文本可以被匹配

字符集

字符集是由一对方括号[]括起来的字符集合。
使用字符集,告诉正则表达式引擎仅匹配多个字符中的一个。

使用正则表达式[ae]
可匹配一个a或一个e

字符集中的字符顺序并没有意义,结果都是相同的。

使用正则表达式gr[ae]y或等价的gr[ea]y
都可匹配 gray 或 grey (在你不确定待匹配的文本是 美/英 英语时特别有用)
都不会匹配 graay 或 graey

用带连字符-的字符集 定义字符范围
如 字符集 [0-9]匹配 0 到 9 之间的某个单个数字。

使用多个范围的字符集

匹配某个单个的十六进制的字符 且 大小写不敏感
[0-9a-fA-F]

同时使用:结合范围定义 与 单个字符定义

[0-9a-fxA-FX]

匹配某个单个的十六进制的字符 且 大小写不敏感,或字符X
再次强调,字符和范围定义的先后顺序 对结果没有影响。

字符集的一些应用

查找一个可能有拼写错误的单词
比如sep[ae]r[ae]teli[cs]en[cs]e

查找程序语言的标识符
A-Za-z_][A-Za-z_0-9]*
*表示重复 0 或多次

查找 C 风格的十六进制数
0[xX][A-Fa-f0-9]+
+表示重复(上一个字符或表达式)一次或无数次

取反字符集

在左方括号[后面紧跟一个^将会对字符集取反,这样的正则表达式表示将匹配任何不在方括号中的字符。

注意 取反字符集一定会要 匹配一个字符,可以匹配回车换行符

例1
匹配一个 q,后面跟着一个非 u 的字符

正则表达式 q[^u]
文本

ffffq
fffff

结果
q 和一个换行符 (非 u 的字符回车换行符 是匹配结果中的一部分)

例2
正则表达式:q[^u]
文本: Iraq
进行匹配 得不到任何结果
因为q后没有任何字符!单个q不会被匹配

如果只想匹配某些条件下的 q字符,如条件是 q 后面有一个非 u 的字符时才匹配这个q字符,则用后面讲到的向前查看

字符集中的元字符

注意,在字符集中只有 4 个 元字符] \ ^ -

]代表字符集定义的结束
\代表转义
^代表取反
-代表范围定义

除了这4个元字符外的其他常见元字符,在字符集定义内部都是文本字符,不需要转义(如果对那些通常的元字符进行转义,正则表达式一样会工作得很好,但会降低人类可读性)

例,要匹配文本 星号*或加号+

正则表达式 [+*]

在字符集定义中,为了将反斜杠作为一个文字字符,而非特殊含义的字符,你需要用另一个反斜杠对它进行转义。

正则表达式[\\x]
会匹配到文本\x

元字符]^-都可以用反斜杠进行转义,或将它们放在一个不可能使用到他们特殊含义的位置(这样可以增加可读性)

比如对于字符^ 将它放在非左括号[后面的位置,使用的是文本字符含义,而非取反含义。

[x^]会匹配一个字符 x^
[]x]会匹配一个字符 ]x
[-x][x-] 会匹配一个-x

字符集的简写【记忆】

很常用的字符集有简写方式

\d代表数字[0-9]
\w代表单词字符,随正则表达式实现的不同,有些差异。绝大多数的正则表达式实现的单词字符集都包含了A-Za-z0-9_

\s代表空白字符
随正则表达式实现的不同而有些差异。在绝大多数的实现中,都包含了空格符和 Tab 符,以及回车换行符\r\n

方括号内外都可以用字符集的缩写形式
\s\d匹配 某单个白字符后面紧跟一个数字
[\s\d]匹配 某单个白字符或数字
[\da-fA-F]匹配一个十六进制数字

取反字符集的简写形式

[\S][^\s]

[\W][^\w]

[\D][^\d]

字符集的重复

如果用?*+操作符来重复一个字符集,将会重复整个字符集(而不是它匹配的那个字符)


正则表达式[0-9]+
会匹配文本 837 以及 222

如果你仅仅想重复被匹配的那个字符,可以用向后引用。以后说

使用?*或+ 进行重复

?
告诉引擎匹配前导字符 0 次或一次。事实上是表示前导字符是可选的。

告诉引擎匹配前导字符 1 次或多次

告诉引擎匹配前导字符 0 次或多次

<[A-Za-z][A-Za-z0-9]*>

匹配没有属性的 HTML 标签,<以及>是文字符号。
第一个字符集匹配一个字母,第二个字符集匹配一个字母或数字。

  • 代表匹配前导字符0或无数次,0次时可以匹配像<p>这样的标签。

我们似乎也可以用正则表达式 <[A-Za-z0-9]+>
但它会匹配<1>

限制重复次数

对前导子表达式重复多少次 语法{min,max}
min 和 max 都是非负整数(整数min>=0 整数max>=0)

例1
{0,1} 对前导子表达式重复0次 或 1次

{1} 对前导子表达式重复1次 如果逗号和max都省略时则重复 min 次

{0,}等价于* 有逗号 不写max值 表示 max值 无限大
{1,}等价于+

例2
正则表达式zo+能匹配zozoo. 但不能匹配z

正则表达式\b[1-9][0-9]{3}\b (\b表示单词边界)
匹配某个 1000~9999 之间的数字。

正则表达式\b[1-9][0-9]{2,4}\b
匹配某个 100~99999 之间的数字。

注意: 懒惰指匹配结果尽可能短、贪婪是匹配结果尽可能长

懒惰的? 匹配尽可能短的文本
贪婪的+ 匹配尽可能长的文本


用一个正则表达式匹配一个 HTML 标签。
输入一个正常有效的 HTML 文件(不需要排除无效标签),所以如果是在两个尖括号之间的内容,就应该是一个 HTML 标签。

贪婪的正则表达式<.+> 匹配结果尽可能长

许多新手用表达式<.+>
测试字符串This is a <EM>first</EM> test
得到匹配结果<EM>first</EM>很显然不是我们想要的结果。
我们期望得到结果<EM> 继续进行匹配 得到</EM>

因为+是贪婪的 即匹配尽量长的文本!
+会导致正则表达式引擎试图尽可能的重复它前面的那个字符(只有当这种重复会引起整个正则表达式匹配失败的情况下,引擎会进行回溯,即放弃最后一次的重复,然后处理正则表达式余下的部分)

+类似,?*的重复也是贪婪的。

详细匹配过程:
< 匹配字符<,匹配成功;
.匹配了字符E,匹配成功;
+重复上一个(表达式符号),即表达式. 点号 可以一直匹配接下来的字符,直到碰到换行符,.不匹配换行符则.匹配失败,即+匹配失败;
此时已匹配到的文本为<EM>first</EM> test
引擎对正则表达式最后一个符号>进行匹配,试图将>与换行符进行匹配,>匹配失败;
此时已匹配到的文本为<EM>first</EM> test
表达式最后一个符号>也匹配失败了,引擎进行回溯(从向后匹配改为向前匹配)
>试图与t匹配,匹配失败;
>继续往前匹配,继续失败..
直到遇见<EM>first</EM后面的字符>,匹配成功!

第1个匹配结果为
<EM>first</EM>

正则导向的引擎是急切的,所以它会急着报告它找到的第一个匹配。而不是继续回溯(所以匹配不到<EM>了)
能看出+的贪婪性使正则表达式引擎得到了一个“最长”的匹配结果。

可行方案1 使用“懒惰的”正则表达式<.+?>匹配标签

在+后面紧跟一个问号?实现懒惰性(匹配尽量短的文本)
*``{}``?表示的重复也可以用类似的方案,使贪婪性变成懒惰性,匹配尽量短的文本。

? 匹配尽量短的文本
+? 匹配尽量短的文本!实现了"懒惰性" 带上问号的+是懒惰的!
+为了尽可能少地重复上一个字符. 所以匹配成功后立即用下一个正则表达式符号进行匹配

正则表达式<.+?>详细匹配过程:
正则表达式符号.匹配文本中的下一个字符E 匹配成功;
懒惰的+?为了尽快得到匹配结果,用下一个正则表达式符号>匹配文本中下一个字符M,匹配失败(引擎进行回溯)
用上一个正则表达式字符.匹配当前字符M,匹配成功;
此时正则表达式进行到<.+
此时已匹配到的文本为<EM

懒惰的+?为了尽快得到匹配结果,立即用下一个正则表达式符号>匹配文本中下一个字符>匹配成功;
正则表达式结束
引擎报告得到一个成功的匹配<EM>

更好的替代方案:取反字符集

正则导向的引擎
用一个取反字符集 跟 一个贪婪重复 加号<[^>]+>
之所以说这是一个更好的方案,因为使用取反字符集,不需要进行回溯
在于使用惰性重复时,引擎会在找到一个成功匹配前对每一个字符进行回溯。而

使用.匹配任意字符 (除了换行符)

在正则表达式中.是最常用也最容易被误用的符号。

.匹配任意单个字符(唯一不匹换行符)
在本教程中谈到的引擎,默认都不匹配换行符

默认.等价于
字符集[^\n\r](Windows)
字符集[^\n](Unix)

不匹配换行符是因为历史的原因:
早期使用正则表达式的工具是基于行的。它们都是一行一行的读入一个文件,将正则表达式分别应用到每一行上去。
所以每一行的字符串里当然没有换行符。
所以.不匹配换行符。
现代的工具和语言能够将正则表达式应用到很大的字符串,甚至整个文件,所有正则表达式实现都提供一个选项:可让.匹配所有的字符(包括换行符)
RegexBuddy,EditPad Pro 或 PowerGREP 等工具中,你可以简单的选中点号匹配换行符
在 Perl 中,.可以匹配换行符的模式被称作单行模式,这是一个很容易混淆的名词。因为还有多行模式
多行模式只影响行首行尾的锚定(anchor),而单行模式只影响.
其他语言和正则表达式库也采用了 Perl 的术语定义。
当在.NET Framework 中使用正则表达式类时,可用类似下面的语句来激活单行模式:
Regex.Match(string,regex,RegexOptions.SingleLine)

强大的点号.

点号可以说是最强大的元字符:用一个点号,匹配几乎所有的字符。
问题是 它也常常会匹配不该匹配的字符!

例子
匹配一个具有mm/dd/yy格式的 月/天/年 日期
允许用户来选择分隔符。

不可行方案1
\d\d.\d\d.\d\d
它能匹配日期02/12/03
问题是 02512703 也会被匹配

不可行方案2
\d\d[-/.]\d\d[-/.]\d\d
稍微好一点,支持3种分隔符。
(点号在一个字符集里,不作元字符)
这个方案远不够完善,它会匹配99/99/99

方案3
[0-1]\d[-/.][0-3]\d[-/.]\d\d
更好一点,尽管会匹配19/39/99

...

如果你想校验用户输入,则需要尽可能的完美。
如果你只是想分析一个已知的源,并且我们知道没有错误的数据,用一个比较好的正则表达式,来匹配你想要搜寻的字符就已经足够。

字符串开始的锚定^ 和 结束的锚定$

锚定和一般的正则表达式符号不同,锚定不匹配任何字符。
锚定匹配的是字符之前或之后的位置

正则表达式^会匹配一行字符串第一个字符前的位置
正则表达式^a会匹配字符串abc中的 a
正则表达式^b不会匹配abc中的任何字符

类似的,$匹配字符串中最后一个字符的后面的位置

#正则表达式
c$
#匹配文本abc中的c

锚定的应用

使用锚定在编程语言中校验用户输入 非常重要的

校验用户的输入为整数 用正则表达式^\d+$

匹配用户输入中常常存在的多余的前导空格或结束空格
^\s*匹配字符串前面的空格
\s*$匹配字符串末尾的空格

如果你有一个包含了多行的字符串 如
first line\n\rsecond line
(其中\n\r 表示一个换行符)

经常需要按行处理,而不是整个字符串。
几乎所有的正则表达式引擎都提供一个选项,可以扩展这两种锚定的含义。
^可以匹配字串的开始位置(在 f 之前),以及每一个换行符后面的位置(这个位置在\n\r 和 s 之间)
$会匹配字串的结束位置(最后一个 e之后),以及每个新行符之前的位置(这个位置在 e 与\n\r 之间)

在.NET 中使用如下代码定义锚定,匹配每一个换行符的前面和后面位置:

Regex.Match("string", "regex", RegexOptions.Multiline)


在每行的行首插入文本字符>

string str = Regex.Replace(Original, "^", ">", RegexOptions.Multiline)

绝对锚定

\A只匹配整个字符串的开始位置
\Z只匹配整个字符串的结束位置

即使你使用了多行模式,这两个也绝不匹配换行符。

例外
如果字符串以换行符(newline)结束,则\Z$将会匹配这个最后的换行符前面的位置,而不是整个字符串的最后面。
这个改进是由 Perl 引进的,然后被许多的正则表达式实现所遵循,包括 Java,.NET 等。


正则表达式:^[a-z]+$
文本:joe\n
匹配结果:joe而不是joe\n

推荐阅读更多精彩内容