要匹配的正则表达式模式,不包括...... /除外

时间:2014-05-11 05:12:55

标签: regex

- 编辑 - 目前的答案有一些有用的想法,但我想要更完整的东西,我可以100%理解和重用;这就是我设置赏金的原因。除了\K

之类的标准语法之外,对我来说无处不在的想法对我来说更好

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

示例

我希望使用\b\d{5}\b匹配五位数,但不是在三种情况下匹配s1 s2 s3:

s1:不在以句子结尾的行上。

s2:不在parens内的任何地方。

s3:不在以if(开头并以//endif

结尾的块内

我知道如何使用前瞻和后瞻来解决s1 s2 s3中的任何一个,特别是在PHP中的C#lookbehind或\K

例如

s1 (?m)(?!\d+.*?\.$)\d+

s3与C#lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

s3 with PHP \ K (?:(?:if\(.*?//endif)\D*)*\K\d+

但条件的混合使我的头脑爆炸。更糟糕的是,我可能需要在其他时间添加其他条件s4 s5。

好消息是,我不介意使用PHP,C#,Python或邻居的洗衣机等大多数常用语言处理文件。 :)我几乎是Python的初学者和Java,但有兴趣了解它是否有解决方案。

所以我来到这里看是否有人想到一个灵活的食谱。

提示没问题:你不需要给我完整的代码。 :)

谢谢。

6 个答案:

答案 0 :(得分:193)

汉斯,我会接受诱饵并充实我之前的回答。你说你想要更完整的东西&#34;所以我希望你不要介意长期的答案 - 只是想取悦。让我们从一些背景开始。

首先,这是一个很好的问题。除了在某些上下文中(例如,在代码块内或括号内),通常存在关于匹配某些模式的问题。这些问题往往会产生相当尴尬的解决方案。因此,您关于多种情境的问题是一项特殊挑战。

<强>惊喜

令人惊讶的是,至少有一种有效的解决方案是通用的,易于实施且易于维护。 适用于所有正则表达式,允许您检查代码中的捕获组。它碰巧回答了一些常见的问题,这些问题最初可能听起来与你的不同:&#34;匹配除甜甜圈以外的所有东西&#34;,&#34;替换所有问题,除了......&#34;,&#34;匹配除了我母亲的黑名单上的所有单词&#34;,&#34;忽略标签&#34;,&#34;匹配温度,除非用斜体标记&#34; ...

遗憾的是,这项技术并不为人所知:我估计在20个可以使用它的SO问题中,只有一个有一个答案提到它 - 这意味着可能是五十或六十个答案中的一个。请参阅我在评论中与Kobi的交流。这项技术在this article中进行了深入的描述,并且(乐观地)称之为“最好的正则表达技巧”#34;在没有详细介绍的情况下,我将尝试让您牢牢掌握该技术的工作原理。有关各种语言的更多详细信息和代码示例,我建议您查阅该资源。

更明确的变化

使用特定于Perl和PHP的语法可以实现相同的变体。您可以在CasimiretHippolyteHamZa这样的正则表达式大师手中看到它。我将在下面告诉您更多相关信息,但我的重点是适用于所有正则表达式的通用解决方案(只要您可以检查代码中的捕获组)。

  

感谢所有的背景,zx81 ......但是配方是什么?

关键事实

  

该方法返回第1组捕获中的匹配。它并不关心   所有关于整体比赛。

事实上,的诀窍是匹配我们不想要的各种情境(使用| OR /交替链接这些上下文)以便& #34;中和它们&#34;。在匹配所有不需要的上下文后,交替的最后部分与我们想要的相匹配,并将其捕获到第1组。

一般食谱是

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

这将匹配Not_this_context,但从某种意义上说,匹配进入垃圾箱,因为我们不会查看整体匹配:我们只查看第1组捕获。

在您的情况下,您的数字和三个上下文要忽略,我们可以这样做:

s1|s2|s3|(\b\d+\b)

请注意,因为我们实际上匹配s1,s2和s3而不是试图通过外观来避免它们,所以s1,s2和s3的各个表达式可以保持清晰。 (它们是|

每一侧的子表达式

整个表达式可以这样写:

(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)

请参阅此demo(但请关注右下方窗格中的捕获组。)

如果你在心理上尝试在每个|分隔符处拆分这个正则表达式,它实际上只是一系列四个非常简单的表达式。

对于支持自由间距的口味,这读得特别好。

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

这是如何运作的?

您不想要的上下文会被添加到左侧的替换列表中:它们将匹配,但这些整体匹配从未被检查过,因此匹配它们是一种将它们放入&#34;垃圾桶&#34;。

但是,您想要的内容会被捕获到组1.然后,您必须以编程方式检查组1是否已设置且不为空。这是一项简单的编程任务(我们稍后将讨论它是如何完成的),特别是考虑到它为您提供了一个简单的正则表达式,您可以一目了然地根据需要进行修改或扩展。 / p>

我并不总是视觉化的粉丝,但是这个方法很好地展示了该方法的简单性。每个&#34;线&#34;对应于潜在匹配,但只有底线被捕获到第1组。

Regular expression visualization

Debuggex Demo

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

demo此版本

<强>应用

以下是此技术通常可以轻松解决的一些常见问题。您会注意到选择这个词可能会使这些问题听起来有些不同,而实际上它们实际上是相同的。

  1. 除了<a stuff...>...</a>等标记中的任何位置外,我如何匹配foo?
  2. 除了<i>代码或javascript代码段(更多条件)外,我如何匹配foo?
  3. 如何匹配此黑名单上没有的所有单词?
  4. 如何忽略SUB ... END SUB块内的任何内容?
  5. 如何匹配除... s1 s2 s3以外的所有内容?
  6. 如何编程第1组捕获

    你没有代码,但是,为了完成......检查第1组的代码显然取决于你选择的语言。无论如何,它不应该为你用来检查匹配的代码添加多行。

    如果有疑问,我建议您查看前面提到的文章的code samples section,它提供了很多语言的代码。

    <强>替代

    根据问题的复杂性以及所使用的正则表达式引擎,有几种选择。以下是适用于大多数情况的两种情况,包括多种情况。在我看来,两者都不如s1|s2|s3|(whatYouWant)配方那么吸引人,只是因为清晰度总是胜出。

    <强> 1。替换然后匹配。

    一个听起来很糟糕但在许多环境中运行良好的好解决方案是分两步完成。第一个正则表达式通过替换可能存在冲突的字符串来中和您要忽略的上下文。如果您只想匹配,则可以使用空字符串替换,然后在第二步中运行匹配。如果你想要替换,你可以先用一些独特的东西替换要忽略的字符串,例如用@@@的固定宽度链包围你的数字。在更换之后,您可以自由地替换您真正想要的内容,然后您必须还原您独特的@@@字符串。

    <强> 2。 Lookarounds。

    您的原始帖子显示您了解如何使用外观排除单个条件。你说C#很适合这个,而你是对的,但它不是唯一的选择。例如,在C#,VB.NET和Visual C ++中找到的.NET正则表达式版本,以及在Python中替换regex的仍在实验的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匹配时,它将被一个空组替换,而不是其他任何东西 - 因此被删除。看到这个demo,谢谢你@HamZa和@Jerry建议这个精彩的补充。

    <强>替换

    这给我们带来了替代,我将简要介绍一下。

    1. 如果没有替换,请参阅&#34;删除&#34;上面的技巧。
    2. 更换时,如果使用Perl或PCRE,请使用上面提到的(*SKIP)(*F)变体来完全匹配您想要的内容,并直接替换。
    3. 在其他版本中,在替换函数调用中,使用回调或lambda检查匹配,如果设置了Group 1则替换。如果您需要帮助,已经引用的文章将为您提供各种语言的代码。
    4. 玩得开心!

      不,等等,还有更多!

      啊,不,我将把我的回忆录保存到二十卷,将于明年春天发布。

答案 1 :(得分: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));
    }
  }
}

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

