regex 正则表达式模式匹配,排除时.../之间除外

3qpi33ja  于 2023-10-22  发布在  其他
关注(0)|答案(6)|浏览(124)

**--编辑--**目前的答案有一些有用的想法,但我想要的东西更完整,我可以100%理解和重用;所以我才设了赏金另外,对我来说,在任何地方都能工作的想法比像\K这样的非标准语法更好。

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

示例

我想用\b\d{5}\b匹配五个数字,但不是在三种情况下s1 s2 s3:

**s1:**不是在像这样的句子以句号结尾的行上。
**s2:**括号内没有任何地方。
**s3:**不在以if(开始并以//endif结束的块内

我知道如何使用lookahead和lookbehind解决s1 s2 s3中的任何一个,特别是在C# lookbehind或PHP中的\K中。
例如
S1 (?m)(?!\d+.*?\.$)\d+
s3与C# lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
s3与PHP \K (?:(?:if\(.*?//endif)\D*)*\K\d+
但各种情况的混合让我的头都要炸了。更坏的消息是,我可能需要在另一个时间添加其他条件s4 s5。
好消息是,我不在乎我是否使用PHP,C#,Python或我邻居的洗衣机等最常见的语言处理文件。:)我几乎是Python和Java的初学者,但有兴趣了解它是否有解决方案。
所以我来看看有没有人能想出一个灵活的食谱。
提示是好的:你不需要给予我完整的代码:)
谢谢

dxxyhpgq

dxxyhpgq1#

汉斯,我会上钩的,然后充实我之前的回答。你说你想要“更完整的东西”,所以我希望你不会介意这个冗长的回答只是想取悦你。让我们从一些背景开始。
首先,这是一个很好的问题。除了在特定的上下文中(例如,在代码块内或括号内),通常会有关于匹配特定模式的问题。这些问题往往给予相当棘手的解决办法。所以你关于多重上下文的问题是一个特殊的挑战。

惊喜

令人惊讶的是,至少有一个通用的、易于实现的、维护愉快的有效解决方案。它适用于所有regex风格,允许您检查代码中的捕获组。它恰好回答了一些常见的问题,这些问题可能一开始听起来与你的不同:“匹配除了甜甜圈以外的所有内容”、“替换除了......以外的所有内容"、“匹配除了我妈妈黑名单上的所有单词”、“忽略标签”、“匹配温度,除非斜体”...
可惜的是,这项技术并不为人所知:我估计,在20个可以使用它的SO问题中,只有一个有一个答案提到它,这意味着可能有五六十个答案。在评论中看到我与Kobi的交流。在this article中对这项技术进行了深入的描述,它(乐观地)称之为“有史以来最好的正则表达式技巧”。在不深入太多细节的情况下,我将努力给予您对该技术如何工作的牢固理解。要了解更多详细信息和各种语言的代码示例,我建议您参考该资源。

一个更好的变化

有一种变体使用Perl和PHP特定的语法来实现相同的功能。你会在SO上看到它掌握在正则表达式大师手中,比如CasimiretHippolyteHamZa。我将在下面告诉你更多关于这一点的信息,但我在这里的重点是与所有正则表达式风格一起工作的通用解决方案(只要你能在你的代码中检查捕获组)。
感谢所有的背景,ZX 81.但秘方是什么?

关键事实

该方法返回组1捕获中的匹配项。他根本不在乎整体比赛。
事实上,诀窍是匹配我们不想要的各种上下文(使用| OR / alternation链接这些上下文)以便“中和它们”。在匹配所有不想要的上下文之后,alternation的最后一部分匹配我们想要的并将其捕获到Group 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)

请参见此demo(但请关注右下窗格中的捕获组)。
如果你在心里尝试在每个| x处拆分这个正则表达式,它实际上只是一个由四个非常简单的表达式组成的序列。
对于支持自由间距的风味,这读起来特别好。

(?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。然后,您必须以编程方式检查Group 1是否已设置且不为空。这是一个简单的编程任务(我们稍后会讨论它是如何完成的),特别是考虑到它给您留下了一个简单的正则表达式,您可以一目了然地理解并根据需要进行修改或扩展。
我并不总是一个可视化的爱好者,但这一个很好地展示了方法是多么简单。每条“线”对应于一个潜在的匹配,但只有底线被捕获到组1中。

Debuggex Demo

Perl/PCRE变体

与上面的一般解决方案相反,在SO上经常可以看到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)(?!)
demo此版本

应用

下面是一些常见的问题,这种技术往往可以很容易地解决。你会注意到,单词的选择可以使这些问题听起来不同,而实际上它们几乎是相同的。

1.除了在<a stuff...>...</a>这样的标记中的任何地方之外,我如何匹配foo?
1.除了在<i>标签或JavaScript片段(更多条件)中,我如何匹配foo?
1.如何匹配不在此黑名单上的所有单词?
1.我怎么能忽视一个人的内心...结束阻塞?
1.我怎么配得上所有的东西除了... s1 s2 s3?

如何编程组1捕获

你不喜欢代码,但是,为了完成…检查组1的代码显然取决于您选择的语言。无论如何,它不应该向您用于检查匹配的代码添加超过两行的内容。
如果有疑问,我建议您查看前面提到的文章的代码示例部分,其中提供了许多语言的代码。

备选方案

根据问题的复杂性和使用的正则表达式引擎,有几种选择。这里有两个可以适用于大多数情况,包括多种条件。在我看来,这两个都没有s1|s2|s3|(whatYouWant)那么有吸引力,如果只是因为清晰度总是获胜的话。

1.替换后匹配。

一个好的解决方案,听起来很古怪,但在许多环境下都能很好地工作,就是分两步工作。第一个正则表达式通过替换潜在冲突的字符串来中和您想要忽略的上下文。如果你只想匹配,那么你可以用一个空字符串替换,然后在第二步中运行你的匹配。如果你想替换,你可以先用一些独特的东西替换要忽略的字符串,例如用一个固定宽度的@@@链包围你的数字。在这个替换之后,你可以自由地替换你真正想要的,然后你必须恢复你独特的@@@字符串。

2.查找。

你最初的帖子表明,你了解如何使用lookarounds排除单个条件。你说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匹配时,它被一个空组替换,没有其他内容,因此被删除。看到这个demo,感谢@HamZa和@Jerry提出这个精彩的补充。

更换人员

这就把我们带到了替代品上,我将简要地谈一谈。
1.当替换为nothing时,请参阅上面的“删除”技巧。
1.在替换时,如果使用Perl或PCRE,请使用上面提到的(*SKIP)(*F)变体来精确匹配您想要的内容,并进行直接替换。
1.在其他风格中,在替换函数调用中,使用回调或lambda检查匹配,如果设置了Group 1,则进行替换。如果您需要这方面的帮助,已经引用的文章将给予各种语言的代码。
玩得开心点!
不等等还有
啊,不,我会把它保存到我的二十卷回忆录里,明年Spring出版。

7bsow1i6

7bsow1i62#

执行三种不同的匹配,并使用程序内条件逻辑处理三种情况的组合。你不需要在一个巨大的正则表达式中处理所有的事情。
编辑:让我扩展一下,因为问题变得更有趣了:-)
您在这里试图捕获的一般思想是匹配特定的正则表达式模式,但不是在测试字符串中存在某些其他(可以是任何数量)模式时。幸运的是,你可以利用你的编程语言:保持正则表达式的简单,只使用复合条件。最佳实践是在可重用组件中捕获此想法,因此让我们创建一个类和一个方法来实现它:

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),然后尝试匹配几个测试字符串。打印结果应如每个测试字符串旁边的注解所示。

