Регулярно выражаемся

Просмотров: 22796

Введение

Регулярные выражения являются мощным, гибким и эффективным средством, предназначенным для анализа и обработки строк и многострочных текстов. Имеются языки программирования (например, Perl), в которых возможность использования регулярных выражения встроена в сам язык; во многих других языках имеются библиотеки для поддержки регулярных выражений. Подобная библиотека реализована и для языков платформы .NET. В настоящей статье мы познакомимся как с основными возможностями языка регулярных выражений, так и с реализацией этих возможностей для PascalABC.NET.
В первой части статьи приводится несколько примеров, позволяющих познакомиться с основными возможностями регулярных выражений и связанными с ними классами стандартной библиотеки .NET. Во второй части дается описание классов .NET, связанных с регулярными выражениями, а в третьей части – краткое описание основных элементов языка регулярных выражений.

Часть 1. Примеры применения регулярных выражений

Разбиение строки

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

Будем считать, что исходная строка содержится в переменной s типа string.

В классе string имеется специальный метод Split, предназначенный для разбиения строки, однако при его использовании для решения поставленной задачи вы получим сложный для восприятия код:

foreach s0: string in s.Split(new char[1](' '), StringSplitOptions.RemoveEmptyEntries) do
  Writeln(s0);

Для того чтобы данный фрагмент откомпилировался, необходимо поместить в начало программы директиву uses System.

Заметим, что если использовать более простой вариант метода Split с единственным символьным параметром - пробелом (s.Split(' ')), то в результирующий массив будут записаны не только "настоящие" слова, но и пустые строки (они считаются "словами", расположенными между соседними пробелами).

Теперь решим эту же задачу с использованием регулярных выражений (в начале программы надо указать директиву uses System.Text.RegularExpressions):

foreach s0: string in Regex.Split(s, ' +') do
  Writeln(s0); 

В данном случае мы вызвали метод Split класса Regex – основного класса библиотеки .NET, связанной с регулярными выражениями. В этом методе вначале указывается обрабатываемая строка, а затем – регулярное выражение, которое определяет вид разделителя между словами. Метод возвращает массив строк, содержащий слова, на которые была разделена исходная строка. В приведенном фрагменте мы использовали регулярное выражение, состоящее из двух символов: пробела и знака "+". На языке регулярных выражений знак "+" означает, что предшествующий символ может повторяться один или более раз. Именно благодаря знаку "+" (одному из так называемых квантификаторов) несколько подряд расположенных пробелов считаются одним разделителем.

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

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

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

foreach m: Match in Regex.Matches(s, '\w+') do
  Write(m.Index); 

Например, при обработке строки

Каучук   барабан  алгебра  диафрагма

на экран будет выведен следующий текст (символы строки индексируются от 0):

0 9 18 27

Обсудим приведенный фрагмент программы. В нем был использован метод Matches, который позволяет найти все фрагменты исходной строки, удовлетворяющие указанному регулярному выражению. В данном случае в регулярном выражении, кроме уже известного нам квантификатора "+", была использована специальная директива \w, обозначающая любой словообразующий (т. е. алфавитно-цифровой) символ. Найденные вхождения возвращаются в виде коллекции типа MatchCollection, элементы которой можно перебрать с помощью цикла foreach. Каждый элемент коллекции имеет тип Match. Набор свойств этого типа позволяет получить не только строковое значение найденного вхождения (свойство Value), но и дополнительную информацию о нем, в частности, индекс, начиная с которого это вхождение содержится в исходной строке (свойство Index), а также длину найденного вхождения (свойство Length).

В рассмотренных примерах мы использовали квантификатор "+", означающий одно или более вхождений ранее указанного символа (или одного из элементов указанного множества символов). Все квантификаторы по умолчанию являются "жадными", т. е. они "пытаются захватить" как можно больше символов из исходной строки. Квантификатор перестанет захватывать символы только в том случае, если захват очередного символа приведет к тому, что результат не будет удовлетворять регулярному выражению. Так, в последнем примере квантификатор захватит все символы первого слова и попытается захватить следующий за ним пробел, однако при этом результат не будет удовлетворять регулярному выражению, поэтому квантификатор "вернет обратно" захваченный пробел и "удовлетворится" ранее захваченным первым словом.

