正则表达式模式匹配:排除某些情况 / 除了在...之间

118

--编辑-- 当前的回答有一些有用的想法,但我希望得到一个更完整的答案,让我能够100%理解和重复使用;这就是为什么我设置了悬赏。此外,在任何地方都能工作的想法对我来说比不标准的语法如\\K更好。

这个问题是关于如何匹配一个模式,除了一些情况s1 s2 s3之外。我会给出一个具体的例子来说明我的意思,但我更喜欢一个通用的答案,这样我就可以在其他情况下100%理解并重复使用它。

例子

我想使用\\b\\d{5}\\b来匹配五位数字,但避免三种情况s1 s2 s3:

s1:不匹配以句点结尾的行,像这个句子一样。

s2:不匹配括号内的任何位置。

s3:不匹配以if(开头,以//endif结尾的块中的内容。

我知道如何通过先行断言和后行断言来解决任何一个s1 s2 s3,特别是在C#后行断言中或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方面都是初学者,但有兴趣学习是否有解决方案。

因此,我来这里看是否有人想出了一个灵活的方法。

提示是可以的:您不需要给我完整的代码。:)

谢谢。


1
\K 不是 PHP 的特殊语法。请详细说明并澄清您想要表达的意思。如果您的目的是告诉我们您不需要“复杂”的解决方案,那么您必须说明什么对您来说是复杂的,以及为什么。 - hakre
@hakre 你的意思是因为 Ruby 现在使用它并且它起源于 Perl? - Hans Schindler
1
不会,因为它是PCRE而不是PHP(或Ruby)。Perl是不同的,但是PCRE的目标是与Perl正则表达式兼容。 - hakre
@hakre 是的,我知道PCRE,但是为了解释,问题是关于编程语言的...它说“特别是在C#中的lookbehind或PHP中的\K”...但是C# lookbehind不仅仅是C#,它是.NET,所以你也可以抱怨,我说C#而不是.NET :)作为回复,我说Ruby而不是Onigurama,那也不好...还有其他使用PCRE的语言吗?不是在谈论Notepad++或服务器工具,这是关于在语言中使用功能的问题,我希望解释清楚,如果看起来不对,我很抱歉。 - Hans Schindler
@hakre 你是100%正确的!例如,在LAMP上的shell命令可以回答问题并使用\K。我从“特殊的PHP语法”改为“非标准语法”。谢谢! - Hans Schindler
显示剩余5条评论
6个回答

225

汉斯,我愿意接受你的挑战并且扩展一下我的早期回答。你说你想要“更全面的内容”,所以我希望你不会介意这个长答案——只是为了取悦你。让我们从一些背景开始。

首先,这是一个很好的问题。通常有一些关于匹配特定模式的问题,除非在某些上下文中(例如,在代码块内或括号内)。 这些问题通常引起相当尴尬的解决方案。 因此,你提出的关于多个上下文的问题是一个特别的挑战。

惊喜!

令人惊讶的是,至少存在一种高效、通用、易于实现并且易于维护的解决方案。 它适用于所有允许您在代码中检查捕获组的正则表达式(flavors),并且恰好回答了许多常见问题,这些问题乍一听与你的问题可能不同:“匹配除甜甜圈外的所有内容”、“替换所有但……”、“匹配除了在妈妈的黑名单上的所有单词之外的所有单词”、“忽略标记”、“匹配温度,除非是斜体”……

可悲的是,这个技巧并不广为人知:我估计在二十个可以使用它的SO问题中,只有一个答案提到了它——这意味着可能有五十或六十个答案中只有一个。 请看我在评论中与Kobi的交流。该技术在这篇文章中描述得比较深入,它(乐观地)称之为“有史以来最好的正则表达式技巧”。 我会尝试给你提供一个坚实的把握,让你更好地理解这个技巧的工作原理,但不会详细介绍各种语言中的代码示例,建议你查阅那个资源。

一个知名的变化

有一种使用Perl和PHP特定语法实现相同效果的变体。你会在SO上看到像CasimiretHippolyteHamZa这样的正则表达式大师使用它。后面我会告诉你更多信息,但我在这里的重点是通用解决方案,它适用于所有正则表达式(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中。

正则表达式可视化

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)(?!)

这个版本的演示

应用程序

以下是一些常见问题,这种技术通常可以轻松解决。您会注意到,单词选择可能使其中一些问题听起来不同,但实际上它们几乎是相同的。

  1. 如何匹配foo,除了在像<a stuff...>...</a>这样的标记中?
  2. 如何匹配foo,除了在标签或javascript片段中?
  3. 如何匹配不在此黑名单上的所有单词?
  4. 如何忽略SUB…END SUB块内的任何内容?
  5. 如何匹配除了…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 进行替换。如果需要帮助,请参阅已引用的文章,以获取各种语言的代码。
玩得开心!
不,等等,还有更多!
啊,不,我会把它保存到我的回忆录里,在明年春天发布的二十卷册中。

