如何单独替换每个组的捕获?

时间:2012-06-29 19:03:14

标签: c# .net regex matchevaluator capturecollection

我有一个正则表达式,在其捕获中使用GroupCollection来捕获一组项目ID(可以用逗号分隔,也可以考虑最后一个用'和'这个词):

(\bItem #(?<ITEMID>\d+))|(,\s?(?<ITEMID>\d+))|(,?\sand\s(?<ITEMID>\d+))

使用C#的Regex类是否可以通过网址替换ITEMID号码?现在,我有以下内容:

foreach (Match match in matches)
{
    var group = match.Groups["ITEMID"];
    var address = String.Format(UnformattedAddress, group.Value);

    CustomReplace(ref myString, group.Value, address,
        group.Index, (group.Index + group.Length));
}

public static int CustomReplace(ref string source, string org, string replace,
    int start, int max)
{
    if (start < 0) throw new System.ArgumentOutOfRangeException("start");
    if (max <= 0) return 0;

    start = source.IndexOf(org, start);

    if (start < 0) return 0;

    var sb = new StringBuilder(source, 0, start, source.Length);

    var found = 0;
    while (max-- > 0)
    {
        var index = source.IndexOf(org, start);

        if (index < 0) break;

        sb.Append(source, start, index - start).Append(replace);
        start = index + org.Length;
        found++;
    }

    sb.Append(source, start, source.Length - start);
    source = sb.ToString();

    return found;
}

我在网上找到的CustomReplace方法是一种简单的方法,可以在字符串源中替换另一个字符串。问题是我确信可能有一种更简单的方法,可能使用Regex类来根据需要替换GroupCollection。我只是无法弄清楚那是什么。谢谢!

示例文字:

Hello the items you are looking for are Item #25, 38, and 45. They total 100 dollars.

253845应替换为我正在创建的网址字符串(这是一个HTML字符串)。

3 个答案:

答案 0 :(得分:2)

您的模式适用于您的输入,但确实存在错误。具体来说,它会匹配输入中出现在逗号或单词“和”之后的任何数字。

我继续重写你的模式以避免这个问题。为实现这一目标,我实际上使用了两种正则表达式模式。可以使用一种模式来解决这个问题,但它比我选择分享的方法相当复杂且可读性差。

主要模式是:\bItem #\d+(?:,? \d+)*(?:,? and \d+)? 这里没有使用捕获组,因为我只对匹配项目感兴趣。 (?: ... )位是非捕获组。 (?:,? \d+)*的用法是匹配字符串中间部分的多个逗号分隔值。

匹配项目后,我使用Regex.Replace格式化项目,然后重新构建字符串以使用格式化项目替换原始项目。

这是一个有几个不同输入的例子:

string[] inputs =
{
    "Hello the items you are looking for are Item #25, 38, 22, and 45. They total 100 dollars.",
    "... Item #25, 38 and 45. Other numbers 100, 20, and 30 untouched.",
    "Item #25, and 45",
    "Item #25 and 45",
    "Item #25"
};

string pattern = @"\bItem #\d+(?:,? \d+)*(?:,? and \d+)?";
string digitPattern = @"(\d+)";
// $1 refers to the first (and only) group in digitPattern
string replacement = @"<a href=""http://url/$1.html"">$1</a>";

foreach (var input in inputs)
{
    Match m = Regex.Match(input, pattern);
    string formatted = Regex.Replace(m.Value, digitPattern, replacement);
    var builder = new StringBuilder(input)
                        .Remove(m.Index, m.Length)
                        .Insert(m.Index, formatted);
    Console.WriteLine(builder.ToString());
}

如果您需要使用现有方法格式化网址,而不是使用正则表达式替换模式,则可以使用接受MatchEvaluatorRegex.Replace重载。这可以使用lambda实现,并且比MSDN文档中显示的繁琐方法更好。

例如,假设您有一个接受字符串并返回格式化字符串的FormatItem方法:

public string FormatItem(string item)
{
    return String.Format("-- {0} --", item);
}

要使用FormatItem,您可以使用以下代码更改早期代码示例中使用的Regex.Replace方法:

string formatted = Regex.Replace(m.Value, digitPattern,
                       d => FormatItem(d.Value));

答案 1 :(得分:0)

这里是一个所需语法的示例,并且还显示您可以通过回调在替换中退回到C#。

How does MatchEvaluator in Regex.Replace work?

答案 2 :(得分:0)

你好像是从两个方向同时来到这里。一方面,你有一个带有三个捕获组的正则表达式,所以你希望解决方案涉及一个GroupCollection。另一方面,所有三个组都具有相同的名称,因此您可能必须将它们视为同一组的单独捕获 - 即。一个CaptureCollection。实际上,你可能不需要其中任何一个。这是你的正则表达式(经过一些美学调整):

string source = @"Total cost for Item #25, 38, and 45 is 100 dollars.";

Regex regex1 = new Regex(
    @"\bItem #(?<ITEMID>\d+)|,\s*(?<ITEMID>\d+)|,?\s+and\s+(?<ITEMID>\d+)",
    RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

foreach (Match m in regex1.Matches(source)) {
  Console.WriteLine(m.Groups["ITEMID"].Value);
}

按预期输出253845。每个备选方案都有自己的捕获组副本,但只有其中一个参与每个匹配。这是.NET正则表达式的一个显着特征;其他一些提供了允许您重用组名的特殊设置或组构造,但它们都没有像.NET那样简单。但是,在这种情况下你并不需要它;你可以合并替代方案,如下:

@"(\bItem #|,\s*|,?\s+and\s+)(?<ITEMID>\d+)"

你的正则表达式存在问题,如果你将源字符串更改为:

@"Total cost for Item #25, 38, and 45 is 1,500 dollars and 42 cents."

输出现在是25384550042。为了防止这些误报,您需要确保每个不以Item #开头的匹配,从最后一个匹配结束的地方开始。为此,您可以使用\G

@"(\bItem #|\G,?\s+and\s+|\G,\s*)(?<ITEMID>\d+)"

(为了提高效率,我还换了最后两个选项的顺序。)把所有这些放在一起,我们只有另一个正则表达式替换。

string source =
   @"Total cost for Item #25, 38, and 45 is 1,500 dollars and 42 cents.";
Regex regex2 = new Regex(
    @"(?<TEXT>\bItem #|\G,?\s+and\s+|\G,\s*)(?<ITEMID>\d+)",
    RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
string result = regex2.Replace(source, 
    @"${TEXT}<a href='URL_${ITEMID}'>${ITEMID}</a>");
Console.WriteLine(result);

不需要明确使用GroupCollections或CaptureCollections,除非你的替换比这更复杂,否则也可能不需要MatchEvaluator。