是否存在不区分大小写的 string.Replace 替代方法?

Posted

技术标签:

【中文标题】是否存在不区分大小写的 string.Replace 替代方法?【英文标题】:Is there an alternative to string.Replace that is case-insensitive? 【发布时间】:2010-09-19 15:51:56 【问题描述】:

我需要搜索一个字符串并将所有出现的%FirstName%%PolicyAmount% 替换为从数据库中提取的值。问题是 FirstName 的大小写不同。这使我无法使用String.Replace() 方法。我看过有关该主题的网页建议

Regex.Replace(strInput, strToken, strReplaceWith, RegexOptions.IgnoreCase);

但是,由于某种原因,当我尝试将 %PolicyAmount% 替换为 $0 时,替换永远不会发生。我认为这与美元符号是正则表达式中的保留字符有关。

我可以使用另一种不涉及清理输入以处理正则表达式特殊字符的方法吗?

【问题讨论】:

如果 "$0" 是一个完全不影响正则表达式的变量。 作为Markus points out,看起来“现代”版本的.NET 现在已经将它与良好的ole StringComparison.OrdinalIgnoreCase 作为第三个参数结合在一起。 【参考方案1】:

似乎string.Replace 应该有一个采用StringComparison 参数的重载。既然没有,你可以试试这样的:

public static string ReplaceString(string str, string oldValue, string newValue, StringComparison comparison)

    StringBuilder sb = new StringBuilder();

    int previousIndex = 0;
    int index = str.IndexOf(oldValue, comparison);
    while (index != -1)
    
        sb.Append(str.Substring(previousIndex, index - previousIndex));
        sb.Append(newValue);
        index += oldValue.Length;

        previousIndex = index;
        index = str.IndexOf(oldValue, index, comparison);
    
    sb.Append(str.Substring(previousIndex));

    return sb.ToString();

【讨论】:

不错。我会将ReplaceString 更改为Replace 同意上面的cmets。这可以制成具有相同方法名称的扩展方法。只需在带有方法签名的静态类中弹出它: public static string Replace(this String str, string oldValue, string newValue, StringComparison comparison) @Helge,一般来说,这可能很好,但我必须从用户那里获取任意字符串,并且不能冒险输入对正则表达式有意义。当然,我想我可以写一个循环并在每个字符前面加上一个反斜杠……到那时,我还不如做上面的(恕我直言)。 在对此进行单元测试时,我遇到了oldValue == newValue == ""时它永远不会返回的情况。 这是错误的; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture) 抛出 ArgumentOutOfRangeException.【参考方案2】:

From MSDN $0 - "替换与组号匹配的最后一个子字符串(十进制)。"

在 .NET 正则表达式中,组 0 始终是整个匹配项。对于文字 $ 你需要

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);

【讨论】:

在这种特殊情况下这很好,但在字符串从外部输入的情况下,不能确定它们不包含在正则表达式中表示特殊含义的字符 你应该像这样转义特殊字符: string value = Regex.Replace("%PolicyAmount%", Regex.Escape("%PolicyAmount%"), Regex.Escape("$0"), RegexOptions .IgnoreCase); 在 Regex.Replace 中使用 Regex.Escape 时请注意。您必须转义所有传递的三个字符串并在结果上调用 Regex.Unescape! 根据 msdn:“字符转义在正则表达式模式中被识别,但在替换模式中不被识别。” (msdn.microsoft.com/en-us/library/4edbef7e.aspx) 最好使用:string value = Regex.Replace("%PolicyAmount%", Regex.Escape("%PolicyAmount%"), "$0".Replace("$", "$$ "), 正则表达式选项.IgnoreCase);因为替换只能识别美元符号。【参考方案3】:

这是一组令人困惑的答案,部分原因是问题的标题实际上比所问的具体问题大。通读后,我不确定是否有任何答案与吸收这里所有的好东西相距甚远,所以我想我会尝试总结一下。

这是一种我认为可以避免这里提到的陷阱并提供最广泛适用的解决方案的扩展方法。

public static string ReplaceCaseInsensitiveFind(this string str, string findMe,
    string newValue)

    return Regex.Replace(str,
        Regex.Escape(findMe),
        Regex.Replace(newValue, "\\$[0-9]+", @"$$$0"),
        RegexOptions.IgnoreCase);

所以...

这是an extension method@MarkRobinson 这个doesn't try to skip Regex@Helge(如果你想在正则表达式之外进行这样的字符串嗅探,你真的必须逐字节进行) 通过了@MichaelLiu 的excellent test case、"œ".ReplaceCaseInsensitiveFind("oe", ""),尽管他的想法可能略有不同。

