成为正则表达式大师的 20 个小步骤
“贪财是万恶之源……”
封面图片由msandersmusic从Pixabay提供
上述圣经经文经常被断章取义,开头几句被删去:“金钱是万恶之源”。完整的引文(如上所示)澄清了作者认为邪恶的并非金钱本身,而是对金钱的贪爱。前后几节经文强调,只要我们衣食无忧,还有什么可缺的呢?这短短的一段话是对贪婪的警示,同时也是对意图的教导,以及无意识事物固有的道德中立性。
一个常见的反对在编程中使用正则表达式(“RegEx”或简称“regex”)的俏皮话是 Jamie Zawinsky 的一句名言:
有些人遇到问题时会想:‘我知道,我会用正则表达式。’现在他们面临两个问题。 [来源]
……但使用正则表达式本身并无好坏之分。它本身不会增加你的问题,也不会解决任何问题。它只是一个工具。你使用它的方式(无论正确与否)决定了你会看到什么样的结果。如果你尝试使用正则表达式构建 HTML 解析器,你肯定会遇到麻烦。但如果你只是想从一些字符串中提取一些时间戳,那么应该没什么问题。
为了帮助您更好地掌握正则表达式,我整理了本教程,只需二十个小步骤,即可帮助您从零开始精通正则表达式。本指南主要侧重于正则表达式的基本概念,并仅在必要时深入探讨更复杂的主题/特定语言的扩展。
目录
步骤 1:正则表达式的用途
步骤 2:方括号[]
步骤 3:转义序列
步骤 4:“任意”字符.
步骤 5:字符范围
步骤 6:“非”插入符号^
步骤 7:字符类
步骤 8:星号*
和加号+
步骤 9:“可选”问号?
步骤 10:“或”管道|
步骤 11:用于捕获组的括号()
步骤 12:首先定义更具体的匹配
步骤 13:用于定义重复的花括号{}
步骤 14:,\b
零宽度边界字符
步骤 15:“行首”插入符号^
和“行尾”美元符号$
步骤 16:非捕获组(?:)
步骤 17:反向引用\N
和命名捕获组
步骤 18:前瞻和后瞻
步骤 19:条件
步骤 20:递归和进一步学习
步骤 1:正则表达式的目的
正则表达式用于在文本中查找模式。就是这样。这个模式可能很简单,比如这句话中的“dog”这个词:
The quick brown fox jumps over the lazy dog.
该正则表达式看起来像
dog
...很简单,是吧?
该模式也可以是任何包含 'o' 的单词。该正则表达式可能如下所示
\w*o\w*
您会发现,随着“匹配”要求的复杂化,正则表达式也变得越来越复杂。有一些额外的符号用于指定字符组和匹配重复的模式,我将在下面进行解释。
但是,一旦我们在文本中发现了某种模式,我们该如何处理呢?现代正则表达式引擎允许你从文本中提取这些子字符串,或者删除它们,或者用其他文本替换它们。正则表达式用于文本解析和操作。
我们可能会提取类似 IP 地址的信息,然后尝试 ping 一下;或者我们可能会提取姓名和电子邮件地址,并将它们存入数据库。又或者,我们可能会使用正则表达式来查找电子邮件中的敏感信息(例如社保号或电话号码),并提醒用户他们可能正在面临风险。正则表达式确实是一个功能多样的工具,易于学习,但难以精通:
“就像演奏好一首乐曲和创作音乐之间存在差异一样,了解正则表达式和真正理解正则表达式之间也存在差异。”
—— Jeffrey EF Friedl,《掌握正则表达式》
第二步:方括号[]
最容易理解的正则表达式是那些简单地寻找正则表达式模式和目标字符串之间的字符到字符匹配的表达式,例如:
pattern: cat
string: The cat was cut when it ran under the car.
matches: ^^^
但我们也可以使用方括号指定替代匹配:
pattern: ca[rt]
string: The cat was cut when it ran under the car.
matches: ^^^ ^^^
打开和关闭方括号告诉正则表达式引擎匹配指定的任意一个字符,但只能匹配一个。例如,上面的正则表达式不会按照您预期的方式执行以下设置:
pattern: ca[rt]
string: The cat was cut when it ran under the cart.
matches: ^^^ ^^^
使用方括号时,就是告诉正则表达式引擎匹配括号内包含的字符中的一个c
。如果引擎先找到一个字符,然后又找到一个a
字符,但下一个字符不是r
或t
,则不匹配。如果引擎先找到ca
,然后又找到 r
或t
,则停止匹配。它不会继续尝试匹配更多字符,因为方括号表示只应搜索括号内包含的字符中的一个ca
。如果引擎先找到,然后又找到r
中的cart
,则停止匹配,因为它在序列 上找到了匹配项car
。
流行测验:
你能写一个正则表达式来匹配这段话中的所有十had
和s吗?Had
pattern:
string: Jim, where Bill had had "had", had had "had had". "Had had" had been correct.
matches: ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^
以下句子中的所有动物名称怎么样?
pattern:
string: A bat, a cat, and a rat walked into a bar...
matches: ^^^ ^^^ ^^^
...或者仅仅是单词bar
和bat
?
pattern:
string: A bat, a cat, and a rat walked into a bar...
matches: ^^^ ^^^
你已经能够编写更复杂的正则表达式了,而我们才刚刚进行到第 2 步!让我们继续!
步骤 3:转义序列
在上一步中,我们学习了方括号[]
,以及它如何帮助我们为正则表达式引擎提供可选的匹配项。但是,如果我们想要匹配一对字面意义上的方括号呢[]
?
You can't match [] using regex! You will regret this!
当我们以前想要一个字符到一个字符的匹配时(比如单词cat
),我们只需准确输入这些字符:
pattern: []
string: You can't match [] using regex! You will regret this!
matches:
但这似乎不起作用。这是因为方括号字符[
和]
是特殊字符,通常用于表示除简单的字符间匹配之外的其他含义。正如我们在步骤 2中看到的,它们用于提供替代匹配,以便正则表达式引擎可以匹配它们包含的任意一个字符。如果您在它们之间没有插入任何字符,则可能会导致错误。
为了匹配这些特殊字符,我们必须在它们前面加上反斜杠字符 来转义\
它们。反斜杠字符是另一个特殊字符,它告诉正则表达式引擎按字面意思处理下一个字符,而不是将其视为特殊字符。通过在[
和字符前面都]
加上一个字符\
,正则表达式引擎将按字面意思匹配每个字符:
pattern: \[\]
string: You can't match [] using regex! You will regret this!
matches: ^^
如果我们想要匹配一个文字\
,我们可以在它前面加上第二个文字来逃避\
它:
pattern: \\
string: C:\Users\Tanja\Pictures\Dogs
matches: ^ ^ ^ ^
只有特殊字符前面才应加上 ,\
才能强制进行字面匹配。所有其他字符默认按字面解释。例如,以下正则表达式t
仅匹配字面小写t
字母:
pattern: t
string: t t t t
matches: ^ ^ ^ ^
但转义序列\t
完全不同。它匹配制表符:
pattern: \t
string: t t t t
matches: ^ ^ ^
其他常见的转义序列包括\n
(UNIX 风格的换行符)和\r
(用于 Windows 风格的换行符,\r\n
)。\r
是“回车”字符,\n
是“换行”字符,这两个字符都是在电传打字机仍然普遍使用时与 ASCII 标准一起定义的。
本教程后面将介绍其他常见的转义序列。
流行测验:
你能用这个正则表达式匹配\[\]
另一个正则表达式吗?你的目标应该是这样的:
pattern:
string: ...match this regex `\[\]` with a regex?
matches: ^^^^
你能匹配这个例子中的所有转义序列吗?
pattern:
string: `\r`, `\t`, and `\n` are all regex escape sequences.
matches: ^^ ^^ ^^
步骤 4:“任意”字符.
在编写解决方案以匹配我们目前为止看到的转义序列时,您可能一直在想......“我不能只匹配反斜杠字符,然后再匹配其后的任何其他字符吗?”嗯,你可以。
还有另一个特殊字符可用于匹配(几乎)任何字符,那就是句点字符.
。
pattern: .
string: I'm sorry, Dave. I'm afraid I can't do that.
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
如果您只想匹配看起来像转义序列的模式,您可以执行以下操作:
pattern: \\.
string: Hi Walmart is my grandson there his name is "\n \r \t".
matches: ^^ ^^ ^^
与所有特殊字符一样,如果要匹配文字.
,则需要在其前面加上一个\
字符:
pattern: \.
string: War is Peace. Freedom is Slavery. Ignorance is Strength.
matches: ^ ^ ^
步骤 5:字符范围
但是,如果您不想匹配任何字符,而只想匹配字母、数字或元音字母,该怎么办?字符类和范围可以帮助我们实现这一点。
`\n`, `\r`, and `\t` are whitespace characters, `\.`, `\\` and `\[` are not.
如果字符在文本中不产生任何可见的标记,则它们被称为“空白” 。空格符' '
、换行符或制表符都是空白。假设我们想要匹配上文中表示空白字符 、 和 的转义序列\n
,\r
而不\t
匹配其他转义序列。该怎么做呢?
pattern: \\[nrt]
string: `\n`, `\r`, and `\t` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^
这虽然可行,但不太优雅。如果我们稍后需要匹配“换页符”的转义序列怎么办\f
?(此字符用于指示文本中的分页符。)
pattern: \\[nrt]
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^
使用这种方法,我们需要在方括号内逐个列出要匹配的每个小写字母。更简单的方法是使用字符范围来匹配任意小写字母:
pattern: \\[a-z]
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^ ^^
字符范围的工作方式与上面的例子一致。将要匹配的首字母和末字母放在方括号中,中间用连字符连接。例如,如果您只想匹配到a
的字母,可以这样写:m
pattern: \\[a-m]
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^
如果要匹配多个范围,只需将它们连续放在方括号中:
pattern: \\[a-gq-z]
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^
其他常见字符范围包括:A-Z
和0-9
。
流行测验:
十六进制数可以包含数字0-9
和字母A-F
。用于指定颜色时,“十六进制”代码最短可由三个字符组成。创建一个正则表达式,在以下列表中查找有效的十六进制代码:
pattern:
string: 1H8 4E2 8FF 0P1 T8B 776 42B G12
matches: ^^^ ^^^ ^^^ ^^^
y
使用字符范围,创建一个正则表达式,该正则表达式将仅选择以下句子中的小写辅音(非元音字符,包括):
pattern:
string: The walls in the mall are totally, totally tall.
matches: ^ ^ ^^^ ^ ^^ ^ ^^ ^ ^ ^ ^^^ ^ ^ ^^^ ^ ^^
第 6 步:非克拉^
我对最后一个问题的解答有点长。“获取除元音字母外的整个字母表”用了 17 个字符。肯定有更简单的方法。事实证明,确实有。
“not”符号^
允许我们指定正则表达式引擎不应匹配的字符和字符范围。对于上面最后一个小测验问题,一个更简单的解决方案是匹配所有非元音字符:
pattern: [^aeiou]
string: The walls in the mall are totally, totally tall.
matches: ^^ ^^ ^^^^ ^^^^ ^^ ^^^ ^ ^^ ^ ^^^^^^ ^ ^^^^^ ^^^
^
方括号内最左边的字符“car”[]
告诉正则表达式引擎匹配一个不在方括号内的字符。这意味着上面的正则表达式也匹配所有空格、句号.
、逗号,
以及句首的大写字母T
。为了排除这些字符,我们也可以将它们放在方括号内:
pattern: [^aeiou .,T]
string: The walls in the mall are totally, totally tall.
matches: ^ ^ ^^^ ^ ^^ ^ ^^ ^ ^ ^ ^^^ ^ ^ ^^^ ^ ^^
请注意,我们不需要.
在这里进行转义。方括号内的许多特殊字符都会按字面意义处理,包括左括号[
-- ,但右括号则不会]
(你能理解为什么吗?)。反斜杠\
字符也不会按字面意义处理。如果要\
使用方括号匹配字面意义的反斜杠,则必须在其前面加上第二个反斜杠 进行转义\\
。为了使空格字符在方括号内可匹配,必须允许此行为:
pattern: [\t]
string: t t t t
matches: ^ ^ ^
克拉符号也可以与范围一起使用。如果我只想捕获字符a
、b
、c
、x
、y
和z
,我可以这样做:
pattern: [abcxyz]
string: abcdefghijklmnopqrstuvwxyz
matches: ^^^ ^^^
...或者,我可以指定我想要不在d
和之间的任何字符w
:
pattern: [^d-w]
string: abcdefghijklmnopqrstuvwxyz
matches: ^^^ ^^^
小心使用“非”插入符号^
。人们很容易想,“好吧,我说了[^b-f]
”,所以应该a
在 后面加上一个小写字母 ,或者其他字符f
。但事实并非如此。该正则表达式会匹配不在该范围内的任何字符,包括数字、符号和空格。
pattern: [^d-w]
string: abcdefg h.i,j-klmnopqrstuvwxyz
matches: ^^^ ^ ^ ^ ^ ^^^
流行测验:
使用方括号内的“not”符号^
匹配下面所有不以 结尾的单词y
:
pattern:
string: day dog hog hay bog bay ray rub
matches: ^^^ ^^^ ^^^ ^^^
使用范围和“非”符号编写正则表达式,^
以查找以下 1977 年至 1982 年(含)之间的所有年份:
pattern:
string: 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984
matches: ^^^^ ^^^^ ^^^^ ^^^^ ^^^^ ^^^^
编写一个正则表达式来匹配下面所有不是文字克拉字符的字符^
:
pattern:
string: abc1^23*()
matches: ^^^^ ^^^^^
步骤 7:字符类
比字符范围更简单的是字符类。不同的正则表达式引擎有不同的可用类,因此我这里只介绍基础知识。(请检查你使用的正则表达式版本,因为可用的类可能比这里显示的更多或不同。)
字符类的工作方式与范围非常相似,但不能指定“开始”和“结束”值:
班级 | 人物 |
---|---|
\d |
“数字”[0-9] |
\w |
“单词字符”[A-Za-z0-9_] |
\s |
“空白”[ \t\r\n\f] |
字符类这个\w
词特别有用,因为各种编程语言中有效的标识符(变量和函数名称等)通常都需要这组字符。
我们可以用来\w
简化我们之前看到的这个正则表达式:
pattern: \\[a-z]
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^ ^^
有了\w
,我们可以改写为:
pattern: \\\w
string: `\n`, `\r`, `\t`, and `\f` are whitespace characters, `\.`, `\\` and `\[` are not.
matches: ^^ ^^ ^^ ^^
流行测验:
在 Java 编程语言中,标识符(变量、类、函数等的名称)必须以字母a-zA-Z
、美元符号$
或下划线开头_
。其余字符必须是单词字符 \w
。使用一个或多个字符类,创建一个正则表达式,在以下 3 个字符序列中查找有效的 Java 标识符:
pattern:
string: __e $12 .x2 foo Bar 3mm
matches: ^^^ ^^^ ^^^ ^^^
美国社会安全号码 (SSN) 是 9 位数字,格式为XXX-XX-XXXX
,其中每个数字X
可以是任意数字[0-9]
。使用一个或多个字符类,编写一个正则表达式,在以下列表中查找格式正确的 SSN:
pattern:
string: 113-25=1902 182-82-0192 H23-_3-9982 1I1-O0-E38B
matches: ^^^^^^^^^^^
步骤 8:星号*
和加号+
到目前为止,我们基本上只匹配了一定长度的字符串。但在上次的突击测验中,我们已经接近了目前为止所见符号所能达到的极限。
例如,假设 Java 标识符的长度不仅限于 3 个字符,而是可以有任意长度。那么,在上一个示例中可能有效的解决方案,在以下示例中可能无效:
pattern: [a-zA-Z_$]\w\w
string: __e $123 3.2 fo Barr a23mm ab x
matches: ^^^ ^^^ ^^^ ^^^
注意,当标识符有效但长度超过 3 个字符时,只有前三个字符会被匹配。而当标识符有效但长度少于 3 个字符时,则根本不会匹配!
问题在于,方括号表达式只能[]
匹配一个字符,像 这样的字符类也是如此\w
。这意味着上述正则表达式的任何匹配项都必须恰好包含三个字符。所以它并没有像我们希望的那样工作。
特殊字符*
和+
可以在这里提供帮助。它们是修饰符,可以添加到任何表达式的右侧,以便多次匹配该表达式。
Kleene 星号(或“星号”)*
会匹配其前的标记任意次,包括零次。“加号”+
会匹配一次或多次。因此,位于 a 之前的表达式+
是必需的(至少一次),而位于 a 之前的表达式*
是可选的,但一旦出现,则可以出现任意次。
有了这些知识,我们可以修复上述正则表达式:
pattern: [a-zA-Z_$]\w*
string: __e $123 3.2 fo Barr a23mm ab x
matches: ^^^ ^^^^ ^^ ^^^^ ^^^^^ ^^ ^
我们现在匹配任意长度的有效标识符!成功!
如果我们使用+
上面的而不是会发生什么*
?
pattern: [a-zA-Z_$]\w+
string: __e $123 3.2 fo Barr a23mm ab x
matches: ^^^ ^^^^ ^^ ^^^^ ^^^^^ ^^
我们删除了最后一个匹配项,x
这是因为至少+
需要一个字符才能匹配,但由于前面的括号表达式已经“吃掉”了该字符,所以没有剩余的字符,因此匹配失败。[]
\w+
x
什么时候用+
?当我们想要至少匹配一次,但不关心匹配给定表达式的次数时。例如,我们可能想匹配任何包含小数点的数字:
pattern: \d*\.\d+
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012
matches: ^^^^^ ^^ ^^^ ^^^^ ^^^^^ ^^^^^^^
请注意,通过使小数点左侧的0.011
数字可选,我们能够同时匹配和.2
。但是,我们要求 恰好有一个小数点\.
,并且 至少有一个小数点右侧的数字\d+
。但是,上面的正则表达式无法匹配 这样的数字3.
,因为我们要求小数点右侧至少有一位数字。
流行测验:
将下面段落中的所有英语单词匹配起来。
pattern:
string: 3 plus 3 is six but 4 plus three is 7
matches: ^^^^ ^^ ^^^ ^^^ ^^^^ ^^^^^ ^^
KB
匹配以下列表中的所有文件大小。文件大小由一个数字(带或不带小数点)加上、MB
、GB
或组成TB
:
pattern:
string: 11TB 13 14.4MB 22HB 9.9GB TB 0KB
matches: ^^^^ ^^^^^^ ^^^^^ ^^^
步骤 9:“可选”问号?
如果你还没有写过正则表达式,可以试试写一个正则表达式来解决最后那个突击测验题。成功了吗?现在就在这里试试:
pattern:
string: 1..3KB 5...GB ..6TB
matches:
显然,这些都不是有效的文件大小,所以一个好的正则表达式不应该匹配它们中的任何一个。我为上一个突击测验问题编写的解决方案至少部分匹配所有匹配:
pattern: \d+\.*\d*[KMGT]B
string: 1..3KB 5...GB ..6TB
matches: ^^^^^^ ^^^^^^ ^^^
有什么问题?我们实际上只想要一个小数点(如果有的话)。但*
允许任意次数的匹配,包括零次。有没有办法只匹配零次或一次?但不能匹配超过一次?有的。
“可选”问号?
是一个修饰符,它匹配零个或一个前面的字符,但不能匹配更多:
pattern: \d+\.?\d*[KMGT]B
string: 1..3KB 5...GB ..6TB
matches: ^^^ ^^^
我们已经接近匹配了,但还差得远。我们将分几步看看如何解决这个问题。
流行测验:
在某些编程语言(例如 Java)中,“长整数”和浮点数后面可以跟l
/L
和f
/ ,F
分别表示它们应该被视为long
s / float
s ,而不是通常的int
s / 。找出下面这行中doubles
所有有效的s :long
pattern:
string: 13L long 2l 19 L lL 0
matches: ^^^ ^^ ^^ ^
步骤 10:“或”管道|
我们之前在匹配各种浮点数时遇到了一些困难:
pattern: \d*\.\d+
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012
matches: ^^^^^ ^^ ^^^ ^^^^ ^^^^^ ^^^^^^^
上述模式匹配带有小数点且小数点右侧至少有一位数字的数字。但是,如果我们还想匹配类似这样的字符串,该怎么办0.
?(小数点右侧没有数字)。
我们可以编写如下正则表达式:
pattern: \d*\.\d*
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012 0. .
matches: ^^^^^ ^^ ^^^ ^^^^ ^^^^^ ^^^^^^^ ^^ ^
它匹配了0.
,但它也只匹配一个.
,如上所示。实际上,我们上面尝试匹配的是两类不同的字符串:
- 小数点右侧至少有一位数字的数字,以及
- 小数点左侧至少有一位数字的数字
这两个正则表达式可以分别独立地写成:
pattern: \d*\.\d+
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012 0. .
matches: ^^^^^ ^^ ^^^ ^^^^ ^^^^^ ^^^^^^^
pattern: \d+\.\d*
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012 0. .
matches: ^^^^^ ^^^ ^^^^ ^^^^^ ^^^^^^^ ^^
我们可以看到,字符串42
、5
、6
或均不.
匹配。我们想要的是这两个正则表达式的并集。该如何实现呢?
“或”竖线允许我们在正则表达式中指定多个可能的匹配序列。与指定交替的单个字符|
类似,使用“或”竖线,我们可以指定交替的多字符表达式。[]
|
例如,如果我们想匹配“狗”或“猫”,我们可以写:
pattern: \w\w\w
string: Obviously, a dog is a better pet than a cat.
matches: ^^^^^^^^^ ^^^ ^^^^^^ ^^^ ^^^ ^^^
……但这匹配了所有三个字符序列。“dog”和“cat”甚至没有任何共同的字母,所以我们不能在这里使用方括号来帮助我们。我们可以编写一个最简单的正则表达式,既匹配这两个单词,又只匹配这两个单词,它是:
pattern: dog|cat
string: Obviously, a dog is a better pet than a cat.
matches: ^^^ ^^^
正则表达式引擎首先尝试匹配竖线左侧的整个序列|
,如果失败,则尝试匹配竖线右侧的序列。多个竖线可以链接在一起,以匹配两个以上的备选序列:
pattern: dog|cat|pet
string: Obviously, a dog is a better pet than a cat.
matches: ^^^ ^^^ ^^^
流行测验:
使用“或”管道|
来修复上面给出的十进制正则表达式:
pattern:
string: 0.011 .2 42 2.0 3.33 4.000 5 6 7.89012 0. .
matches: ^^^^^ ^^ ^^^ ^^^^ ^^^^^ ^^^^^^^ ^^
使用“或”管道|
、字符类、“可选”问号?
等来创建一个匹配长整数和浮点数的正则表达式,正如上一步结束时的突击测验中所讨论的那样(这是一个非常困难的问题):
pattern:
string: 42L 12 x 3.4f 6l 3.3 0F L F .2F 0.
matches: ^^^ ^^ ^^^^ ^^ ^^^ ^^ ^^^ ^^
步骤 11:()
用于捕获组的括号
在最后一个突击测验问题中,我们能够捕获不同类型的整数和浮点数值。但是正则表达式引擎没有区分这两种值,因为所有内容都被捕获在一个庞大的正则表达式中。
我们可以告诉正则表达式引擎通过用括号括起来来区分不同类型的匹配:
pattern: ([A-Z])|([a-z])
string: The current President of Bolivia is Evo Morales.
matches: ^^^ ^^^^^^^ ^^^^^^^^^ ^^ ^^^^^^^ ^^ ^^^ ^^^^^^^
group: 122 2222222 122222222 22 1222222 22 122 1222222
上述正则表达式定义了两个捕获组,它们的索引从 1 开始。第一个捕获组匹配任意单个大写字母,第二个捕获组匹配任意单个小写字母。使用“或”竖线|
和“捕获组”括号,()
我们可以定义一个匹配多种字符串的正则表达式。
如果我们将其应用于上面的长/浮点型正则表达式,正则表达式引擎将在适当的组中捕获适当的匹配项。通过检查字符串匹配到哪个组,我们可以立即判断它是浮点型值还是长整型值:
pattern: (\d*\.\d+[fF]|\d+\.\d*[fF]|\d+[fF])|(\d+[lL])
string: 42L 12 x 3.4f 6l 3.3 0F L F .2F 0.
matches: ^^^ ^^^^ ^^ ^^ ^^^
group: 222 1111 22 11 111
这个正则表达式相当复杂,但现在你应该能够理解它的每个部分了。让我们把它拆开来复习一下这些符号:
( // match any "float" string
\d*\.\d+[fF]
|
\d+\.\d*[fF]
|
\d+[fF]
)
| // OR
( // match any "long" string
\d+[lL]
)
“or” 竖线|
和括号捕获组()
允许我们匹配不同类型的字符串。在本例中,我们匹配的是“float”浮点数或“long”长整数。
(
\d*\.\d+[fF] // 1+ digits to the right of the decimal point
|
\d+\.\d*[fF] // 1+ digits to the left of the decimal point
|
\d+[fF] // no decimal point, only 1+ digits
)
|
(
\d+[lL] // no decimal point, only 1+ digits
)
在“浮点型”捕获组中,我们有三种选择:小数点右侧至少有 1 位数字、小数点左侧至少有 1 位数字以及没有小数点的数字。只要在末尾附加一个f
或,这些数字都是“浮点型”。F
在“长”捕获组中,我们只有一个选项——我们必须有一个或多个数字,后跟一个l
或一个L
字符。
正则表达式引擎将在给定的字符串中查找这些子字符串,并在适当的捕获组中对它们进行索引。
请注意,我们不会匹配任何未l
附加、L
、f
或的数字。这些数字应该归类为哪一类F
?嗯,如果它们有小数点,则默认double
为 Java 语言中的小数。否则,它们应该是int
s。
流行测验:
在上述正则表达式中添加两个捕获组,以便它也将数字分类为double
或int
。(这又是一个难题,如果需要花一些时间或者你需要看一下我的解决方案,请不要灰心。)
pattern:
string: 42L 12 x 3.4f 6l 3.3 0F L F .2F 0.
matches: ^^^ ^^ ^^^^ ^^ ^^^ ^^ ^^^ ^^
group: 333 44 1111 33 222 11 111 22
这里稍微简单一点。使用括号捕获组()
、“或”竖线|
和字符范围,将以下年龄分为“在美国合法饮酒”(>= 21)和“在美国非法饮酒”(< 21)组:
pattern:
string: 7 10 17 18 19 20 21 22 23 24 30 40 100 120
matches: ^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^^ ^^^
group: 2 22 22 22 22 22 11 11 11 11 11 11 111 111
步骤 12:首先定义更具体的匹配
如果您尝试将“合法饮酒者”定义为第一个捕获组而不是第二个捕获组,那么在回答最后一个突击测验问题时可能会遇到一些麻烦。为了了解原因,让我们看一个不同的例子。假设我们想要分别捕获少于 4 个字符的姓氏和包含 4 个或更多字符的姓氏。如果我们将较短的姓氏设为第一个捕获组,请观察会发生什么:
pattern: ([A-Z][a-z]?[a-z]?)|([A-Z][a-z][a-z][a-z]+)
string: Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^ ^^ ^^^ ^^^ ^^^ ^^^
group: 111 111 11 111 111 111 111
默认情况下,大多数正则表达式引擎会对我们目前为止见过的基本字符使用贪婪匹配。这意味着正则表达式引擎会捕获最长的组,该组在提供的正则表达式中尽可能早地定义。因此,即使上面的第二个组可以捕获更多字符,例如“Jobs”和“Cloyd”,但由于这些名字的前三个字符已经被第一个捕获组捕获,所以它们无法被第二个捕获组再次捕获。
不过,这是一个简单的修复——只需切换捕获组的顺序,将更具体(更长)的组放在第一位:
pattern: ([A-Z][a-z][a-z][a-z]+)|([A-Z][a-z]?[a-z]?)
string: Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^
group: 222 1111 22 11111 1111 222 111
流行测验:
“更具体”几乎总是意味着“更长”。假设我们想要捕获两种“单词”:以元音字母开头的单词(更具体)和不以元音字母开头的单词(任何其他单词)。我们该如何编写一个正则表达式来捕获并识别与这两组匹配的字符串?(以下分组按字母而不是数字排序。您必须确定应该先匹配哪个组。)
pattern:
string: pds6f uub 24r2gp ewqrty l ui_op
matches: ^^^^^ ^^^ ^^^^^^ ^^^^^^ ^ ^^^^^
group: NNNNN VVV NNNNNN VVVVVV N VVVVV
一般来说,正则表达式越精确,它就越长。越精确,捕获不想要的内容的可能性就越小。所以,尽管它们看起来很吓人,但更长的正则表达式~=更好的正则表达式。很遗憾。
步骤 13:花括号{}
用于定义重复
在上一步的姓氏示例中,我们有一个看起来相当重复的正则表达式:
pattern: ([A-Z][a-z][a-z][a-z]+)|([A-Z][a-z]?[a-z]?)
string: Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^
group: 222 1111 22 11111 1111 222 111
对于第一组,我们需要包含四个或四个以上字母的姓氏。第二组旨在捕获包含三个或三个以下字母的姓氏。除了[a-z]
一遍又一遍地重复这些分组之外,有没有更简单的方法来编写它们呢?答案是肯定的,那就是使用花括号{}
。
花括号{}
允许我们指定匹配前一个字符或捕获组的最小次数和(可选)最大次数。 有三种可能性{}
:
{X} // match exactly X times
{X,} // match >= X times
{X,Y} // match >= X and <= Y times
以下是这三种不同语法的示例:
pattern: [a-z]{11}
string: humuhumunukunukuapua'a
matches: ^^^^^^^^^^^
pattern: [a-z]{18,}
string: humuhumunukunukuapua'a
matches: ^^^^^^^^^^^^^^^^^^^^
pattern: [a-z]{11,18}
string: humuhumunukunukuapua'a
matches: ^^^^^^^^^^^^^^^^^^
以上示例中有几点需要注意。首先,使用{X}
表示法时,其前面的字符或字符组将匹配该次数(X
)。如果存在更多字符,而这些字符如果大于则可匹配X
(如第一个示例中所示),则它们将不会被包含在匹配中。如果字符数少于X
,则整个匹配失败(请尝试在第一个示例中更改11
为)。99
其次, 和 都是{X,}
贪婪{X,Y}
的。它们会匹配尽可能多的字符,同时满足定义的正则表达式。如果 表示 ,{3,7}
则匹配 3 到 7 个字符,并且接下来的 7 个字符有效,则所有 7 个字符都会匹配。如果 表示{1,}
,但接下来的 14,000 个字符全部匹配,则所有这 14,000 个字符都会包含在匹配的字符串中。
那么,我们如何利用这个来重写上面的表达式呢?一个非常简单的改进方法是将相邻的[a-z]
组替换为[a-z]{N}
,其中N
是合适的选择:
pattern: ([A-Z][a-z]{2}[a-z]+)|([A-Z][a-z]?[a-z]?)
……但这并没有让它变得更好。看看第一个捕获组:我们有[a-z]{2}
(匹配恰好两个小写字母)后面跟着[a-z]+
(匹配一个或多个小写字母)。我们可以通过使用花括号来简化匹配,要求匹配三个或更多小写字母:
pattern: ([A-Z][a-z]{3,})|([A-Z][a-z]?[a-z]?)
第二个捕获组有所不同。我们希望这些姓氏最多包含三个字符,这意味着我们有一个上限,但下限是零:
pattern: ([A-Z][a-z]{3,})|([A-Z][a-z]{0,2})
现在,使用正则表达式时,特异性总是更好的,所以我们最好就此打住,但我不禁注意到,这两个相邻的字符范围([A-Z]
和])几乎看起来像“单词字符”类()。如果我们确定我们的数据只包含格式正确的姓氏,我们可以简化正则表达式,只需这样写:[a-z
\w
[A-Za-z0-9_]
pattern: (\w{4,})|(\w{1,3})
第一组捕获任意 4 个或更多单词字符( )的序列[A-Za-z0-9_]
,第二组捕获任意 1 到 3 个单词字符(含)的序列。这样可以吗?
pattern: (\w{4,})|(\w{1,3})
string: Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^
group: 222 1111 22 11111 1111 222 111
确实如此!怎么样?而且比我们原来的例子简洁多了。由于第一个捕获组匹配所有包含四个或更多字符的姓氏,我们甚至可以将第二个捕获组改为仅包含\w+
,因为这将捕获所有剩余的姓氏(包含 1、2 或 3 个字符的姓氏):
pattern: (\w{4,})|(\w+)
string: Kim Jobs Xu Cloyd Mohr Ngo Rock
matches: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^
group: 222 1111 22 11111 1111 222 111
简洁的!
流行测验:
使用花括号{}
重写步骤 7中的社会安全号码正则表达式:
pattern:
string: 113-25=1902 182-82-0192 H23-_3-9982 1I1-O0-E38B
matches: ^^^^^^^^^^^
假设网站上的密码强度验证系统要求用户密码长度在 6 到 12 个非空格字符之间。请编写一个正则表达式,标记以下列表中的错误密码。每个密码都包含在括号中,()
以便于正则表达式的使用,因此请确保正则表达式以文字(
和)
字符开头和结尾。(提示:请确保在带有或类似字符的密码中禁止使用[^()]
文字括号,否则您可能会匹配整行!)
pattern:
string: (12345) (my password) (Xanadu.2112) (su_do) (OfSalesmen!)
matches: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^
步骤 14:,\b
零宽度边界字符
最后一道突击测验题很难。但如果我们用引号而不是括号来包围密码,让它变得更难呢?我们能不能简单地用 替换掉所有文字和字符,写出类似的解决方案?""
()
(
)
"
pattern: \"[^"]{0,5}\"|\"[^"]+\s[^"]*\"
string: "12345" "my password" "Xanadu.2112" "su_do" "OfSalesmen!"
matches: ^^^^^^^ ^^^^^^^^^^^^^ ^^^ ^^^
这显然失败了。你能看出原因吗?
问题在于,我们在这里寻找的是错误的密码。"Xanadu.2112"
是一个好密码,所以当正则表达式意识到它不包含任何空格或文字字符时,它会在右侧限制密码的字符之前放弃。(因为我们使用 . 指定了在密码"
中找不到字符。)"
"
[^"]
一旦正则表达式引擎确定这些字符与定义的正则表达式不匹配,它就会重新开始,从上次中断的位置——也就是右侧"
边界的位置——开始执行。从那里,它看到一个空格字符,然后又看到一个——一个错误的密码!所以它匹配了,然后继续。"Xanadu.2112"
"
" "
如果我们可以规定密码的第一个字符必须非空格就太好了。有办法吗?(你现在应该知道,我所有反问的答案都是“是”。)有!有!
许多正则表达式引擎都提供了“单词边界”转义序列\b
. 。\b
它是一个零宽度转义序列,有趣的是,它匹配的是单词的边界。记住,当我们说“单词”时,我们指的是 类中的任何字符序列\w
,也就是 . [a-zA-Z0-9_]
。
单词边界匹配意味着序列之前或之后的字符\b
必须是非单词字符。但我们实际上并没有将该字符包含在捕获的字符串中。为了了解其工作原理,我们来看一个小例子:
pattern: \b[^ ]+\b
string: Ve still vant ze money, Lebowski.
matches: ^^ ^^^^^ ^^^^ ^^ ^^^^^ ^^^^^^^^
该序列[^ ]
应该匹配任何非字面空格字符的字符,
。那么为什么它不匹配aftermoney
或.
after呢Lebowski
?这是因为,
和.
不是单词字符,所以在单词字符和非单词字符之间存在边界y
。这些边界出现在ofmoney
和,
其后的 the 之间,以及i
ofLebowski
和其后的句号/句点之间。正则表达式匹配这些单词边界(但不匹配有助于定义它们的非单词字符)。
如果我们不包括该序列会发生什么\b
?
pattern: [^ ]+
string: Ve still vant ze money, Lebowski.
matches: ^^ ^^^^^ ^^^^ ^^ ^^^^^^ ^^^^^^^^^
啊哈,现在我们确实匹配了这些标点符号。
现在让我们使用单词边界来帮助修复引用的密码正则表达式:
pattern: \"\b[^"]{0,5}\b\"|\"\b[^"]+\s[^"]*\b\"
string: "12345" "my password" "Xanadu.2112" "su_do" "OfSalesmen!"
matches: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^
通过将单词边界放在引号 ( "\b...\b"
) 的“内部”,我们实际上是在表示匹配密码的第一个和最后一个字符必须是“单词”字符。所以这在这里可以正常工作,但如果用户密码的第一个或最后一个字符不是单词字符,则效果会不太好:
pattern: \"\b[^"]{0,5}\b\"|\"\b[^"]+\s[^"]*\b\"
string: "thefollowingpasswordistooshort" "C++"
matches:
注意到第二个密码没有被标记为“无效”吗?尽管它显然太短了。你需要小心使用\b
序列,因为它们只匹配\w
和非\w
字符之间的边界。在上面的例子中,由于我们允许密码中包含非字符,因此 和 密码的第一个/最后一个字符\w
之间的边界不能保证是单词边界。"
\b
流行测验:
单词边界在语法高亮引擎中非常有用,我们想要匹配特定的字符序列,但又希望确保它们只出现在单词的开头或结尾(或者单独出现)。假设我们正在编写一个语法高亮器,并且希望高亮单词var
,但只在它单独出现时(不接触任何其他单词字符)。你能写一个正则表达式来实现吗?
pattern:
string: var varx _var (var j) barvarcar *var var-> {var}
matches: ^^^ ^^^ ^^^ ^^^ ^^^
步骤 15:“行首”克拉^
和“行尾”美元符号$
上一步中的单词边界序列\b
并非正则表达式中唯一可用的零宽度^
特殊序列。其中两个比较常用的零宽度特殊序列包括“行首”插入符号和“行尾”美元符号$
。在正则表达式中包含其中一个符号意味着给定的匹配项必须出现在你尝试匹配的字符串的行首或行尾:
pattern: ^start|end$
string: start end start end start end start end
matches: ^^^^^ ^^^
如果字符串包含换行符,则将匹配任意行首的^start
序列,也将匹配任意行尾的序列(尽管这些字符在这里很难显示)。这些字符在处理分隔数据时特别有用。start
end$
end
让我们重新审视步骤 9中的“文件大小”问题,并使用“行首”符号。在本例中,我们的文件大小由空格字符 ' ' 分隔。因此,我们希望每个文件大小都以一个数字开头,该数字前面加一个空格或一行的开头:
pattern: (^| )(\d+|\d+\.\d+)[KMGT]B
string: 6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB
matches: ^^^^^ ^^^^^ ^^^^^^ ^^^^
groups: 222 122 1222 12
我们已经非常接近了!你可以看到我们还有一个小问题,那就是我们在匹配有效文件大小前的空格字符。现在,1
当我们的正则表达式引擎找到它时,我们可以忽略该捕获组 ( ),或者我们可以使用非捕获组,我们将在下一步中看到。
流行测验:
继续上一步的语法高亮示例,有些语法高亮器会标记尾随空格——即非空白字符和行尾之间的任何空格。你能写一个正则表达式高亮规则来标记尾随空格吗?
pattern:
string: myvec <- c(1, 2, 3, 4, 5)
matches: ^^^^^^^
一个简单的逗号分隔值 (CSV) 解析器会查找以逗号分隔的“标记”。通常,空格不重要,除非它在引号内""
。您能否编写一个简单的 CSV 解析正则表达式,匹配逗号之间的标记,但忽略(不捕获)非引号内的空格?
pattern:
string: a, "b", "c d",e,f, "g h", dfgi,, k, "", l
matches: ^^ ^^^^ ^^^^^^^^^^ ^^^^^^ ^^^^^^ ^^ ^^^ ^
groups: 21 2221 2222212121 222221 222211 21 221 2
步骤 16:非捕获组(?:)
在上一步中的两个示例中,我们捕获了实际上不需要的文本。在“文件大小”挑战中,我们捕获了文件大小第一位数字前的空格字符;在“CSV”挑战中,我们捕获了每个标记之间的逗号。我们不需要捕获这些字符,但需要使用它们来构造我们的正则表达式。这些是非捕获组,的完美用例(?:)
。
非捕获组的作用正如其名——它允许你对字符进行分组,并在正则表达式中使用它们,但它不会在编号组中捕获它们:
pattern: (?:")([^"]+)(?:")
string: I only want "the text inside these quotes".
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
groups: 1111111111111111111111111111
现在,正则表达式匹配了引号内的文本以及引号字符本身,但捕获组只捕获了引号内的文本。我们为什么要这样做呢?
嗯,大多数正则表达式引擎允许你从正则表达式中定义的捕获组中恢复文本。如果我们可以去掉那些不需要的多余字符,通过不将它们包含在捕获组中,那么以后解析和操作文本就会更容易。
这是另一个示例,清理上一步中的 CSV 解析器:
pattern: (?:^|,)\s*(?:\"([^",]*)\"|([^", ]*))
string: a, "b", "c d",e,f, "g h", dfgi,, k, "", l
matches: ^ ^ ^^^ ^ ^ ^^^ ^^^^ ^ ^
groups: 2 1 111 2 2 111 2222 2 2
这里有几点需要注意。首先,我们不再捕获逗号分隔符,因为我们将(^|,)
捕获组改为了(?:^|,)
非捕获组。其次,我们将捕获组嵌套在非捕获组中。这在以下情况下很有用:例如,当你需要一组字符按特定顺序出现,但你只关心其中一部分字符时。
在我们的例子中,我们需要非引号、非逗号字符[^",]*
出现在引号内,但我们实际上并不关心引号字符本身,因此不需要捕获它们。
最后,请注意,在上面的例子中,在和字符之间也有一个零长度匹配。有一个匹配的子字符串,但是引号之间没有字符(我们没有捕获),因此匹配的子字符串不包含任何字符(长度为零)。k
l
""
流行测验:
使用非捕获组(以及捕获组、字符类等),编写一个正则表达式,仅捕获以下字符串中格式正确的文件大小:
pattern:
string: 6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB
matches: ^^^^^ ^^^^^ ^^^^^^ ^^^^
groups: 11111 1111 11111 111
HTML 起始标签以一个<
字符开头,以一个>
字符结尾。HTML 结束标签以一个</
字符序列开头,以一个>
字符结尾。标签的名称包含在这些字符中。你能编写一个正则表达式来仅捕获以下标签中的名称吗?(你或许可以不使用非捕获组来解决这个问题。尝试用两种方法解决它!一次使用捕获组,一次不使用。)
pattern:
string: <p> </span> <div> </kbd> <link>
matches: ^^^ ^^^^^^ ^^^^^ ^^^^^ ^^^^^^
groups: 1 1111 111 111 1111
步骤 17:反向引用\N
和命名捕获组
尽管我在介绍中警告过你,尝试用正则表达式构建 HTML 解析器通常会导致痛苦,但最后一个例子很好地过渡到大多数正则表达式的另一个(有时)有用的特性:反向引用。
反向引用类似于重复组,因为您可以尝试捕获相同的文本两次。但它们在一个重要方面有所不同——它们只会逐个字符地捕获完全相同的文本。
因此,虽然重复的组可以让我们捕捉到类似
pattern: (he(?:[a-z])+)
string: heyabcdefg hey heyo heyellow heyyyyyyyyy
matches: ^^^^^^^^^^ ^^^ ^^^^ ^^^^^^^^ ^^^^^^^^^^^
groups: 1111111111 111 1111 11111111 11111111111
...反向引用只会匹配
pattern: (he([a-z])(\2+))
string: heyabcdefg hey heyo heyellow heyyyyyyyyy
matches: ^^^^^^^^^^^
groups: 11233333333
重复捕获组适用于重复匹配相同模式的情况,而反向引用适用于匹配完全相同的文本的情况。例如,我们可以使用反向引用来尝试查找匹配的 HTML 开始和结束标签:
pattern: <(\w+)[^>]*>[^<]+<\/\1>
string: <span style="color: red">hey</span>
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
groups: 1111
请注意,这是一个极其简化的示例,我强烈建议您不要尝试编写基于正则表达式的 HTML 解析器。它的语法非常复杂,您可能会遇到麻烦。
命名捕获组与反向引用非常相似,因此我也会在这里简要介绍一下。反向引用和命名捕获组之间的唯一区别是……命名捕获组的命名方式如下:
pattern: <(?<tag>\w+)[^>]*>[^<]+<\/(?P=tag)>
string: <span style="color: red">hey</span>
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
groups: 1111
(?<name>...)
您可以使用or(?'name'...)
语法(.NET 兼容正则表达式)或(?P<name>...)
or 语法(Python 兼容正则表达式)创建命名捕获组(?P'name'...)
。由于我们使用的是 PCRE(Perl 兼容正则表达式),它支持这两个版本,因此我们可以使用其中任意一种。
要在正则表达式中重复命名捕获组,我们使用\k<name>
或\k'name'
(.NET) 或(?P=name)
(Python)。同样,PCRE 支持所有这些不同的变体。您可以在此处阅读更多关于命名捕获组的信息,但这已经是您真正需要了解的大部分内容了。
流行测验:
使用反向引用来帮助我记住......呃......那个人的名字。
pattern:
string: "Hi my name's Joe." [later] "What's that guy's name? Joe?"
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
groups: 111
步骤 18:前瞻和后瞻
我们现在开始学习正则表达式的一些更高级的功能。包括第 16 步在内的所有内容我都经常使用。但最后几个步骤只适合那些非常认真地使用正则表达式来匹配非常复杂的表达式的人。换句话说,就是正则表达式大师。
前向循环和后向循环乍一看可能有点复杂,但实际上并没有那么难。它们可以让你做一些类似于我们之前使用非捕获组的操作——检查在我们要匹配的实际文本之前或之后是否存在某些文本。例如,假设我们只想匹配人们喜爱的事物的名称,但前提是他们对它非常热情(前提是他们的句子以感叹号结尾)。我们可以这样做:
pattern: (\w+)(?=!)
string: I like desk. I appreciate stapler. I love lamp!
matches: ^^^^
groups: 1111
您可以看到,上面的捕获组(\w+)
通常会匹配文章中的任何单词,但它只匹配单词lamp
。正向前瞻 (?=...)
意味着我们只能匹配以字符结尾的序列!
,但实际上并不匹配感叹号字符本身。这是一个重要的区别,因为对于非捕获组,我们匹配字符,但不会捕获它。使用前瞻和后瞻时,我们使用字符来构建正则表达式,但之后我们甚至不会匹配字符。我们可以稍后在正则表达式中随意匹配它。
总共有四种类型的前瞻和后瞻:正向前瞻(?=...)
、负向前瞻(?!...)
、正向后瞻(?<=...)
和负向后瞻(?<!...)
。它们的作用正如其名——正向前瞻和后瞻仅允许正则表达式引擎在前瞻/后瞻中包含的文本匹配时继续匹配。负向前瞻和后瞻则相反——它们仅允许正则表达式在前瞻/后瞻中包含的文本不匹配时继续匹配。
例如,我们可能只想匹配方法链中的方法名称,而不匹配它们所操作的对象。在这种情况下,每个方法名称前面都应该有一个文字.
字符。使用简单的后向查找的正则表达式可以解决这个问题:
pattern: (?<=\.)(\w+)
string: myArray.flatMap.aggregate.summarise.print
matches: ^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^
groups: 1111111 111111111 111111111 11111
在上面的文本中,我们匹配任意的单词字符序列\w+
,但前提是它们前面有一个字面量.
。我们可以使用非捕获组来实现类似的效果,但这样会比较麻烦:
pattern: (?:\.)(\w+)
string: myArray.flatMap.aggregate.summarise.print
matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
groups: 1111111 111111111 111111111 11111
虽然它更短,但它匹配了我们不想要的字符。虽然这个例子看起来很简单,但前向循环和后向循环确实可以帮助我们简化正则表达式。
流行测验:
(?<!...)
只有当负向后行查找中包含的文本未出现在待匹配文本的其余部分之前时,负向后行查找才会允许正则表达式引擎继续尝试查找匹配项。例如,我们可能想使用正则表达式来匹配参加会议的女性的姓氏。为此,我们需要确保姓氏前面不Mr.
带。你能编写一个正则表达式来实现这一点吗?(你可以假设姓氏至少有四个字符。)
pattern:
string: Mr. Brown, Ms. Smith, Mrs. Jones, Miss Daisy, Mr. Green
matches: ^^^^^ ^^^^^ ^^^^^
groups: 11111 11111 11111
假设我们正在清理数据库,其中有一列信息需要表示为百分比。不幸的是,有些人将数字写成了 [0.0, 1.0] 范围内的小数,而另一些人则将百分号写成了 [0.0%, 100.0%] 范围内的百分号,还有些人写了百分号,但忘记了百分号%
。使用负向前瞻(?!...)
,能否只标记那些应该为百分号但缺少符号的值%
?这些值应该严格大于 1.00,但尾部没有%
。(小数点前后的数字不能超过两位。)
请注意,这个答案极其困难。如果你能在不偷看我的答案的情况下解决这个问题,那么你的正则表达式技能已经非常强大了。
pattern:
string: 0.32 100.00 5.6 0.27 98% 12.2% 1.01 0.99% 0.99 13.13 1.10
matches: ^^^^^^ ^^^ ^^^^ ^^^^^ ^^^^
groups: 111111 111 1111 11111 1111
步骤19:条件
现在,大多数人都会停止使用正则表达式了。我们已经涵盖了简单正则表达式大概 95% 的用例,而第 19 步和第 20 步的操作通常都可以用功能更齐全的文本操作语言(例如 或 )来完成awk
。sed
不过,我们还是继续吧,这样你就能真正了解正则表达式的功能了。
虽然正则表达式并非图灵完备,但某些正则表达式引擎提供的功能非常接近完整的编程语言。其中一项功能就是条件语句。正则表达式条件语句允许使用if-then-else语句,其中执行的分支由前向循环或后向循环决定,我们在上一步中学习过这一点。
例如,您可能希望仅匹配日期列表中的有效条目:
pattern: (?<=Feb )([1-2][0-9])|(?<=Mar )([1-2][0-9]|3[0-1])
string: Dates worked: Feb 28, Feb 29, Feb 30, Mar 30, Mar 31
matches: ^^ ^^ ^^ ^^
groups: 11 11 22 22
注意上面的组也是按月份索引的。我们可以为所有 12 个月编写一个正则表达式,只捕获有效日期,然后将其捕获到按月份索引的组中。
上面的代码使用了一种类似if的结构,即只有当"Feb "
数字在第一个组之前时,它才会匹配(第二个组类似)。但是,如果我们只想对二月进行特殊处理呢?比如“如果数字前面有"Feb "
,则执行此操作,否则执行其他操作”。这就是条件语句的作用:
pattern: (?(?<=Feb )([1-2][0-9])|([1-2][0-9]|3[0-1]))
string: Dates worked: Feb 28, Feb 29, Feb 30, Mar 30, Mar 31
matches: ^^ ^^ ^^ ^^
groups: 11 11 22 22
if-then-else结构类似于(?(if)then|else)
,其中(if)
被前向循环或后向循环所取代。在上面的例子中,(if)
是(?<=Feb )
。您可以看到,我们匹配了大于 29 的日期,但前提是它们不在 之后"Feb "
。当您想确保匹配项前面有文本时,在条件语句中使用后向循环非常有用。
正向前向条件可能会令人困惑,因为条件本身并不匹配任何文本。因此,如果您希望if子句能够求值,它需要能够从前向条件中匹配,如下所示:
pattern: (?(?=exact)exact|else)wo
string: exact else exactwo elsewo
matches: ^^^^^^^ ^^^^^^
这意味着正向前向条件几乎毫无用处。你只是检查文本是否在前面,然后在文本在前面时提供一个匹配模板来执行。这个条件对我们一点帮助都没有。你还不如直接用一个更简单的正则表达式替换上面的代码:
pattern: (?:exact|else)wo
string: exact else exactwo elsewo
matches: ^^^^^^^ ^^^^^^
所以,条件语句的经验法则是:测试,测试,再测试。你认为显而易见的事情,最终会以令人兴奋和意想不到的方式失败。
流行测验:
编写一个正则表达式,使用负向前瞻条件来检查下一个单词是否以大写字母开头。如果是,则只捕获单个大写字母,然后是小写字母。如果不是,则捕获任何单词字符。
pattern:
string: Jones Smith 9sfjn Hobbes 23r4tgr9h CSV Csv vVv
matches: ^^^^^ ^^^^^ ^^^^^ ^^^^^^ ^^^^^^^^^ ^^^ ^^^
groups: 22222 22222 11111 222222 111111111 222 111
编写一个负向后视条件,仅当文本 前面没有owns
文本时才捕获文本,并且仅当文本 前面有文本时才捕获文本。(这个例子有点牵强,但你能做什么呢?)cl
ouds
cl
pattern:
string: Those clowns owns some clouds. ouds.
matches: ^^^^ ^^^^
步骤20:递归和进一步学习
任何主题的20步介绍中能塞进的内容都有限,正则表达式也不例外。正则表达式有很多不同的实现和标准,你可以在互联网上找到它们。如果你想了解更多,我建议你访问regularexpressions.info这个很棒的网站,它是一个很棒的参考资料,我确实从中学到了很多关于正则表达式的知识。我强烈推荐这个网站,以及regex101.com ,你可以用它来测试和分享你的作品。
我将向您介绍有关正则表达式的一点知识:如何编写递归表达式。
简单的递归其实很简单,但让我们想想它在正则表达式中的含义。正则表达式中简单递归的语法(?R)?
是。当然,这种语法必须出现在表达式本身中。所以我们要做的就是将表达式嵌套在其自身中,嵌套次数任意。例如:
pattern: (hey(?R)?oh)
string: heyoh heyyoh heyheyohoh hey oh heyhey heyheyheyohoh
matches: ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^
groups: 11111 1111111111 1111111111
由于嵌套表达式是可选的((?R)
后跟一个?
),最简单的匹配就是完全忽略递归。因此,hey
后跟oh
( heyoh
) 即可匹配。要匹配任何比这更复杂的表达式,我们必须在表达式中插入序列的位置找到嵌套在其内部的匹配子字符串(?R)
。换句话说,我们可以找到heyheyohoh
或heyheyheyohohoh
,等等。
这些嵌套表达式的一个很酷的特点是,与反向引用和命名捕获组不同,它们不会限制你逐个字符地匹配之前匹配过的精确文本。例如:
pattern: ([Hh][Ee][Yy](?R)?oh)
string: heyoh heyyoh hEyHeYohoh hey oh heyhey hEyHeYHEyohohoh
matches: ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^
groups: 11111 1111111111 111111111111111
你可以想象,正则表达式引擎实际上是在将你的正则表达式复制粘贴到自身内部任意多次。当然,这意味着有时它可能不会按照你期望的方式运行:
pattern: ((?:\(\*)[^*)]*(?R)?(?:\*\)))
string: (* comment (* nested *) not *)
matches: ^^^^^^^^^^^^
groups: 111111111111
你能解释一下为什么这个正则表达式只捕获了最内层的嵌套注释,而没有捕获最外层的注释吗?有一件事是肯定的:在编写复杂的正则表达式时,一定要测试它们,确保它们按照你预期的方式工作。
希望你喜欢这篇快速入门的正则表达式教程。如果你有任何疑问或发现教程中有任何错误,请在下方评论区留言。也请分享本指南给你认识的、需要轻松了解正则表达式魅力的人。
一如往常,感谢您的阅读!
在Dev.To和Twitter.com上关注我(但请不要在现实生活中)。
有一天,有人会在Ko-Fi.com上请我喝杯咖啡,我会非常感激。如果您愿意支持我的工作,请捐款,让我能够创作更多类似的指南。
文章来源:https://dev.to/awwsmm/20-small-steps-to-become-a-regex-master-mpc