如何在String.replace中忽略大小写

41
string sentence = "We know it contains 'camel' word.";
// Camel can be in different cases:
string s1 = "CAMEL";
string s2 = "CaMEL";
string s3 = "CAMeL";
// ...
string s4 = "Camel";
// ...
string s5 = "camel";

如何在句子中将“camel”替换为“horse”,尽管string.Replace不支持在左字符串上忽略大小写?


10个回答

58

使用正则表达式:

var regex = new Regex( "camel", RegexOptions.IgnoreCase );
var newSentence = regex.Replace( sentence, "horse" );

当然,这也会匹配包含camel的单词,但不清楚您是否需要。

如果您需要精确匹配,可以使用自定义的MatchEvaluator。

public static class Evaluators
{
    public static string Wrap( Match m, string original, string format )
    {
        // doesn't match the entire string, otherwise it is a match
        if (m.Length != original.Length)
        {
            // has a preceding letter or digit (i.e., not a real match).
            if (m.Index != 0 && char.IsLetterOrDigit( original[m.Index - 1] ))
            {
                return m.Value;
            }
            // has a trailing letter or digit (i.e., not a real match).
            if (m.Index + m.Length != original.Length && char.IsLetterOrDigit( original[m.Index + m.Length] ))
            {
                return m.Value;
            }
        }
        // it is a match, apply the format
        return string.Format( format, m.Value );
    }
} 

结合前面的示例,可以将匹配项包装在一个 span 中:

var regex = new Regex( highlightedWord, RegexOptions.IgnoreCase );
foreach (var sentence in sentences)
{
    var evaluator = new MatchEvaluator( match => Evaluators.Wrap( match, sentence, "<span class='red'>{0}</span>" ) );
    Console.WriteLine( regex.Replace( sentence, evaluator ) );
}

如果我只想匹配特定的单词怎么办?我需要在文本中匹配一些关键词并将它们(仅限它们)替换为<span style="color:red">关键词</span>以使它们呈现红色。 - Francesco
@Luca - 我可能会使用自定义的MatchEvaluator来检查匹配到的子字符串前后是否有字母或数字,并且只在没有发生这种情况时返回替换字符串。我将添加一个示例(不完整,可能性较高,并且经过最少的测试)。 - tvanfosson
@minitech 如果单词边界确实有效,那当然可以,但在他的例子中,他想要替换引号内的一个单词。我认为在这种情况下,单词边界是引号字符,而不是第一个“c”。自定义匹配评估器允许使用简化的正则表达式进行任意逻辑处理。 - tvanfosson
啊。回顾先行断言也可能有效,但我能理解为什么两者都值得一试! - Ry-
你还应该在正则表达式的模式上使用Regex.Escape()。 - laurian

27

为字符串添加一个扩展方法来完成这个技巧:

用法:

string yourString = "TEXTTOREPLACE";
yourString.Replace("texttoreplace", "Look, I Got Replaced!", StringComparison.OrdinalIgnoreCase);

代码:

using System;
using System.Collections.Generic;
using System.IO;

public static class Extensions
{       
    public static string Replace(this string source, string oldString, string newString, StringComparison comp)
    {
        int index = source.IndexOf(oldString, comp);

        // Determine if we found a match
        bool MatchFound = index >= 0;

        if (MatchFound)
        {
            // Remove the old text
            source = source.Remove(index, oldString.Length);

            // Add the replacemenet text
            source = source.Insert(index, newString);
        }

        // recurse for multiple instances of the name
        if (source.IndexOf(oldString, comp) != -1)
        {
            source = Replace(source, oldString, newString, comp);
        }

        return source;
    }
}

6
这只替换source中第一个出现的oldString,与核心的Replace不同。 - bdukes
1
谢谢!我刚看到这个,已经更新了答案并包括了递归。 - Tom Beech

12

这是一个扩展方法,使用字符串的IndexOf方法并带有StringComparison参数:

    [Pure]
    public static string Replace(this string source, string oldValue, string newValue, StringComparison comparisonType)
    {
        if (source.Length == 0 || oldValue.Length == 0)
            return source;

        var result = new System.Text.StringBuilder();
        int startingPos = 0;
        int nextMatch;
        while ((nextMatch = source.IndexOf(oldValue, startingPos, comparisonType)) > -1)
        {
            result.Append(source, startingPos, nextMatch - startingPos);
            result.Append(newValue);
            startingPos = nextMatch + oldValue.Length;
        }
        result.Append(source, startingPos, source.Length - startingPos);

        return result.ToString();
    }