很遗憾,@HA 's comment that you have to Escape all three isn't correct。初始值和newValue 不需要。

注意:但是,如果 $s 是您插入的新值 如果它们是看似“捕获的值”标记。因此,Regex.Replace [sic] 中的 Regex.Replace 中的三个美元符号。没有它,这样的事情就会中断......

"This is HIS fork, hIs spoon, hissssssss knife.".ReplaceCaseInsensitiveFind("his", @"he$0r")

这是错误:

An unhandled exception of type 'System.ArgumentException' occurred in System.dll

Additional information: parsing "The\hisr\ is\ he\HISr\ fork,\ he\hIsr\ spoon,\ he\hisrsssssss\ knife\." - Unrecognized escape sequence \h.

告诉你什么,我知道熟悉 Regex 的人会觉得他们的使用可以避免错误,但我通常仍然偏爱字节嗅探字符串(但只有在阅读 Spolsky on encodings 之后)才能绝对确定你是得到你想要的重要用例。让我想起了“insecure regular expressions”上的 Crockford。我们经常编写允许我们想要的正则表达式(如果我们幸运的话),但无意中允许更多(例如,$10 在上面的我的 newValue 正则表达式中真的是一个有效的“捕获值”字符串吗?)因为我们不是t 够周到。这两种方法都有价值,并且都鼓励不同类型的无意错误。通常很容易低估复杂性。

那种奇怪的$ 转义(并且Regex.Escape 没有像我在替换值中所期望的那样逃脱捕获的值模式,如$0)让我发疯了一段时间。编程很难(c)1842

【讨论】:

【参考方案4】:

这是一个扩展方法。不知道在哪里找到的。

public static class StringExtensions

    public static string Replace(this string originalString, string oldValue, string newValue, StringComparison comparisonType)
    
        int startIndex = 0;
        while (true)
        
            startIndex = originalString.IndexOf(oldValue, startIndex, comparisonType);
            if (startIndex == -1)
                break;

            originalString = originalString.Substring(0, startIndex) + newValue + originalString.Substring(startIndex + oldValue.Length);

            startIndex += newValue.Length;
        

        return originalString;
    


【讨论】:

您可能需要处理空/空字符串情况。 此解决方案中的多个错误: 1. 检查 originalString、oldValue 和 newValue 是否为空。 2.不给回orginalString(不行,简单类型不通过引用传递),而是先将orginalValue的值赋给一个新的字符串,修改后再给回。【参考方案5】:

似乎最简单的方法就是使用 .Net 附带的 Replace 方法,并且自 .Net 1.0 以来一直存在:

string res = Microsoft.VisualBasic.Strings.Replace(res, 
                                   "%PolicyAmount%", 
                                   "$0", 
                                   Compare: Microsoft.VisualBasic.CompareMethod.Text);

为了使用此方法,您必须添加对 Microsoft.VisualBasic 程序集的引用。此程序集是 .Net 运行时的标准部分,它不是额外下载或标记为过时的。

【讨论】:

它有效。您需要添加对 Microsoft.VisualBasic 程序集的引用。 奇怪,这个方法在我使用的时候出现了一些问题(行首的字符丢失了)。来自C. Dragon 76 的最受欢迎的答案按预期工作。 这个问题是即使没有进行替换,它也会返回一个新字符串,其中 string.replace( ) 返回指向同一字符串的指针。如果您正在执行格式信函合并之类的操作,可能会变得低效。 Brain2000,你错了。 .NET 中的所有字符串都是不可变的。 Der_Meister,虽然你说的是对的,但这并不代表 Brain2000 说的是错的。【参考方案6】:
    /// <summary>
    /// A case insenstive replace function.
    /// </summary>
    /// <param name="originalString">The string to examine.(HayStack)</param>
    /// <param name="oldValue">The value to replace.(Needle)</param>
    /// <param name="newValue">The new value to be inserted</param>
    /// <returns>A string</returns>
    public static string CaseInsenstiveReplace(string originalString, string oldValue, string newValue)
    
        Regex regEx = new Regex(oldValue,
           RegexOptions.IgnoreCase | RegexOptions.Multiline);
        return regEx.Replace(originalString, newValue);
    

【讨论】:

哪种方法更好? ***.com/a/244933/206730 呢?性能更好?【参考方案7】:

