Чи є альтернатива string.Замініть, що не відрізняється від регістру?


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
в цьому конкретному випадку це добре, але у випадках, коли рядки вводяться ззовні, не можна бути впевненим, що вони не містять символів, що означають щось особливе у регулярних виразах
Алланрбо

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
Найкраще використовувати: string value = 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 Замініть (це String str, string oldValue, string newValue, StringComppareration порівняння)
Марк Робінсон,

8
@Helge, загалом, це може бути нормально, але я мушу брати у користувача довільні рядки і не можу ризикувати, що введення має значення для регулярного вираження. Звичайно, я думаю, що я міг би написати цикл і поставити зворотну косу рису перед кожним символом ... У цей момент я міг би також зробити вищезазначене (ІМХО).
Джим

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не повинно бути.

Примітка. Однак ви повинні уникати $нового значення, яке ви вставляєте, якщо вони є частиною того, що, здавалося б, є маркером "захопленого значення" . Таким чином, три знаки долара в Regex.Замініть всередині 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, відчувають, що їх використання уникає помилок, але я часто все ще частково баю нюхаючі рядки (але лише після того, як прочитав Спольського на кодуваннях ), щоб бути абсолютно впевненим, що ти отримуєш те, що ти призначені для важливих випадків використання. Трохи нагадує про Крокфорда про " небезпечні регулярні вирази ". Занадто часто ми пишемо регулярні вирази, які дозволяють нам те, що ми хочемо (якщо нам пощастило), але ненавмисно дозволяють отримати більше в (наприклад, чи $10справді це дійсна строка "значення захоплення" в моєму newgealge regexp, вище?), Тому що ми не були достатньо продумані . Обидва методи мають цінність, і обидва заохочують різні типи ненавмисних помилок. Часто важко недооцінити складність.

Це дивне $втеча (і що 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

Здається, найпростішим методом є просто використовувати метод Замінити, який постачається з .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 ? кращі показники?
Кікенет

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's, але якщо вам потрібна лише одна заміна:

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 посилається на вихідне місце в рядку, нам потрібно відрегулювати цей рух позицій завдяки нашій заміні. Іншим підходом було б виконання Remove / Insert справа наліво.
Брендон

Я це розумію. Саме для цього потрібна змінна "offset". Я не розумію, чому ви множите на matchNo. Моя інтуїція підказує мені, що розташування збігу в рядку не матиме відношення до фактичного підрахунку попередніх подій.
Ахехо

Неважливо, я зараз це розумію. Зсув потрібно масштабувати на основі числа подій. Якщо ви втрачаєте 2 символи кожного разу, коли вам потрібно зробити заміну, вам потрібно це врахувати під час обчислення параметрів методу видалення
Aheho

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

3
Це не працює. $ Не в лексемі. Це в strReplace із рядком.
Ахехо

9
І ви не можете пристосувати це для цього?
Joel Coehoorn

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

0

Метод регулярного вираження повинен працювати. Однак ви також можете зробити це з нижнього регістру рядка з бази даних, з нижнього регістру у вас є% змінних%, а потім знайдіть позиції та довжини в нижній обробці рядка з бази даних. Пам’ятайте, позиції в рядку не змінюються лише тому, що нижній обкладений.

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


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

Ви могли, або можете просто скористатися Regex :)
Рей

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, я вважаю, що важливо відповідати тому, як це працює, тому важливим є викидання винятків для тих же питань аргументів, як повернення початкового рядка, якщо заміна не була здійснена.

Я вважаю, що наявність параметра StringComppare - це не дуже гарна ідея. Я спробував це, але тестовий випадок, спочатку згаданий michael-liu, показав проблему: -

[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.