Regex Pattern to Match, Excluding when... / Except between

目前的答案有一些有用的想法,但我想要一些更完整的东西,我可以100% 理解和重用,这就是为什么我设置了一个赏金。对我来说,在任何地方都适用的想法也比不像 \K这样的标准语法更好

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

例子

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

不在以句号结尾的句子上。

S2: 括号内没有任何地方。

S3: 不在以 if(开始并以 //endif结束的块中

I know how to solve any one of s1 s2 s3 with a lookahead and lookbehind, especially in C# lookbehind or \K in PHP.

比如说

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

使用 C # 查看 (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b后面的 s3

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

但是混杂在一起的环境让我的脑袋爆炸了。更坏的消息是,我可能需要在另一个时间添加其他条件 s4s5。

好消息是,我不在乎是否使用 PHP、 C # 、 Python 或我邻居的洗衣机等最常用的语言来处理文件。:)我基本上是 Python & Java 的初学者,但我很想知道它是否有解决方案。

所以我来看看有没有人能想出一个灵活的食谱。

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

谢谢你。

38359 次浏览

汉斯,我会上钩,把我之前的回答补充一下。你说你想要“更完整的东西”,所以我希望你不要介意长话短说ーー只是想取悦你。让我们从背景开始。

首先,这是个很好的问题。除了在特定的上下文中(例如,在代码块内或括号内) ,经常会有关于匹配特定模式的问题。这些问题往往会产生相当棘手的解决方案。因此,你关于 多种语境的问题是一个特殊的挑战。

惊喜吧

Surprisingly, there is at least one efficient solution that is general, easy to implement and a pleasure to maintain. It 适用于所有正则表达式口味 that allow you to inspect capture groups in your code. And it happens to answer a number of common questions that may at first sound different from yours: "match everything except Donuts", "replace all but...", "match all words except those on my mom's black list", "ignore tags", "match temperature unless italicized"...

Sadly, the technique is not well known: I estimate that in twenty SO questions that could use it, only one has one answer that mentions it—which means maybe one in fifty or sixty answers. See my exchange with Kobi in the comments. The technique is described in some depth in 这篇文章 which calls it (optimistically) the "best regex trick ever". Without going into as much detail, I'll try to give you a firm grasp of how the technique works. For more detail and code samples in various languages I encourage you to consult that resource.

A Better-Known Variation

有一种变体使用特定于 Perl 和 PHP 的语法来实现相同的功能。您将在 SO 中看到它出现在正则表达式母版(如 CasimiretHippolyte哈姆扎)中。我将在下面详细介绍这个问题,但是我的重点是使用所有正则表达式的通用解决方案(只要您可以检查代码中的捕获组)。

谢谢你的背景知识,但是食谱是什么?

关键事实

该方法返回第1组捕获中的匹配项 all about the overall match.

事实上,the trick is to match the various contexts we don't want(使用 |或/交替链接这些上下文) so as to "neutralize them".在匹配所有不需要的上下文后,交替的最后一部分匹配我们 想要的,并将其捕获到第1组。

一般的配方是

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

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

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

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

Note that because we actually match s1, s2 and s3 instead of trying to avoid them with lookarounds, the individual expressions for s1, s2 and s3 can remain clear as day. (They are the subexpressions on each side of a | )

整个表达式可以这样写:

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

这非常容易阅读和维护。

Extending the regex

当你想忽略更多的情况 s4和 s5时,你可以在左边添加更多的变化:

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

这是怎么回事?

The contexts you don't want are added to a list of alternations on the left: they will match, but these overall matches are never examined, so matching them is a way to put them in a "garbage bin".

The content you do want, however, is captured to Group 1. You then have to check programmatically that Group 1 is set and not empty. This is a trivial programming task (and we'll later talk about how it's done), especially considering that it leaves you with a simple regex that you can understand at a glance and revise or extend as required.

我并不总是喜欢可视化,但是这一个很好地展示了这个方法是多么的简单。每个“行”对应于一个潜在的匹配,但只有底线被捕获到第1组。

Regular expression visualization

调试演示

Perl/PCRE 变化

与上面的通用解决方案相反,Perl 和 PCRE 存在一个变体,这个变体经常出现在 SO 上,至少在@CasimiretHippolyte 和@HamZa 这样的正则表达式之神手中。它是:

(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant

就你而言:

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

这种变化更容易使用,因为在上下文 s1、 s2和 s3中匹配的内容被简单地跳过,所以您不需要检查 Group 1捕获(注意没有括号)。匹配只包含 whatYouWant

请注意,(*F)(*FAIL)(?!)都是相同的。如果您想更加晦涩,可以使用 (*SKIP)(?!)

这个版本的 demo

申请表

这里有一些常见的问题,这种技术通常可以很容易地解决。你会注意到这个词的选择可以使这些问题中的一些听起来不同,而实际上它们实际上是相同的。

  1. 除了像 <a stuff...>...</a>这样的标记中的任何地方,我如何才能匹配 foo?
  2. 除了在 <i>标记或 javascript 代码片段(更多条件)之外,如何匹配 foo?
  3. 我怎样才能匹配所有不在黑名单上的单词?
  4. How can I ignore anything inside a SUB... END SUB block?
  5. 我怎样才能匹配所有的东西,除了... s1 s2 s3?

如何编程第1组捕获

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

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

替代品

根据问题的复杂性和所使用的正则表达式引擎,有几种替代方案。下面是适用于大多数情况的两种方法,包括多种条件。在我看来,这两种方法都不如 s1|s2|s3|(whatYouWant)食谱有吸引力,即使只是因为清晰度总是胜出。

1. 替换然后匹配

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

2. 环顾四周

您最初的文章表明您了解如何使用查找来排除单个条件。您说过 C # 非常适合这种情况,您是对的,但它不是唯一的选择。那个。例如,在 C # 、 VB.NET 和 Visual C + + 中发现的 NET regex 风格,以及在 Python 中用于取代 re的仍处于实验阶段的 regex模块,是我所知道的仅有的两个支持无限宽度后退的引擎。使用这些工具,一次回顾中的一个条件不仅可以照顾到回顾,而且还可以照顾到匹配和超越匹配,从而避免了与前瞻协调的需要。更多条件?多找找。

回收 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 建议这个奇妙的加法。

替代品

This brings us to replacements, on which I'll touch briefly.

  1. 当替换为无时,请参见上面的“删除”技巧。
  2. When replacing, if using Perl or PCRE, use the (*SKIP)(*F) variation mentioned above to match exactly what you want, and do a straight replacement.
  3. 在替换函数调用中,使用回调或 lambda 检查匹配,如果设置了 Group 1,则替换。如果您需要这方面的帮助,已经引用的文章将为您提供各种语言的代码。

玩得开心!

不,等等,还有呢!

啊,不,我会把它留到我的二十卷回忆录里,明年春天出版。

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

编辑: 让我展开一点,因为这个问题变得更有趣了: -)

The general idea you are trying to capture here is to match against a certain regex pattern, but not when there are certain other (could be any number) patterns present in the test string. Fortunately, you can take advantage of your programming language: keep the regexes simple and just use a compound conditional. A best practice would be to capture this idea in a reusable component, so let's create a class and a method that implement it:

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

因此,上面我们设置了搜索字符串(5位数字)、多个异常字符串(中一第2条中三) ,然后尝试匹配多个测试字符串。打印的结果应该如每个测试字符串旁边的注释所示。

你的要求,它不在括号内在不可能满足所有情况。 也就是说,如果你能在左边找到一个 (,在右边找到一个 ),这并不总是意味着你在括号内。艾格。

(....) + 55555 + (.....)-不在括号内,但有 ()到左和右

现在你可能认为自己很聪明,只有当你之前没有遇到 )的时候才会在左边寻找 (,反之亦然。这对这个案子没用:

((.....) + 55555 + (.....))-内括号,即使有关闭 )(到左边和右边。

使用正则表达式不可能知道是否在括号内,因为正则表达式无法计算打开了多少个括号和关闭了多少个括号。

Consider this easier task: using regex, find out if all (possibly nested) parens in a string are closed, that is for every ( you need to find ). You will find out that it's impossible to solve and if you can't solve that with regex then you can't figure out if a word is inside parens for all cases, since you can't figure out at a some position in string if all preceeding ( have a corresponding ).

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

不知道这是否会帮助你或不,但我提供了一个解决方案,考虑到以下假设-

  1. 您需要一个优雅的解决方案来检查所有条件
  2. 未来和任何时候,情况都可能发生变化。
  3. 一个条件不应该依赖于其他条件。

不过我还考虑了以下几点-

  1. 给出的文件错误最少。如果是这样,那么我的代码可能需要一些修改来处理这个问题。
  2. 我使用 Stack 来跟踪 if(块。

好吧,这就是解决办法

我使用 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. 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();
    }
    }
    }
    

For testing I took @Tiago's sample file as Test.txt which had the following lines -

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的 (*SKIP)(*F)相同,但使用了负面的前瞻断言。

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

演示

在巨蟒中,我很容易就会这样做,

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