受 cfeduke 的回答启发,我制作了这个函数,它使用 IndexOf 来查找字符串中的旧值,然后用新值替换它。我在处理数百万行的 SSIS 脚本中使用了它,而正则表达式方法比这慢得多。

public static string ReplaceCaseInsensitive(this string str, string oldValue, string newValue)

    int prevPos = 0;
    string retval = str;
    // find the first occurence of oldValue
    int pos = retval.IndexOf(oldValue, StringComparison.InvariantCultureIgnoreCase);

    while (pos > -1)
    
        // remove oldValue from the string
        retval = retval.Remove(pos, oldValue.Length);

        // insert newValue in it's place
        retval = retval.Insert(pos, newValue);

        // check if oldValue is found further down
        prevPos = pos + newValue.Length;
        pos = retval.IndexOf(oldValue, prevPos, StringComparison.InvariantCultureIgnoreCase);
    

    return retval;

【讨论】:

+1 表示不需要时不使用正则表达式。当然,您会使用更多的代码行,但它比基于正则表达式的替换更有效,除非您需要 $ 功能。【参考方案8】:

扩展C. Dragon 76 的流行答案,将他的代码变成重载默认Replace 方法的扩展。

public static class StringExtensions

    public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
    
        StringBuilder sb = new StringBuilder();

        int previousIndex = 0;
        int index = str.IndexOf(oldValue, comparison);
        while (index != -1)
        
            sb.Append(str.Substring(previousIndex, index - previousIndex));
            sb.Append(newValue);
            index += oldValue.Length;

            previousIndex = index;
            index = str.IndexOf(oldValue, index, comparison);
        
        sb.Append(str.Substring(previousIndex));
        return sb.ToString();
     

【讨论】:

【参考方案9】:

基于 Jeff Reddy 的回答,并进行了一些优化和验证:

public static string Replace(string str, string oldValue, string newValue, StringComparison comparison)

    if (oldValue == null)
        throw new ArgumentNullException("oldValue");
    if (oldValue.Length == 0)
        throw new ArgumentException("String cannot be of zero length.", "oldValue");

    StringBuilder sb = null;

    int startIndex = 0;
    int foundIndex = str.IndexOf(oldValue, comparison);
    while (foundIndex != -1)
    
        if (sb == null)
            sb = new StringBuilder(str.Length + (newValue != null ? Math.Max(0, 5 * (newValue.Length - oldValue.Length)) : 0));
        sb.Append(str, startIndex, foundIndex - startIndex);
        sb.Append(newValue);

        startIndex = foundIndex + oldValue.Length;
        foundIndex = str.IndexOf(oldValue, startIndex, comparison);
    

    if (startIndex == 0)
        return str;
    sb.Append(str, startIndex, str.Length - startIndex);
    return sb.ToString();

【讨论】:

【参考方案10】:

类似于 C.Dragon 的版本,但如果您只需要一个替换:

int n = myText.IndexOf(oldValue, System.StringComparison.InvariantCultureIgnoreCase);
if (n >= 0)

    myText = myText.Substring(0, n)
        + newValue
        + myText.Substring(n + oldValue.Length);

【讨论】:

【参考方案11】:

从 .NET Core 2.0 或 .NET Standard 2.1 开始,这分别被纳入 .NET 运行时 [1]:

"hello world".Replace("World", "csharp", StringComparison.CurrentCultureIgnoreCase); // "hello csharp"

[1]https://docs.microsoft.com/en-us/dotnet/api/system.string.replace#System_String_Replace_System_String_System_String_System_StringComparison_

【讨论】:

【参考方案12】:

这是执行正则表达式替换的另一个选项,因为似乎没有多少人注意到匹配包含字符串中的位置:

    public static string ReplaceCaseInsensative( this string s, string oldValue, string newValue ) 
        var sb = new StringBuilder(s);
        int offset = oldValue.Length - newValue.Length;
        int matchNo = 0;
        foreach (Match match in Regex.Matches(s, Regex.Escape(oldValue), RegexOptions.IgnoreCase))
        
            sb.Remove(match.Index - (offset * matchNo), match.Length).Insert(match.Index - (offset * matchNo), newValue);
            matchNo++;
        
        return sb.ToString();
    

【讨论】:

您能解释一下为什么要乘以 MatchNo 吗? 如果 oldValue 和 newValue 之间的长度不同,则在替换值时字符串会变长或变短。 match.Index 指的是字符串中的原始位置,由于我们的替换,我们需要针对该位置的移动进行调整。另一种方法是从右到左执行删除/插入。 我明白了。这就是“偏移”变量的用途。我不明白为什么你要乘以matchNo。我的直觉告诉我,字符串中匹配的位置与之前出现的实际计数无关。 没关系,我现在明白了。偏移量需要根据出现次数进行缩放。如果每次需要替换时丢失 2 个字符,则需要在计算 remove 方法的参数时考虑到这一点【参考方案13】:
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);