另外,这里还有一个类似的 Contains 方法,也可以接受 StringComparison 参数:

    [Pure]
    public static bool Contains(this string source, string value, StringComparison comparisonType)
    {
        return source.IndexOf(value, comparisonType) >= 0;
    }

一些测试:

[TestFixture]
public class ExternalTests
{
    private static string[] TestReplace_args =
        {
            "ab/B/c/ac",
            "HELLO World/Hello/Goodbye/Goodbye World",
            "Hello World/world/there!/Hello there!",
            "hello WoRlD/world/there!/hello there!",
            "///",
            "ab///ab",
            "/ab/cd/",
            "a|b|c|d|e|f/|//abcdef",
            "a|b|c|d|e|f|/|/:/a:b:c:d:e:f:",
        };

    [Test, TestCaseSource("TestReplace_args")]
    public void TestReplace(string teststring)
    {
        var split = teststring.Split("/");
        var source = split[0];
        var oldValue = split[1];
        var newValue = split[2];
        var result = split[3];
        Assert.That(source.Replace(oldValue, newValue, StringComparison.OrdinalIgnoreCase), Is.EqualTo(result));
    }
}

8

这是我的扩展方法,它结合了Tom Beech的方法和sntbob的递归方法,并对ksun指出的bug进行了修复。

代码:

public static string Replace(this string source, string oldString, 
                             string newString, StringComparison comparison)
{
    int index = source.IndexOf(oldString, comparison);

    while (index > -1)
    {
        source = source.Remove(index, oldString.Length);
        source = source.Insert(index, newString);

        index = source.IndexOf(oldString, index + newString.Length, comparison);
    }

    return source;
}

使用方法:

string source = "banana";
Console.WriteLine(source.Replace("AN", "banana", StringComparison.OrdinalIgnoreCase));

结果:

bbananabananaa

如果你仍然希望递归性质是可选的:

代码:

public static string Replace(this string source, string oldString, 
                             string newString, StringComparison comparison,
                             bool recursive = true)
{
    int index = source.IndexOf(oldString, comparison);

    while (index > -1)
    {
        source = source.Remove(index, oldString.Length);
        source = source.Insert(index, newString);

        if (!recursive)
        {
            return source;
        }
        index = source.IndexOf(oldString, index + newString.Length, comparison);
    }

    return source;
}

使用方法:

string source = "banana";
Console.WriteLine(source.Replace("AN", "banana", StringComparison.OrdinalIgnoreCase, false));

结果:

香蕉


3

使用StringComparison,因为它方便的OrdinalIgnoreCase

    string sentence = "We know it contains 'camel' word."; 
    string wordToFind = "camel";
    string replacementWord = "horse";

    int index = sentence.IndexOf(wordToFind , StringComparison.OrdinalIgnoreCase)
    // Did we match the word regardless of case
    bool match = index >= 0;

    // perform the replace on the matched word
    if(match) {
        sentence = sentence.Remove(index, wordToFind.Length)
        sentence = sentence.Insert(index, replacementWord)
    }

如果C# String类有一个像Java一样的ignoreCase()方法就太好了。


2
你也可以使用String.IndexOf来实现。

http://msdn.microsoft.com/en-us/library/system.string.indexof.aspx

通过这种方式进行操作可能会比使用正则表达式获得略微更好的性能(我讨厌它们,因为它们不直观且容易出错,尽管这个简单的 .Net 函数调用抽象了实际混乱的正则表达式,并且提供了很少的错误空间),但这可能对您并不重要;现在计算机非常快,对吧?:) 采用接受 StringComparison 对象的 IndexOf 重载函数可以选择忽略大小写,由于 IndexOf 返回指定位置处的第一个匹配项,因此您需要编写一个循环来处理具有多个匹配项的字符串。

在库和框架的时代,这需要大量的检查和调试,性能往往是受害者。 - Xaqron
这就是 Stack Overflow 的用途。 ;) - Shavais

1

虽然不如其他答案高效,但我还是挺喜欢sntbob编写的CustomReplace函数。

但是,它有一个缺陷。如果文本替换是递归的,它将导致无限循环。例如,CustomReplace("I eat bananas!","an","banana",false,false)会导致无限循环,字符串会继续变大。 例如,在第四次迭代之后,字符串将变为"I eat bbbbbananaanaanaanaanas!"

如果你只想替换"banana"中的两个"an"实例,那么你需要采取另一种方法。我修改了sntbob的代码以解决这个问题。我承认这更加复杂,但它可以处理递归替换。

