正则表达式高尔夫的提示


43

类似于我们针对特定语言的高尔夫技巧的主题:缩短正则表达式的一般技巧是什么?

在打高尔夫球时,我可以看到正则表达式的三种用法:经典的正则表达式高尔夫(“这里是一个应该匹配的列表,这里应该是一个失败的列表”),使用正则表达式来解决计算问题并将正则表达式用作其中的一部分较大的高尔夫球代码。随时发布解决任何或所有这些问题的技巧。如果您的小费仅限于一种或多种口味,请在顶部注明这些口味。

像往常一样,请为每个答案坚持一个提示(或一系列密切相关的提示),以便最有用的提示可以通过投票升至最高位置。


公然的自我推广:这属于哪种正则表达式用途?codegolf.stackexchange.com/a/37685/8048
凯尔·斯特兰德

@KyleStrand“正则表达式用作较大的高尔夫球代码的一部分。”
马丁·恩德

Answers:


24

什么时候不逃脱

这些规则适用于大多数风味,如果不是全部的话:

  • ] 不匹配时不需要转义。

  • {并且}当它们不是重复的一部分(例如从字面上{a}匹配)时,不需要转义{a}。即使您想匹配类似的内容{2},也只需要转义其中之一即可,例如{2\}

在角色类中:

  • ]当它是字符集中的第一个字符(例如,[]abc]匹配一个]abc)或当它是a之后的第二个字符^(例如,[^]]匹配除以外的其他字符)时,不需要转义]。(值得注意的例外:ECMAScript风味!)

  • [根本不需要转义。结合以上技巧,这意味着您可以将两个方括号与可怕的违反直觉的字符类进行匹配[][]

  • ^如果它不是字符集中的第一个字符,则不需要转义[ab^c]

  • -不需要逃避时,它的第一(第二后^的字符集,例如)或最后一个字符[-abc][^-abc][abc-]

  • 即使其他字符是字符类之外的元字符,也无需在字符类中转义(反斜杠\本身除外)。

同样,在某些风味中^$当它们不在正则表达式的开头或结尾时,在字面上会进行匹配。

(感谢@MartinBüttner填写了一些详细信息)


有些人喜欢通过将实际点括在不需要转义的字符类中来进行转义(例如[.])。摆脱它通常会在这种情况下保存1个字节\.
CSᵠ

注意,[必须在Java中转义。不过,不确定ICU(在Android和iOS中使用)还是.NET。
n̴̖̋h̷͉̃a̷̭̿h̸̡̅ẗ̵̨́d̷̰̀ĥ̷̳ 2015年

18

一个简单的正则表达式,用于匹配ASCII表中的所有可打印字符。

[ -~]

1
绝对令人赞叹,所有来自标准美式键盘的字符!注意:标准ASCII表(不包括扩展范围127-255
CSᵠ

我经常使用它,但是它缺少一个常见的“常规”字符:TAB。并且假定您正在使用LC_ALL =“ C”(或类似名称),因为其他一些语言环境将失败。
奥利维尔·杜拉克

可以使用连字符那样在ASCII表中指定任意范围的字符吗?这对所有正则表达式都有效吗?
Josh Withee '17

14

了解您的正则表达式口味

令人惊讶的是,有很多人认为正则表达式本质上与语言无关。但是,口味之间实际上存在相当大的差异,尤其是对于打码高尔夫,很高兴知道其中的几种口味以及它们有趣的功能,因此您可以为每种任务选择最佳口味。这里是几种重要口味的概述,以及它们与众不同的地方。(此列表无法真正完成,但是请让我知道是否错过了确实令人眼花something乱的内容。)

Perl和PCRE

我将它们放入一个容器中,因为我不太熟悉Perl的风格,而且它们几乎是等效的(PCRE毕竟是与Perl兼容的正则表达式)。Perl风格的主要优点是您实际上可以从正则表达式和替换内部调用Perl代码。

  • 递归/子例程。可能是打高尔夫球最重要的功能(仅存在于几种口味中)。
  • 条件模式(?(group)yes|no)
  • 在替换字符串以案例都支持更改\l\u\L\U
  • PCRE允许在后面进行交替,其中每个替代可以具有不同(但固定)的长度。(包括Perl在内的大多数口味都要求回头角具有固定的总长度。)
  • \G 将比赛锚定到上一场比赛的末尾。
  • \K 重置比赛开始
  • PCRE同时支持Unicode字符属性和脚本
  • \Q...\E逃脱更长的角色。在尝试匹配包含许多元字符的字符串时很有用。

。净

这可能是最有力的口味,只有很少的缺点。

在打高尔夫球方面的一个重要缺点是,它不像其他口味那样支持所有格量​​词。而不是.?+你必须写(?>.?)

爪哇

  • 由于存在错误(请参阅附录),Java支持有限类型的可变长度lookbehind:您可以.*从头开始一直往后看,直到现在为止都可以从字符串开始,例如(?<=(?=lookahead).*)
  • 支持字符类的并集和交集。
  • 具有对Unicode的最广泛支持,带有“ Unicode脚本,块,类别和二进制属性”的字符类。
  • \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\zs.m

蟒蛇

Python具有浓郁的风味,但我不知道您在其他任何地方都找不到的任何特别有用的功能。

但是,还有一种替代风味,该风味旨在re在某个时候替换模块,并且包含许多有趣的功能。除了增加对递归,变长后向查找和字符类组合运算符的支持外,它还具有模糊匹配的独特功能。本质上,您可以指定允许的许多错误(插入,删除,替换),并且引擎还会为您提供近似匹配。

ECMAScript

ECMAScript的味道非常有限,因此很少用于打高尔夫球。唯一要做的就是否定空字符类 [^]以匹配任何字符,以及无条件失败的空字符类[](与通常的相对(?!))。不幸的是,风味没有任何特征,使得后者对于正常问题没有用。

a

Lua有其自己独特的风味,其味道是有限的(例如,您甚至无法量化组),但确实具有一些有用和有趣的功能。

  • 对于内置的字符类,它有大量的简写形式,包括标点符号,大写/小写字符和十六进制数字。
  • 有了%b它,它支持非常紧凑的语法来匹配平衡的字符串。例如,先%b()匹配一个(,然后再匹配所有匹配项)(正确跳过内部匹配对)。(并且)可以是任意两个字符。

促进

Boost的regex风味本质上是Perl的风味。但是,它具有用于regex替换的一些不错的新功能,包括大小写更改和条件语句。据我所知,后者是Boost特有的。


请注意,“向前看”中的“先行”将突破“向前看”中的界限。经过Java和PCRE测试。
n̴̖̋h̷͉̃a̷̭̿h̸̡̅ẗ̵̨́d̷̰̀ĥ̷̳ 2015年

.?+等于.*吗?
CalculatorFeline

@CalculatorFeline前者是所有格0或1量词(在支持所有格量​​词的风格中),后者是0或更多量词。
Martin Ender

@CalculatorFeline啊,我明白了。有一个错字。
Martin Ender

13

了解你的角色类别

大多数正则表达式风格都有预定义的字符类。例如,\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指出这些)


3
\R用于PCRE中的换行符。
HamZa 2015年

12

不要打扰那些不打招呼的人(除非...)

本技巧适用于(至少)所有受Perl启发的流行口味。

这可能是显而易见的,但是(在不打高尔夫球时)最好(?:...)在可能的情况下使用非捕获组。但是,这两个额外的角色?:在打高尔夫球时很浪费,因此即使不打算反向引用它们,也请使用捕获组。

但是,有一个(很少)例外:如果您10至少对反向引用组进行了3次操作,则可以通过将较早的组转换为非捕获组,从而使所有\10s变为\9s ,从而实际上可以节省字节。(如果您使用群组11至少5次,依此类推,也有类似的技巧。)


为什么11需要5倍才值得,而10需要3倍呢?
Nic Hartley's

1
可以使用@QPaysTaxes $9代替$10$11一次保存一个字节。谈及$10$9需要一个?:,这是两个字节,所以你需要三个$10s保存的东西。谈及$11$9需要两个?:S的是四个字节,所以你需5个$11s保存的东西(或五$10$11合并)。
马丁·恩德

10

递归以进行模式重用

少数风味支持递归(据我所知,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

请注意,在保留行为的同时不能在此处使用反向引用。在上述字符串上的反向引用将失败,因为B0!不相同。但是,通过子例程调用,实际上会重新评估该模式。上面的模式完全等同于

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将与上面的第一个示例匹配。


您可以使用\1Javascript。还有PHP(我猜)。
Ismael Miguel

5
@IsmaelMiguel这不是反向引用。实际上,这会再次评估模式。例如(..)\1将匹配abab但失败,abba(..)(?1)将匹配后者。从某种意义上说,实际上是一个子例程调用,而不是从字面上匹配上次匹配的表达式,再次应用了表达式。
马丁·恩德

哇,我不知道!每天学习新东西
Ismael Miguel

在.NET(或其他不含此功能的版本)中:(?=a.b.c)(.[0_B!$]){3}d
jimmy23013 2015年

@ user23013似乎非常特定于此特定示例。如果在各种环顾中重用某个子模式,我不确定是否适用。
马丁·恩德

9

导致比赛失败

使用正则表达式解决计算问题或匹配高度不规则的语言时,有时有必要使模式的分支失败,无论您在字符串中的位置如何。天真的方法是使用空的负前瞻:

(?!)

内容(空模式)始终匹配,因此负前瞻始终失败。但是通常有一个更简单的选择:只使用您知道永远不会出现在输入中的字符。例如,如果您知道您的输入将始终仅包含数字,则可以简单地使用

!

或任何其他非数字,非元字符导致失败。

即使您的输入可能包含任何子字符串,也有比短的方法(?!)。允许锚出现在模式中而不是结尾中的任何样式,都可以使用以下两个字符的解决方案之一:

a^
$a

然而,一些口味将把注意^,并$在这些位置的文字字符,因为他们显然并没有真正意义的锚。

在ECMAScript风格中,还有一个非常优雅的2字符解决方案

[]

这是一个空字符类,它试图确保下一个字符是该类中的一个字符-但是该类中没有字符,因此这总是失败。请注意,这不会有任何其他作用,因为字符类通常不能为空。


8

优化您的

每当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)/

^在这种情况下,我们确信我们没有得到任何cluecrown slack Ryan

“根据一些测试”还提高了性能,因为它提供了一个开始的。


1
如果共同的开始或结尾长于一个字符,则即使将两个字符分组也会有所不同。像aqua|aquamarineaqua(|marine)aqua(marine)?
圣保罗Ebermann

6

这很简单,但是值得说明:

如果你发现自己重复角色职业[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}


1
除JavaScript外,s不支持修饰符。:(但是,你可以使用JavaScript专有的/[^]/伎俩。
manatwork

请注意,(.|\n)这甚至在某些口味上.都不起作用,因为通常也与其他类型的行分隔符不匹配。但是,执行此操作的常规方法(不带s[\s\S]是与相同的字节(.|\n)
马丁·恩德

@MartinBüttner,我的想法是将其与其他结尾的提示保持在一起。但是,如果您认为此答案更多地与修饰符有关,那么如果您重新发布它,我不反对。
manatwork'Mar

@manatwork完成(并添加了一个与非ES相关的技巧)
Martin Ender

6

一个简单的语言解析器

您可以使用RE这样构建一个非常简单的解析器\d+|\w+|".*?"|\n|\S。您需要匹配的令牌用RE'或'字符分隔。

每次RE引擎尝试在文本中的当前位置进行匹配时,它将尝试第一个模式,然后尝试第二个模式,等等。如果失败(例如,在此处为空格字符),它将继续并再次尝试匹配。顺序很重要。如果我们将\S术语放置在术语之前\d+,则\S它将首先与任何会破坏解析器的非空格字符匹配。

".*?"字符串匹配使用非贪婪的修改,所以我们只匹配一次一个字符串。如果您的RE没有非贪婪功能,则可以使用 "[^"]*"等效功能。

Python示例:

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', ':', ']', ')']

打高尔夫球的Python示例:

# 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中成功使用了很多次,但是应该足够通用,可以在其他环境中使用。


6

\K 而不是积极地往后看

PCRE和Perl支持转义序列\K,该序列可重置比赛的开始。这ab\Kcd将要求您包含输入字符串,abcd但报告的匹配项将仅为cd

如果在模式的开头(可能是最可能出现的位置)使用正向后视,则在大多数情况下,可以\K改用并节省3个字节:

(?<=abc)def
abc\Kdef

对于大多数目的,这是等效的,但并非完全等效。差异带来了优点和缺点:

  • 好处:PCRE和Perl不支持任意长度的lookbehinds(仅.NET支持)。也就是说,您无法执行(?<=ab*)。但是有了它,\K您可以在它前面放置任何样式!这样ab*\K工作。实际上,在适用的情况下,这项技术的功能大大增强。
  • 上行空间:环视不会回溯。如果您要捕获后面的回溯中的某些东西,则这是相关的,但是有几种可能的捕获都导致有效的匹配。在这种情况下,正则表达式引擎只会尝试这些可能性之一。当使用\K正则表达式的那一部分时,像其他所有东西一样被回溯。
  • 缺点:您可能知道,一个正则表达式的多个匹配项不能重叠。通常,环视通常用于部分解决此限制,因为前瞻可以验证早期匹配已消耗的一部分字符串。因此,如果要匹配后面的 所有字符,ab可以使用(?<=ab).。给定输入

    ababc
    

    这将匹配第二个ac。这不能与再现\K。如果您使用了ab\K.,那么您只会得到第一个匹配项,因为现在ab不在寻找范围内。


如果模式\K在肯定断言中使用转义序列,则成功匹配的报告开始时间可能大于匹配结束时间。
2015年

@hwnd我的观点是,给定ababc,没有办法到第二都匹配ac使用\K。您只会得到一场比赛。
马丁·恩德

您是正确的,而不是功能本身。您将不得不锚定\G
hwnd

@hwnd啊,我明白你的意思了。但我想从高尔夫角度来看,从负面的角度来看,你最好还是这样做,因为您可能甚至仍然需要它,因为您不能确定.最后一场比赛的结果实际上是a
马丁·恩德

1
\ K =)的有趣用法
2015年

