Есть ли альтернатива string.Replace без учета регистра?


306

Мне нужно найти строку и заменить все вхождения %FirstName%и %PolicyAmount%значение извлекается из базы данных. Проблема в том, что заглавные буквы FirstName меняются. Это мешает мне использовать String.Replace()метод. Я видел веб-страницы на эту тему, которые предлагают

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

Однако по какой-то причине, когда я пытаюсь заменить %PolicyAmount%на $0, замена никогда не происходит. Я предполагаю, что это как-то связано с тем, что знак доллара является зарезервированным символом в регулярном выражении.

Есть ли другой метод, который я могу использовать, который не включает в себя очистку ввода для работы со специальными символами регулярного выражения?


1
Если «0» - это переменная, которая не влияет на регулярное выражение.
cfeduke

Ответы:


132

От MSDN
$ 0 - «Подставляет последнюю подстроку, соответствующую номеру группы (десятичному)».

В .NET Регулярные выражения группа 0 - это всегда полное совпадение. Для буквального $ вам нужно

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

16
в данном конкретном случае это нормально, но в случаях, когда строки вводятся извне, нельзя быть уверенным, что они не содержат символов, которые означают что-то особенное в регулярных выражениях
Allanrbo

23
Вы должны экранировать специальные символы, такие как: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), Regex.Escape ("$ 0"), RegexOptions.IgnoreCase);
Хельге Кляйн

8
Пожалуйста, следите за использованием Regex.Escape в Regex.Replace. Вам нужно будет избежать всех трех пропущенных строк и вызвать Regex.Unescape для получения результата!
Хольгер Адам

4
Согласно msdn: «Экранирование символов распознается в шаблонах регулярных выражений, но не в шаблонах замены». ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Бронек

1
Лучше всего использовать: строковое значение = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), "$ 0" .Replace ("$", "$$"), RegexOptions.IgnoreCase); в качестве замены распознаются только доларские знаки.
Скорек

295

Похоже, 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();
}

9
Ницца. Я бы изменился ReplaceStringна Replace.
AMissico

41
Согласитесь с комментариями выше. Это может быть сделано в метод расширения с тем же именем метода. Просто вставьте его в статический класс с помощью сигнатуры метода: public static string Replace (эта строка String, строка oldValue, строка newValue, сравнение StringComparison)
Марк Робинсон,

8
@ Хельге, в общем, это может быть хорошо, но я должен взять произвольные строки от пользователя и не могу рисковать, чтобы ввод был значимым для регулярного выражения. Конечно, я думаю, что я мог бы написать цикл и поставить обратную косую черту перед каждым и каждым символом ... В этот момент я мог бы сделать то же самое (ИМХО).
Джим

9
Во время модульного тестирования я столкнулся с тем случаем, когда он никогда не вернется oldValue == newValue == "".
Измаил

10
Это глючит; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)броски ArgumentOutOfRangeException.
Майкл Лю

45

Вид запутанной группы ответов, отчасти потому, что название вопроса на самом деле намного больше, чем конкретный вопрос, который задают. После прочтения, я не уверен, что какой-либо ответ будет в нескольких редакциях от усвоения всех хороших вещей здесь, поэтому я решил, что постараюсь подвести итог.

Вот метод расширения, который, я думаю, позволяет избежать ловушек, упомянутых здесь, и обеспечивает наиболее широкое применение.

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);
}

Так...

К сожалению, комментарий @HA о том, что у вас есть Escapeвсе три , неверен . Начальное значение и newValueне должно быть.

Примечание: вы, однако, должны экранировать $s в новом значении, которое вы вставляете, если они являются частью того, что может показаться маркером «захваченного значения» . Таким образом, три знака доллара в Regex.Replace внутри Regex.Replace [sic]. Без этого что-то подобное ломается ...

"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 в кодировках ), чтобы быть абсолютно уверенным, что вы получаете то, что вы предназначен для важных случаев использования. Немного напоминает мне Крокфорда о « небезопасных регулярных выражениях ». Слишком часто мы пишем $10регулярные выражения, которые разрешают то, что мы хотим (если нам повезет), но непреднамеренно допускают больше в (например, действительно ли является допустимой строкой «значения захвата» в моем регулярном выражении newValue, выше?), Потому что мы не были достаточно вдумчивыми , Оба метода имеют ценность, и оба поощряют различные типы непреднамеренных ошибок. Часто легко недооценить сложность.

Это странное $побег (и это Regex.Escapeне ускользнуло от шаблонов захваченных значений, таких $0как, как я ожидал бы от значений замещения), на какое-то время сводило меня с ума. Программирование сложно (с) 1842


32

Вот метод расширения. Не уверен, где я нашел это.

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;
    }

}

Возможно, вам придется обрабатывать пустые / нулевые строковые регистры.
Вад