public static string CustomReplace(string srcText, string toFind, string toReplace, bool matchCase, bool replaceOnce)
    {
        StringComparison sc = StringComparison.OrdinalIgnoreCase;
        if (matchCase)
            sc = StringComparison.Ordinal;

        int pos;
        int previousProcessedLength = 0;
        string alreadyProcessedTxt = "";
        string remainingToProcessTxt = srcText;
        while ((pos = remainingToProcessTxt.IndexOf(toFind, sc)) > -1)
        {
            previousProcessedLength = alreadyProcessedTxt.Length;
            //Append processed text up until the end of the found string and perform replacement
            alreadyProcessedTxt += remainingToProcessTxt.Substring(0, pos + toFind.Length);
            alreadyProcessedTxt = alreadyProcessedTxt.Remove(previousProcessedLength + pos, toFind.Length);
            alreadyProcessedTxt = alreadyProcessedTxt.Insert(previousProcessedLength + pos, toReplace);

            //Remove processed text from remaining
            remainingToProcessTxt = remainingToProcessTxt.Substring(pos + toFind.Length);                

            if (replaceOnce)
                break;
        }

        return alreadyProcessedTxt + remainingToProcessTxt;
    }

1
    public static string CustomReplace(string srcText, string toFind, string toReplace, bool matchCase, bool replace0nce)
    {
        StringComparison sc = StringComparison.OrdinalIgnoreCase;
        if (matchCase)
            sc = StringComparison.Ordinal;

        int pos;
        while ((pos = srcText.IndexOf(toFind, sc)) > -1)
        {
            srcText = srcText.Remove(pos, toFind.Length);
            srcText = srcText.Insert(pos, toReplace);

            if (replace0nce)
                break;
        }

        return srcText;
    }

0
为什么不直接导入Microsoft.VisualBasic命名空间并使用VB Strings.Replace方法呢?

https://msdn.microsoft.com/en-us/library/microsoft.visualbasic.strings.replace(v=vs.110).aspx

例如

var newString = Strings.Replace(SourceString, FindTextValue, ReplacementTextValue, 1, -1, Constants.vbTextCompare);

vbTextCompare强制进行不区分大小写的替换。工作完成。
好吧,这并不是“纯粹”的C#,但它可以让你以更少的复杂性和麻烦实现你想要的结果。

仅仅因为这样一个简单的情况,你不能从Visual Basic导入命名空间。 - Rey
真的吗?对我来说没问题。 - haughtonomous
使用 System.Collections.Generic; 使用 System.Linq; 使用 System.Text; 使用 System.Threading.Tasks; 使用 Microsoft.VisualBasic;命名空间 stringreplace { class Program { static void Main(string[] args) { var x = Microsoft.VisualBasic.Strings.Replace("a String string", "string", "strong", 1, -1, CompareMethod.Text); var y = x; } } } - haughtonomous
没有比这更简单的情况了! - haughtonomous

0

这里还有一种使用StringComparison作为扩展方法的替代方案,它可以在StringBuilder对象上使用。我读过一些文章表明,相比于使用字符串,StringBuilder可能会更有效地利用内存。如果需要,您可以轻松地将其改为适用于字符串。

/// <summary>
/// Extension method to find/replace replaces text in a StringBuilder object
/// </summary>
/// <param name="original">Source StringBuilder object</param>
/// <param name="oldString">String to search for</param>
/// <param name="newString">String to replace each occurrance of oldString</param>
/// <param name="stringComparison">String comparison to use</param>
/// <returns>Original Stringbuilder with replacements made</returns>
public static StringBuilder Replace(this StringBuilder original,
                    string oldString, string newString, StringComparison stringComparison)
    {
        //If anything is null, or oldString is blank, exit with original value
        if ( newString == null || original == null || string.IsNullOrEmpty(oldString))
            return original;

        //Convert to a string and get starting position using
        //IndexOf which allows us to use StringComparison.
        int pos = original.ToString().IndexOf(oldString, 0, stringComparison);

        //Loop through until we find and replace all matches
        while ( pos >= 0 )
        {
            //Remove the old string and insert the new one.
            original.Remove(pos, oldString.Length).Insert(pos, newString);

            //Get the next match starting 1 character after last replacement (to avoid a possible infinite loop)
            pos = original.ToString().IndexOf(oldString, pos + newString.Length + 1, stringComparison);
        }
        return original;
    }

你能添加注释以使其更易理解吗? - Kabira K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接