среда, 18 августа 2010 г.

Регулярные выражения в .NET Framework

Данная статья посвящена регулярным выражениям и работе с ними в .NET Framework. Исходный код к статье и ее саму в формате Microsoft Word вы можете найти здесь.

Оглавление

Регулярные выражения в .NET Framework. 1

Оглавление. 1

Что такое регулярные выражения и зачем они нужны.. 3

Простейшее регулярное выражение. 3

Учет регистра символов. 4

Поиск вхождения подстроки. 4

Поиск всех вхождений подстроки. 5

Поиск любого символа. 6

Поиск специальных символов. 6

Соответствие набору символов. 7

Использование диапазонов символов. 7

Поиск всего, кроме... 7

Использование метасимволов. 8

Поиск пробельных символов. 8

Диапазоны символов. 8

Повторение совпадений. 9

Соответствие с одним или несколькими символами. 9

Соответствие с нулем вхождений или с вхождением одного символа. 9

Поиск нуля или более символов. 10

Использование интервалов. 10

Жадные и ленивые кванторы.. 11

Соответствие позиций. 11

Обработка переносов строки. 12

Подвыражения. 12

Группы и захваты.. 12

Оператор ИЛИ.. 13

Использование ссылок. 14

Именованные ссылки. 14

Просмотр вперед и назад. 15

Негативный просмотр. 15

Встроенные условия. 16

Условия поиска в контексте. 16

Разделение строк. 16

Замена. 17

Использование ссылок при замене. 17

Ручное управление заменой. 18

Повышение производительности регулярных выражений. 18

Заключение. 19

Литература. 19


Что такое регулярные выражения и зачем они нужны

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

Иногда обработка строк является довольно простой. Издавна все языки программирования предоставляют возможность соединения нескольких строк в одну, замены в тексте одной подстроки на другую, поиска подстроки в тексте. Это простые примеры работы со строками. Но существуют и более сложные вопросы, на которые приходится отвечать при работе с текстом. Является ли введенная пользователем строка целым числом? Или вещественным числом? Сколько слов содержит некоторый текст. Ответить на эти вопросы более сложно. А ведь существуют и более сложные задачи обработки текста. Являтся ли введенная пользователем строка правильным IP-адресом? Как найти все e-mail-адреса в тексте? Как во всех гиперссылках HTML-страницы заменить адрес сервера на другой?

В этом месте на сцену и выходят регулярные выражения. Говоря кратко, они позволяют отвечать на следующие вопросы:

1. Соответствует ли строка некоторому шаблону.

2. Как найти в тексте участок, соответствующий некоторому шаблону.

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

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

Но, поскольку лучше один раз увидеть, чем сто раз услышать, давайте перейдем непосредственно к демонстрации.

Простейшее регулярное выражение

Для того, чтобы использовать регулярные выражения в ваших программах на .NET, вы должны подключить пространство имен System.Text.RegularExpressions из сборки System.dll:

using System.Text.RegularExpressions;

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

Итак, предположим, что у вас есть список файлов, и вы хотите выбрать из него только те, которые содержат слово «car» (проект RegularExpressions, класс Exercise1).

var filesList = new[]

{

"scar.txt",

"carry.txt",

"wave.txt",

"incarcerate.doc",

"repair.jpg",

"Cartoon.avi"

};

foreach(var fileName in filesList)

{

if( Regex.IsMatch(fileName, "car") )

Console.WriteLine(fileName);

}

Вся основная работа выполняется для нас статическим методом IsMatch класса Regex. Первый его параметр – строка, которую мы хотим проверить на соответствие шаблону. Второй параметр – собственно шаблон. Как видите, он не представляет собой ничего страшного, обычная строка. Собственно, ту же задачу мы могли бы решить с помощью метода Contains класса String. Однако, прошу вас подождать еще немного. Вся мощь регулярных выражений расскроется перед вами постепенно.

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

Учет регистра символов

Возможно вы заметили, что наш код не вернул нам строку Cartoon.avi. Дело в том, что по умолчанию регулярные выражения чувствительны в регистру символов. Однако при необходимости вы можете изменить это поведение. Для этого нужно использовать перегрузку метода , принимающую третий параметр (класс Exercise2):

Regex.IsMatch(fileName, "car", RegexOptions.IgnoreCase)