В классе Regex имеются еще два метода, предназначенных для поиска в строке и имеющих те же параметры, что и метод Matches: это IsMatch и Match. Метод IsMatch проверяет, имеются ли в исходной строке требуемые вхождения, и в зависимости от результата поиска возвращает значение True или False. Метод Match возвращает первое найденное вхождение в виде объекта типа Match (если ни одно вхождение на найдено, то свойство Success возвращенного объекта будет равно False). Заметим, что, имея объект типа Match, мы можем продолжить поиск в текущей строке, используя для этого его метод NextMatch.

Замена в строке

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

Текст   Клавиатура  Поле    Монитор

должна быть преобразована в следующую:

<Текст>   Клавиатура  <Поле>    Монитор.

Метод класса Regex, предназначенный для замены, имеет имя Replace. Он имеет больше параметров, чем рассмотренные ранее методы, так как, помимо исходной строки и регулярного выражения, определяющего, что надо искать, требуется указать, как именно изменять найденные вхождения. В простых случаях достаточно указать строку - выражение замены, в более сложных ситуациях можно определить функцию, обрабатывающую каждое найденное вхождение и возвращающую строку, которая должна его заменить. В выражениях замены можно указывать, помимо обычных символов, специальные управляющие последовательности (подстановки), соответствующие исходной строке или ее фрагментам. Одной из таких подстановок является $0, означающая текущее найденное вхождение. Метод Replace возвращает строку, в которой выполнены все требуемые замены.

При решении задачи нам надо использовать особый квантификатор, который явно определяет количество повторений требуемых символов. Если допускается ровно N повторений, то подобный квантификатор имеет вид {N}; если число повторений может меняться от M до N, то надо использовать вариант {M,N}.
Используя все уже известные нам сведения о регулярных выражениях, мы можем попытаться решить задачу следующим образом:

Writeln(Regex.Replace(s, '\w{1,5}', '<$0>'));

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

<Текст>   <Клави><атура>  <Поле>    <Монит><ор>

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

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

В нашем случае проще всего воспользоваться директивой \b, обозначающей границу слова, т. е. позицию в строке, разделяющую начало/конец слова и предшествующий/следующий пробел (или другой символ-разделитель) или начало/конец строки. Для того чтобы регулярное выражение выделяло из строки полные слова, достаточно добавить директиву \b в его начало и конец:

Writeln(Regex.Replace(s, '\b\w{1,5}\b', '<$0>'));

С использованием данного варианта регулярного выражения задача будет решена правильно.
Иногда требуется выполнить замену только для некоторого количества начальных вхождений нужных подстрок. Например, в нашей задаче можно дополнительно потребовать, чтобы в угловые скобки было заключено только первое из слов, имеющих длину не более 5 символов. Такая возможность отсутствует в классовом методе Replace, однако имеется в одноименном экземплярном методе. Таким образом, для решения задачи надо создать экземпляр класса Regex и вызвать для него метод Replace:

var r := new Regex('\b\w{1,5}\b');
Writeln(r.Replace(s, '<$0>', 1));

В результате будет выведена строка

<Текст>   Клавиатура  Поле    Монитор.

Для объекта r типа Regex можно вызывать все ранее рассмотренные методы класса Regex: Split, IsMatch, Match, Matches и Replace. При этом само регулярное выражение не указывается, так как оно определяется в конструкторе объекта, однако можно указать дополнительные параметры, отсутствующие в одноименных классовых методах (например, можно дополнительно указать индекс символа исходной строки, начиная с которого нужно выполнять требуемое действие).

