汉斯,我愿意接受你的挑战并且扩展一下我的早期回答。你说你想要“更全面的内容”,所以我希望你不会介意这个长答案——只是为了取悦你。让我们从一些背景开始。
首先,这是一个很好的问题。通常有一些关于匹配特定模式的问题,除非在某些上下文中(例如,在代码块内或括号内)。 这些问题通常引起相当尴尬的解决方案。 因此,你提出的关于多个上下文的问题是一个特别的挑战。
惊喜!
令人惊讶的是,至少存在一种高效、通用、易于实现并且易于维护的解决方案。 它适用于所有允许您在代码中检查捕获组的正则表达式(flavors),并且恰好回答了许多常见问题,这些问题乍一听与你的问题可能不同:“匹配除甜甜圈外的所有内容”、“替换所有但……”、“匹配除了在妈妈的黑名单上的所有单词之外的所有单词”、“忽略标记”、“匹配温度,除非是斜体”……
可悲的是,这个技巧并不广为人知:我估计在二十个可以使用它的SO问题中,只有一个答案提到了它——这意味着可能有五十或六十个答案中只有一个。 请看我在评论中与Kobi的交流。该技术在这篇文章中描述得比较深入,它(乐观地)称之为“有史以来最好的正则表达式技巧”。 我会尝试给你提供一个坚实的把握,让你更好地理解这个技巧的工作原理,但不会详细介绍各种语言中的代码示例,建议你查阅那个资源。
一个知名的变化
有一种使用Perl和PHP特定语法实现相同效果的变体。你会在SO上看到像CasimiretHippolyte和HamZa这样的正则表达式大师使用它。后面我会告诉你更多信息,但我在这里的重点是通用解决方案,它适用于所有正则表达式(flavors)(只要您可以在代码中检查捕获组)。
感谢你提供的所有背景信息,zx81...但是到底该怎么做呢?
关键事实
该方法将匹配结果返回到第1个捕获组。它一点也不关心整个匹配结果。
事实上,技巧就是匹配我们不想要的各种上下文(使用|
或/选择运算符链接这些上下文),以“中和”它们。在匹配所有不想要的上下文后,选择运算
Not_this_context|Not_this_either|StayAway|(WhatYouWant)
这将匹配Not_this_context
,但从某种意义上说,该匹配项会进入垃圾箱,因为我们不会查看整体匹配项:我们只查看第一组捕获。
在您的情况下,有您的数字和要忽略的三个上下文,我们可以这样做:
s1|s2|s3|(\b\d+\b)
注意,因为我们实际上是匹配s1、s2和s3而不是试图用向前/向后查找避免它们,所以s1、s2和s3的个别表达式可以保持清晰明了。它们是
|
两侧的子表达式。整个表达式可以写成这样:
(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)
请查看此演示(但要关注右下角窗格中的捕获组)。
如果您尝试在每个|
定界符处将此正则表达式精神分裂,它实际上只是一系列非常简单的四个表达式。
对于支持自由间距的语言,这读起来特别好。
(?mx)
### s1: Match line that ends with a period ###
^.*\.$
| ### OR s2: Match anything between parentheses ###
\([^\)]*\)
| ### OR s3: Match any if(...//endif block ###
if\(.*?//endif
| ### OR capture digits to Group 1 ###
(\b\d+\b)
阅读和维护起来非常容易。
扩展正则表达式
当你想要忽略更多情况 s4 和 s5 时,将它们添加到左侧的更多交替中:
s4|s5|s1|s2|s3|(\b\d+\b)
这是如何工作的?
您不想要的上下文会被添加到左侧的替代列表中:它们会匹配,但这些整体匹配从未被检查,因此将它们匹配是将它们放入“垃圾桶”的一种方式。
然而,您想要的内容被捕获到组1中。接下来,您必须以编程方式检查组1是否设置且非空。考虑到这只是一个微不足道的编程任务(我们稍后会谈论如何完成它),尤其是它留给您一个简单的正则表达式,您可以一眼看懂并根据需要修改或扩展它。
我并不总是喜欢可视化效果,但这个效果很好地展示了方法的简单性。每条“线”对应于一个潜在的匹配,但只有最后一行被捕获到组1中。
![正则表达式可视化](https://www.debuggex.com/i/JBtmNJUA3NvZqG3t.png)
Debuggex 演示
Perl/PCRE 变体
与上述通用解决方案相反,在 Perl 和 PCRE 中存在一种变体,通常在像 @CasimiretHippolyte 和 @HamZa 这样的正则表达式大师手中看到。它是:
(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant
对于你的情况:
(?m)(?:^.*\.$|\([^()]*\)|if\(.*?//endif)(*SKIP)(*F)|\b\d+\b
这种变体更容易使用,因为在上下文s1、s2和s3中匹配的内容被简单地跳过了,所以您不需要检查第1组捕获(请注意,括号已经消失了)。匹配只包含whatYouWant
请注意,(*F)
、(*FAIL)
和(?!)
都是相同的东西。如果你想更加隐晦,可以使用(*SKIP)(?!)
这个版本的演示
应用程序
以下是一些常见问题,这种技术通常可以轻松解决。您会注意到,单词选择可能使其中一些问题听起来不同,但实际上它们几乎是相同的。
- 如何匹配foo,除了在像
<a stuff...>...</a>
这样的标记中? - 如何匹配foo,除了在标签或javascript片段中?
- 如何匹配不在此黑名单上的所有单词?
- 如何忽略SUB…END SUB块内的任何内容?
- 如何匹配除了…s1 s2 s3之外的所有内容?
如何编写第1组捕获的程序
您没有要求代码,但是为了完整起见……检查第1组的代码显然取决于您选择的语言。无论如何,它不应该比您用来检查匹配的代码多出几行。
如果有疑问,我建议您查看本文提到的代码示例部分,其中提供了许多语言的代码。
替代方案
根据问题的复杂性和所使用的正则表达式引擎,有几种替代方案。以下是适用于大多数情况,包括多个条件的两种替代方案。在我看来,它们都不如s1|s2|s3|(whatYouWant)
方法简单明了。
1. 先替换,再匹配。
这是一个好的解决方案,在许多环境中都可以很好地工作,虽然听起来有些巧妙。首先,通过替换可能会产生冲突的字符串,第一步中的正则表达式将中立化您想要忽略的上下文。如果您只想匹配,那么可以用空字符串替换,然后在第二步中运行匹配。如果您想要替换,可以首先将要忽略的字符串替换为某些独特的东西,例如使用固定宽度的@@@
链包围数字。进行此替换后,您就可以自由地替换您真正想要的内容,然后必须恢复您独特的@@@
字符串。
2. 前后断言。
你的原始帖子表明你知道如何使用前后断言来排除一个条件。你说C#很适合这个问题,你是对的,但这不是唯一的选择。在C#、VB.NET和Visual C++中找到的.NET正则表达式引擎以及仍在试验阶段的regex
模块(替换Python中的re
)是我所知道的仅有的两个支持无限宽度后向查找的引擎。使用这些工具,一个前向或后向断言中的一个条件可以处理匹配以及匹配之后的内容,避免了需要与前瞻协调的麻烦。需要更多条件?再加上前后断言即可。
重新利用你在C#中用于s3的正则表达式,整个模式看起来像这样:
(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
但你现在应该知道我不是在推荐这个方法,对吧?
删除
@HamZa 和 @Jerry 建议我提及另一种技巧,用于仅删除 WhatYouWant
的情况。你还记得匹配 WhatYouWant
(捕获到第1组)的正则表达式是 s1|s2|s3|(WhatYouWant)
,对吧?要删除所有实例的 WhatYouWant
,你需要将正则表达式改为:
(s1|s2|s3)|WhatYouWant
对于替换字符串,您使用
$1
。这里发生的情况是对于每个匹配的
s1|s2|s3
实例,替换
$1
用自己(由
$1
引用)替换该实例。另一方面,当匹配到
WhatYouWant
时,它被替换为一个空组,什么也没有 - 因此被删除。请参见此
演示,感谢@HamZa和@Jerry提供这个精彩的补充。
替换
这使我们进入了替换主题,我将简要地介绍一下。
1.当替换为空时,请参考上面的“删除”技巧。
2.在替换时,如果使用Perl或PCRE,则使用上面提到的
(*SKIP)(*F)
变体来精确匹配所需内容,并进行直接替换。
3.在其他版本中,在替换函数调用内部,使用回调或lambda检查匹配,并根据 Group 1 进行替换。如果需要帮助,请参阅已引用的文章,以获取各种语言的代码。
玩得开心!
不,等等,还有更多!
啊,不,我会把它保存到我的回忆录里,在明年春天发布的二十卷册中。
\K
不是 PHP 的特殊语法。请详细说明并澄清您想要表达的意思。如果您的目的是告诉我们您不需要“复杂”的解决方案,那么您必须说明什么对您来说是复杂的,以及为什么。 - hakre\K
。我从“特殊的PHP语法”改为“非标准语法”。谢谢! - Hans Schindler