如何将 C# 正则表达式 Lookbehind 与线锚一起使用

How to use C# Regular Expression Lookbehind with line anchors

同时使用行开始和行结束锚点时,我在 C# 的正则表达式匹配中遇到后向断言问题。 在下面的示例中,正则表达式 B 的行为完全符合我的预期(并且如此处记录:https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference

最初我对 RegEx A 不匹配第 1 行感到惊讶。现在我想我明白了为什么 RegEx A 不匹配第 1 行。[因为断言宽度为零 - 表达式基本上是 ^\d{2} $,它显然不匹配 4 位数年份 - 这就是它匹配第 6 和 7 行的原因。

我知道我可以像这样重写肯定断言 (RegEx A):^19\d{2}$。

但我的最终目标是像 RegEx C 这样的正则表达式 - 使用否定断言来查找所有不以给定前缀开头的字符串。也就是说,我正在尝试创建一个带有否定断言的表达式,即第 3 行和第 4 行的 returns 为真,而不是第 5-7 行。

RegEx D 是来自 C# 文档的类似否定断言样本,但不使用 begin/end 锚点,第 3 行和第 4 行以及第 5-7 行也是如此。

考虑到这一点,我如何使否定断言(如 RegEx C)与 line-begin/-end 锚点一起工作,以便它在验证输入是单行时像 RegEx D 中的示例一样运行?

我想知道这是否根本不可能使用断言。这意味着替代方案是表达所有评估为否定异常的积极案例(类似于在 Regex E 中使用 19),但是当目标是排除异常时表达大量积极案例是不可能或不切实际的特定的单个(可能是复杂的)案例。

谢谢!

示例程序:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

namespace RegExTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] reList = new string[]
            {
                @"^(?<=19)\d{2}$",   // RegEx A
                @"(?<=19)\d{2}",     // RegEx B
                @"^(?<!19)\d{2}$",   // RegEx C
                @"(?<!19)\d{2}\b",   // RegEx D
                @"^19\d{2}$",        // RegEx E
            };

            string[] tests = new string[]
            {
                "1999",                     // Line 1
                "1851 1999 1950 1905 2003", // Line 2
                "1895",                     // Line 3
                "2095",                     // Line 4
                "195",                      // Line 5
                "18",                       // Line 6
                "19",                       // Line 7
            };
            foreach (var r in reList)
            {
                var re = new Regex(r);
                Console.WriteLine("");
                Console.WriteLine($"{r}");
                Console.WriteLine("==========================");
                foreach (var s in tests)
                {
                    Console.WriteLine($"{s}={re.IsMatch(s)}");
                    if (re.IsMatch(s))
                    {
                        foreach (Match m in re.Matches(s))
                        {
                            Console.WriteLine($"Match @ ({m.Index}, {m.Length})");
                        }
                    }
                }
            }
        }
    }
}

输出:

^(?<=19)\d{2}$
==========================
1999=False
1851 1999 1950 1905 2003=False
1895=False
2095=False
195=False
18=False
19=False

(?<=19)\d{2}
==========================
1999=True
Match @ (2, 2)
1851 1999 1950 1905 2003=True
Match @ (7, 2)
Match @ (12, 2)
Match @ (17, 2)
1895=False
2095=False
195=False
18=False
19=False

^(?<!19)\d{2}$
==========================
1999=False
1851 1999 1950 1905 2003=False
1895=False
2095=False
195=False
18=True
Match @ (0, 2)
19=True
Match @ (0, 2)

(?<!19)\d{2}\b
==========================
1999=False
1851 1999 1950 1905 2003=True
Match @ (2, 2)
Match @ (22, 2)
1895=True
Match @ (2, 2)
2095=True
Match @ (2, 2)
195=True
Match @ (1, 2)
18=True
Match @ (0, 2)
19=True
Match @ (0, 2)

^19\d{2}$
==========================
1999=True
Match @ (0, 4)
1851 1999 1950 1905 2003=False
1895=False
2095=False
195=False
18=False
19=False

您将环视断言与正常模式的默认行为混淆了。环顾四周断言这意味着它不消耗字符。

它会寻找一个条件,如果满足则将光标带回到它开始的地方,否则它会使引擎回溯或立即失败。

正则表达式 A ^(?<!19)\d{2}$ 不应匹配字符串 1 1999 因为引擎是这样工作的:

  1. ^ 断言字符串的开头(我们在位置 0)
  2. (?<!19) 检查前面的字符是否不是 19(肯定在 位置 0 我们没有前面的字符所以这满足)
  3. \d{2}消耗两位数(我们在位置2)
  4. $ 断言字符串结束(实际上我们还有 2 个字符要到达 字符串结尾,因此引擎立即失败)

所以你必须这样做^\d{2}(?<!19)\d{2}$或者^(?!19)\d{4}$第二种更合适