Ошибка в .net Regex.Replace?
Следующий код...
using System;
using System.Text.RegularExpressions;
public class Program
{
public static void Main()
{
var r = new Regex("(.*)");
var c = "XYZ";
var uc = r.Replace(c, "A $1 B");
Console.WriteLine(uc);
}
}
. Ссылка Net Fiddle
выводит следующий результат...
A XYZ BA B
Считаете ли вы, что это правильно?
Не должно быть выход...
A XYZ B
Думаю, я делаю здесь что-то глупое. Я был бы признателен за любую помощь, которую вы можете предоставить, помогая мне понять эту проблему.
Вот что-то интересное...
using System;
using System.Text.RegularExpressions;
public class Program
{
public static void Main()
{
var r = new Regex("(.*)");
var c = "XYZ";
var uc = r.Replace(c, "$1");
Console.WriteLine(uc);
}
}
.NET Fiddle
Вывод...
XYZ
Ответы
Ответ 1
Что касается того, почему движок возвращает 2 совпадения, это связано с тем, как .NET(также Perl и Java) обрабатывает глобальное соответствие, т.е. находит все совпадения с данным шаблоном во входной строке.
Процесс можно описать следующим образом (текущий индекс обычно устанавливается в 0 в начале поиска, если не указано):
- Из текущего индекса выполните поиск.
- Если нет совпадения:
- Если текущий индекс уже указывает в конце строки (текущий индекs >= строка .length), верните результат до сих пор.
- Инкремент текущего индекса на 1, перейдите к шагу 1.
- Если основное совпадение (
$0
) не пусто (по крайней мере один символ потребляется), добавьте результат и установите текущий индекс в конец основного соответствия ($0
). Затем перейдите к шагу 1.
- Если главное совпадение (
$0
) пусто:
- Если предыдущее совпадение не пустое, добавьте результат и перейдите к шагу 1.
- Если предыдущее совпадение пустое, назад и продолжить поиск.
- Если попытка обратного отслеживания находит непустое совпадение, добавьте результат, установите текущий индекс в конец совпадения и перейдите к шагу 1.
- В противном случае увеличивайте текущий индекс на 1. Перейдите к шагу 1.
Двигатель должен проверить наличие пустого совпадения; в противном случае это закончится бесконечным циклом. Дизайнер признает использование пустого совпадения (например, при разбиении строки на символы), поэтому двигатель должен быть сконструирован таким образом, чтобы избежать застревания в определенной позиции навсегда.
Этот процесс объясняет, почему в конце есть пустое совпадение: поскольку поиск ведется в конце строки (индекс 3) после (.*)
соответствует abc
, а (.*)
может соответствовать пустой строке, найдено пустое совпадение. И движок не создает бесконечное количество пустых совпадений, так как в конце уже найдено пустое совпадение.
a b c
^ ^ ^ ^
0 1 2 3
Первое совпадение:
a b c
^ ^
0-----3
Второе совпадение:
a b c
^
3
В соответствии с вышеописанным глобальным алгоритмом сопоставления может быть не более двух совпадений, начиная с одного и того же индекса, и такой случай может произойти только тогда, когда первый из них является пустым.
Обратите внимание, что JavaScript просто увеличивает текущий индекс на 1, если основное совпадение пуст, поэтому не более 1 соответствует индексу. Однако в этом случае (.*)
, если вы используете глобальный флаг g
для глобального сопоставления, тот же результат произойдет:
(Результат ниже из Firefox, обратите внимание на флаг g
)
> "XYZ".replace(/(.*)/g, "A $1 B")
"A XYZ BA B"
Ответ 2
Мне нужно подумать, почему это происходит. Уверен, что тебе что-то не хватает. Хотя это исправить проблему. Просто привяжите регулярное выражение.
var r = new Regex("^(.*)$");
Здесь . Демо-версия NetFiddle
Ответ 3
У вас регулярное выражение имеет два совпадения, и Replace заменит их оба. Первый - "XYZ", а второй - пустая строка. Я не уверен, почему у него есть два матча в первую очередь. Вы можете исправить его с помощью ^ (. *) $, Чтобы заставить его рассмотреть начало и конец строки.
Или используйте +
вместо *
, чтобы он соответствовал хотя бы одному символу.
.*
соответствует пустой строке, поскольку она имеет нулевые символы.
.+
не соответствует пустой строке, потому что требуется хотя бы один символ.
Интересно, что в Javascript (в Chrome):
var r = /(.*)/;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");
Выведет ожидаемый A XYZ B
без ложного дополнительного соответствия.
Изменить (спасибо @nhahtdh): добавив флаг g
в регулярное выражение Javascript, дайте тот же результат, что и в .NET:
var r = /(.*)/g;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");
Ответ 4
Коэффициент *
соответствует 0 или более. Это приводит к 2-мя совпадениям. XYZ и ничего.
Попробуйте использовать квантор +
, который соответствует 1 или более.
Простое объяснение заключается в том, чтобы посмотреть на строку следующим образом: XYZ<nothing>
- У нас есть совпадения
XYZ
и <nothing>
- Для каждого матча
- Матч 1: Заменить
XYZ
на A $1 B
($ 1 здесь XYZ
) Результат: A XYZ B
- Матч 2: Заменить
<nothing>
на A $1 B
($ 1 здесь <nothing>
) Результат: A B
Конечный результат: A XYZ BA B
Почему <nothing>
является совпадением сам по себе интересен и что-то, о чем я действительно не думал. (Почему нет бесконечных <nothing>
совпадений?)
Ответ 5
Regex - своеобразный язык. Вы должны точно понимать, что (. *) Будет соответствовать. Вам также нужно понимать жадность.
-
(. *) будет жадно соответствовать 0 или более символам. Итак, в строке "XYZ"
она будет соответствовать всей строке с ее первым совпадением и поместить ее в позицию $1, давая вам следующее:
A XYZ B
Затем он будет продолжать пытаться сопоставить и сопоставить null
в конце строки, установив ваш $1 в null, давая вам следующее:
A B
Результат в строке, которую вы видите:
A XYZ BA B
-
Если вы хотите ограничить жадность и сопоставить каждый символ, вы должны использовать это выражение:
(. *?)
Это будет соответствовать каждому символу X, Y и Z отдельно, а также null
в конце и приведет к следующему:
A BXA BYA BZA B
Если вы не хотите, чтобы ваше регулярное выражение превышало границы вашей строки, ограничьте регулярное выражение идентификаторами ^
и $
.
Чтобы дать вам более точную информацию о том, что происходит, рассмотрите этот тест и результирующие сопоставимые группы.
[TestMethod()]
public void TestMethod3()
{
var myText = "XYZ";
var regex = new Regex("(.*)");
var m = regex.Match(myText);
var matchCount = 0;
while (m.Success)
{
Console.WriteLine("Match" + (++matchCount));
for (int i = 1; i <= 2; i++)
{
Group g = m.Groups[i];
Console.WriteLine("Group" + i + "='" + g + "'");
CaptureCollection cc = g.Captures;
for (int j = 0; j < cc.Count; j++)
{
Capture c = cc[j];
Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index);
}
}
m = m.NextMatch();
}
Вывод:
Match1
Group1='XYZ'
Capture0='XYZ', Position=0
Group2=''
Match2
Group1=''
Capture0='', Position=3
Group2=''
Обратите внимание, что есть две группы, которые соответствуют. Первой была вся группа XYZ, а вторая - пустая. Тем не менее, были две группы. Таким образом, $1 был заменен на XYZ в первом случае и null
для второго.
Также обратите внимание, что передняя косая черта /
- это еще один символ, который рассматривается в двигателе regex.net и не имеет особого значения. Парсер javascript обрабатывает /
иначе, потому что он должен потому, что он существует в рамках парсеров HTML, где особое внимание уделяется </
.
Наконец, чтобы получить то, что вы действительно хотите, рассмотрите этот тест:
[TestMethod]
public void TestMethod1()
{
var r = new Regex(@"^(.*)$");
var c = "XYZ";
var uc = r.Replace(c, "A $1 B");
Assert.AreEqual("A XYZ B", uc);
}