Регулярно выражаемся
Введение
Регулярные выражения — мощное, гибкое и эффективное средство для анализа и обработки строк и многострочных текстов.
В некоторых языках программирования работа с регулярными выражениями встроена непосредственно в язык, в других для этого используются библиотеки. В 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
Здесь определены четыре группы:
- месяц;
- символ-разделитель;
- день;
- год.
В выражении замены они используются в другом порядке:
$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 для разбиения текста на слова. Поэтому регулярные выражения лучше применять там, где они действительно делают решение короче и понятнее.