Этот третий параметр принимает флаговое перечисление RegexOptions, позволяющее настроить поведение регулярного выражения. В данном случае мы указываем, что регистр символов должен игнорироваться. Это перечисление содержит еще ряд полезных членов, с частью из которых мы столкнемся позднее.

Поиск вхождения подстроки

Как я уже говорил ранее, метод IsMatch проверяет, находится ли в переданной ему строке подстрока, соответствующая шаблону. Но что, если вам необходимо знать не только то, что такая подстрока есть, но и то, где именно она находится в исходной строке (класс Exercise3)? Для этого используется другой метод класса RegexMatch:

Match match = Regex.Match(fileName, "car");

if (match.Success)

Console.WriteLine("String '{0}' contains 'car' at position {1} with length {2}",

fileName,

match.Index,

match.Length);

Данный метод всегда возвращает объект класса Match. Хочу подчеркнуть еще раз, всегда возвращается не null. Чтобы узнать, действительно ли найдено совпадение, вы должны проверить свойство Success объекта Match. Если оно равно true, то совпадение действительно найдено. В этом случае в свойствах Index и Length содержатся позиция начала найденной подстроки и ее длина.

Поиск всех вхождений подстроки

Иногда вам может потребоваться найти не только первую подстроку, соответствующую шаблону, но все такие подстроки в тексте. Сделать это можно двумя способами. Первый заключается в использовании метода NextMatch класса Match (класс Exercise4). Предположим, что мы ищем вхождения глагола «is» в текст.

Match match = Regex.Match(text, " is ");

while (match.Success)

{

Console.WriteLine("String contains ' is ' at position {0}",

match.Index);

match = match.NextMatch();

}

Таким образом вы можете перебрать все вхождения искомых подстрок в текст. Использовать этот подход лучше в том случае, если вы хотите после нахождения определенной подстроки прервать дальнейший поиск. Например, вам требуется найти все строки, в которых глагол «is» содержится не менее 3-х раз. В этом случае, найдя третье вхождение, вы можете прекратить дальнейший поиск, не тратя время на нахождение остальных вхождений этого глагола в строку.

Но если вам точно необходимы абсолютно все подстроки, соответствующие шаблону, лучше использовать более короткий способ (класс Exercise5):

foreach (Match match in Regex.Matches(text, " is "))

{

Console.WriteLine("String contains ' is ' at position {0}",

match.Index);

}

Метод Matches класса Regex возвращает коллекцию объектов Match, содержащих информацию обо всех найденных подстроках, соответствующих шаблону. В данном случае не нужно беспокоиться о проверке свойства Success: если не найдено ни одного совпадения, то коллекция будет пустой.

На данный момент вы познакомились со всеми основными возможностями класса Regex по поиску в строках. Теперь пришло время перейти к более подробному рассмотрению возможностей самих регулярных выражений.

Поиск любого символа

Пусть мы хотим найти все строки в которых между символами «c» и «t» стоит один произвольный символ. В регулярных выражениях за один произвольный символ отвечает шаблон «.» (класс Exercise6):

var wordsList = new[]

{

"car",

"category",

"concatenation",

"recital",

"sentence",

"cotillion"

};

foreach (var fileName in wordsList)

{

if (Regex.IsMatch(fileName, "c.t"))

Console.WriteLine(fileName);

}

Данная программа вернет слова category, concatenation, recital, и cotillion, в которых между «c» и «t» находится один произвольный символ.

Поиск специальных символов

Но что, если мы хотим искать именно точку в наших словах? Например, мы хотим найти все файлы с расширением «.txt». Точка является специальным символом (метасимволом) в регулярных выражениях. Так же к специальным символам относятся [, ], (, ), *, ? и некоторые другие. Каким же образом мы можем искать в строках именно эти символы? Во всех случаях решение будет одинаково. В шаблоне перед этими символами нужно поставить косую черту «\». Тогда они перестанут иметь специальное значение в регулярном выражении (класс Exercise7).

Regex.IsMatch(fileName, @"\.txt")

Обратите внимание, что мы должны поставить символ «@» перед строкой в C#, чтобы компилятор не интерпретировал ее, заменяя некоторые последовательности на специальные символы (например, «\n» на символ перевода строки), а использовал как есть.

Соответствие набору символов

