提问人:Hans Schindler 提问时间:5/11/2014 最后编辑:zx81Hans Schindler 更新时间:9/27/2023 访问量:41512
要匹配的正则表达式模式,排除何时... / 除此以外
Regex Pattern to Match, Excluding when... / Except between
问:
--编辑--目前的答案有一些有用的想法,但我想要一些更完整的东西,我可以 100% 理解和重用;这就是我设定赏金的原因。此外,在任何地方都有效的想法对我来说比不标准语法更好,例如\K
这个问题是关于我如何匹配模式,但某些情况除外:s1、s2、s3。我举了一个具体的例子来说明我的意思,但更喜欢一个我可以 100% 理解的一般答案,这样我就可以在其他情况下重复使用它。
例
我想在三种情况下使用但不匹配五位数字 s1 s2 s3:\b\d{5}\b
S1:不是在以句号结尾的行上,就像这句话一样。
S2:不在parens的任何地方。
S3:不在以 开头和结尾的块内if(
//endif
我知道如何通过前瞻和后视来解决 s1、s2、s3 中的任何一个,尤其是在 C# lookbehind 或 PHP 中。\K
例如
S1型(?m)(?!\d+.*?\.$)\d+
s3 与 C# 的后视(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
s3 与 PHP \K(?:(?:if\(.*?//endif)\D*)*\K\d+
但各种条件的混合让我头晕目眩。更坏的消息是,我可能需要在另一个时间添加其他条件 s4 s5。
好消息是,我不在乎我是否使用 PHP、C#、Python 或我邻居的洗衣机等最常用的语言处理文件。:)我几乎是 Python 和 Java 的初学者,但有兴趣了解它是否有解决方案。
所以我来这里,看看是否有人想到一个灵活的食谱。
提示是可以的:你不需要给我完整的代码。:)
谢谢。
答:
汉斯,我会上钩并充实我之前的答案。你说你想要“更完整的东西”,所以我希望你不会介意冗长的答案——只是想取悦。让我们从一些背景开始。
首先,这是一个很好的问题。除了某些上下文(例如,在代码块内或括号内)之外,经常存在有关匹配某些模式的问题。这些问题往往会产生相当尴尬的解决方案。因此,您关于多个上下文的问题是一个特殊的挑战。
惊喜
令人惊讶的是,至少有一种有效的解决方案是通用的、易于实施的、易于维护的。它适用于所有正则表达式风格,允许您检查代码中的捕获组。它恰好回答了一些常见问题,乍一听可能与您的问题不同:“匹配除甜甜圈以外的所有内容”、“替换所有但......”、“匹配除我妈妈黑名单上的单词之外的所有单词”、“忽略标签”、“除非斜体,否则匹配温度”......
可悲的是,这种技术并不为人所知:我估计,在二十个可以使用它的 SO 问题中,只有一个答案提到了它——这意味着可能每五六十个答案中就有一个。在评论中查看我与 Kobi 的交流。本文对该技术进行了深入描述,称其为(乐观地)“有史以来最好的正则表达式技巧”。在不赘述太多细节的情况下,我将尝试让您牢牢掌握该技术的工作原理。有关各种语言的更多详细信息和代码示例,我鼓励您查阅该资源。
一个更广为人知的变体
使用特定于 Perl 和 PHP 的语法的变体可以实现相同的目的。你会在 SO 上看到它掌握在 CasimiretHippolyte 和 HamZa 等正则表达式大师手中。我将在下面详细介绍这一点,但我在这里的重点是适用于所有正则表达式风格的通用解决方案(只要您可以在代码中检查捕获组)。
感谢所有的背景,zx81...但是配方是什么?
关键事实
该方法返回组 1 捕获中的匹配项。它不在乎 所有关于整体比赛。
事实上,诀窍是匹配我们不想要的各种上下文(使用 OR / alternation 链接这些上下文)以“中和它们”。在匹配所有不需要的上下文后,交替的最后一部分会匹配我们想要的内容,并将其捕获到第 1 组。|
一般配方是
Not_this_context|Not_this_either|StayAway|(WhatYouWant)
这将匹配,但从某种意义上说,该匹配会进入垃圾箱,因为我们不会查看整体匹配:我们只查看第 1 组捕获。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 组中。
Perl/PCRE 变体
与上面的一般解决方案相比,Perl 和 PCRE 存在一种变体,这种变体在 SO 上很常见,至少在正则表达式之神(如 @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>
- 除了标签或javascript片段(更多条件)之外,我如何匹配foo?
<i>
- 如何匹配所有不在此黑名单上的单词?
- 我怎么能忽略 SUB 中的任何内容......结束子块?
- 我怎样才能匹配所有东西,除了......S1 S2 S3?
如何对第 1 组捕获进行编程
你没有代码,但是,为了完成......检查组 1 的代码显然取决于您选择的语言。无论如何,它不应该在用于检查匹配项的代码中添加超过几行。
如有疑问,我建议您查看前面提到的文章的代码示例部分,其中提供了许多语言的代码。
选择
根据问题的复杂性和所使用的正则表达式引擎,有几种选择。以下是适用于大多数情况的两种情况,包括多种情况。在我看来,两者都不如食谱那么有吸引力,因为清晰度总是胜出。s1|s2|s3|(whatYouWant)
1. 替换然后匹配。
一个好的解决方案听起来很笨拙,但在许多环境中都运行良好,是分两步工作。第一个正则表达式通过替换可能冲突的字符串来抵消要忽略的上下文。如果您只想匹配,则可以替换为空字符串,然后在第二步中运行匹配。如果要替换,可以先将要忽略的字符串替换为独特的字符串,例如用固定宽度的 .在这次替换之后,你可以自由地替换你真正想要的东西,然后你必须恢复你独特的字符串。@@@
@@@
2. 环顾四周。
您的原始帖子表明您了解如何使用环顾来排除单个条件。您说 C# 非常适合此,您是对的,但这不是唯一的选择。例如,在 C#、VB.NET 和 Visual C++ 中发现的 .NET 正则表达式风格,以及在 Python 中替换的仍处于实验阶段的模块,是我所知道的仅有的两个支持无限宽度后视的引擎。有了这些工具,一个情况中的一个情况不仅可以照顾到后面,还可以照顾到比赛和比赛之外,避免了与展望协调的需要。更多条件?更多环顾周。regex
re
回收 C# 中 s3 的正则表达式,整个模式将如下所示。
(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
但现在你知道我不推荐这个,对吧?
删除
@HamZa和@Jerry建议我提到一个额外的技巧,当你试图删除.您还记得匹配(将其捕获到第 1 组)的配方是 ,对吧?若要删除 的所有实例,请将正则表达式更改为WhatYouWant
WhatYouWant
s1|s2|s3|(WhatYouWant)
WhatYouWant
(s1|s2|s3)|WhatYouWant
对于替换字符串,请使用 .这里发生的情况是,对于匹配的每个实例,替换将该实例替换为其自身(由 引用)。另一方面,当匹配时,它将被一个空组替换,而不是其他任何内容,因此被删除。请看这个演示,感谢@HamZa和@Jerry提出这个精彩的补充。$1
s1|s2|s3
$1
$1
WhatYouWant
更换
这就引出了替代品,我将简要介绍一下。
- 当替换为任何内容时,请参阅上面的“删除”技巧。
- 替换时,如果使用 Perl 或 PCRE,请使用上面提到的变体来完全匹配您想要的内容,并直接替换。
(*SKIP)(*F)
- 在其他风格中,在替换函数调用中,使用回调或 lambda 检查匹配项,如果设置了组 1,则进行替换。如果你需要这方面的帮助,已经引用的文章将为你提供各种语言的代码。
玩得愉快!
不,等等,还有更多!
啊,不,我会把它留到我的回忆录中,共二十卷,明年春天出版。
评论
Tarzan
/no|no|(yes)/
/"[^"]*"|Tarzan/
var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";
var bug1 = /"[^"]*"|(Tarzan)/gi;
(?<!\\)"(?:\\"|[^"\r\n])*+"
执行三种不同的匹配,并使用程序内条件逻辑处理三种情况的组合。你不需要在一个巨大的正则表达式中处理所有事情。
编辑:让我扩展一下,因为这个问题变得更加有趣:-)
您在此处尝试捕获的一般思路是与某个正则表达式模式进行匹配,但当测试字符串中存在某些其他(可能是任意数字)模式时,则不匹配。幸运的是,您可以利用您的编程语言:保持正则表达式简单,只使用复合条件。最佳做法是在可重用的组件中捕获此想法,因此让我们创建一个类和一个实现它的方法:
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class MatcherWithExceptions {
private string m_searchStr;
private Regex m_searchRegex;
private IEnumerable<Regex> m_exceptionRegexes;
public string SearchString {
get { return m_searchStr; }
set {
m_searchStr = value;
m_searchRegex = new Regex(value);
}
}
public string[] ExceptionStrings {
set { m_exceptionRegexes = from es in value select new Regex(es); }
}
public bool IsMatch(string testStr) {
return (
m_searchRegex.IsMatch(testStr)
&& !m_exceptionRegexes.Any(er => er.IsMatch(testStr))
);
}
}
public class App {
public static void Main() {
var mwe = new MatcherWithExceptions();
// Set up the matcher object.
mwe.SearchString = @"\b\d{5}\b";
mwe.ExceptionStrings = new string[] {
@"\.$"
, @"\(.*" + mwe.SearchString + @".*\)"
, @"if\(.*" + mwe.SearchString + @".*//endif"
};
var testStrs = new string[] {
"1." // False
, "11111." // False
, "(11111)" // False
, "if(11111//endif" // False
, "if(11111" // True
, "11111" // True
};
// Perform the tests.
foreach (var ts in testStrs) {
System.Console.WriteLine(mwe.IsMatch(ts));
}
}
}
因此,在上面,我们设置了搜索字符串(五位数字)、多个异常字符串(s1、s2 和 s3),然后尝试与多个测试字符串进行匹配。打印的结果应如每个测试字符串旁边的注释所示。
评论
您的要求是它不在parens内部,不可能满足所有情况。
也就是说,如果你能以某种方式在左边和右边找到一个,这并不总是意味着你在parens里面。例如。(
)
(....) + 55555 + (.....)
- 不在parens里面,但左边和右边都有(
)
现在你可能会认为自己很聪明,只有在你以前没有遇到过的情况下才会向左寻找,反之亦然。这不适用于这种情况:(
)
((.....) + 55555 + (.....))
- 在Parens内部,即使有关闭,向左和向右。)
(
使用正则表达式无法确定您是否在 parens 内,因为正则表达式无法计算已打开的 paren 数量和关闭的 parens 数量。
考虑这个更简单的任务:使用正则表达式,找出字符串中的所有(可能是嵌套的)parens 是否都已关闭,即您需要查找的每个 paren。你会发现这是不可能解决的,如果你不能用正则表达式解决这个问题,那么你就无法弄清楚一个词是否在所有情况下都在括号内,因为你无法弄清楚在字符串中的某个位置是否有对应的.(
)
(
)
评论
汉斯,如果你不介意的话,我用过你邻居的洗衣机,叫perl:)
编辑:在伪代码下方:
loop through input
if line contains 'if(' set skip=true
if skip= true do nothing
else
if line match '\b\d{5}\b' set s0=true
if line does not match s1 condition set s1=true
if line does not match s2 condition set s2=true
if s0,s1,s2 are true print line
if line contains '//endif' set skip=false
给定文件 input.txt:
tiago@dell:~$ cat input.txt
this is a text
it should match 12345
if(
it should not match 12345
//endif
it should match 12345
it should not match 12345.
it should not match ( blabla 12345 blablabla )
it should not match ( 12345 )
it should match 12345
脚本 validator.pl:
tiago@dell:~$ cat validator.pl
#! /usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
sub validate_s0 {
my $line = $_[0];
if ( $line =~ \d{5/ ){
return "true";
}
return "false";
}
sub validate_s1 {
my $line = $_[0];
if ( $line =~ /\.$/ ){
return "false";
}
return "true";
}
sub validate_s2 {
my $line = $_[0];
if ( $line =~ /.*?\(.*\d{5.*?\).*/ ){
return "false";
}
return "true";
}
my $skip = "false";
while (<>){
my $line = $_;
if( $line =~ /if\(/ ){
$skip = "true";
}
if ( $skip eq "false" ) {
my $s0_status = validate_s0 "$line";
my $s1_status = validate_s1 "$line";
my $s2_status = validate_s2 "$line";
if ( $s0_status eq "true"){
if ( $s1_status eq "true"){
if ( $s2_status eq "true"){
print "$line";
}
}
}
}
if ( $line =~ /\/\/endif/) {
$skip="false";
}
}
执行:
tiago@dell:~$ cat input.txt | perl validator.pl it should match 12345 it should match 12345 it should match 12345
不确定这是否会对您有所帮助,但我提供了一个考虑到以下假设的解决方案 -
- 您需要一个优雅的解决方案来检查所有条件
- 未来和任何时候,情况都可能发生变化。
- 一个条件不应该依赖于其他条件。
但是,我还考虑了以下几点——
- 给定的文件中的错误最小。如果是这样,那么我的代码可能需要一些修改来应对这种情况。
- 我使用 Stack 来跟踪块。
if(
好的,这是解决方案 -
我使用 C# 和 MEF(Microsoft 扩展性框架)来实现可配置的解析器。这个想法是,使用单个解析器进行解析,并使用可配置的验证器类列表来验证行,并根据验证返回 true 或 false。然后,您可以随时添加或删除任何验证器,也可以根据需要添加新的验证器。到目前为止,我已经为您提到的 S1、S2 和 S3 实现了,请在第 3 点检查类。如果将来需要,您必须为 s4、s5 添加类。
首先,创建接口 -
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo.Contracts { public interface IParser { String[] GetMatchedLines(String filename); } public interface IPatternMatcher { Boolean IsMatched(String line, Stack<string> stack); } }
然后是文件读取器和检查器 -
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FileParserDemo.Contracts; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition; using System.IO; using System.Collections; namespace FileParserDemo.Parsers { public class Parser : IParser { [ImportMany] IEnumerable<Lazy<IPatternMatcher>> parsers; private CompositionContainer _container; public void ComposeParts() { var catalog = new AggregateCatalog(); catalog.Catalogs.Add(new AssemblyCatalog(typeof(IParser).Assembly)); _container = new CompositionContainer(catalog); try { this._container.ComposeParts(this); } catch { } } public String[] GetMatchedLines(String filename) { var matched = new List<String>(); var stack = new Stack<string>(); using (StreamReader sr = File.OpenText(filename)) { String line = ""; while (!sr.EndOfStream) { line = sr.ReadLine(); var m = true; foreach(var matcher in this.parsers){ m = m && matcher.Value.IsMatched(line, stack); } if (m) { matched.Add(line); } } } return matched.ToArray(); } } }
然后是单个检查器的实现,类名是不言自明的,所以我认为它们不需要更多的描述。
using FileParserDemo.Contracts; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace FileParserDemo.PatternMatchers { [Export(typeof(IPatternMatcher))] public class MatchAllNumbers : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("\\d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveIfBlock : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("if\\("); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { //push the if stack.Push(m.ToString()); } //ignore current line, and will validate on next line with stack return true; } regex = new Regex("//endif"); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { stack.Pop(); } } return stack.Count == 0; //if stack has an item then ignoring this block } } [Export(typeof(IPatternMatcher))] public class RemoveWithEndPeriod : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("(?m)(?!\\d+.*?\\.$)\\d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveWithInParenthesis : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("\\(.*\\d+.*\\)"); return !regex.IsMatch(line); } } }
程序 -
using FileParserDemo.Contracts; using FileParserDemo.Parsers; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo { class Program { static void Main(string[] args) { var parser = new Parser(); parser.ComposeParts(); var matches = parser.GetMatchedLines(Path.GetFullPath("test.txt")); foreach (var s in matches) { Console.WriteLine(s); } Console.ReadLine(); } } }
为了进行测试,我采用了@Tiago的示例文件,其中包含以下几行 -Test.txt
this is a text
it should match 12345
if(
it should not match 12345
//endif
it should match 12345
it should not match 12345.
it should not match ( blabla 12345 blablabla )
it should not match ( 12345 )
it should match 12345
给出输出 -
it should match 12345
it should match 12345
it should match 12345
不知道这是否会对你有所帮助,我确实玩得很开心...... :)
最好的部分是,要添加一个新条件,您所要做的就是提供 的实现,它将自动被调用,从而进行验证。IPatternMatcher
与@zx81相同,但使用否定的 lookahead 断言。(*SKIP)(*F)
(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)
在python中,我可以很容易地做到这一点,
import re
string = """cat 123 sat.
I like 000 not (456) though 111 is fine
222 if( //endif if(cat==789 stuff //endif 333"""
for line in string.split('\n'): # Split the input according to the `\n` character and then iterate over the parts.
if not line.endswith('.'): # Don't consider the part which ends with a dot.
for i in re.split(r'\([^()]*\)|if\(.*?//endif', line): # Again split the part by brackets or if condition which endswith `//endif` and then iterate over the inner parts.
for j in re.findall(r'\b\d+\b', i): # Then find all the numbers which are present inside the inner parts and then loop through the fetched numbers.
print(j) # Prints the number one ny one.
输出:
000
111
222
333
评论
\K
没有特殊的PHP语法。请详细说明并澄清您想说的内容。如果你的目的是告诉我们你不需要一个“复杂”的解决方案,你必须说出什么对你来说很复杂,以及为什么。"if("
")"
"//endif"
"//endif)"
especially in C# lookbehind or \K in PHP