ep6jt1vc

ep6jt1vc3#

你要求不在括号内,这不可能在所有情况下都满足。也就是说,如果你能找到左边的(和右边的),这并不总是意味着你在括号内。Eg.
(....) + 55555 + (.....)-不在括号内,但在左和右有()
现在你可能会认为自己很聪明,只有在你之前没有遇到)的情况下才去左边找(,反之亦然。这对本案不起作用:
((.....) + 55555 + (.....))-在括号内,尽管左括号和右括号都有)(
使用正则表达式是不可能发现你是否在括号内的,因为正则表达式不能计算有多少个括号被打开,有多少个被关闭。
考虑一下这个更简单的任务:使用正则表达式,找出字符串中的所有(可能嵌套的)括号是否都是闭合的,也就是说,对于每个(,你需要找到)。你会发现这是不可能解决的,如果你不能用正则表达式解决这个问题,那么你就不能确定一个词是否在所有情况下都在括号内,因为你不能确定在字符串的某个位置,是否所有前面的(都有对应的)

t30tvxxf

t30tvxxf4#

汉斯,如果你不介意我用你邻居的洗衣机叫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
wko9yo5t

wko9yo5t5#

不知道这是否会帮助你,但我提供了一个解决方案,考虑到以下假设-
1.你需要一个优雅的解决方案来检查所有的条件
1.未来和任何时候情况都可能发生变化。
1.一个条件不应该依赖于其他条件。
不过,我也考虑到以下几点-
1.给出的文件中有最小的错误。如果是这样,那么我的代码可能需要一些修改来科普。
1.我使用Stack来跟踪if(块。
好吧,解决方案是这样的--
我使用C#和MEF(Microsoft Extensibility Framework)来实现可配置的解析器。这个想法是,使用一个解析器来解析,使用一个可配置的验证器类列表来验证行,并根据验证结果返回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);
    }
}

1.然后是文件阅读器和检查器-

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

1.然后是单个检查器的实现,类名是自解释的,所以我认为它们不需要更多的描述。

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

1.程序-

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

hc8w905p

hc8w905p6#

与@zx81的(*SKIP)(*F)相同,但使用了负先行Assert。

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

相关问题