5

匹配任何字符

ECMAScript风格缺少s使.任何字符(包括换行符)匹配的修饰符。这意味着没有单个字符的解决方案可以完全匹配任意字符。其他口味的标准解决方案(当s由于某种原因而不想使用时)是[\s\S]。但是,据我所知,ECMAScript是唯一支持空字符类的样式,因此具有更短的选择:[^]。这是一个否定的空字符类-也就是说,它与任何字符都匹配。

即使是其他口味,我们也可以从这种技术中学习:如果我们不想使用s(例如,因为我们仍然需要.在其他地方具有通常的含义),那么仍然可以使用较短的方式来匹配换行符和可打印字符,前提是我们知道某些字符不会出现在输入中。假设我们正在处理以换行符分隔的数字。然后,我们可以将任何字符与匹配[^!],因为我们知道该字符!永远不会成为字符串的一部分。这样可以在朴素的[\s\S]或上节省两个字节[\d\n]


4
在Perl中,除了模式不受模式影响外,它的含义与模式之外的\N含义完全相同。./s
Konrad Borowski

4

使用原子团和所有格修饰词

我发现原子团((?>...))和占有欲量词(?+*+++{m,n}+)有时高尔夫是非常有用的。它匹配一个字符串,并且以后不允许回溯。因此,它将仅匹配由正则表达式引擎找到的第一个可匹配字符串。