Довольно часто при замене приходится выполнять преобразования, которые нельзя описать в виде выражения замены, даже если использовать в нем специальные подстановки. Предположим, что нам требуется добавить к каждому слову исходной строки информацию о его длине, заключив ее в круглые скобки (например, строку Текст надо заменить на Текст(5). Для выполнения подобных "сложных" замен предусмотрен вариант метода Replace, в котором в качестве параметра, определяющего способ замены, указывается не строка, а делегат (процедурная переменная). Этот делегат должен указывать на функцию, которая определяет, каким образом надо преобразовывать каждое найденное вхождение в исходной строке. Единственным параметром данной функции должно быть текущее найденное вхождение (типа Match), а возвращаемым значением является строка, на которую надо заменить найденное вхождение.

Воспользуемся данным вариантом метода Replace, предварительно описав требуемую функцию:

function AddLength(m: Match): string;
begin
result := m.Value + '(' + IntToStr(m.Length) + ')';
end;
...
Writeln(Regex.Replace(s, '\w+', AddLength));

При обработке приведенной ранее тестовой строки будет выведена строка

Текст(5)   Клавиатура(10)  Поле(4)    Монитор(7)

Заметим, что без предварительного описания функции AddLength можно обойтись, если воспользоваться соответствующим лямбда-выражением, указав его в качестве третьего параметра метода Replace:

Writeln(Regex.Replace(s, '\w+',(m: Match) -> m.Value + '(' + IntToStr(m.Length) + ')'));

Использование групп и опций поиска

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

Еще одним важным элементом языка регулярных выражений являются группы. Группу образует часть регулярного выражения, заключенная в круглые скобки. Приведем простейший пример использования группы: (ab)+. Данное выражение означает строку, в которой текст ab повторяется один или более раз (например, ababab). Понятно, что без группирования символов смысл выражения будет другим: ab+ означает строку, начинающуюся с символа a, за которым следует один или более символов b (например, abbb).

Однако простой группировкой роль групп в языке регулярных выражений не ограничивается. Каждая группа в регулярном выражении получает свой порядковый номер и может быть использована в последующих фрагментах регулярного выражения с помощью специальной директивы \N, где N - порядковый номер группы (можно также присваивать некоторым группам имена-идентификаторы - см. третью часть статьи). Порядок нумерации групп определяется по расположению их открывающих скобок. Нумерация групп начинается от 1 (все найденное вхождение также считается группой; этой особой группе присваивается номер 0).

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

foreach a: match in Regex.Matches(s, '\b(\w)\w*\1\b') do
  write(a.Value, ' ');

Если строка s содержит текст Каучук   барабан  алгебра  диафрагма, то на экран будет выведено слово алгебра.
Обсудим смысл элементов регулярного выражения. В начале и конце выражения указываются директивы нулевой длины \b, обеспечивающие поиск полных слов (если не указывать эти директивы, то при обработке этой же строки мы получим следующий результат: учу бараб алгебра афрагма). Далее указывается один словообразующий символ, заключенный в скобки и тем самым превращенный в первую группу нашего регулярного выражения. После этого указывается словообразующий символ, снабженный квантификатором "*"; этот квантификатор означает, что предшествующий элемент регулярного выражения может повторяться 0 или более раз. Наконец, указывается специальная директива \1, означающая, что на данной позиции должен находится текст, совпадающий с текстом ранее найденной первой группы.
Приведенное решение имеет два недочета. Первый недочет очевиден: при подобном поиске естественно было бы включить в список найденных слов и слово "Каучук", несмотря на то что оно начинается с заглавной буквы. Для исправления этого недочета достаточно в методе Matches указать в качестве последнего параметра соответствующую опцию поиска:

foreach a: match in Regex.Matches(s, '\b(\w)\w*\1\b', RegexOptions.IgnoreCase) do
  Write(a.Value, ' ');

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

foreach a: match in Regex.Matches(s, '(?i)\b(\w)\w*\1\b') do
  Write(a.Value, ' ');

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

(?i)\b(\w)\w*\1\b|\b\w\b

Нетрудно заметить, что в начале и конце любого из двух вариантов содержатся директивы \b. Чтобы не указывать эту пару директив дважды, ее можно вынести за знак операции |:

(?i)\b((\w)\w*\2|\w)\b

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

Следующий пример демонстрирует дополнительные возможности, которые предоставляет использование групп при замене подстрок. Предположим, что имеется строка, содержащая несколько дат в "американском" формате: "месяц/день/год", причем в качестве разделителя может использоваться как косая черта "/", так и точка ".". Значения месяцев и дней могут указываться одной или двумя цифрами, а значения лет - четырьмя цифрами. Будем считать, что все даты в указанном формате, содержащиеся в исходной строке, являются правильными и отделяются от предшествующих и последующих слов по крайней мере одним пробелом (либо находятся в начале или конце строки). Требуется преобразовать все подобные даты к "российскому" формату "день/месяц/год", сохранив тот же разделитель, который использовался в исходной дате.

Чтобы получить наиболее простой вариант решения данной задачи, нам необходимо познакомиться еще с тремя элементами языка регулярных выражений. Первые два из них относятся к директивам, определяющим множество символов (подобным ранее рассмотренной директиве \w): это директива \d, означающая любую десятичную цифру, и "универсальная" директива для определения множества, имеющая вид [символы], где в квадратных скобках указываются все символы, допустимые для данного множества (более подробное описание данной директивы и ее вариантов приводится в третьей части статьи).

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

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

Writeln(Regex.Replace(s, '\b(\d{1,2})([./])(\d{1,2})\2(\d{4})\b', '$3$2$1$2$4'));

Если строка s равна '12/30/1998  1.3.2001  обычный текст  10/2/2002', то в результате выполнения указанного оператора на экран будет выведена строка

30/12/1998  3.1.2001  обычный текст  2/10/2002

В использованном регулярном выражении были определены четыре группы: первая связана с месяцем, вторая - с символом-разделителем, третья - с днем и четвертая - с годом. Все эти группы, только в другом порядке, используются в третьем параметре, определяющем выражение замены.
Завершая обсуждение возможностей, связанных с группами, отметим, что в классе Match имеется свойство Groups типа GroupCollection, доступное только для чтения и позволяющее получить информацию обо всех группах, связанных с найденным вхождением (группы индексируются от 0, причем группа номер 0 совпадает со всем найденным вхождением). Подробнее о типах, связанных с группами, рассказывается во второй части статьи.

Часть 2. Классы стандартной библиотеки .NET для работы с регулярными выражениями

Классы, предназначенные для работы с регулярными выражениями, содержатся в сборке System.dll и описаны в пространстве имен System.Text.RegularExpressions. Основным классом, обеспечивающим реализацию всех действий, связанных с использованием регулярных выражений (поиск, замена, разбиение строки), является класс Regex. Все прочие классы являются вспомогательными, и используются при описании параметров методов класса Regex или их возвращаемых значений.

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

Класс Regex: конструктор и свойства

Regex(pattern: string [; options: RegexOptions]);

При создании экземпляра Regex регулярное выражение, указанное в качестве первого параметра pattern, специальным образом обрабатывается, что в дальнейшем ускоряет его использование. Все вызовы основных методов класса Regex для созданного экземпляра будут использовать регулярное выражение, указанное в конструкторе. В конструкторе можно также указывать дополнительные опции поиска (параметр options).

Для экземпляра класса Regex определены два свойства (доступных только для чтения):

  • Options (типа RegexOptions) - набор опций поиска, указанных в конструкторе;
  • RightToLeft (типа boolean) - направление поиска регулярного выражения (одна из опций).

Для обработки строк с помощью регулярных выражений необязательно создавать экземпляры класса Regex; вместо этого можно использовать статические методы данного класса.

Класс Regex: основные методы

Все основные методы класса Regex реализованы в двух вариантах: статическом и экземплярном; при этом каждый из вариантов реализован для нескольких наборов параметров. Имеется шесть основных методов:

  • IsMatch (типа boolean) - возвращает True, если требуемое выражение найдено, и False в противном случае;
  • Match (типа Match) - возвращает первое найденное выражение;
  • Matches (типа MatchCollection) - возвращает все найденные выражения;
  • Split (типа string[]) - разбивает строку на фрагменты; разделители фрагментов определяются регулярным выражением;
  • Replace (типа string) - заменяет найденные выражения.

В приводимых ниже списках параметров input всегда определяет строку, в которой выполняется поиск, pattern (и replacement для Replace) - регулярное выражение, start - индекс первого символа, начиная с которого выполняется поиск, count - число возвращаемых фрагментов (Split) или число выполняемых замен (Replace).

Статические методы IsMatch, Match, Matches и Split:

  • (input: string; pattern: string[; options: RegexOptions])
  • Статические методы Replace:

  • (input: string; pattern: string; evaluator: MatchEvaluator[; options: RegexOptions])
  • (input: string; pattern: string; replacement: string[; options: RegexOptions])

Экземплярные методы IsMatch, Match и Matches:

  • (input: string[; start: integer])

Экземплярные методы Split:

  • (input: string[; count: integer[; start: integer]])

Экземплярные методы Replace:

  • (input: string; evaluator: MatchEvaluator[; count: integer[; start: integer]])
  • (input: string; replacement: string[; count: integer[; start: integer]])

Класс Regex: некоторые вспомогательные методы

  • Escape(str: string) - классовый метод типа string; возвращает вариант строки str, в котором экранированы все специальные символы, используемые в регулярных выражениях (\, *, +, ?, |, {, [, (, ), ^, $, ., # и пробельные символы);
  • Unescape(str: string) - классовый метод типа string; восстанавливает строку, символы которой ранее были экранированы.

Вспомогательные классы

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

  • IgnoreCase (i) - игнорировать регистр при поиске;
  • Multiline (m) - режим многострочного текста, при котором символы ^ и $ соответствуют началу и концу каждой строчки текста (а не всей содержащей его строки типа string);
  • ExplicitCapture (n) - нумеровать только те группы, которым явно присвоено имя;
  • Singleline (s) - режим, при котором символ . (точка) соответствует любому символу (а не любому символу, кроме \n);
  • IgnorePatternWhitespaces (x) - игнорировать неэкранированные пробельные символы в регулярном выражении;
  • RightToLeft (r) - выполнять поиск справа налево;
  • None - дополнительных опций нет.

Несколько опций должны объединяться побитовой операцией or.

Group - класс, инкапсулирующий свойства группы регулярного выражения. Основные свойства (только для чтения):

  • Success (типа boolean) - True, если группа найдена, иначе - False;
  • Index (типа integer) - индекс начала найденной группы;
  • Length (типа integer) - длина найденной группы;
  • Value (типа string) - значение найденной группы (если группа не найдена, то равно пустой строке).

Значение группы может быть также получено с помощью метода ToString.

Match - класс (потомок Group), инкапсулирующий свойства найденного вхождения регулярного выражения. Имеет те же свойства, что и класс Group, которые в данном случае относятся не к группе, а ко всему найденному вхождению. Кроме того, имеет свойство только для чтения Groups типа GroupCollection - коллекцию всех групп, связанных с найденным вхождением; первый элемент этой коллекции (с индексом 0) соответствует нулевой группе, т. е. всему найденному вхождению.

Метод NextMatch класса Match (без параметров, возвращает объект типа Match) позволяет получить следующее вхождение того же регулярного выражения (если оно отсутствует, то свойство Success возвращенного объекта Match будет равно False).

GroupCollection - класс-коллекция групп, реализующий интерфейсы ICollection и IEnumerable. Имеет свойство Count типа integer - количество групп (только для чтения) и два индексатора типа Group (только для чтения): первый с числовым параметром - номером группы (нумерация от 0), второй - со строковым параметром - именем группы.

MatchCollection - класс-коллекция найденных вхождений, реализующий интерфейсы ICollection и IEnumerable. Имеет свойство Count типа integer - количество найденных вхождений (только для чтения) и индексатор типа Match (только для чтения) с числовым параметром - номером найденного вхождения (нумерация от 0).

MatchEvaluator - делегат (процедурный тип) с сигнатурой (m: Match): string, используемый в методе Replace и определяющий строку, на которую надо заменить найденное вхождение m.

Часть 3. Язык регулярных выражений

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

Специальные символы и их экранирование

Язык регулярных выражений использует некоторые символы в качестве специальных (управляющих) символов; к ним относятся \, *, +, ?, |, {, [, (, ), ^, $, ., #. Если символ, указанный в регулярном выражении, не совпадает с одним из его специальных символов, то он считается обычным символом. Для того чтобы специальный символ языка регулярных выражений интерпретировался как обычный символ, его надо экранировать, поместив перед ним символ \ (обратная косая черта). Для автоматического экранирования всех требуемых символов строки s достаточно вызвать для нее метод Regex.Escape(s), возвращающий преобразованную строку.

Для указания некоторых символов, не имеющих визуального представления, а также для указания символов по их коду можно использовать следующие управляющие последовательности:

  • \t - табуляция;
  • \r - возврат каретки;
  • \f - новая страница;
  • \n - новая строка;
  • \xNN - ASCII-символ в 16-ричной системе счисления;
  • \uNNNN - Unicode-символ в 16-ричной системе счисления.

Множества символов

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

  • [abcd] - допускается один из символов, указанных в списке;
  • [^abcd] - допускается любой из символов, кроме тех, которые указаны в списке;
  • [a-d] - допускается один из символов, лежащих в указанном диапазоне;
  • [^a-d] - допускается любой из символов, кроме тех, которые лежат в указанном диапазоне;
  • \d - десятичная цифра, т. е. [0-9];
  • \D - любой символ, кроме десятичной цифры;
  • \w - словообразующий символ (буква, цифра или символ подчеркивания);
  • \W - любой символ, не являющийся словообразующим;
  • \s - пробельный символ, т. е. [ \t\r\f\n];
  • \S - любой непробельный символ;
  • . - любой символ, кроме \n (в режиме SingleLine - любой символ).

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

Квантификаторы

Любой квантификатор, указанный после символа, множества или группы символов, означает, что этот символ, элемент из множества или группа могут повторяться несколько раз. Количество повторений определяется типом квантификатора:

  • * - 0 или более повторений;
  • + - 1 или более повторений;
  • ? - 0 или 1 повторение;
  • {N} - ровно N повторений;
  • {N,} - не менее N повторений;
  • {N,M} - от N до M повторений.

Примеры.

Ищется имя файла cv.doc, возможно, снабженное нумерацией (обратите внимание на экранирование точки):

Regex.IsMatch('cv12.doc', 'cv\d*\.doc') // True

Ищется имя файла cv.doc, которое оканчивается произвольным текстом:

Regex.IsMatch('cvnew.doc', 'cv.*\.doc') // True

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

Regex.Match('zzABCzz', '.*')  // ABC
Regex.Match('zzABCzz', '.*?') // AB

Директивы нулевой длины

Директивы нулевой длины указывают дополнительные условия, связанные с расположением требуемого вхождения. Их название объясняется тем, что они никогда не включаются в найденные вхождения:

  • ^ - начало строки (в режиме Multiline - начало любой строчки многострочного текста);
  • $ - конец строки (в режиме Multiline - конец любой строчки многострочного текста);
  • \A - начало строки (в любом режиме);
  • \z - конец строки (в любом режиме);
  • \Z - конец строки или строчки многострочного текста;
  • \b - позиция на границе слова (границей слова считается позиция, в которой словообразующий символ \w соседствует либо с началом/концом строки, либо с символом, который не является словообразующим);
  • \B - позиция не на границе (т. е. внутри) слова;
  • (?=expr) - продолжать поиск, если для выражения expr есть соответствие справа (положительный просмотр вперед);
  • (?!expr) - продолжать поиск, если для выражения expr нет соответствия справа (отрицательный просмотр вперед);
  • (?<=expr) - продолжать поиск, если для выражения expr есть соответствие слева (положительный просмотр назад);

Примеры.

Regex.Match('zzABCzz', '.*(?=)') // zzABC
Regex.Match('zzABCzz', '(?<=).*(?=)') // ABC

Распознавание концов строчек многострочного текста, которые в Windows помечаются двумя символами #13#10.

foreach m: Match in Regex.Matches('a.txt'#13#10'b.doc'#13#10'c.txt'#13#10'd.txt', '.*\.txt(?=\r?$)', RegexOptions.Multiline) do
  Write(m, ' '); // a.txt c.txt d.txt

Подсчет числа пустых строк в исходном тексте text (квантификатор ? в этом, как и в предыдущем, примере нужен для того, чтобы правильно обработать последнюю строку, после которой могут отсутствовать символы #13#10):

Regex.Matches(text, '^\r?$', RegexOptions.Multiline).Count

Группирование и ссылки на группы

С помощью круглых скобок можно формировать группы регулярного выражения, на которые в дальнейшем можно ссылаться по номеру или по имени:

  • (expr) - включить соответствие для выражения expr в нумерованную группу (группы нумеруются от 1 согласно порядку следования их открывающих скобок; группа 0 соответствует всему найденному вхождению);
  • (?expr) или (?'name'expr) - включить соответствие для выражения expr в именованную группу с именем name;
  • (?:expr) - группирующее выражение, не связываемое с нумерованной или именованной группой;
  • \N - ссылка на ранее найденную группу с номером N, означающая, что в данной позиции регулярного выражения должен содержаться текст этой группы;
  • \k  - ссылка на группу с именем nаme.

Пример.

Чтобы выделить в регулярном выражении для телефонного номера \d{3}-\d{3}-\d{4} начальную группу из трех цифр (код региона) и завершающую группу из 7 цифр (собственно телефонный номер), достаточно заключить их в круглые скобки и воспользоваться свойством Groups:

 

var m := Regex.Match('123-456-7890', '(\d{3})-(\d{3}-\d{4})');
Writeln(m.Groups[0]); // 123-456-7890
Writeln(m.Groups[1]); // 123
Writeln(m.Groups[2]); // 456-7890

Альтернативные варианты

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

Примеры.

 

Regex.Matches('10', '1|10') // 1 (одно вхождение)
Regex.Matches('10', '10|1') // 10 (одно вхождение)
Regex.Matches('10', '0|1|10') // 1 и 0 (два вхождения)

Комментарии

Комментарии, указанные в регулярном выражении, игнорируются при его обработке:

  • (?#comment) - комментарий;
  • #comment - комментарий до конца строки (только в режиме IgnorePatternWhitespaces).

Некоторые подстановки в выражениях замены

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

  • $$ - символ $;
  • $0 - все найденное вхождение;
  • $_ - вся исходная строка;
  • $N - найденная группа с номером N (или пустая строка, если группа не найдена);
  • ${name} - найденная группа с именем name (или пустая строка, если группа не найдена).

Пример

Заключаем все числа в угловые скобки.

 

Regex.Replace('10+2=12', '\d+', '<$0>') // <10>+<2>=<12>

В случае сложных видов замены используется вариант метода Replace с параметром-делегатом MatchEvaluator.

Пример

Удваиваем все найденные числа.

 

Regex.Replace('10+2=12', '\d+', (m: Match) -> (StrToInt(m.Value)*2).ToString())
// 20+4=24

Опции поиска

Опции поиска можно указывать не только в виде параметра методов класса Regex и его конструктора, но и непосредственно в регулярном выражении. Для этого предназначена особая директива (?opt), в которой в качестве opt указывается буква соответствующей опции (см. описание класса RegexOptions). Любая опция, кроме (?r), может указываться в любом месте регулярного выражения и впоследствии может быть отменена директивой (?-opt). Опция (?r) должна быть указана в начале регулярного выражения и не может быть отменена. Опции можно объединять; например, директива (?i-ms) включает опцию i и одновременно отключает опции m и s.

Примеры.

 

Regex.Match('a', 'A', RegexOptions.IgnoreCase) // a
Regex.Match('a', '(?i)A') // a
Regex.Match('BaAaAab', '(?i)A+') // aAaAa
Regex.Match('BaAAaab', '(?i)a(?-i)a') // Aa

Новости

19.01.17. Добавлена операция безопасного среза: a?[-1:5:2]

29.08.16. Вышла версия 3.2. Реализован оператор yield.

12.02.16. Вышла версия 3.1. Добавлены кортежи в стиле (a,b) и кортежное присваивание (a,b) := (b,a)

31.12.15. Версия 3.0.0.1128. Реализованы обобщенные методы расширения для операций

Случайная программа

// Пузырьковая сортировка с флагом
// Уровень сложности: 1
procedure BubbleSort(a: array of integer);
begin
  var i := a.Length - 1;
  var q: boolean;
  repeat
    q := true;
    for var j := 0 to i - 1 do
      if a[j + 1] < a[j] then
      begin
        Swap(a[j + 1], a[j]);
        q := false;
      end;
    i -= 1;
  until q;
end;

const N = 10;

begin
  var a := ArrRandom(N);
  writeln('Исходный массив: ');
  a.Println;
  BubbleSort(a);
  writeln('Отсортированный массив: ');
  a.Println;
end.