В предыдущих примерах мы искали строки, в которых между «c» и «t» стоял произвольный символ. Но что, если вы хотите ограничить набор возможных символов, которые могут стоять между «c» и «t»? Например, вам нужны только те слова, в которых на этом месте стоят «a» или «o». Как этого добиться? С помощью квадратных скобок (класс Exercise8):

Regex.IsMatch(fileName, "c[ao]t")

Квадратные скобки соответствуют одному символу искомой подстроки. Внутри квадратных скобок находятся те символы, которые могут стоять на этом месте. В нашем случае там стоят символы «a» и «o».

Использование диапазонов символов

Вы можете указать внутри скобок сколько вам угодно символов, определяя любой набор. Однако, если вам нужно, чтобы на данном месте стоял любой символ английского алфавита, то в квадратных скобках вам придется указать 23 символа. И это при условии, что вы не учитываете регистр. В противном случае потребуется добавить еще столько же. К счастью этого можно избежать. Регулярные выражения предлагают вам механизм диапазонов символов (класс Exercise9). Вот наиболее известные примеры определения диапазонов:

[0-9]

Любая цифра

[a-z]

Любой строчный символ английского алфавита

[a-zA-Z]

Любой строчный или заглавный символ английского алфавита

Учтите, что порядок границ в диапазоне очень важен. Написать «[9-0]» было бы ошибкой.

Поиск всего, кроме...

Иногда проще указать не то, какой символ должен стоять в данной позиции строки, а то, каких символов тут не должно быть. Регулярные выражения позволяют вам сделать и это (класс Exercise10). Для этого после открывающей квадратной скобки вы должны указать символ «^». В таком случае в искомой строке в данной позиции не будет находиться ни один из символов, указанных далее. Вы так же можете воспользоваться диапазонами в этой форме записи условия отбора. Вот так, например, вы можете найти все строки, в которых после «sn» нет цифры:

Regex.IsMatch(fileName, "sn[^0-9]")

Еще раз хочу отметить, что для того, чтобы искать в строках символы «[» и «]» вы должны поставить перед ними косую черту «\»:

"\["

Найти строки, содержащие открывающую квадратную скобку.

"\[i\]"

Найти строки, в которых между квадратными скобками стоит символ «i».

"\[[0-9]\]"

Найти строки, в которых между квадратными скобками стоит число.

Таким же приемом вы можете пользоваться и для символов «^» и «-» внутри квадратных скобок, чтобы отменить их специальное значение: "[0-9\^\-]" – на данном месте может стоять любая цифра или символы «^» или «-».

Использование метасимволов

Как уже было сказано ранее, метасимволы – последовательности символов, имеющие в регулярных выражениях специальные значения. К ним относятся точка, различные скобки и т.д. Здесь мы познакомимся и с другими метасимволами.

Поиск пробельных символов

В регулярных выражениях следующие метасимволы применяются для поиска пробельных символов, т.е. тех символов, которые либо не имеют видимого отображения на экране, либо отображаются в виде пустого пространства, как пробел (класс Exercise11):

\f

Перевод страницы

\n

Перевод строки

\r

Перевод каретки

\t

Табуляция

\v

Вертикальная табуляция

Так следующий пример позволяет найти все строки, в которых имеется перевод строки:

Regex.IsMatch(word, @"\n")

Диапазоны символов

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

\d

Любой числовой символ. То же, что и [0-9]

\D

Любой нечисловой символ. То же, что и [^0-9]

\w

Любой алфавитно-числовой символ или подчеркивание. То же, что и [0-9a-zA-Z_]

\W

Любой символ, не являющийся алфавитно-числовым символом или подчеркиванием. То же, что и [^0-9a-zA-Z_]

\s

Любой пробельный символ. То же, что и [\f\n\r\t\v ]

\S

Любой непробельный символ. То же, что и [^\f\n\r\t\v ]

Вот, например, как выглядит регулярное выражение, выбирающее строки, имеющие подряд 6 цифр (класс Exercise12):

Regex.IsMatch(word, @"\d\d\d\d\d\d")

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

Повторение совпадений

