Улучшим регулярные выражения
Вчера закинул эту статью на Хабр, сейчас опубликую ее здесь.
После прочтения книги Фридла про регулярные выражения (далее просто РВ) у меня появились кое-какие мысли по поводу их читаемости. Когда РВ только появлялись, и в них было довольно мало условных обозначений вроде \d, \w и тому подобных, то, наверное, все было не так страшно, хотя уже тогда стоило задуматься о наглядности. Сейчас чтение кода с РВ — это тихий ужас. Нет, если РВ короткое, то особых проблем нет, но по мере их усложнения и появления различных скобок все становится просто кошмарно. Ситуация усугубляется тем, что в некоторых языках (не будем указывать пальцем) постоянно приходится удваивать слеши.
Кроме того в той нотации РВ, которая сейчас используется в большинстве языках программирования, в некоторых, казалось бы простых ситуациях, приходится выкручиваться с помощью различных финтов. Первый пример, что пришел в голову — составить регулярное выражение если «abc», то затем НЕ «xyz».
На мой взгляд пора уже отказаться от той нотации, которая сейчас используется, и создать новую, которая будет ближе к обычному языку программирования, ведь нотация РВ — это по сути и есть язык, но оформленный просто ужасно. Самое худшее, что есть в сегодняшней нотации — это обилие скобок вроде (…), (:…), (?:…), (?=…), (?!…), (?<=…), (?<!…), (?<). Именно благодаря ним выражения становятся запутанными и невозможно охватить взглядов все РВ, чтобы сразу сказать, что оно ищет, а приходится проверять каждый символ в строке, не забыв, например, что ^ в середине РВ — это начало строки, а в начале квадратных скобок [^…] — это инверсия. Ну неправильно то, что при появлении новой возможности разработчики создают новое обозначение, какое-нибудь (&^%$#@…), которое само по себе абсолютно ничего не говорит.
Ведь в чем прелесть обычных языков программирования (не беря какие-то крайние случаи)? Если мы видим оператор if или whileв незнакомом языке, то можем сразу сказать, что он примерно он делает. Да, можно заменить эти операторы на символы вроде @#$% и #&$^ соответственно, к ним можно даже привыкнуть, но как говорилось в анекдоте про урок русского языка в Грузии, «это надо запомнить, понять этого невозможно».
Возможно, ситуацию могли бы улучшить умные редакторы кода, которые в регулярном выражении по-разному подсвечивали бы скобки (?:…), (?=…) и т.д., чтобы сразу видеть области их действий, но для большинства языков программирования этого сделать почти невозможно, т.к. РВ там — это строка и редактор должен был бы уметь определять по содержимому строки, что перед ним: РВ или обычный текст. Да и все-равно при большой вложенности скобок РВ превратится в разноцветную радугу.
Вообще говоря, довольно неплохие изменения в плане читаемости РВ уже произошли благодаря появлению режимов, когда РВ записываются на нескольких строках, а также благодаря комментариям внутри РВ. Появилась даже конструкция с понятным видом (?if…), в Perl (не к ночи будет упомянут) в регулярное выражение можно встраивать программный код, а в .NET вместо простой замены по РВ заменяемое значение может генериться с помощью специально обученного специального делегата. В целом уже можно даже написать более-менее понятное РВ, но все-равно это не то, это больше похоже на костыли.
Уже пора создать язык РВ, похожий на остальные «человеческие» языки программирования, а не на Brainfuck. Тогда в нем можно было бы организовать понятную подсветку, подсказки а ля IntelliSense, а в будущем, возможно, и пошаговую отладку РВ.
Дальше хотелось бы показать какими бы мне хотелось видеть РВ.
Во-первых, их надо как-то отделить РВ от обычных строк. Понятно, что функции для их работы требуют именно строк, не уверен, что РВ стоит встраивать в сами языки, как это сделано в Perl, пусть остаются строками, но чтобы их как-то выделить внутри кавычек стоит использовать какие-нибудь дополнительные обозначения. Это может быть что угодно, например, вместо «\d\w» (для наглядности я не буду удваивать слеши) стоит использовать «!\d\w!» или «<\d\w>», тогда редактор сможет легко отличить РВ от строк. В дальнейшем я буду использовать запись «!…!», но это не важно, как и остальные обозначения, главное суть.
Во-вторых, РВ нужно записывать только в режиме, когда игнорируются пробелы и переводы строк, причем, чтобы отделить внутри выражения литералы, которые всегда остаются неизменными от конструкций самих РВ, литералы можно брать в кавычки (не важно какие). Например, вместо «abcd\d\wxyz» можно будет написать:
"! 'abcd'
\d\w
'xyz'
!"
Или даже «!’abcd’ \d\w ‘xyz’!»
Редактор кода здесь отдельно сможет подкрасить abcd и xyz. Возможно, стоит использовать знак «+», чтобы связать эти части. Так даже будет нагляднее: «!’abcd’ + \d\w + ‘xyz’!», т.к. отдельные части РВ больше разделяются визуально.
Вас может смутить то, что знак «+» сейчас используется в значении «1 или больше совпадений», но это не страшно, потому что в этом значении его никто использовать больше не собирается. Это же не логично. Есть же такие наглядные конструкции как {min, max}, давайте их использовать вместе с оператором «*». Оператор «*» стоит использовать как раз в значении «умножить», то есть выражение «!’abc’ * 3!» означает, что строка ‘abc’ должна повториться 3 раза. РВ «!’abc’ * {1, 3}!» означает, что ‘abc’ должна повториться от 1 до 3 раз. Аналогично можно использовать запись «!’abc’ * {1, }!» в значении «1 и больше совпадений» вместо «+», а вместо оператора «*» писать: «!’abc’ * {0, }!». А запись «!’abc’ * {3, 3}!» равносильна той, что мы уже видели «!’abc’ * 3!». Старый оператор «*» тогда будет заменен на выражение «!’abc’ * {,} !».
Возможно, вместо фигурных скобок стоит использовать квадратные или круглые, тогда это будет даже ближе к математической записи отрезков и интервалов.
Остается вопрос с тем, как обозначить оператор минимальный «*» (он же не жадный). Можно было бы использовать оператор деления, но это тоже не логично, поэтому можно записать прямо в виде «!’abc’ min* 3 !». Здесь min* — это один оператор без пробела. Этот вариант записи мне не очень нравится, но по крайней мере он своим именем поясняет суть.
Большинство скобок стоит заменить на встроенные функции. Например, вместо «[abc]» стоит записывать в виде «!any (a, b, c)!», тогда можно будет таким же образом заменить выражение «(:abc)|(:xyz)» на «! any (‘abc’, ‘xyz’) !» и мы сможем избавиться еще и от оператора «|». В качестве параметров функции можно использовать РВ, например «! any (\d\w, ‘abc’) !».
Надо решить как поступать с простейшими выражениями вроде \w, \b, \d и т.п. С одной стороны, они довольно компактные, но мне, например, нравится запись, которая сейчас может использоваться в квадратных скобках — [:alnum:]. Для удобства можно заменить их на запись вида _alnum_. А может быть самые простейшие \d и \w стоит оставить как есть. А вместо «.», которая не особо видна в тексте можно использовать запись _any_. Те же пробелы и табуляции, которые игнорируются в самом выражении можно записывать в виде _space_ или просто брать их в кавычки.
Обязательно нужно ввести нормальный оператор if — then — else, суть которого заключается в том, что если выражение после if выполняется, то затем проверяется РВ в ветке then, иначе после ветки else. Думаю, что слово then можно опустить. Тогда можно будет составить такое РВ:
"! 'abc'
if (\w * 3)
{
'xyz'
}
else
{
\d * {1, } 'klmn'
}
!"
Здесь я использовал синтаксис как в С-подобных языках, но это не критично. Дословно это выражение обозначает: Сначала идет строка ‘abc’, затем проверяется РВ ‘\w * 3’, если оно выполняется, то затем должно идти ‘xyz’, в противном случае должно идти как минимум одно число, а затем ‘klmn’.
Может быть, даже стоит ввести операторы типа case, while и for. Кроме того нужно ввести логические операции И, ИЛИ, НЕ, чтобы их использовать в условии. Не уверен на счет И и ИЛИ, ведь выражение «! if (‘abc’ && ‘xyz’) !» равносильно «! if (‘abcxyz’) !», а «! if (‘abc’ || ‘xyz’) !» — «! if (any (‘abc’, ‘xyz’) ) !». Но оператор отрицания нужен точно, чтобы определять что в данном месте не должно находиться.
Нужно ввести переменную, которая обозначает позицию в строке, где сейчас осуществляется поиск (пусть будет переменная _pos_), а так же переменная, хранящая саму строку, к которой применяется РВ (_this_). Тогда оператор «^» можно заменить более понятным «! _pos_ == 0 !», а «$» на «! _pos_ == (strlen(_this_) — 1) !» Может быть стоит ввести отдельное обозначение для конца строки, например, по аналогии с Python: _pos_ == -1. Эти же переменные позволят сделать опережающую и ретроспективную проверку.
Нужно оставить комментарии. Как они будут выглядеть это уже не важно.
Оператор присваивания должен работать в двух режимах. Первый — это проверка и присваивание переменной строки, соответствующей регулярному выражению, то, для чего сейчас используется запись вроде «(?<foo>…)»: «!foo = \w\d*; !». Точку с запятой придется использовать, чтобы показать где кончается оператор присваивания.
Второй режим присваивания — сохранение регулярного выражения без его проверки. Используется для наглядности, например,
"!
foo = !\d\w*!‘abc’ foo ‘xyz’ foo
!»
Здесь выражение !\d\w*! (обратите внимание на восклицательные знаки) используется затем по имени переменной foo.
Это основные идеи, которые появились относительно РВ. Было бы интересно попробовать такие выражения в деле, но, к сожалению, руки до реализации такого парсера у меня вряд ли дойдут. А вообще, начать можно было бы с того, что такие выражения преобразовывались к классическому виду РВ, а затем обрабатывались бы готовой библиотекой.
В завершении маленький пример для поиска URL. Возможно, там не все учел, например, считается, что доменная зона может быть только com, net, info или двухбуквенная.
"!
unicode = !% any(\d, A-F) * 2 ! // Представление Unicode в адресе.
// Переменная только создается, но не проверяется
domain = !any ('com', 'net', 'info', (a-z) * {1, 2})!
host = !any (\w, '_', unicode)!«http://» (host ‘.’) * {1,} domain ‘/’ * {0, 1}
«!
Надеюсь, что нигде не ошибся, но даже если и ошибся, не страшно, главное хотелось показать суть.
В завершении еще раз скажу, что главной целью всего этого было придумать как можно повысить читаемость РВ. Разумеется, при этом объем набираемого текста увеличится, но, как сказала [ljuser]linda_kaioh[/ljuser]: «Целая клавиатура не стоит поломанных мозгов«. Согласен с ней на 100%. 🙂
PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
Александр:
Как выбрать текст от начала документа до слова в верхнем регистре в минимум 5 букв для последующего удаления? Само это слово и текст после него до конца документа не должно попадать в выделение.
9 ноября 2012, 10:38 дп