Pull to refresh

Тонкости регулярных выражений. Часть 1: метасимволы внутри и вне символьных классов

Reading time 5 min
Views 16K

Вместо вступления



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

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



Не секрет, что многие при упоминании о регулярных выражениях вспоминают язык Perl. И не зря! Perl — один из немногих языков, где регулярные выражения закреплены на уровне синтаксиса, базовых конструкций языка. В то же время Perl прославился как язык, программы на котором очень сложно понять спустя 5 минут после написания. Обилие одно-двух-символьных функций и переменных делает своё дело. Текст больше похож на набор смайликов, чем на программу. Особенно, если в ней используются регулярные выражения.

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

Диалекты регулярных выражений



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

Конечно же, в таком важном деле как стандартизация не обошлось без всемогущего POSIX. Тем более, что регулярные выражения берут своё начало как раз в unix-среде.

POSIX описывает синтаксис и семантику регулярных выражений. Основных стандартов два: POSIX BRE (Base Regular Expressions) и POSIX ERE (Extended Regular Expressions). Отличаются они, как понятно из названия, тем что второй стандарт расширяет первый. Я не буду подробно описывать, что входит в каждый из стандартов, а особенно семантику того, что в них входит, поскольку это всегда можно посмотреть в Википедии. Я лишь скажу, что несмотря на то, что такие стандарты есть, разработчики движков регулярных выражений не спешат им полностью следовать. Особенно в семантике! И на то есть веские причины.

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

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

Следующее частое различие — в интепретации скобок. Фигурных, круглых, квадратных. Где-то скобки нужно квотировать, где-то нет. Например в .NET, Java скобки квотировать нужно, потому что это метасимволы. В утилите grep по умолчанию скобки квотировать не нужно! А чтобы использовать функциональность групп и прочее нужно использовать выражения вида \(\).

Метасимволы внутри символьных классов



И сразу пока не забыли про метасимволы, рассмотрим символьные классы. Очень частая ошибка новичков — это квотирование метасимволов внутри символьных классов. Такая ошибка зачастую не несет никаких последствий (зачастую), зато наглядно показывает, что человек не понимает до конца, как работают символьные классы.

С символьными классами встречался каждый, кто использовал регулярное выражение. Я уверен в этом. Для тех кто забыл, что это такое, напомню — символьные классы — это последовательности внутри квадратных скобок, если говорить языком дилетанта. Пример: [abc0-9] — на месте где находится символьный класс в совпадении должен присутствовать символ a или b, или c, или цифра от 0 до 9. Все просто.

Но не так просто, как хотелось бы. Первое что следует запомнить: символьный класс — это другой мир! Как только вы попадаете внутрь квадратных скобок, все правила игры меняются. Одни метасимволы перестают быть таковыми, семантика других меняется в корне. Чтобы не быть голословным приведу примеры:
  • метасимвол ^ — вне символьного класса метасимвол обозначает «начало строки», либо «начало логической строки» в зависимости от режима работы. А внутри символьного класса этот метасимвол обозначает инверсию символьного класса. Заметьте, я не сказал «не совпадение», потому что это не так. Когда мы инвертируем символьный класс, семантика его работы — «здесь должен находиться символ, которого нет в символьном классе», а вовсе не «здесь должен не находиться символ, который есть в символьном классе». Различия семантики огромны. Рассмотрите, например, регулярное выражение ^abc[^abc] применительно к строке abc. В первом случае (правильная интерпретация) совпадения нет! Потому что «пусто» не может совпадать с символом. А во втором случае совпадение должно быть, потому что символа-то там (на 4-ой позиции строки) как раз нет.

    Но я отвлекся. Итак, один и тот же метасимвол интерпретируется совершенно по-разному в зависимости от того, где он находится: в символьном классе или вне его. Но и это еще не все! Инверсия символьного класса происходит только если метасимвол ^ является первым символом после открывающейся квадратной скобки! Т.е. в символьном классе [abc^] уже нет никакой инверсии и крышка — просто крышка.
  • метасимвол - — вне символьного класса является просто дефисом. Он не метасимвол. Зато внутри символьного класса он обозначает диапазон. Но есть нюанс. Если этот символ идет сразу после отрывающейся квадратной скобки, то естественно не может обозначать диапазон. И тогда он интерпретируется как… просто дефис. Как и вне символьного класса.

    Другая распространенная ошибка с метасимволом - — задание неправильного диапазона в символьном классе. Например, [a-Z], тут все ясно — вместо всех строчных и заглавных латинских букв мы получим все символы от 0x61 до… 0x5A (в кодировке ASCII). Т.е. пустое множество (в некоторых диалектах получим только символы a и Z). Поэтому опять очень важно знать семантику дефиса — в диапазон попадают символы, коды которых расположены между кодами начала и конца диапазона, включительно. Я не встречал языков, который производил бы интерпретацию диапазонов особым образом (например как символьный класс \w или \d).


Другие метасимволы я рассматривать не буду за неимением места. Теперь становится понятно, почему излишне писать [\.\(\)\{\^]. Просто потому, что эти метасимволы внутри символьного класса уже не являются таковыми. А квотируя их «на всякий случай», вы сами показываете, что не очень понимаете, что происходит внутри.

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

По мотивам книги Jeffrey Friedl, Mastering Regular Expressions.
Часть 2.
Tags:
Hubs:
+56
Comments 69
Comments Comments 69

Articles