← Все статьи

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

Введение

Регулярные выражения — мощное, гибкое и эффективное средство для анализа и обработки строк и многострочных текстов.

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

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

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

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

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

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

Для обычного разбиения текста на слова в PascalABC.NET достаточно воспользоваться методом ToWords:

foreach var s0 in s.ToWords do
  Println(s0);

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

foreach var s0 in Regex.Split(s, ' +') do
  Println(s0);

В данном случае мы вызвали метод Split класса Regex — основного класса .NET для работы с регулярными выражениями.

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

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

 +

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

Знак + относится к так называемым квантификаторам.

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

Предположим теперь, что требуется определить индексы символов строки s, с которых начинается каждое слово.

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

foreach var m in s.Matches('\w+') do
  Print(m.Index);

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

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

будут получены индексы:

0 9 18 27

Символы строки индексируются начиная с нуля.

В регулярном выражении

\w+

использованы:

  • \w — любой словообразующий символ;
  • + — один или более повторов предыдущего элемента.

Метод Matches находит все фрагменты строки, удовлетворяющие регулярному выражению.

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

  • Value — найденный текст;
  • Index — позиция начала в исходной строке;
  • Length — длина найденного фрагмента.

Например:

foreach var m in s.Matches('\w+') do
  Println(m.Value,m.Index,m.Length);

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

Кроме Matches, для поиска используются методы:

  • IsMatch — проверяет, имеется ли хотя бы одно совпадение;
  • Match — возвращает первое совпадение;
  • Matches — возвращает все совпадения.

Например:

if Regex.IsMatch(s, '\d+') then
  Println('В строке есть число');

Объект Match позволяет продолжить поиск с помощью метода NextMatch.

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

Регулярные выражения позволяют не только искать фрагменты строки, но и заменять их.

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

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

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

должна превратиться в

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

Для замены используется метод Regex.Replace.

Попробуем написать:

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

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

{1,5}

означает от одного до пяти повторений предыдущего элемента.

Однако это решение неверно. Мы получим примерно такой результат:

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

Причина в том, что выражение \w{1,5} ищет не слова длиной до пяти символов, а любые последовательности из одного-пяти словообразующих символов.

Нужно дополнительно потребовать, чтобы совпадение находилось на границах слова.

Для этого используется директива \b:

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

\b обозначает границу слова и является директивой нулевой длины: она влияет на поиск, но сама не входит в найденный фрагмент.

Теперь задача решена правильно.

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

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

В результате:

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

Регулярное выражение задается один раз при создании объекта Regex, после чего для него можно вызывать Split, IsMatch, Match, Matches, Replace и другие методы.

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

Замена с помощью функции

Иногда простого выражения замены недостаточно.

Предположим, что к каждому слову требуется добавить его длину:

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

Для этого можно передать методу Replace функцию:

function AddLength(m: &Match): string;
begin
  Result := m.Value + '(' + m.Length.ToString + ')';
end;

...

Println(Regex.Replace(s, '\w+', AddLength));

Еще короче это записывается с помощью лямбда-выражения:

Println(
  Regex.Replace(
    s,
    '\w+',
    (m: &Match) -> m.Value + '(' + m.Length.ToString + ')'
  )
);

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

Еще один важный элемент языка регулярных выражений — группы.

Группа — часть регулярного выражения, заключенная в круглые скобки.

Например:

(ab)+

означает последовательность, в которой текст ab повторяется один или более раз:

ab
abab
ababab

Без скобок выражение

ab+

имеет совсем другой смысл: символ a, после которого идет один или более символов b.

Например:

abbb

Группы используются не только для объединения элементов.

Каждая группа получает номер и может быть использована далее в том же регулярном выражении. Ссылка на группу с номером N записывается как \N.

Рассмотрим задачу: найти все слова, начинающиеся и заканчивающиеся одной и той же буквой.

foreach var a in s.Matches('\b(\w)\w*\1\b') do
  Print(a.Value);

Если строка s содержит:

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

то будет найдено слово:

алгебра

Разберем выражение:

\b(\w)\w*\1\b

