div.main {margin-left: 20pt; margin-right: 20pt}
Регулярные выражения в .NET
http://www.dotSITE.spb.ru
Часто приходится слышать от разных разработчиков, познакомившихся с Perl, что основной и уникальной "фичей" этого языка являются регулярные выражения или просто "регэкспы" (сокращение от REGular EXPressions). Ну, во-первых, регулярные выражения не являются изобретением разработчиков Perl-а, просто язык изначально создавался для обработки текстов, а использование регулярных выражений (далее РВ) заметно упрощает эту задачу и РВ были слиты с синтаксисом Perl. На самом деле возможность использования РВ присутствует во многих программных системах, таких как awk, Python, Emacs и многих других, существуют библиотеки для работы с ними практически для всех языков и платформ. Хотя, для вышеупомянутых приверженцев Perl, РВ неизменно ассоциируются с чем-то слитым с синтаксисом языка, а никак не с библиотекой и фраза "А в .NET Framework реализованы регэкспы" вызывает изумленный ответ "Вау! Значит, в C# можно использовать РВ? Все, подайте мне сюда компилер". На самом деле, наличие библиотеки работы с РВ в .NET Framework позволяет использовать РВ в люблм языке, для которого существует компилятор, совместисый с .NET. Возможно даже написать интерпретатор языка Perl, который будет использовать эту библиотеку для выполнения кода. Причем возможность генерации напрямую MSIL-кода (MSIL - MicroSoft Indermediate Language) позволит заметно повысить эффективность (хотя этой возможностью нужно пользоваться осторожно, но об этом позже). Итак, для работы с РВ в .NET Framework присутствует целая библиотека классов, объявленных в пространстве имен System.Text.RegularExpressions. Разберем эти классы подробнее.
Первый класс, который мы разберем, это класс Regex. Regex реализует собственно парсер для РВ. Для разбора используется недетерминированный конечный автомат (Nondetermined Finite Automation) или НКА, который генерирует либо набор специальных инструкций по применению этого РВ, либо напрямую MSIL-код. Арзитектура (алгоритм и возможности РВ), принятая у Microsoft, хорошо балансирует между скоростью и возможностями, такими как обратные ссылки (backreferences) и откат (backtracking). Синтаксис соответствует исползуемому в Perl 5. В целом, Regex - неизменяемый (immutable) класс, что позволяет использовать один его экземпляр в нескольких потоках одновременно. При создании экземпляра указывается РВ, которое будет применять вновь созданный экземпляр:
Regex r = new Regex("w+:d+"); |
Этот фрагмент кода создает экземпляр класса Regex для разборки выражения, описывающего строки вида "abc23:0123", то есть состоящих из набора алфавитно-цифровах символов, затем двоеточия, и в конце - ряда цифр.
Основными возможностями Regex являются поиск и замена. Поиск (matching) - позволяет найти в переданной строке все неперекрывающиеся подстроки, соответствующие заданому РВ. Для поиска моджно использовать метод Match или коллекцию Matches. Поясним сказанное на примере:
using System;
using System.Text.RegularExpressions;
public class RegexTest
{
public static void Main()
{
Regex r = new Regex(@"abc");
Match m = r.Match("adsfsdfsfabcafaabc");
Console.WriteLine("Match: {0}", m);
//////////////////////////////////////////////////////////////////////////////
MatchCollection mc = r.Matches("abcdefabcabcabghdkeabc");
foreach (Match mt in mc)
{
Console.WriteLine("> Match: {0}", mt);
}
}
}
|
Разберем код по порядку. Сначала создается экземпляр класса Regex, отвечающий выражению "abc", которое представляет собой просто строку "abc". Затем выполняется поиск подстрок, которым отвечает вышеуказанное выражение в строке "adsfsdfsfabcafaabc" и первое совпадение записывается в объект m класса Match, хранящего информацию о совпадении: текст найденной подстроки , позицию в которой она была найдена и некоторую дополнительную информацию, которая будет подробнее разобрана позже.
Вторая часть кода (она отделена комментарием) демонстрирует использование коллекции Matches: выполняется поиск подстрок, отвечающих выражению, описываемому созданным объектом Regex в строке "abcdefabcabcabghdkeabc", затем все совпадения заносятся в коллекцию соответствий (представляемую объектом mc класса MatchCollection). После этого на экран выводятся все соответствия, для чего используется перечислитель (enumerator). Я не зря обратил внимание на способ перебора всех соответствий - использование перечислителей может вызвать проблемы при многопоточном программировании - это практически единственный узкий момент при использовании пакета классов System.Text.ResularExpressions в многопоточых приложениях. В остальных случаях классы этого пакета устойчивы к одновременной работе в нескольких потоках (thread safe).
Как уже упоминалось выше, от класса Match можно получить некоторую дополнительную информацию. Первое, что мы разберем - это список сохраненных групп (captured groups). Поясним их смысл на примере: мы хотим получить адреса всех сотрудников, данные о которых перечислены в файле вида:
Name=John Smith;BirthDate=18.09.1976;Address:29, 7th st.
using System;
using System.Text.RegularExpressions;
public class GetAddress
{
public static void Main()
{
Regex r = new Regex(@"Address:(?[^;]+)");
MatchCollection mc = r.Matches("Name=John Smith;BirthDate=
18.09.1976;Address:29, 7th st.
");
foreach (Match mt in mc)
{
Console.WriteLine("Address: {0}", mt.Groups["address"]);
}
}
}
|
Часть регулярного выражения (?[^;]+) сопоставляет части соответствующей подстроки, отвечающей выражению [^;]+ имя "address". То есть, если в строчке была найдена подстрока, соответствующая выражению "Address:(?[^;]+)", то часть ее, соответствующая "[^;]+" будет сохранена в группе с именем "address". К этой группе можно обращаться не только внешним образом, но и в самом выражении, используя обратные ссылки (backreferences), о которых несколько подробнее будет сказано в дальнейшем. Группы можно выделить не только явным назначением имен. Имена даются автоматичекси всем выражениям, заключенным в скобки, причем им даются номерные имена, соответствующие вложенности озватывающих скобок. Доступ к таким группам извне осуществляется с помощью числового индексатора (m.Groups[1]). Группе с индексом 0 соответствует все выражение. Также можно перебрать все группы коллекции Groups (представляющей собой экземпляр класса GroupCollection) с помощью перечислителей.
Но и это еще не все… На самом деле каждая группа может иметь несколько захватов (captures) вследствие использования множителей (quantifiers). Опять же, вместо долгого и туманного описания того, что имеется в виду, продуктивнее показать пример:
using System;
using System.Text.RegularExpressions;
public class RegexTest
{
public static void Main()
{
Regex r = new Regex(@"(dotsite)+");
Match m = r.Match("dotsitedotsitedotsiterulez");
foreach (Group g in m.Groups)
{
Console.WriteLine("> Group: {0} ({1})", g.Value, g.Index);
foreach (Capture c in g.Captures)
{
Console.WriteLine("> Capture: {0} ({1})", c.Value, c.Index);
}
}
}
}
|
Эта программа найдет две группы - "dotsite" (она представлена индексом 1) и "dotsitedotsitedotsite" (она представлена индексом 0 и представляет все выражение). При этом захват второй группы произойдет один раз (это и понятно, одно соответствие всегда содерджит ровно один захват всего выражения), а вторая - 3 раза (по числу подстрок "dotsite" в строке поиска).
Еще одна возможность Regex, которую мы здесь разберем - это замена. Она осуществляется при вызове метода Replace. При вызове метода передаются три строки - первая задает строку, в которой необходимо произвести замену, вторая представляет собой регулярное выражение и описывает фрагменты, которые подлежат замене, а вторая - на что нужно заменить найденный подстроки, причем во второй строке можно использовать подстановки (substitutions) - в указанные места будут подставлены значения захваченных групп с заданными именами. Синтаксис подстановок следующий:
Синтаксическая конструкция
Значение
$n
|
Подставляет последнюю подстроку, соответствующую группе с номером n. Например
Replace(str, "(a+)", "_$1_") обрамляет все последовательности букв 'a' символами подчеркивания (например, строка "bacghghaaab" превратится в "b_a_cghgh_aaa_b"
|
$(имя)
|
Подставит последнюю подстроку, соответствующую группе с именем имя.Например, Replace(str, @"b(?d{1,2})/(?d{1,2})/(?d{2,4})b", "${day)-${month}-${year) переведет дату из формата мм/дд/гггг в дд-мм-гггг
|
Работа класса Regex зависит от установленных опций. Опции задаются параметром оptions расширенного конструктора экземпляра класса Regex (значение этого параметра должно представлять собой побитовую комбинацию значений перечисления RegexOptions или заданы прямо в регулярном выражении с помощью синтаксической конструкции (?imnsx-imnsx), в которой буквы до дефиса указывают какие опции необходимо включить, а после дефиса - какие выключить. Следует заметить, что некоторые опции не могут быть заданы в строке поиска. Ниже следует список всех опций класса Regex:
Имя опции-члена RegexOptions
Символ, задающий опцию в строке поиска
Описание
None
|
отсутствует
|
Указывает, что ни одна опция не является активной.
|
IgnoreCase
|
i
|
Задает нечувствительный к регистру поиск.
|
Multiline
|
m
|
Задает многострочный режим. Изменяет значение символов ^ и $ таким образом, что они соответствуют началу и концу каждой строки
|
ExplicitCapture
|
n
|
Указывает, что только верные явно названные или пронумерованные (с помощью конструкции (?)) группы сохраняются. Это позволяет избежать излишнего использования конструкции (?:…).
|
Compiled
|
Отсутствует
|
Указывает, что регулярное выражение должно быть скомпилировано в MSIL-код, чем достигается более быстрая обработка, но приводит к увеличению времени загрузки. При выключенно опции выражение компилируется во внутренний код, специфичный для класса Regex. При исользовани этой опции также следует помнить о том, что полученный MSIL-код будет удален из памыти только при удалении всего домена приложения (application domain) и не увлекаться использованием этой опции для разных выражений. Код для выражений кешируется и для разных экземпляров Regex, использующих одно и то же выражения, используется один и тот же код.
|
Singleline
|
s
|
Задает однострочный режим. В этом режиме символ . означает любой символ (включая символ перевода строки).
|
IgnorePatternWhitespace
|
x
|
Указывает что пробелы в регулярном выражении (за исключением предваренный обратной косой чертой) должны игнорироваться и позволяет использовать комментарии, начинающиеся с символа '#'. NB: Пробелы не игнорируются в символьных классах.
|
RightToLeft
|
отсутствует
|
Задает поиск справа налево. Регулярное выражение подствляется начиная с конца.
|
EcmaScript
|
отсутствует
|
Задает стиль EcmaScript. Может быть использован только совместно с аттрибутами Multiline и IgnoreCase.
|
Как упоминалось ранее, группы могут быть использованы и непосредственно внутри регулярного выражения. Это достигается использованием обратных ссылок. На практике это означает, что вы можете сказать "а здесь должно быть то же самое, что соответствует группе с именем ВасяПупкинКрутойПрограммист". Для ссылок используется следующий синтаксис:
Синтаксис
Определение
номер
|
Задает ссылку на нумерованную группу. Например, конструкция (w)1 ищет двойные буквы (например, в словах tall, small, millennium)
|
k<имя>
k'имя'
|
Задает ссылку на именованную группу. Например, поведение, описанное выше, реализуется выражением (?w)k.
|
Итак, в заключение хочется подвести итоги. Компания Microsoft предоставила в составе .NET Framework действительно мощное средство работы с текстом, которое может быть использовано в самых разных приложениях (например, оно используется в ASP.NET для одного из валидаторов (RegularExpressionValidatior)).
|