В прошлом примере мы искали строки, имеющие подряд 6 цифр. Для этого нам пришлось написать «\d» шесть раз. Это не очень удобно. Что, если мы ищем строки, имеющие 20 цифр подряд? Писать «\d» 20 раз? К счастью в регулярных выражениях предусмотрена возможность указать, что некоторый шаблон должен повторяться в искомой подстроке. Давайте рассмотрим, как это делается.

Соответствие с одним или несколькими символами

Пусть мы хотим найти все подстроки, являющиеся целыми числами, независимо от того, сколько цифр содержит такое число (класс Exercise13). Для простоты будем считать, что число может начинаться с нулей. Проблема в том, что число цифр в числе заранее не известно. Оно может быть от одного до практически бесконечности. Для указания такого числа повторений в регулярных выражениях используется метасимвол «+»:

Match match = Regex.Match(word, @"\d+");

if (match.Success)

Console.WriteLine(match.Value);

Метасимвол «+» говорит о том, что стоящий перед ним символ может повторяться в искомой подстроке один или более раз.

Соответствие с нулем вхождений или с вхождением одного символа

Давайте усложним задачу. Пусть теперь мы будем искать в строках не только положительные, но и отрицательные числа. Перед отрицательными числами конечно стоит знак «–», перед положительными его нет. Как указать в регулярном выражении, что символ может присутствовать, а может и отсутствовать? С помощью метасимвола «?» (класс Exercise14):

Match match = Regex.Match(word, @"-?\d+");

Метасимвол «?» говорит о том, что стоящий перед ним символ может быть в искомой подстроке, а может и не быть.

Поиск нуля или более символов

Предположим, у вас есть некоторый текст, содержащий адреса электронной почты, и вы хотите эти адреса из него извлечь. Для простоты будем считать, что нас интересуют только адреса некоторого провайдера example.com (класс Exercise15). Их можно извлечь с помощью следующего выражения:

Regex.Matches(text, @"[\w.]+@example\.com")

Но что, если в текст прокралась опечатка и там содержится следующий адрес ".Fedor@example.com"? Несомненно, адрес электронной почты не может начинаться с точки. Он должен начинаться с алфавитно-числового символа, за которым идут ноль или более алфавитно-числовых символов или точек. Как нам указать это «ноль или более»? С помощью метасимвола «*»:

Regex.Matches(text, @"\w[\w.]*@example\.com")

Метасимвол «*» означает, что в искомой подстроке может встречаться ноль или более символов, стоящих перед ним.

Использование интервалов

Но вернемся к нашему примеру с 6-ю цифрами. Как нам указать, что в искомой подстроке должно быть именно 6 цифр, не более, но и не менее? Для этого в регулярных выражениях присутствует механизм интервалов (класс Exercise16). Вот как задается поиск ровно шести цифр:

Regex.IsMatch(word, @"\d{6}")

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

Интервал

Число повторений

{n}

Ровно n повторений ( {6} ).

{n,}

n или более повторений ( {3,} )

{,n}

Не более чем n повторений ( {,5} )

{n,m}

От n до m повторений ( {2,4} )

Отсюда следует, что метасимвол «?» соответствует интервалу {0,1}, «+» – {1,}, а «*» – {0,}

Жадные и ленивые кванторы

Пусть у нас есть некоторый HTML-документ, и мы хотим получить из него строки, выделенные жирным шрифтом. Мы знает, что такие строки заключены в тэги <b>. Как нам это сделать (класс Exercise17)? Казалось бы, следующий код решает эту задачу:

var text = @"It is very important to be carefull writing regular expressions";

foreach (Match match in Regex.Matches(text, @".*"))

{

Console.WriteLine(match.Value);

}

Но не все так просто. Этот код возвращает следующий результат:

very important to be carefull writing regular expressions

Действительно, открывающие и закрывающие тэги так же подходят под шаблон «.*». По умолчанию регулярные выражения используют «жадный» алгоритм, ища строку наибольшей длины, соответствующую шаблону. Как же быть в этом случае? Выходом является использование «ленивого» квантора:

Regex.Matches(text, @".*?")

Для этого после метасимволов «*», «+» или определения интервала {n,}, которые не ограничивают длину искомой строки, ставится знак «?» в этом случае регулярное выражение будет искать наименьшую строку, удовлетворяющую шаблону.

Соответствие позиций