答案 2 :(得分:2)

你的要求是,不能在所有情况下都不能满足于parens。 也就是说,如果你能以某种方式找到左边的(和右边的),那么它并不总是意味着你在parens里面。例如

(....) + 55555 + (.....) - 不在内部,但左右有()

现在,您可能会认为自己很聪明,只有在您之前没有遇到(而在右侧遇到)时才会找到((.....) + 55555 + (.....))。对于这种情况,这不会起作用:

) - 即使向左和向右关闭((,也会在内部显示。

使用正则表达式无法确定您是否在内部,因为正则表达式无法计算已打开多少个parens以及关闭了多少个parens。

考虑这个更简单的任务:使用正则表达式,找出字符串中的所有(可能是嵌套的)parens是否已关闭,即每个)需要查找(。你会发现它是不可能解决的,如果你用正则表达式无法解决这个问题,那么你就无法弄清楚一个词是否适用于所有情况,因为你可以&#39;如果)之前的所有内容都有相应的{{1}},请找出字符串中的某个位置。

答案 3 :(得分: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 
it should match 12345
it should match 12345
it should match 12345

答案 4 :(得分:2)

不确定这是否会对您有所帮助,但考虑到以下假设,我正在提供解决方案 -

  1. 您需要一个优雅的解决方案来检查所有条件
  2. 条件可能在未来和任何时候发生变化。
  3. 一个条件不应该依赖于其他条件。
  4. 但我还考虑了以下内容 -

    1. 给出的文件中的错误最小。如果它,那么我的代码可能需要一些修改来应对。
    2. 我使用Stack来跟踪if(块。
    3. 好的,这是解决方案 -

      我使用C#和MEF(Microsoft可扩展性框架)来实现可配置的解析器。我们的想法是,使用单个解析器进行解析,并使用可配置的验证器类列表来验证该行,并根据验证返回true或false。然后,您可以随时添加或删除任何验证器,或者如果您愿意,可以添加新的验证器。到目前为止,我已经为你提到的S1,S2和S3实现了,在第3点检查类。如果你将来需要,你必须为s4,s5添加类。

      1. 首先,创建接口 -

        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. 然后是文件阅读器和检查器 -

        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. 然后是各个检查器的实现,类名是自解释的,所以我认为他们不需要更多描述。

        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. 该计划 -

        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();
                }
            }
        }
        
      5. 为了进行测试,我将@ 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的实现,它将自动被调用,从而进行验证。

答案 5 :(得分:2)

与@ zx81&#39; s (*SKIP)(*F)相同,但使用否定前瞻断言。

(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)

DEMO

在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