2
@Kobi 两部分回复。是的,昨晚写得有点过头了,底部写着我会睡一觉,稍后整理一下。:) 是的,这个技巧很简单,但我不认为它是“基础”的,因为它似乎不是人们用来解决排除问题的常见工具之一。当我在SO上搜索“except”、“unless”或“not inside”问题时,只有一个答案(没有投票)建议使用它,其他答案都没有。顺便说一句,我还没有看到你的答案,它们非常棒。 :) - zx81
2
抱歉,但 Rex 的“最佳技巧”并不可靠。比如你想匹配 Tarzan,但不希望它出现在双引号内。使用 /no|no|(yes)/ 技巧的正则表达式可能是:/"[^"]*"|Tarzan/(忽略转义字符)。这对许多情况都有效,但当应用于以下有效的 JavaScript 文本时完全失败:var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";。Rex 的技巧仅在匹配所有可能的结构时才有效 - 换句话说 - 你需要完全解析文本以保证 100% 的准确性。 - ridgerunner
1
抱歉如果我的话听起来很苛刻 - 那绝对不是我的意图。我的观点(正如我在上述原始问题的第二个评论中所说)是,正确的解决方案高度依赖于正在搜索的目标文本。我的示例具有JavaScript源代码作为目标文本,其中一个双引号包含在单引号字符串中。它可以很容易地成为文字RegExp,例如:var bug1 = /"[^"]*"|(Tarzan)/gi;并产生相同的效果(这第二个示例肯定不是边缘情况)。我可以引用更多的例子来证明这种技术无法可靠地工作。 - ridgerunner
1
@ridgerunner,我总是很享受听到你的声音,但这对我来说听起来太过苛刻了。当我们知道我们的字符串可能包含“false alerts”,我们都会调整模式。例如,为了匹配可能包含转义引号的字符串,这可能会使字符串匹配器失效,您可以使用 (?<!\\)"(?:\\"|[^"\r\n])*+"。除非你有理由,否则不要使用重型武器。解决方案的原则仍然有效。如果我们无法表达放在左侧的模式,那就是一个不同的问题,我们需要另一种解决方案。但是该解决方案确实做到了它所宣传的。 - zx81
1
此答案已被用户@funkwurm添加到Stack Overflow正则表达式FAQ中。 - aliteralmind
显示剩余10条评论

11

进行三个不同的匹配,并使用程序内的条件逻辑处理这三种情况的组合。您不需要在一个巨大的正则表达式中处理所有内容。

编辑:让我稍微解释一下,因为问题刚刚变得更有趣了:-)

您试图捕获的一般思想是根据某个正则表达式模式进行匹配,但当测试字符串中存在某些其他模式(可能是任意数量)时,则不匹配。幸运的是,您可以利用编程语言:保持正则表达式简单,只使用复合条件。最佳实践是将这个想法封装成可重用组件,因此让我们创建一个类和一个实现它的方法:

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));
    }
  }
}

因此,在上面,我们设置了搜索字符串(五个数字),多个异常字符串(s1s2s3),然后尝试匹配几个测试字符串。打印的结果应该如每个测试字符串旁边的注释所示。


2
你的意思是可能要连续匹配三个正则表达式吗?正则表达式1消除情况1(也许只需删除错误数字),r2移除s2,r3移除s3并匹配剩余数字?这是一个有趣的想法。 - Hans Schindler
哈,没错,这就是为什么我给你点了赞。 :) 别误会,我仍然认为在这种特定情况下,我的答案更有效率和可维护性。你看到我昨天添加的自由空格版本了吗?那是一次通过,非常容易阅读和维护。但我确实喜欢你的工作和你扩展的答案。很抱歉我不能再点赞了,否则我会的。 :) - zx81

2
与@zx81的(*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

2

如果您不介意,我使用了您邻居的名为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 
应该匹配12345
应该匹配12345
应该匹配12345

2
我不确定这是否能帮到你,但我提供一个解决方案,考虑以下假设:
  1. 您需要一种优雅的解决方案来检查所有条件
  2. 条件可能会在未来和任何时候更改。
  3. 一个条件不应取决于其他条件。
然而,我还考虑了以下内容:
  1. 给定的文件中错误很少。如果有,则我的代码可能需要进行一些修改。
  2. 我使用了堆栈来跟踪if(块。
好的,这里是解决方案 -
我使用C#和MEF(Microsoft Extensibility Framework)来实现可配置的解析器。想法是使用单个解析器来解析,并使用可配置的验证器类列表来验证行并根据验证返回true或false。然后,您可以随时添加或删除任何验证器,或者如果您喜欢,可以添加新的验证器。到目前为止,我已经为您提到的S1、S2和S3实现了,检查第3点的类。如果您将来需要s4、s5的类,请添加它们。
  1. First, Create the Interfaces -

    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);
        }
    }
    
  2. Then comes the file reader and checker -

    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();
            }
        }
    }
    
  3. Then comes the implementation of individual checkers, the class names are self explanatory, so I don't think they need more descriptions.

    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);
            }
        }
    }
    
  4. The program -

    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的实现,它会自动被调用并验证。


2
您的要求是无法满足所有情况的,即使在左边找到一个(,在右边找到一个),也并不总是意味着您在括号内。例如:(....) + 55555 + (.....) - 尽管左右两侧都有(),但仍未在括号内。
现在您可能认为自己很聪明,只有在没有遇到)之前才会向左查找(,反之亦然。但对于这种情况,这种方法行不通:((.....) + 55555 + (.....)) - 尽管左右两侧都有关闭的)(,但仍在括号内。
使用正则表达式无法确定当前是否在括号内,因为它无法计算已打开和已关闭的括号数目。考虑一下这个更简单的任务:使用正则表达式查找字符串中所有(可能嵌套的)括号是否已关闭,即对于每个(,您需要找到)。您会发现这是不可能解决的问题,如果您无法使用正则表达式解决这个问题,则无法确定一个单词是否在所有情况下都在括号内,因为您无法确定在字符串中的某个位置之前的所有(是否都有相应的)

2
没有人提到嵌套括号,而且 zx81 的答案已经很好地处理了你的情况 #1。 - Dan Bechard
谢谢你的好意 :) 但是嵌套括号对于这个问题并不是我的担忧,更多的是关于不良情况s1 s2 s3的想法。 - Hans Schindler
当然不是不可能的!这正是为什么您需要跟踪当前解析的括号级别。 - MrWonderful
如果你正在解析类似 OP 的 CFG,那么最好生成一个 LALR 或类似的解析器,这样就不会出现这种问题。 - RokL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接