Давайте рассмотрим еще раз уже знакомое вам регулярное выражение: "-?\d+". Способно ли оно сказать нам, является ли некоторая строка представлением целого числа или нет? Надеюсь, что вы уже можете с уверенностью сказать, что не может. Регулярные выражения, использовавшиеся нами до сих пор, искали во входной строке подстроку, удовлетворяющую заданному шаблону. Поэтому метод IsMatch вернет true для таких строк, как "123ABD", "AD876SD" и "G-245367N". В данном случае является важным не только то что в строке содержится искомая подстрока, но и то, где она содержится. Регулярные выражения определяют следующие позиции, которые вы можете использовать в своих шаблонах:

Позиция

Описание

\b

Граница слова. На самом деле это граница между алфавитно-цифровым символом (\w) и символом, не являющимся таковым (\W).

\B

Не граница слова (все, что не соответствует \b).

^

Начало строки.

$

Конец строки.

Таким образом, регулярное выражение, способное сказать, является ли вся строка представлением целого числа, выглядит следующим образом (класс Exercise18):

Regex.IsMatch(word, @"^-?\d+$")

Здесь метасимволы «^» и «$» явно указывают, где именно должны находиться начало и конец строки.

Обработка переносов строки

Предположим, что у вас есть текст, из которого вы хотите извлечь первое слово каждого абзаца (класс Exercise19). Абзац начинается с новой строки, поэтому на первый взгляд подходящим регулярным выражением для нашей задачи было бы "^\w+\b". Но не все так просто. Метасимвол «^» по умолчанию относится именно к первому символу текста, не рассматривая переносы строки «\n» как новую строку. Но это поведение можно изменить с помощью перечисления RegexOptions:

Regex.Matches(text, @"^\w+\b", RegexOptions.Multiline)

Элемент перечисления RegexOptions.Multiline изменяет поведение метасимволов «^» и «$» так, что они соответствуют не началу и концу всей строки, а началу и концу каждой линии в строке.

Подвыражения

Давайте рассмотрим следующую задачу. Пусть у вас есть текст, и вы знаете, что в нем в некоторых местах есть ошибка, заключающаяся в том, что определенный артикль «the» написан дважды. Вы хотите обнаружить все такие места в тексте. Мы уже знаем, как обнаружить два и более вхождения, но только для одного символа. Как же сделать это для целой строки. На помощь приходят подвыражения (класс Exercise20). Подвыражение представляет собой обычное регулярное выражение, заключенное в круглые скобки и использованное как часть большего регулярного выражения. Вот, например, регулярное выражение, решающее нашу задачу:

Regex.Matches(text, @" (the ){2,}")

В данном случае подвыражением является (the ). Подвыражение всегда рассматривается как единое целое, и метасимволы повторения, стоящие после него, относятся к нему целиком.

Группы и захваты

Очень хорошо, мы нашли, где расположено двойное вхождение определенного артикля. Теперь нам, несомненно, потребуется удалить второе его вхождение. Но для этого требуется узнать, где оно расположено. Конечно, в нашем случае это сделать легко, анализируя свойство Index объекта Match. Но в общем случае все будет не так просто. Где расположена строка, соответствующая определенному подвыражению? Определить это помогают группы (groups) (класс Exercise21). Группы доступны через свойство Groups объекта Match. Объект Match содержит столько групп, сколько подвыражений есть в вашем регулярном выражении, плюс еще одна группа, соответствующая всему регулярному выражению. Эта общая группа всегда имеет индекс 0. Как же узнать индекс нужной вам группы? Ведь подвыражений в регулярном выражении может быть много, они могут быть вложены друг в друга. Сделать это очень просто. Считайте открывающиеся круглые скобки в вашем регулярном выражении начиная с 1. Когда вы дойдете до нужного вам подвыражения, номер скобки и будет равен индексу искомой вами группы.

for (int i = 0; i < style="">Groups.Count; i++)

{

Group group = match.Groups[i];

Console.WriteLine("Group {0} contains '{1}' and is found at position {2}", i, group.Value, group.Index);

}

Итак, группы соответствуют подвыражениям в регулярном выражении. Однако, в нашем случае после подвыражения стоит модификатор повторения {2,}. Это означает, что подвыражение будет присутствовать в найденной подстроке не менее 2-х раз. Нам и нужно найти все вхождения подвыражения кроме первого. Как же это сделать? Ведь объекты Group содержат информацию только о последнем вхождении повторения. На помощь нам приходят захваты (captures) (класс Exercise22). Захват представляется классом Capture и соответствует одному повторению подвыражения. Все захваты группы доступны через свойство Captures объекта Group.