【讨论】:

这不起作用。 $ 不在令牌中。它在 strReplace With 字符串中。 你不能适应它吗? 这个站点应该是正确答案的存储库。不是几乎正确的答案。【参考方案14】:

正则表达式方法应该可以工作。但是,您还可以将数据库中的字符串小写,将您拥有的 %variables% 小写,然后从数据库中找到小写字符串中的位置和长度。请记住,字符串中的位置不会因为小写而改变。

然后使用一个反向循环(它更容易,如果你不这样做,你将不得不保持后续点移动到的位置的运行计数)从数据库中的非小写字符串中删除 %variables%它们的位置和长度并插入替换值。

【讨论】:

反向,我的意思是从最远到最短反向处理找到的位置,而不是反向遍历数据库中的字符串。 你可以,或者你可以只使用正则表达式:)【参考方案15】:

(因为每个人都在尝试这个)。这是我的版本(带有空检查,以及正确的输入和替换转义)** 灵感来自互联网和其他版本:

using System;
using System.Text.RegularExpressions;

public static class MyExtensions 
    public static string ReplaceIgnoreCase(this string search, string find, string replace) 
        return Regex.Replace(search ?? "", Regex.Escape(find ?? ""), (replace ?? "").Replace("$", "$$"), RegexOptions.IgnoreCase);          
    

用法:

var result = "This is a test".ReplaceIgnoreCase("IS", "was");

【讨论】:

【参考方案16】:

让我证明我的情况,然后如果你愿意,你可以把我撕成碎片。

正则表达式不是解决这个问题的办法 - 相对来说太慢而且内存占用很大。

StringBuilder 比字符串修饰要好得多。

由于这将是补充string.Replace 的扩展方法,我认为匹配它的工作原理很重要 - 因此对于相同的参数问题抛出异常很重要,如果没有进行替换则返回原始字符串。

我认为拥有 StringComparison 参数不是一个好主意。 我确实尝试过,但michael-liu最初提到的测试用例显示了一个问题:-

[TestCase("œ", "oe", "", StringComparison.InvariantCultureIgnoreCase, Result = "")]

虽然 IndexOf 会匹配,但源字符串 (1) 和 oldValue.Length (2) 中的匹配长度不匹配。当 oldValue.Length 添加到当前匹配位置时,这通过在其他一些解决方案中导致 IndexOutOfRange 表现出来,我找不到解决这个问题的方法。 无论如何,正则表达式都无法匹配案例,所以我采取了只使用StringComparison.OrdinalIgnoreCase 作为我的解决方案的务实解决方案。

我的代码与其他答案相似,但我的转折是在创建StringBuilder 之前先寻找匹配项。如果没有找到,则避免潜在的大分配。然后代码变为do...while 而不是while...

我已经针对其他答案进行了一些广泛的测试,结果速度快了一点,使用的内存也少了一点。

    public static string ReplaceCaseInsensitive(this string str, string oldValue, string newValue)
    
        if (str == null) throw new ArgumentNullException(nameof(str));
        if (oldValue == null) throw new ArgumentNullException(nameof(oldValue));
        if (oldValue.Length == 0) throw new ArgumentException("String cannot be of zero length.", nameof(oldValue));

        var position = str.IndexOf(oldValue, 0, StringComparison.OrdinalIgnoreCase);
        if (position == -1) return str;

        var sb = new StringBuilder(str.Length);

        var lastPosition = 0;

        do
        
            sb.Append(str, lastPosition, position - lastPosition);

            sb.Append(newValue);

         while ((position = str.IndexOf(oldValue, lastPosition = position + oldValue.Length, StringComparison.OrdinalIgnoreCase)) != -1);

        sb.Append(str, lastPosition, str.Length - lastPosition);

        return sb.ToString();
    

【讨论】:

以上是关于是否存在不区分大小写的 string.Replace 替代方法?的主要内容,如果未能解决你的问题,请参考以下文章

nginx 正则及rewrite常用规则实例

不区分大小写地检查字符串是不是存在于数组中

Nginx配置文件nginx.conf详解

nginx 正则表达式

区分大小写的文件扩展名和存在检查

Nginx中的正则如何匹配数字