【问题标题】:Bug in .net Regex.Replace?.net Regex.Replace 中的错误?
【发布时间】:2014-01-24 14:04:39
【问题描述】:

以下代码...

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Console.WriteLine(uc);
    }
}

.Net Fiddle Link

产生以下输出...

A XYZ BA B

你认为这是正确的吗?

输出不应该是……

A XYZ B

我想我在这里做了一些愚蠢的事情。如果您能提供任何帮助帮助我理解这个问题,我将不胜感激。


这里有一些有趣的东西......

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "$1");

        Console.WriteLine(uc);
    }
}

.Net Fiddle

输出...

XYZ

【问题讨论】:

  • 你的正则表达式有两个匹配项,Replace 将替换它们。第一个是“XYZ”,第二个是空字符串。我不确定的是为什么它首先有两场比赛。您可以使用^(.*)$ 对其进行修复,以强制它考虑字符串的开头和结尾。
  • 如果你的模式匹配一​​个没有进一步限制的空字符串,你总是会在一个字符串的末尾得到一个额外的匹配。
  • 请参阅stackoverflow.com/questions/16103346/… 以了解字符串解释末尾的额外匹配
  • @w0lf:可能因正则表达式引擎而异。 Ruby 的引擎似乎以同样的方式解释它(参见匹配:rubular.com/r/cRaG0rPowZ

标签: .net regex


【解决方案1】:

至于引擎返回 2 个匹配项的原因,这是由于 .NET(也包括 Perl 和 Java)处理全局匹配的方式,即在输入字符串中找到与给定模式的所有匹配项。

过程可以描述如下(当前索引通常在搜索开始时设置为0,除非指定):

  1. 从当前索引执行搜索。
  2. 如果没有匹配:
    1. 如果当前索引已经指向字符串的末尾(当前索引 >= string.length),则返回到目前为止的结果。
    2. 将当前索引增加 1,转到步骤 1。
  3. 如果主匹配 ($0) 非空(至少消耗一个字符),则添加结果并将当前索引设置到主匹配末尾 ($0)。然后转到第 1 步。
  4. 如果主匹配 ($0) 为空:
    1. 如果上一个匹配项不为空,则添加结果并转到步骤 1。
    2. 如果上一个匹配项为空,则回溯并继续搜索。
    3. 如果回溯尝试找到非空匹配,则添加结果,将当前索引设置为匹配的末尾,然后转到步骤 1。
    4. 否则,将当前索引增加 1。转到步骤 1。

引擎需要检查空匹配;否则,它将陷入无限循环。设计者认识到空匹配的用法(例如将字符串拆分为字符),因此必须设计引擎以避免永远卡在某个位置。

这个过程解释了为什么末尾有一个空匹配:因为在(.*)匹配abc之后在字符串的末尾(索引3)进行搜索,而(.*)可以匹配一个空字符串,找到一个空匹配。并且引擎不会产生无限数量的空匹配,因为最后已经找到了一个空匹配。

 a b c
^ ^ ^ ^
0 1 2 3

第一场比赛:

 a b c
^     ^
0-----3

第二场比赛:

 a b c
      ^
      3

使用上面的全局匹配算法,最多只能有2个从同一个索引开始的匹配,这种情况只有在第一个是空匹配时才会发生。

请注意,如果主匹配项为空,JavaScript 只会将当前索引加 1,因此每个索引最多有 1 个匹配项。但是,在这种情况下(.*),如果你使用全局标志g进行全局匹配,也会出现同样的结果:

(以下结果来自 Firefox,注意 g 标志)

> "XYZ".replace(/(.*)/g, "A $1 B")
"A XYZ BA  B"

【讨论】:

  • 我明白你在说什么。在末尾匹配一次空字符串是此实现工作方式的产物。但我认为这是一个错误而不是一个功能。 .net 实现应该做 JavaScript 正则表达式引擎所做的事情。为这项努力投票。
  • @SandeepDatta:我想这是对语言特性和怪癖的理解和熟悉的问题。
【解决方案2】:

我将不得不考虑为什么会发生这种情况。确定你错过了什么。虽然这解决了问题。只需锚定正则表达式。

var r = new Regex("^(.*)$");

这是.NetFiddle demo

【讨论】:

  • 我更喜欢这个正则表达式,因为它也适用于空输入字符串。
【解决方案3】:

* 量词匹配 0 或更多。这导致有 2 个匹配项。 XYZ 什么都没有。

尝试使用匹配 1 个或多个的 + 量词。

一个简单的解释是这样查看字符串:XYZ<nothing>

  1. 我们有匹配的 XYZ<nothing>
  2. 对于每场比赛
    • 匹配 1:将 XYZ 替换为 A $1 B(此处为 $1 XYZ) 结果:A XYZ B
    • 匹配 2:将 <nothing> 替换为 A $1 B(此处为 $1 <nothing>) 结果:A B

最终结果:A XYZ BA B

为什么<nothing> 本身就是一场比赛很有趣,而且我还没有真正考虑过。 (为什么没有无限的<nothing> 匹配?)

【讨论】:

  • 非常有意义!这也解释了第二个 sn-p。
  • * 匹配 0 个或更多,但默认情况下它的正则表达式是贪婪的,不是吗?所以它应该匹配所有的字符!有错请指正?
  • 是的,这让我很困惑。
  • @SandeepDatta 不一定是错误。可能我们都错过了一些非常基本的东西!
  • 我认为这是一个定义问题。贪婪地,所有的东西都不是匹配的,所有的东西都是匹配的。什么都不是什么,如果和什么放在一起就不是什么了,所以它们必须是两个单独的匹配......?
【解决方案4】:

您的正则表达式有两个匹配项,Replace 将替换它们。第一个是“XYZ”,第二个是空字符串。我不确定的是为什么它首先有两场比赛。您可以使用 ^(.*)$ 对其进行修复,以强制它考虑字符串的开头和结尾。

或者使用+而不是*来强制匹配至少一个字符

.* 匹配一个空字符串,因为它有零个字符。

.+ 不匹配空字符串,因为它需要至少一个字符。

有趣的是,在 Javascript(在 Chrome 中):

var r = /(.*)/;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

将输出预期的A XYZ B 而没有虚假的额外匹配。

编辑(感谢@nhahtdh):但是将g 标志添加到 Javascript 正则表达式中,可以得到与 .NET 中相同的结果:

var r = /(.*)/g;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

【讨论】:

  • * 匹配 0 个或更多,但默认情况下它的正则表达式是贪婪的,不是吗?所以它应该匹配所有的字符!有错请指正?
  • 接受,因为您是第一个回答评论的人。我已将评论添加到您的答案中。
  • @SriramSakthivel:是的,我会给你。我在 Javascript 中尝试了相同的操作(请参阅我的编辑),但它不匹配并替换空字符串。
  • @MattBurland 好奇地尝试了RegexOptions.EcmaScript 但失败了:(
  • 您忘记了g 标志:"XYZ".replace(/(.*)/g, "A $1 B")。 JS 没有理由在这里返回不同的结果。如果匹配一次(没有g 标志),那么不会发生任何有趣的事情。
【解决方案5】:

正则表达式是一种特殊的语言。您必须准确了解 (.*) 将匹配的内容。您还需要了解贪婪。

  • (.*) 将贪婪地匹配 0 个或多个字符。因此,在字符串"XYZ" 中,它会将整个字符串与其第一个匹配项进行匹配,并将其放在 $1 位置,从而为您提供:

    A XYZ B 然后它将继续尝试匹配并匹配字符串末尾的null,将您的 $1 设置为 null,从而为您提供:

    A B 导致您看到的字符串:

    A XYZ BA B

  • 如果你想限制贪心并匹配每个字符,你可以使用这个表达式:

    (.*?)
    这将分别匹配每个字符 X、Y 和 Z,以及最后的 null 并导致:

    A BXA BYA BZA B

如果您不希望您的正则表达式超出给定字符串的范围,请使用 ^$ 标识符限制您的正则表达式。

为了让您更好地了解正在发生的事情,请考虑此测试和生成的匹配组。

    [TestMethod()]
    public void TestMethod3()
    {
        var myText = "XYZ";
        var regex = new Regex("(.*)");
        var m = regex.Match(myText);
        var matchCount = 0;
        while (m.Success)
        {
            Console.WriteLine("Match" + (++matchCount));
            for (int i = 1; i <= 2; i++)
            {
                Group g = m.Groups[i];
                Console.WriteLine("Group" + i + "='" + g + "'");
                CaptureCollection cc = g.Captures;
                for (int j = 0; j < cc.Count; j++)
                {
                    Capture c = cc[j];
                    Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index);
                }
            }
            m = m.NextMatch();
        }

输出:

Match1
Group1='XYZ'
Capture0='XYZ', Position=0
Group2=''
Match2
Group1=''
Capture0='', Position=3
Group2=''

请注意,有两个组匹配。第一个是整个组 XYZ,第二个是空组。然而,有两组匹配。因此,在第一种情况下,$1 被替换为 XYZ,在第二种情况下被替换为 null

另请注意,正斜杠/ 只是.net 正则表达式引擎中考虑的另一个字符,没有特殊含义。 javascript 解析器处理 / 的方式不同,因为它必须存在于 HTML 解析器的框架中,其中 &lt;/ 是一个特殊考虑因素。

最后,为了得到你真正想要的,考虑这个测试:

    [TestMethod]
    public void TestMethod1()
    {
        var r = new Regex(@"^(.*)$");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Assert.AreEqual("A XYZ B", uc);
    }

【讨论】:

  • 所以...您是否建议 C# 和 Javascript 行为之间的区别是因为 C# 字符串是空终止的(至少在内部),而 Javascript 字符串不是(据我所知) ?
  • 考虑到 C# 字符串不是以空值结尾的,这确实是一个有趣的问题。但是,如果不受 ^ 和 $ 的约束,.net 正则表达式引擎似乎会尝试超出给定字符串的范围进行匹配。
  • 来自C# in Depth:Although strings aren't null-terminated as far as the API is concerned, the character array is null-terminated, as this means it can be passed directly to unmanaged functions without any copying being involved, assuming the inter-op specifies that the string should be marshalled as Unicode. 所以我的想法是内部正则表达式引擎必须直接使用字符数组并看到空终止字符。但我真的不知道。
  • @AaronPalmer:我认为它不符合界限。引擎仅将长度为 3 abc 的字符串视为具有 4 个索引(0、1、2、3)。搜索在索引 3 处进行,它解释了空字符串结果。当然,访问字符串的索引 3 根本行不通,但我们应该将索引视为此处字符之间的空格。
  • @MattBurland,很有可能,这也很符合 nhahtdh 的解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-24
相关资源
最近更新 更多