for (int j = 0; j < style="">Captures.Count; j++)

{

Capture capture = group.Captures[j];

Console.WriteLine("Capture {0} contains '{1}' and is found at position {2}", j, capture.Value, capture.Index);

}

Как и объекты Match и Group объект Capture содержит свойства Index, Length и Value, позволяющие полностью определить место вхождения найденной подстроки и саму подстроку. Таким образом, эти три объекта позволяют полностью решить задачу поиска чего бы то ни было в строке, предоставляя в наше распоряжение всю мощь регулярных выражений.

Оператор ИЛИ

Следующая задача, которую мы рассмотрим, будет связана с поиском дат. В некотором тексте содержатся годы рождения людей, и я хочу их получить. Я знаю, что интересующие меня люди родились либо в 20-м, либо в 21 веке, поэтому искомые мной года представляют собой 4-хзначные числа, начинающиеся с 19 или 20. Как мне указать это «или» в моем регулярном выражении (класс Exercise23)? Задача решается использованием оператора «ИЛИ» представляемого в регулярном выражении символом «|»:

Regex.Matches(text, @"\b(19|20)\d{2}\b")

Обратите внимание, что числа 19 и 20, между которыми и должен производиться выбор, заключены вместе с оператором «ИЛИ» внутрь подвыражения. Что будет, если мы этого не сделаем? Выражение 19|20\d{2} будет читаться так: «или 19, или четырехзначное число, начинающееся на 20». Таким образом, использование оператора «ИЛИ» практически всегда должно совмещаться с использованием подвыражений.

Ярким примером использования подвыражений и оператора «ИЛИ» является выражение, позволяющее выделить IP-адрес. Вот оно:

(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))

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

Использование ссылок

Помните нашу задачу, где мы искали в тексте повторения определенного артикля «the»? Давайте усложним ее. Теперь мы хотим найти любые повторяющиеся подряд слова. Если бы речь шла о каком-то конкретном слове, то задача решалась бы точно так же, как мы это сделали для «the». Но как это сделать для произвольного слова? Выделить все слова текста и проводить поиск для каждого? Очень долго. К счастью, есть способ лучше. Он называется ссылками назад (класс Exercise24). Регулярное выражение позволяет сослаться из себя на любое подвыражение, уже введенное к текущему месту. Давайте рассмотрим шаблон, решающий нашу задачу:

Regex.Matches(text, @"\b(\w+) \1\b")

Он содержит подвыражение (\w+), которое соответствует первому слову в паре. Нам нужно, чтобы после пробела стояло такое же слово. Для этого используется ссылка \1, номер в которой соответствует номеру подвыражения в регулярном выражении (помните, как мы считали открывающиеся скобки?). Ссылка похожа на переменную. Каждое подвыражение вводит некоторое значение, а ссылки просто ссылаются на него.

Хочу еще раз отметить, что данная ссылка является ссылкой назад, т.е. подвыражение должно быть объявлено до ссылки на него. Поэтому шаблон "\b\1 (\w+)\b" будет неверным.

Именованные ссылки

У указанного использования ссылок назад есть определенные недостатки. Хорошо, если ваше регулярное выражение содержит только одно подвыражение. А что, если их множество, и они обладают сложной вложенностью? Считая скобки легко ошибиться. А если вы еще и изменяете регулярное выражение, добавляя или удаляя подвыражения, то использование ссылок назад может стать настоящим мучением. К счастью реализация регулярных выражений в .NET позволяет вам использовать именованные ссылки (класс Exercise25):

Regex.Matches(text, @"\b(?\w+) \k\b")

Чтобы дать имя какому-либо подвыражению, после его открывающей скобки используйте ?<имя>. Теперь, чтобы сослаться на это подвыражение вы можете использовать конструкцию \k<имя>.

Просмотр вперед и назад