2
Многочисленные ошибки в этом решении: 1. Проверьте originalString, oldValue и newValue на null. 2. Не возвращайте orginalString (не работает, простые типы не передаются по ссылке), но сначала присвойте значение orginalValue новой строке, измените ее и верните.
RWC

31

Кажется, самый простой способ - это просто использовать метод Replace, который поставляется с .Net и существует с .Net 1.0:

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

Чтобы использовать этот метод, необходимо добавить ссылку на сборку Microsoft.VisualBasic. Эта сборка является стандартной частью среды выполнения .Net, она не является дополнительной загрузкой или помечена как устаревшая.


4
Оно работает. Вам необходимо добавить ссылку на сборку Microsoft.VisualBasic.
CleverPatrick

Странно, что у этого метода были некоторые проблемы, когда я его использовал (пропали символы в начале строки). Самый популярный ответ здесь C. Dragon 76сработал, как и ожидалось.
Джереми Томпсон

1
Проблема в том, что он возвращает НОВУЮ строку, даже если замена не сделана, где string.replace () возвращает указатель на эту же строку. Может стать неэффективным, если вы делаете что-то вроде формы слияния букв.
Brain2000

4
Brain2000, вы не правы. Все строки в .NET являются неизменяемыми.
Der_Meister

Der_Meister, хотя то, что вы говорите правильно, не означает, что Brain2000 сказал неправильно.
Саймон Хьюитт

11
    /// <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);
    }

Какой способ лучше? как насчет stackoverflow.com/a/244933/206730 ? лучшая производительность?
Kiquenet

8

Вдохновленный ответом 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 за неиспользование регулярных выражений, когда это не нужно. Конечно, вы используете еще несколько строк кода, но это гораздо эффективнее, чем замена на основе регулярных выражений, если вам не нужна функциональность $.
ChrisG

6

Расширяем популярный ответ 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();
     }
}

3

На основании ответа Джеффа Редди, с некоторыми оптимизациями и проверками:

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();
}

2

версия, аналогичная версии C. Dragon, но если вам нужна только одна замена:

int n = myText.IndexOf(oldValue, System.StringComparison.InvariantCultureIgnoreCase);
if (n >= 0)
{
    myText = myText.Substring(0, n)
        + newValue
        + myText.Substring(n + oldValue.Length);
}

1

Вот еще один вариант выполнения замен Regex, так как не многие люди замечают, что совпадения содержат расположение в строке:

    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 символа каждый раз, когда вам нужно сделать замену, вы должны учитывать это при вычислении параметров для метода удаления
Aheho

0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);

3
Это не работает $ Не в токене. Это в strReplace со строкой.
Ахехо

9
И вы не можете приспособить это для этого?
Джоэл Коухорн

18
Этот сайт должен быть хранилищем для правильных ответов. Не ответы, которые почти правильны.
Ахехо

0

Метод регулярного выражения должен работать. Однако то, что вы также можете сделать, это нижний регистр строки из базы данных, нижний регистр% переменных%, который у вас есть, а затем найти позиции и длины в строчной строке из базы данных. Помните, что позиции в строке не меняются только потому, что ее нижний регистр.

Затем, используя цикл, который идет в обратном порядке (проще, если вы этого не сделаете, вам придется вести счетчик количества движений, в которые перемещаются более поздние точки), удалить из вашей строки без нижестоящего регистра из базы данных% variable% по их положению и длина и вставить значения замены.


Под обратным я подразумеваю обработку найденных местоположений в обратном порядке от самого дальнего к самому короткому, а не прохождение строки из базы данных в обратном порядке.
cfeduke

Вы могли бы, или вы могли бы просто использовать Regex :)
Ray

0

(Так как все это делают). Вот моя версия (с нулевыми проверками и корректным выходом и заменой при выходе) ** Вдохновленные из Интернета и других версий:

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");

0

Позвольте мне сделать мое дело, и тогда вы можете разорвать меня на куски, если хотите.

Regex не является ответом на эту проблему - слишком медленно и требует много памяти, условно говоря.

StringBuilder намного лучше, чем искажение строк.

Поскольку это будет дополнительный метод расширения string.Replace, я считаю, что важно соответствовать тому, как это работает, поэтому важно создавать исключения для тех же проблем с аргументами, как и возврат исходной строки, если замена не была сделана.

Я считаю, что наличие параметра StringComparison не очень хорошая идея. Я попробовал, но тестовый пример, упомянутый Майклом Лю, показал проблему:

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

Хотя IndexOf будет совпадать, существует несоответствие между длиной совпадения в исходной строке (1) и oldValue.Length (2). Это проявилось в появлении IndexOutOfRange в некоторых других решениях, когда oldValue.Length был добавлен к текущей позиции совпадения, и я не смог найти способ обойти это. В любом случае, Regex не подходит к этому случаю, поэтому я выбрал прагматичное решение - использовать только 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();
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.