Здесь:

  • \b — граница слова;
  • (\w) — первый символ слова, сохраненный в группе 1;
  • \w* — ноль или более словообразующих символов;
  • \1 — тот же текст, который был найден группой 1;
  • \b — конец слова.

Однако слово Каучук не будет найдено, поскольку первая К — заглавная, а последняя к — строчная.

Для поиска без учета регистра можно использовать RegexOptions.IgnoreCase:

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

Ту же опцию можно указать непосредственно в регулярном выражении:

foreach var a in s.Matches('(?i)\b(\w)\w*\1\b') do
  Print(a.Value);

Директива

(?i)

включает поиск без учета регистра.

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

Для описания альтернатив используется операция |.

Можно написать:

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

или вынести общие границы слова:

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

Обратите внимание: после добавления внешней группы (\w) стала группой номер 2, поэтому ссылка изменилась с \1 на \2.

Перестановка групп при замене

Рассмотрим еще один пример.

Пусть в строке содержатся даты в американском формате

месяц/день/год

или

месяц.день.год

Требуется преобразовать их в формат

день/месяц/год

с сохранением исходного разделителя.

Используем выражение:

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

Если строка равна:

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

получим:

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

Здесь определены четыре группы:

  1. месяц;
  2. символ-разделитель;
  3. день;
  4. год.

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

$3$2$1$2$4

У объекта Match имеется свойство Groups, позволяющее получить информацию обо всех группах найденного совпадения.

Группа с индексом 0 соответствует всему найденному фрагменту.


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

Средства регулярных выражений .NET находятся в пространстве имен System.Text.RegularExpressions.

Основным классом является Regex.

Он обеспечивает:

  • поиск;
  • проверку наличия совпадения;
  • получение всех совпадений;
  • замену;
  • разбиение строки.

Остальные классы используются главным образом для представления результатов поиска и задания параметров.

Класс Regex

Объект регулярного выражения создается следующим образом:

var r := new Regex(pattern);

или с дополнительными опциями:

var r := new Regex(pattern,options);

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

Основные свойства объекта Regex:

  • Options — набор используемых опций;
  • RightToLeft — выполняется ли поиск справа налево.

Для однократных операций создавать объект Regex обычно необязательно.

Основные методы Regex

Основные операции:

  • IsMatch — проверить наличие совпадения;
  • Match — получить первое совпадение;
  • Matches — получить все совпадения;
  • Split — разбить строку;
  • Replace — выполнить замену.

Примеры:

Regex.IsMatch(s, '\d+')
Regex.Match(s, '\d+')
Regex.Matches(s, '\d+')
Regex.Split(s, '\s+')
Regex.Replace(s, '\d+', '#')

В PascalABC.NET для ряда операций доступна более компактная точечная запись. Например:

s.Matches('\d+')

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

Regex.Escape

Экранирует специальные символы регулярных выражений:

var pattern := Regex.Escape(s);

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

Regex.Unescape

Выполняет обратное преобразование:

var s1 := Regex.Unescape(s);

RegexOptions

RegexOptions — перечисление, определяющее дополнительные параметры поиска.

Наиболее часто используются:

  • IgnoreCase — не учитывать регистр;
  • Multiline — многострочный режим;
  • ExplicitCapture — захватывать только явно именованные группы;
  • Singleline — точка . соответствует в том числе символу новой строки;
  • IgnorePatternWhitespace — игнорировать неэкранированные пробельные символы в шаблоне;
  • RightToLeft — выполнять поиск справа налево;
  • None — дополнительные опции отсутствуют.

Несколько опций объединяются операцией or:

var options := RegexOptions.IgnoreCase or RegexOptions.Multiline;

Класс Group

Group описывает группу регулярного выражения.

Основные свойства:

  • Success: boolean — была ли найдена группа;
  • Index: integer — индекс начала;
  • Length: integer — длина;
  • Value: string — найденный текст.

Класс Match

Match является потомком Group и представляет все найденное совпадение.

Поэтому у него имеются те же основные свойства:

  • Success;
  • Index;
  • Length;
  • Value.

Кроме того:

m.Groups