Помните, рассматривая жадные и ленивые кванторы, мы искали в HTML-документе текст выделенный жирным. Такой текст заключен между тэгами <b>. Наша программа прекрасно справлялась с поставленной задачей, однако присутствует маленькое неудобство. Дело в том, что в строках, которые возвращало нам наше регулярное выражение, присутствовали и сами тэги <b>. Нам же нужно, чтобы возвращен был только текст между ними. Конечно, этого легко добиться, правильно введя подвыражения и используя механизм групп. Но стоит ли вообще возвращать то, что нам не нужно? В общем проблема формулируется так: нужно найти некоторый текст, стоящий в определенном окружении, но само это окружение нам не нужно. Решить эту проблему помогает просмотр вперед и назад (класс Exercise26):

Regex.Matches(text, @"(?<=).*?(?=)")

Просмотр вперед означает, что нечно должно стоять после искомого нами текста. В нашем случае это закрывающий тэг b>. То, что должно стоять после искомого текста, должно быть оформлено как одно подвыражение, после открывающей скобки которого стоит ?=: (?=b>). Просмотр назад предполагает наличие чего-то перед искомым текстом. В регулярное выражение оно вводится аналогично, но после открывающей скобки должен стоять ?<=: (?<=<b>).

Негативный просмотр

Иногда вместо того, чтобы перед/после искомого текста что-либо стояло, нам нужно, чтобы перед/после него что-то не стояло. Например, нам нужно из текста выделить все числа, не являющиеся ценами в долларах, т.е. числа, перед которыми не стоит знак «$» (класс Exercise27). Для этого используется негативный просмотр:

Regex.Matches(text, @"\b(?\d+\b")

Синтаксически он очень похож на обычный просмотр, только знак «=» заменяется на «!». Вместо ?= пишется ?!, а вместо ?<= ?.

Встроенные условия

