带有所有可选标记的正则表达式令人费解的行为

问题描述 投票:0回答:1

我正在读取时间跨度数据,对于所有非零时间单位,将显示一个值和单位。例如,如果(正好)过去了 1 天 1 分钟,则字符串将显示为:

1 d, 1 m

如果经过 1 小时 50 毫秒,字符串将显示为:

1 h, 50 ms

如果经过了 1 天、2 小时、3 分钟、20 秒和 479 毫秒,则字符串将显示为:

1 d, 2 h, 3 m, 20 s, 479 ms

如果只过去了 47 毫秒,则字符串将显示为:

47 ms

我在捕获每个单位的数量时想出的匹配该类型模式的正则表达式是:

(?:([\d]+) d, )?(?:([\d]+) h, )?(?:([\d]+) m, )?(?:([\d]+) s, )?(?:([\d]+) ms)?

它有效,只要我有一个标记为成功匹配的捕获组,我就可以轻松获取特定单元的值,以便构造一个

TimeSpan
结构。

我已经在 RegexBuddy 中运行了该正则表达式(使用“C# (.NET 2.0-7.0)”引擎)并且它似乎有效(切换到 RegexBuddy 中的 Python 引擎给出了相同的结果),但在我的 .NET 7.0 程序中,它匹配了不应该匹配的数据,而且它在返回的

MatchCollection
中提供了比我预期更多的元素。

如果我给它一个字符串,例如

"1 d, 12 m, 25 s, 751 ms"
,那么
Regex.IsMatch
返回
true
(如预期)并且
Regex.Matches
有两个元素:索引 0 处的元素有六个
Groups
,其中填充了我所期望的内容,索引为 1 的那个也有 6 个
Groups
,但基本上什么也没有填充。我只期望
MatchCollection
中的一个元素。

一个更大的问题是,到目前为止,正则表达式也匹配我向它抛出的任何内容。它匹配:

hello

22ms

等等

当我说“匹配”时,我的意思是

Regex.IsMatch
返回
true
。对于
hello
Regex.Matches
返回具有 7 个元素的
MatchCollection
,全部为空,全部显示
Success
true
属性。

起初我想:嗯,除了可选标记之外什么都没有,所以,它有点像一个空的正则表达式,但在 RegexBuddy 中,相同的正则表达式并不匹配所有内容:它只匹配(突出显示)我所期望的内容。

下面的代码可以轻松地重复该问题。到目前为止,无论我给

str
赋予什么值,它都会“匹配”(
regex.IsMatch(str)
总是返回
true
),这似乎是不正确的,特别是考虑到 RegexBuddy 没有表现出这种行为。

public static TimeSpan StackOverflow(string str)
{
    var regex = new Regex("(?:([\\d]+) d, )?(?:([\\d]+) h, )?(?:([\\d]+) m, )?(?:([\\d]+) s, )?(?:([\\d]+) ms)?",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);
    if (false == regex.IsMatch(str))
    {
        throw new ArgumentException($"'{str}' is not a recognized pattern");
    }
    var matchCollection = regex.Matches(str);
    Match match = matchCollection[0];
    return new TimeSpan(match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0,
        match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0,
        match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0,
        match.Groups[4].Success ? int.Parse(match.Groups[4].Value) : 0,
        match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0);
}

我刚刚使用相同的正则表达式在 regex101.com 上测试了

hello
,它似乎反映了我的 .NET 程序显示的内容
hello
:它显示了 6 个匹配项,全部在“空字符串”上。

我的猜测是问题出在正则表达式上,我的直觉与上面已经相关,问题在于有一个由所有可选标记组成的正则表达式。我最初想要完成(但未能完成)的是一个正则表达式,该表达式将与所描述的模式匹配,但至少要匹配其中一个数量/单位。如有任何帮助,我们将不胜感激。

谢谢。

c# .net regex
1个回答
0
投票

将问题分为两个:匹配和验证。

匹配

我相信 RegexBuddy 有一些自己的规则需要应用,这导致结果与 C# 代码(和 regex101)的结果不同。不过,我不会在这个答案中对其进行逆向工程。

让我们从两个问题开始:

  1. 首先,你需要知道自己想要什么。您可能认为您想要匹配,但是什么样的匹配?
  2. 您已经知道您的正则表达式将始终匹配,因为它的所有部分都是可选的。但这种情况是怎么发生的,不是一次而是多次?

答案:正则表达式引擎从字符串的第一个位置/字符启动;它会尝试匹配您的正则表达式,如果不顺利,它将在下一个位置重试。因此,它在位置 1 处获得了一场比赛,该位置是空的,然后在位置 2 处获得了另一场比赛,依此类推。

为了确保正则表达式匹配整个字符串而不留下任何前导或尾随字符(通常也称为完全匹配),您需要anchors

^
(表示开始)和
$
,这意味着结束。请注意,在多行模式下 (
RegexOptions.Multiline
),它们具有不同的含义。

因此,更新后的正则表达式应如下所示:

^
<original>
$
。这不再匹配
hello
。这是第一步。

正在验证

如果您曾经尝试过使用正则表达式验证密码,您至少会遇到这种先行模式:

^                 # start-of-string
(?=.*[0-9])       # a digit must occur at least once
(?=.*[a-z])       # a lower case letter must occur at least once
(?=.*[A-Z])       # an upper case letter must occur at least once
(?=.*[@#$%^&+=])  # a special character must occur at least once
(?=\S+$)          # no whitespace allowed in the entire string
.{8,}             # anything, at least eight places though
$                 # end-of-string

同样适用于您的:

(?=.)
,表示“至少一个字符”。

瞧瞧

^(?=.)(?:([\d]+) d, )?(?:([\d]+) h, )?(?:([\d]+) m, )?(?:([\d]+) s, )?(?:([\d]+) ms)?$

在 regex101.com 上尝试一下

附录

正如您可能已经看到的,新的正则表达式(甚至您原来的正则表达式)与

1 d, 1 m

 不匹配。这是因为 
,
 部分不是可选的。将它们替换为 
(?:$|, )
(字符串结尾,或逗号和空格)。弄清楚这个小修改是如何工作的,留给读者作为练习。

另请注意,

[\d]

\d
 完全相同,并且 
(?!$)
 可以用来代替 
(?=.)

© www.soinside.com 2019 - 2024. All rights reserved.