例如:要匹配以奇数a开头的字符串,而不是后面跟多个的字符串a,可以使用:

^(aa)*+a
^(?>(aa)*)a

这使您可以.*自由使用类似的东西,并且如果存在明显的匹配,则不会再有太多或太少的字符匹配,这可能会破坏您的模式。

在.NET正则表达式(没有所有格限定词)中,您可以使用此选项将组1弹出3次(最多30次)的最大倍数(打得不好):

(?>((?<-1>){3}|){10})

1
ECMAScript中还缺少占有欲量词或原子团:(
CSᵠ

4

子表达式(PCRE)后忘记捕获的组

对于此正则表达式:

^((a)(?=\2))(?!\2)

如果要在组1之后清除\ 2,可以使用递归:

^((a)(?=\2)){0}(?1)(?!\2)

它会匹配,aa而前一个不会匹配。有时您也可以使用??甚至?代替{0}

如果您经常使用递归,并且某些反向引用或条件组出现在正则表达式的不同位置,则这可能很有用。

还应注意,PCRE中的递归被假定为原子团。所以这不会匹配一个字母a

^(a?){0}(?1)a

我还没有尝试其他口味。

对于前瞻,您也可以为此使用双底片:

^(?!(?!(a)(?=\1))).(?!\1)

4

可选表达式

有时记住

(abc)?

大多是一样的

(abc|)

不过,两者之间的差别很小:在第一种情况下,小组要么捕获abc要么根本不捕获。后一种情况将使反向引用无条件失败。在第二个表达式中,组将捕获abc或为空字符串,后一种情况将无条件地使反向引用匹配。为了模拟后一种行为,?您需要将所有内容包围在另一个组中,这将花费两个字节:

((abc)?)

|当您仍然希望将表达式包装成其他形式的组并且不关心捕获时,使用版本使用也很有用:

(?=(abc)?)
(?=abc|)

(?>(abc)?)
(?>abc|)

最后,此技巧还可以应用于不贪婪的?地方,即使它以原始形式保存一个字节(因此与其他形式的组组合时也保存3个字节):

(abc)??
(|abc)

1

始终匹配的多个前瞻(.NET)

如果您具有3个或更多始终匹配的前瞻性构造(以捕获子表达式),或者前瞻上有一个量词,后跟其他内容,则它们应属于未捕获的组:

(?=a)(?=b)(?=c)
((?=a)b){...}

这些较短:

(?(?(?(a)b)c))
(?(a)b){...}

其中a不应是已捕获组的名称。不能使用|是指在正常不过的事情b,并c没有添加其他对括号。

不幸的是,在条件条件下平衡组似乎有问题,从而使它在许多情况下毫无用处。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.