Изложенные выше возможности регулярных выражений весьма впечатляют. Но иногда не достаточно и их. Рассмотрим следующую задачу. В Северной Америке допустимыми считаются два формата телефонных номеров: (123)456-7890 и 123-456-7890. Как нам написать регулярное выражение, которое позволяет проверить правильность введенного телефонного номера (класс Exercise28)? Приходящее на ум регулярное выражение ^\(?\d{3}[)\-]\d{3}-\d{4}$ к сожалению пропускает номера типа 123)456-7890 и (123-456-7890. Нам бы пригодилось условие типа «если есть открывающая скобка, то искать закрывающую скобку, в противном случае искать дефис». И такое условие можно записать. Выглядит оно так: (?(ссылка назад)условие истинно|условие ложно). Ссылка назад – номер введенного ранее подвыражения. Если данное подвыражение действительно было найдено в строке, то ищется подвыражение условие истинно. В противном случае ищется подвыражение условие ложно. Давайте посмотрим, как это выглядит на практике:

Regex.IsMatch(number, @"^(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}$")

Условие используется в подвыражении (?(1)\)|-). Здесь написано: «если подвыражение 1 найдено, искать «(», в противном случае искать дефис». Подвыражение 1 в нашем случае соответствует открывающейся круглой скобке: (\().

Условия поиска в контексте

В качестве условия может выступать не только некоторое подвыражение, которое найдено или нет в тексте, но и некоторый контекст (т.е. окружение). Рассмотрим следующий пример. В США почтовые индексы могут существовать в 2-х формах: 12345 или 12345-6789. Нам нужно узнать, ввел ли пользователь правильный почтовый индекс или нет (класс Exercise29). Первое приходящее на ум регулярное выражение:

Regex.IsMatch(code, @"\d{5}(-\d{4})?")

к сожалению пропускает строки вида 33333-. То, что нам нужно, это «если после пяти цифр стоит дефис, то искать дефис и еще четыре цифры». Следующее регулярное выражение показывает, как это делается:

Regex.IsMatch(code, @"\d{5}(?(?=-)-\d{4})")

Используется условие, но в круглых скобках указывается не номер подвыражения, а выражение просмотра вперед: ?(?=-). Оно проверяет, находится ли в следующей позиции дефис. Если это так, то ищется дефис и еще 4 цифры за ним: -\d{4}. В качестве условия можно использовать не только просмотр вперед, но и просмотр назад, и негативный просмотр.

Разделение строк

Надеюсь, вы уже понимаете, насколько мощным механизмом являются регулярные выражения. Однако, на этом их возможности не ограничиваются. Давайте рассмотрим возможности регулярных выражений по изменению текста. И начнем мы с разбиения строк на части (класс Exercise30). Как известно, Windows для разделения строк в текстовых файлах использует комбинацию «\n\r», в то время, как Unix использует только «\n». Что, если нам нужно разбить текст на строки вне зависимости от того, в какой системе он был создан? Для этого можно использовать метод Split класса Regex:

Regex.Split(text, @"\n\r?")

Он возвращает из текста набор строк, разделенных подстроками, соответствующими заданному шаблону.

Но здесь нужно помнить одну тонкость. Что если мы заменим шаблон на "\n(\r)?"? С точки зрения находимых подстрок ничего не изменилось. Но функция Split работает уже иначе. Если ваше регулярное выражение содержит подвыражения в круглых скобках, то эти подвыражения будут включены в результирующий набор строк, возвращенный функцией Split.

Замена

Регулярные выражения позволяют не только производить поиск подстрок, но и их замену. Помните, мы искали в тексте повторение определенного артикля? Давайте посмотрим, как можно заменить повторяющиеся «the» на один (класс Exercise31). Для замены используется функция Replace класса Regex:

var text = Regex.Replace(text, @" (the ){2,}", " the ");

Она заменяет все подстроки, соответствующие указанному шаблону, на новую строку.

Использование ссылок при замене

Однако этим возможности замены в регулярных выражениях не исчерпываются. Давайте вспомним усложненную задачу, когда нам требовалось найти любые повторяющиеся слова. Теперь мы попытаемся исключить такие повторения с помощью механизмов замены (класс Exercise32):

var text = Regex.Replace(text, @"\b(\w+) \1\b", "$1");

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

Конечно же и здесь мы можем использовать именованные ссылки, чтобы не считать номера подвыражений (класс Exercise33):

var text = Regex.Replace(text, @"\b(?\w+) \1\b", "${word}");

Для этого в строке замены используйте выражение ${имя}.

Ручное управление заменой

Для самых сложных случаев вы можете прибегнуть к ручному управлению заменой. Она реализуется перегруженным вариантом метода Replace, которому передается объект MatchEvaluator. Этот класс является делегатом с единственным параметром типа Match, возвращающим строку, которая и будет использована для замены (класс Exercise34). Вот, например, как решается та же задача с помощью объекта MatchEvaluator:

var text = Regex.Replace(text, @"\b(\w+) \1\b", new MatchEvaluator(Replacer));

private static string Replacer(Match match)

{

return match.Groups[1].Value;

}

Надеюсь, я сумел показать, что регулярные выражения являются мощнейшим инструментом для задач поиска и замены в строках. Так же вы уже знаете, как использовать регулярные выражения в программах .NET. Давайте перейдем к рассмотрению сопутствующих вопросов, которые, тем не менее, зачастую так же важны.

Повышение производительности регулярных выражений

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

В предыдущих примерах мы использовали статические методы класса Regex. Но, если посмотреть код этих методов, то выяснится, что внутри каждый из них создает экземпляр класса Regex и вызывает соответствующий экземплярный метод. Конечно, при создании экземпляра класса происходит преобразование шаблона в конечный автомат. Т.е. такое преобразование происходит при каждом вызове любого статического метода. Это не очень эффективный способ использования регулярных выражений. Поэтому, если есть такая возможность, рассмотрите создание одного экземпляра класса Regex и его использование по необходимости. Например, вы можете создать статическое поле для вашего класса, использующего регулярные выражения:

private static readonly Regex regex = new Regex(@"\b(\w+) \1\b");

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

Однако есть и другой метод повышения производительности, позволяющий избежать повторного создания конечного автомата даже при использовании статических методов. Это использование члена перечисления RegexOptions.Compiled:

Regex.IsMatch(code, @"\d{5}", RegexOptions.Compiled)

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

Кроме этих способов повышения производительности существует еще один, о котором не следует забывать. Он связан с самими шаблонами, которые вы пишете. Чем «уже» шаблон, чем меньшее количество строк может ему соответствовать, тем быстрее будет происходить поиск. Об этом всегда следует помнить, создавая ваши регулярные выражения.

Заключение

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

Литература

1. Форта Б. Освой самостоятельно регулярные выражения. 10 минут на урок. : Пер. с англ. – М. : Издательский дом «Вильямс», 2005.