возвращает коллекцию групп.

Группа Groups[0] соответствует всему найденному совпадению.

Метод

m.NextMatch

возвращает следующее совпадение того же регулярного выражения.

GroupCollection

Коллекция групп.

Основные возможности:

groups.Count
groups[0]
groups[1]
groups['name']

Группы нумеруются начиная с нуля.

MatchCollection

Коллекция найденных совпадений.

Например:

var matches := s.Matches('\w+');

Println(matches.Count);

foreach var m in matches do
  Println(m.Value);

MatchEvaluator

MatchEvaluator — делегат, используемый в Replace для вычисления строки замены.

Ему соответствует функция вида:

function F(m: &Match): string;

На практике вместо отдельной функции часто удобно использовать лямбда-выражение.


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

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

В регулярных выражениях ряд символов имеет специальное значение:

\ * + ? | { [ ( ) ^ $ . #

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

Например, точка обозначает любой символ:

.

а обычная точка записывается как:

\.

Для автоматического экранирования строки можно использовать:

Regex.Escape(s)

Некоторые специальные последовательности:

  • \t — табуляция;
  • \r — возврат каретки;
  • \f — перевод страницы;
  • \n — новая строка;
  • \xNN — символ с указанным шестнадцатеричным кодом;
  • \uNNNN — Unicode-символ с указанным шестнадцатеричным кодом.

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

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

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

Например:

'abc123'.Matches('\d+')

найдет:

123

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

Квантификатор задает число повторений предыдущего элемента.

Основные квантификаторы:

  • * — 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

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

По умолчанию квантификаторы являются жадными: они стараются захватить максимально возможное количество символов.

Добавление ? делает квантификатор ленивым:

.*
.*?

Например, для строки

<abc><def>

выражение

<.*>

попытается захватить:

<abc><def>

а выражение

<.*?>

сначала найдет:

<abc>

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

Эти конструкции задают условие для позиции в строке, но сами символов не захватывают.

Основные:

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

Пример

Найдем имена .txt-файлов в многострочном тексте:

var text := 'a.txt'#13#10'b.doc'#13#10'c.txt'#13#10'd.txt';

foreach var m in Regex.Matches(
  text,
  '.*\.txt(?=\r?$)',
  RegexOptions.Multiline
) do
  Print(m.Value);

Получим:

a.txt c.txt d.txt

Подсчитать число пустых строк можно так:

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

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

Круглые скобки создают группы.

(expr)

Группы нумеруются начиная с 1 в порядке появления открывающих скобок.

Группа 0 соответствует всему найденному совпадению.

Основные конструкции:

  • (expr) — захватывающая группа;
  • (?<name>expr) — именованная группа;
  • (?:expr) — незахватывающая группа;
  • \N — ссылка на ранее найденную группу с номером N;
  • \k<name> — ссылка на именованную группу.

Пример

Выделим код региона и номер телефона:

var m := Regex.Match('123-456-7890', '(\d{3})-(\d{3}-\d{4})');

Println(m.Groups[0]); // 123-456-7890
Println(m.Groups[1]); // 123
Println(m.Groups[2]); // 456-7890

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

Операция

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)

При включенной опции IgnorePatternWhitespace комментарий до конца строки можно начинать символом #.

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

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

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

Пример

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

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

Результат:

<10>+<2>=<12>

Для более сложной замены можно использовать лямбда-выражение.

Например, удвоим все найденные числа:

Regex.Replace(
  '10+2=12',
  '\d+',
  (m: &Match) -> (m.Value.ToInteger * 2).ToString
)

Результат:

20+4=24

Опции поиска

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

Например:

Regex.Match('a', 'A', RegexOptions.IgnoreCase)

и

Regex.Match('a', '(?i)A')

эквивалентны.

Основные встроенные обозначения:

  • i — игнорировать регистр;
  • m — многострочный режим;
  • n — явно захватывать только именованные группы;
  • s — режим Singleline;
  • x — игнорировать незначащие пробелы в шаблоне.

Опцию можно включить:

(?i)

и отключить:

(?-i)

Несколько изменений можно объединить:

(?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

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