类似于我们针对特定语言的高尔夫技巧的主题:缩短正则表达式的一般技巧是什么?
在打高尔夫球时,我可以看到正则表达式的三种用法:经典的正则表达式高尔夫(“这里是一个应该匹配的列表,这里应该是一个失败的列表”),使用正则表达式来解决计算问题并将正则表达式用作其中的一部分较大的高尔夫球代码。随时发布解决任何或所有这些问题的技巧。如果您的小费仅限于一种或多种口味,请在顶部注明这些口味。
像往常一样,请为每个答案坚持一个提示(或一系列密切相关的提示),以便最有用的提示可以通过投票升至最高位置。
类似于我们针对特定语言的高尔夫技巧的主题:缩短正则表达式的一般技巧是什么?
在打高尔夫球时,我可以看到正则表达式的三种用法:经典的正则表达式高尔夫(“这里是一个应该匹配的列表,这里应该是一个失败的列表”),使用正则表达式来解决计算问题并将正则表达式用作其中的一部分较大的高尔夫球代码。随时发布解决任何或所有这些问题的技巧。如果您的小费仅限于一种或多种口味,请在顶部注明这些口味。
像往常一样,请为每个答案坚持一个提示(或一系列密切相关的提示),以便最有用的提示可以通过投票升至最高位置。
Answers:
这些规则适用于大多数风味,如果不是全部的话:
]
不匹配时不需要转义。
{
并且}
当它们不是重复的一部分(例如从字面上{a}
匹配)时,不需要转义{a}
。即使您想匹配类似的内容{2}
,也只需要转义其中之一即可,例如{2\}
。
在角色类中:
]
当它是字符集中的第一个字符(例如,[]abc]
匹配一个]abc
)或当它是a之后的第二个字符^
(例如,[^]]
匹配除以外的其他字符)时,不需要转义]
。(值得注意的例外:ECMAScript风味!)
[
根本不需要转义。结合以上技巧,这意味着您可以将两个方括号与可怕的违反直觉的字符类进行匹配[][]
。
^
如果它不是字符集中的第一个字符,则不需要转义[ab^c]
。
-
不需要逃避时,它的第一(第二后^
的字符集,例如)或最后一个字符[-abc]
,[^-abc]
或[abc-]
。
即使其他字符是字符类之外的元字符,也无需在字符类中转义(反斜杠\
本身除外)。
同样,在某些风味中^
,$
当它们不在正则表达式的开头或结尾时,在字面上会进行匹配。
(感谢@MartinBüttner填写了一些详细信息)
[.]
)。摆脱它通常会在这种情况下保存1个字节\.
[
必须在Java中转义。不过,不确定ICU(在Android和iOS中使用)还是.NET。
令人惊讶的是,有很多人认为正则表达式本质上与语言无关。但是,口味之间实际上存在相当大的差异,尤其是对于打码高尔夫,很高兴知道其中的几种口味以及它们有趣的功能,因此您可以为每种任务选择最佳口味。这里是几种重要口味的概述,以及它们与众不同的地方。(此列表无法真正完成,但是请让我知道是否错过了确实令人眼花something乱的内容。)
我将它们放入一个容器中,因为我不太熟悉Perl的风格,而且它们几乎是等效的(PCRE毕竟是与Perl兼容的正则表达式)。Perl风格的主要优点是您实际上可以从正则表达式和替换内部调用Perl代码。
(?(group)yes|no)
。\l
,\u
,\L
和\U
。\G
将比赛锚定到上一场比赛的末尾。\K
重置比赛开始\Q...\E
逃脱更长的角色。在尝试匹配包含许多元字符的字符串时很有用。这可能是最有力的口味,只有很少的缺点。
[\w-[aeiou]]
\d
可以识别Unicode。在打高尔夫球方面的一个重要缺点是,它不像其他口味那样支持所有格量词。而不是.?+
你必须写(?>.?)
。
.*
从头开始一直往后看,直到现在为止都可以从字符串开始,例如(?<=(?=lookahead).*)
。\Q...\E
如在Perl / PCRE中一样。在最新版本中,此功能与PCRE类似,包括对子例程调用的支持。与Java一样,它也支持字符类的并集和交集。一个特殊功能是用于十六进制数字的内置字符类:(\h
和\H
)。
高尔夫最有用的功能是Ruby处理量词的方式。最值得注意的是,可以在没有括号的情况下嵌套量词。.{5,7}+
是可行的.{3}?
。此外,与大多数其他口味相反,如果量词的下限是0
它的上限,则可以省略,例如.{,5}
等效于.{0,5}
。
至于子例程,PCRE的子例程和Ruby的子例程之间的主要区别是Ruby的语法(?n)
比vs 长一个字节\g<n>
,但是Ruby的子例程可用于捕获,而PCRE在子例程完成后重置捕获。
最后,与大多数其他样式相比,Ruby对于与行相关的修饰符具有不同的语义。通常m
以其他形式调用的修饰符始终在Ruby中启用。所以,^
和$
总是匹配的开始和结束行不会仅仅是个开始和结束的字符串。如果您需要此行为,则可以节省一个字节,但是如果不需要,则将花费额外的字节,因为您必须分别用和替换^
和。除此之外,通常在Ruby中调用通常称为的修饰符(使之匹配换行)。这不会影响字节数,但应注意避免混淆。$
\A
\z
s
.
m
Python具有浓郁的风味,但我不知道您在其他任何地方都找不到的任何特别有用的功能。
但是,还有一种替代风味,该风味旨在re
在某个时候替换模块,并且包含许多有趣的功能。除了增加对递归,变长后向查找和字符类组合运算符的支持外,它还具有模糊匹配的独特功能。本质上,您可以指定允许的许多错误(插入,删除,替换),并且引擎还会为您提供近似匹配。
ECMAScript的味道非常有限,因此很少用于打高尔夫球。唯一要做的就是否定空字符类 [^]
以匹配任何字符,以及无条件失败的空字符类[]
(与通常的相对(?!)
)。不幸的是,风味没有任何特征,使得后者对于正常问题没有用。
Lua有其自己独特的风味,其味道是有限的(例如,您甚至无法量化组),但确实具有一些有用和有趣的功能。
%b
它,它支持非常紧凑的语法来匹配平衡的字符串。例如,先%b()
匹配一个(
,然后再匹配所有匹配项)
(正确跳过内部匹配对)。(
并且)
可以是任意两个字符。Boost的regex风味本质上是Perl的风味。但是,它具有用于regex替换的一些不错的新功能,包括大小写更改和条件语句。据我所知,后者是Boost特有的。
.?+
等于.*
吗?
大多数正则表达式风格都有预定义的字符类。例如,\d
匹配一个十进制数字,该数字比短三字节[0-9]
。是的,它们可能略有不同,\d
在某些方面也可能与Unicode数字匹配,但是对于大多数挑战而言,这没有什么区别。
以下是大多数正则表达式中发现的一些字符类:
\d Match a decimal digit character
\s Match a whitespace character
\w Match a word character (typically [a-zA-Z0-9_])
此外,我们还有:
\D \S \W
是上述的否定版本。
请确保检查您的口味以了解它可能具有的其他任何字符类。例如,PCRE具有\R
换行符,Lua甚至具有小写和大写字符之类的类。
(感谢@HamZa和@MartinBüttner指出这些)
\R
用于PCRE中的换行符。
本技巧适用于(至少)所有受Perl启发的流行口味。
这可能是显而易见的,但是(在不打高尔夫球时)最好(?:...)
在可能的情况下使用非捕获组。但是,这两个额外的角色?:
在打高尔夫球时很浪费,因此即使不打算反向引用它们,也请使用捕获组。
但是,有一个(很少)例外:如果您10
至少对反向引用组进行了3次操作,则可以通过将较早的组转换为非捕获组,从而使所有\10
s变为\9
s ,从而实际上可以节省字节。(如果您使用群组11
至少5次,依此类推,也有类似的技巧。)
$9
代替$10
或$11
一次保存一个字节。谈及$10
到$9
需要一个?:
,这是两个字节,所以你需要三个$10
s保存的东西。谈及$11
到$9
需要两个?:
S的是四个字节,所以你需5个$11
s保存的东西(或五$10
和$11
合并)。
少数风味支持递归(据我所知,Perl,PCRE和Ruby)。即使您不打算解决递归问题,此功能也可以以更复杂的模式节省大量字节。无需调用该组本身内的另一个(命名或编号)组。如果您在正则表达式中多次出现某个模式,则将其分组并在该组之外引用它。这与普通编程语言中的子例程调用没有什么不同。所以代替
...someComplexPatternHere...someComplexPatternHere...someComplexPatternHere...
在Perl / PCRE中,您可以执行以下操作:
...(someComplexPatternHere)...(?1)...(?1)...
或在Ruby中:
...(someComplexPatternHere)...\g<1>...\g<1>...
如果是第一组(当然,您可以在递归调用中使用任何数字)。
请注意,这是不一样的反向引用(\1
)。后向引用与该组上一次完全匹配的字符串匹配。这些子例程调用实际上会再次评估模式。someComplexPatternHere
以一个冗长的字符类为例:
a[0_B!$]b[0_B!$]c[0_B!$]d
这将匹配类似
aBb0c!d
请注意,在保留行为的同时不能在此处使用反向引用。在上述字符串上的反向引用将失败,因为B
和0
与!
不相同。但是,通过子例程调用,实际上会重新评估该模式。上面的模式完全等同于
a([0_B!$])b(?1)c(?1)d
关于Perl和PCRE的注意事项:如果1
以上示例中的组包含其他组,则子例程调用将不会记住其捕获。考虑以下示例:
(\w(\d):)\2 (?1)\2 (?1)\2
这将不匹配
x1:1 y2:2 z3:3
因为在子例程调用返回之后,新捕获的组将2
被丢弃。相反,此模式将匹配以下字符串:
x1:1 y2:1 z3:1
这与Ruby不同,后者的子例程调用确实会保留其捕获内容,因此等效的Ruby正则表达式(\w(\d):)\2 \g<1>\2 \g<1>\2
将与上面的第一个示例匹配。
\1
Javascript。还有PHP(我猜)。
(..)\1
将匹配abab
但失败,abba
而(..)(?1)
将匹配后者。从某种意义上说,实际上是一个子例程调用,而不是从字面上匹配上次匹配的表达式,再次应用了表达式。
(?=a.b.c)(.[0_B!$]){3}d
使用正则表达式解决计算问题或匹配高度不规则的语言时,有时有必要使模式的分支失败,无论您在字符串中的位置如何。天真的方法是使用空的负前瞻:
(?!)
内容(空模式)始终匹配,因此负前瞻始终失败。但是通常有一个更简单的选择:只使用您知道永远不会出现在输入中的字符。例如,如果您知道您的输入将始终仅包含数字,则可以简单地使用
!
或任何其他非数字,非元字符导致失败。
即使您的输入可能包含任何子字符串,也有比短的方法(?!)
。允许锚出现在模式中而不是结尾中的任何样式,都可以使用以下两个字符的解决方案之一:
a^
$a
然而,一些口味将把注意^
,并$
在这些位置的文字字符,因为他们显然并没有真正意义的锚。
在ECMAScript风格中,还有一个非常优雅的2字符解决方案
[]
这是一个空字符类,它试图确保下一个字符是该类中的一个字符-但是该类中没有字符,因此这总是失败。请注意,这不会有任何其他作用,因为字符类通常不能为空。
每当RegEx中有3种或更多替代品时:
/aliceblue|antiquewhite|aquamarine|azure/
检查是否有一个共同的起点:
/a(liceblue|ntiquewhite|quamarine|zure)/
甚至一个共同的结局?
/a(liceblu|ntiquewhit|quamarin|zur)e/
注意:3只是开始,并且会占相同的长度,4+会有所不同
但是,如果不是所有人都有一个共同的前缀怎么办?(仅为了清楚起见添加了空格)
/aliceblue|antiquewhite|aqua|aquamarine|azure
|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood
|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan/
只要3+规则有意义,就将它们分组:
/a(liceblue|ntiquewhite|qua|quamarine|zure)
|b(eige|isque|lack|lanchedalmond|lue|lueviolet|rown|urlywood)
|c(adetblue|hartreuse|hocolate|oral|ornflowerblue|ornsilk|rimson|yan)/
甚至可以概括一下熵是否满足您的用例:
/\w(liceblue|ntiquewhite|qua|quamarine|zure
|eige|isque|lack|lanchedalmond|lue|lueviolet|rown|urlywood
|adetblue|hartreuse|hocolate|oral|ornflowerblue|ornsilk|rimson|yan)/
^在这种情况下,我们确信我们没有得到任何clue
或crown
slack
Ryan
这“根据一些测试”还提高了性能,因为它提供了一个锚开始的。
aqua|aquamarine
→ aqua(|marine)
或aqua(marine)?
。
这很简单,但是值得说明:
如果你发现自己重复角色职业[a-zA-Z]
,你可能只需要使用[a-z]
并追加i
(区分我 nsensitive调节剂)的正则表达式。
例如,在Ruby中,以下两个正则表达式是等效的:
/[a-zA-Z]+\d{3}[a-zA-Z]+/
/[a-z]+\d{3}[a-z]/i
-短7个字节
因此,其他修饰符也可以缩短您的总长度。而不是这样做:
/(.|\n)/
匹配任何字符(因为点与换行符不匹配),请使用s ingle-line修饰符s
,使点与换行符匹配。
/./s
-短3个字节
在Ruby中,有大量用于regex的内置字符类。参见本页并搜索“字符属性”。
一个很好的例子是“货币符号”。根据Wikipedia的说法,存在大量可能的货币符号,将它们放在字符类中将非常昂贵([$฿¢₡Ð₫€.....
]),而您可以将它们中的任何一个匹配为6个字节:\p{Sc}
s
不支持修饰符。:(但是,你可以使用JavaScript专有的/[^]/
伎俩。
(.|\n)
这甚至在某些口味上.
都不起作用,因为通常也与其他类型的行分隔符不匹配。但是,执行此操作的常规方法(不带s
)[\s\S]
是与相同的字节(.|\n)
。
您可以使用RE这样构建一个非常简单的解析器\d+|\w+|".*?"|\n|\S
。您需要匹配的令牌用RE'或'字符分隔。
每次RE引擎尝试在文本中的当前位置进行匹配时,它将尝试第一个模式,然后尝试第二个模式,等等。如果失败(例如,在此处为空格字符),它将继续并再次尝试匹配。顺序很重要。如果我们将\S
术语放置在术语之前\d+
,则\S
它将首先与任何会破坏解析器的非空格字符匹配。
该".*?"
字符串匹配使用非贪婪的修改,所以我们只匹配一次一个字符串。如果您的RE没有非贪婪功能,则可以使用
"[^"]*"
等效功能。
text = 'd="dogfinder"\nx=sum(ord(c)*872 for c in "fish"+d[3:])'
pat = r'\d+|\w+|".*?"|\n|\S'
print re.findall(pat, text)
['d', '=', '"dogfinder"', '\n', 'x', '=', 'sum', '(', 'ord', '(', 'c', ')',
'*', '872', 'for', 'c', 'in', '"fish"', '+', 'd', '[', '3', ':', ']', ')']
# assume we have language text in A, and a token processing function P
map(P,findall(r'\d+|\w+|".*?"|\n|\S',A))
您可以根据需要匹配的语言调整模式及其顺序。该技术适用于JSON,基本HTML和数字表达式。它已经在Python 2中成功使用了很多次,但是应该足够通用,可以在其他环境中使用。
\K
而不是积极地往后看PCRE和Perl支持转义序列\K
,该序列可重置比赛的开始。这ab\Kcd
将要求您包含输入字符串,abcd
但报告的匹配项将仅为cd
。
如果在模式的开头(可能是最可能出现的位置)使用正向后视,则在大多数情况下,可以\K
改用并节省3个字节:
(?<=abc)def
abc\Kdef
对于大多数目的,这是等效的,但并非完全等效。差异带来了优点和缺点:
(?<=ab*)
。但是有了它,\K
您可以在它前面放置任何样式!这样ab*\K
工作。实际上,在适用的情况下,这项技术的功能大大增强。\K
正则表达式的那一部分时,像其他所有东西一样被回溯。缺点:您可能知道,一个正则表达式的多个匹配项不能重叠。通常,环视通常用于部分解决此限制,因为前瞻可以验证早期匹配已消耗的一部分字符串。因此,如果要匹配后面的 所有字符,ab
可以使用(?<=ab).
。给定输入
ababc
这将匹配第二个a
和c
。这不能与再现\K
。如果您使用了ab\K.
,那么您只会得到第一个匹配项,因为现在ab
不在寻找范围内。
\K
在肯定断言中使用转义序列,则成功匹配的报告开始时间可能大于匹配结束时间。
ababc
,没有办法到第二都匹配a
和c
使用\K
。您只会得到一场比赛。
\G
.
最后一场比赛的结果实际上是a
。
ECMAScript风格缺少s
使.
任何字符(包括换行符)匹配的修饰符。这意味着没有单个字符的解决方案可以完全匹配任意字符。其他口味的标准解决方案(当s
由于某种原因而不想使用时)是[\s\S]
。但是,据我所知,ECMAScript是唯一支持空字符类的样式,因此具有更短的选择:[^]
。这是一个否定的空字符类-也就是说,它与任何字符都匹配。
即使是其他口味,我们也可以从这种技术中学习:如果我们不想使用s
(例如,因为我们仍然需要.
在其他地方具有通常的含义),那么仍然可以使用较短的方式来匹配换行符和可打印字符,前提是我们知道某些字符不会出现在输入中。假设我们正在处理以换行符分隔的数字。然后,我们可以将任何字符与匹配[^!]
,因为我们知道该字符!
永远不会成为字符串的一部分。这样可以在朴素的[\s\S]
或上节省两个字节[\d\n]
。
\N
含义完全相同。.
/s
我发现原子团((?>...)
)和占有欲量词(?+
,*+
,++
,{m,n}+
)有时高尔夫是非常有用的。它匹配一个字符串,并且以后不允许回溯。因此,它将仅匹配由正则表达式引擎找到的第一个可匹配字符串。
例如:要匹配以奇数a
开头的字符串,而不是后面跟多个的字符串a
,可以使用:
^(aa)*+a
^(?>(aa)*)a
这使您可以.*
自由使用类似的东西,并且如果存在明显的匹配,则不会再有太多或太少的字符匹配,这可能会破坏您的模式。
在.NET正则表达式(没有所有格限定词)中,您可以使用此选项将组1弹出3次(最多30次)的最大倍数(打得不好):
(?>((?<-1>){3}|){10})
有时记住
(abc)?
是大多是一样的
(abc|)
不过,两者之间的差别很小:在第一种情况下,小组要么捕获abc
要么根本不捕获。后一种情况将使反向引用无条件失败。在第二个表达式中,组将捕获abc
或为空字符串,后一种情况将无条件地使反向引用匹配。为了模拟后一种行为,?
您需要将所有内容包围在另一个组中,这将花费两个字节:
((abc)?)
|
当您仍然希望将表达式包装成其他形式的组并且不关心捕获时,使用版本使用也很有用:
(?=(abc)?)
(?=abc|)
(?>(abc)?)
(?>abc|)
最后,此技巧还可以应用于不贪婪的?
地方,即使它以原始形式保存一个字节(因此与其他形式的组组合时也保存3个字节):
(abc)??
(|abc)