Comments 51
Обьясню почему — вы привнесли в регулярки еще один язык высокого уровня, который во-первых замедляет их выполнение, во-вторых лишает кроссмплатформенности и быстроты проверки. Да, я понимаю, что можно в конце концов вытащить итоговое выражение и оно будет работать везде — но его еще собрать надо.
Кроме того, большинство программистов привыкли именно к такому виду, который Вы показали в начале статьи + есть огромное количество инструментов для визуализации и работы с регулярками.
Если хочется более-менее порядка и стройности в данных выражениях, то для этого есть флаг PCRE-EXTENDED и после этого у вас появляется возможность использовать пробелы, табуляцию и перевод строки для разделения кусков регулярного выражения, возможность писать комментарии, а если совместить это с именованием групп — то вообще все будет с читаемостью отлично. И при этом никаких лишних сущностей не требуется.
Ну и в результате те, кто применяют регулярки редко точно не будут заниматься тем, что описано в статье, а те, которые с ними работают часто — точно также не будут этого оделать, так как есть стандарт + инструменты + опыт взаимодействия со всем этим — и то, что описано у вас, будет восприниматься как пятая нога.
Насчет того, что привыкли – а насколько это ценно? В плюсах раньше триграфами писали - а глядишь, переучились. Можно простую метрику ввести. Берем сто человек, даем им почитать длинную регулярку и декомпозированную. Засекаем время, которое им потребуется для того, чтобы решить типичные задачи программиста: понять/рассказать, что в коде происходит, устранить ошибку, переместить код куда-то, модифицировать поведение и прочее. У меня определенный оптимизм есть насчет результатов.
В приведенном вами варианте регулярка становится размазанной по коду, начинает зависеть от еще одного языка иперестает быть воспринимаемой как регулярка и читаемости прибавляется только на последнем шаге при сборке — оно того ИМХО не стоит никаким образом, тем более — не стоит того чтобы менять устоявшиеся практики, ибо минусов куда больше, чем плюсов.
В современном мире новых программистов приходит больше чем есть привыкнувших. А порог входа таки высокий.
На одной из конференций в 2021 делали опрос про регулярки — примерно 60% их знало, около 35% могли писать что-то, и только около 8% понимали как оно работает. А теперь внезапно возраст — первая группа 18-45, вторая — 20-45, третья 29-45. Так что увы и ах.
Это, естественно, не означает какую-то оценку ума — это лишь означает важность опыта. Я встречал и 18-летнего дева, который в регулярках был, как в своем родном болоте — просто потому, что ему было интересно, и он на системника учился — но это скорее исключение, чем правило.
А вот попытки перетащить то, что надо просто понимать в то, в чем ты уже понимаешь — это как раз черта новых программистов — не в обиду вам будет сказано. Это как вечные попытки натянуть классическое ООП на JS с прототипами, хотя последнее при понимании отнюдь не хуже. Ну и из-за количества этих программистов в конце-концов в JS таки втянули это дело, до регулярок пока не добрались т-т-т )
Думаю, что знало там тоже на уровне: знало, что звездочка - это повтор.
Программисты уже 70 лет строят высокоуровневые языки поверх низкоуровневых. Олдфагов с хекскодами я не видел уже лет 20 (с ассемблером не сильно меньше), но ворчание их хорошо помню. Но все равно люди будут это делать, потому что удобно всем.
А вот если преимущество сводится лишь к тому «хочу, чтобы было так, как я знаю сейчас и как мне удобно, а как там было до меня — все равно» — это, ИМХО, тупик.
тема неоднозначная. Нехватка читаемости регулярок очевидна и обычно при повышение читаемости кода всегда страдает производительность так, что это просто цена. А вот платить ее или нет - зависит сугубо от проекта. Если предметная пестрит обилием регулярок, то почему и не попробовать из декомпозировать. а если у вас дюжина регулярок на вес проект, то наверно это не оправданно.
кроссмплатформенности
извините, не удержался

замедляет их выполнение
А вот это сильно не правда. В компилируемых языках можно такие конструкции сделать статически вычисляемыми и оно ничем не будет отличаться от того же месива, что и сплошной строке. В каком-нибудь JS почти наверняка в какой-то момент сработает оптимизация hotpath, однако я слабо представляю ситуацию, когда вы с каждым циклом пересобираете регулярку заново. То есть единоразовый вызов сборки регулярки не сказать чтобы сильно замедлит ваш код. Если конечно вы не запускаете эти регулярки на чем-то особенном.
В компилируемых языкахВ статье JS. Ну и если не приложить усилий — JS может каждый раз билдить рэгсп, причем это еще и от движка будет зависеть.
Грубо — все это очень сильно зависит от среды и языка — фишка в том, что вполне можно обойтись без этого и вообще не напрягаться по этому поводу.
В статье примеры на JS, но суть статьи про декомпозицию регулярок, а не про JS.
Ну и суть моего комментария тоже не про JS, а про то, что придумывать себе ветряные мельницы, а потом героически с ними бороться — плохо, ибо можно было всего этого избежать и вообще не заморачиваться со всем этим.
Декомпозиция регулярок возможна как стандартными, встроенными методами (о которых я писал в первом комментарии), так и спец. инструментарием, а не дополнительными сущностями на абсолютно другом языке.
Тогда можно сразу к старой доброй фразе вернуться - если у вас есть проблема и вы решаете её регулярками - у вас две проблемы.
Кстати, про кроссплатформенность пошутил, но забыл спросить, а какие собственно с этим могут быть проблемы? perl/js/c++/вставить свой вариант вроде везде относительно единообразно работают.
С кроссплатформенностью тоже есть нюансы — как и любой инструмент — регулярки развивались, мало того, что там существуют как минимум два стандарта, так еще и они внутри могут отличаться — например не поддерживать именованные группы — о чем тут уже было. Не поддерживать просмотр вперед/назад (или даже отмену или включение жадности). Есть очень интересная и крутая фича современных регулярок — рекурсивные регулярки (правда она еще сложнее для понимания, чем обычные варианты) — вот она поддерживается далеко не всеми. Короче, нюансов тоже предостаточно.
А, ну то есть кроссдвижковость, а не кроссплатформенность.
Но вообще, если брать PCRE — то таки да, с небольшими допущениями он практически везде работает одинаково.
Выражу другое мнение. Размазанная регулярка это как собираемый из кусков sql запрос. Кажется, лучше даже разделять пробелами и комменты сверху над блоками
Для того, чтобы рег выражение потом проверить где-то на стороне - его надо собрать из исходного кода, что капец гемор будет в вашем случае.
function wholeInput(regex) {
return "/^" + regex + "$/";
}
function zeroOrMore(regex) {
return "(" + regex + ")*";
}
const or = "|"
const onlyOneWhiteSpace="\\s(?!\\s)";
const suffix = zeroOrMore ("[a-zA-Z0-9\\!\\#\\%]" + or + onlyOneWhiteSpace)
const someEngPattern = wholeInput( "[A-Z0-9]+" + suffix)
Я вот не могу согласиться с тем, что этот код менее мерзкий.
Я знаю синтаксис регулярок, мне не нужно объяснять, что
(regex)*
— это сколько угодно повторов, я это сразу вижу. Если уж что-то и нужно объяснять, то мотивацию того, почему или зачем вы делаете именно так.Зато регулярку текстом я могу скопировать и вставить в условный regex101, чтобы её потестировать (или поправить). А такой код мне нужно частично исполнить, чтобы получить регулярку, которую можно тестировать. И после того, как я её потестирую, мне придётся исправления назад мучительно накатывать в код (с возможными ошибками в процессе).
Вот пример того, как длинная и сложная регулярка оформена в модуле fractions.py:
_RATIONAL_FORMAT = re.compile(r"""
\A\s* # optional whitespace at the start,
(?P<sign>[-+]?) # an optional sign, then
(?=\d|\.\d) # lookahead for digit or .digit
(?P<num>\d*|\d+(_\d+)*) # numerator (possibly empty)
(?: # followed by
(?:\s*/\s*(?P<denom>\d+(_\d+)*))? # an optional denominator
| # or
(?:\.(?P<decimal>d*|\d+(_\d+)*))? # an optional fractional part
(?:E(?P<exp>[-+]?\d+(_\d+)*))? # and optional exponent
)
\s*\Z # and optional whitespace to finish
""", re.VERBOSE | re.IGNORECASE)
Эту регулярку можно как есть вставить в regex101 (нужно не забывать ставить флаг extended).
Вот так это выглядит:

Там, если нужно, можно отладить все аспекты работы регулярки (группы и т.п.), а потом как есть скопировать назад в код.
Собрать конкретно эту регулярку часто можно даже на этапе препроцессинга, не исключено, что даже плагин к vscode есть, который ее подсветит: все куски-то статичные. Ну а если не подсветит, всегда можно console.log впихнуть и посмотреть.
wholeInput
— это провал.Такие функции первые претендуют на то, чтобы выехать в какой-то общий для всех модуль.
И вот после этого, чтобы собрать регулярку в консоли, мне нужно искать по проекту, где живут эти функции, копировать их отдельно в консольку, после этого уже собирать регулярку.
Кроме того, хоть слова
wholeInput
довольно понятные, но регулярки очень формальный язык, в которых каждый символ может иметь большое значение. Если я такое увижу в коде, то мне придётся полезть посмотреть, что именно имел в виду автор. А то мало ли, может он исходил из того, что у регулярки ещё какие-то флаги не выставлены?Если я сделаю так:
const reg = new RegExp(wholeInput('\d+'), 'gm'); // wholeInput же умная, наверное?
// [...'123\nasdf\n53'.matchAll(reg)] — это два match'а
то могу получить не тот результат, который ожидаю.
Странно, что никто не упомянул визуализаторы регулярок типа этого. Получается вполне понятная картинка. Так-то глаз уже набит, но пару раз приходилось пользоваться.
Визуализация

А если воспользоваться встроенными возможностями, чтобы сразу увидеть, какая группа для чего и заодно получить их по имени?
/^(?<day>0[1-9]|1[012])[- /.](?<month>0[1-9]|[12][0-9]|3[01])[- /.](?<year>(19|20)\d\d)$/
Какой кошмар. Любят же некоторые на пустом месте всё усложнить и понавязывать другим правила.
А чтобы быть конструктивным, предложу альтернативное решение проблемы запутанных регэкспов в коде. Некое расширение для IDE, превращающее регэкспы в интерактивные объекты, по нажатию на которые открывается окошко с парсером и редактором (подобных инструментов создано достаточно). Почти уверен, что в каком-нибудь VS Code это точно уже реализовано.
Зачастую regexp это write-only code. Его не надо читать, в него надо верить.
Если есть недоверие -- переписать. Благо, однострочник.
Да, я знаю про https://emailregex.com/ вариации. Это использование неподходящего инструмента для неподходящей цели. Но ведь можно же!
Это уж совсем капитуляция перед задачей. По-моему, не должно быть write-only кода, ни в тестах, ни в регулярках, ни в css или xpath-селекторах, ни даже в bash и perl-скриптах, если их читают и запускают несколько раз несколько людей.
Это избегание постановки ненужных задач. Задача вообще решить бизнес-проблему.
Регулярка это хороший способ быстро (по времени разработчика) решить часть из них.
Обертка если и нужна, то нужна не над регуляркой, а над доменной областью. Тут как с ORM -- мы упрощаем доступ к стандартно-структурированным данным, получая одновременно гомогенность и гарантии минимального качества. Когда ORM начинают использовать чтобы конструировать произвольной сложности SQL запросы вместо использования SQL запросов -- это серьёзная протечка абстракции, а не "мы сделали SQL читаемым".
Так и здесь, регулярка -- это упрощение одного из шагов разбора выражения. Когда регулярка перерастает однострочник надо её выкидывать и декомпозировать в грамматику а не в монстроидное дерево вызовов функций.
А клиенту и не надо знать код приложения. Написание регулярок - задача программиста. А разбираться, как она работает будут тестировщики. Если там написана фигня- претензии к программисту. Я понимаю, что есть уж совсем маленькие компании, где нет отдельного штата тестировщиков, и отдел разработки занимается несколькими задачами одновременно. Тут программист может подложить свинью коллегам.
Бывает нечто, о чем говорят: "смотри, вот это новое"; но это было уже в веках, бывших прежде нас. Эккл. 1:10
Сказать по правде, уже известное решение, с его действительно читабельным
const myRegex = SuperExpressive()
.startOfInput
.optional.string('0x')
.capture
.exactly(4).anyOf
.range('A', 'F')
.range('a', 'f')
.range('0', '9')
.end()
.end()
.endOfInput
.toRegex();
выглядит куда более стройным и последовательным, чем все эти метания между константами и функциями. Но даже и оно как-то не особо популярно. РНР форк вообще не взлетел. О причинах можно гадать, но факт налицо — попытки улучшить регулярные выражения не находят отклика у программистов.
Неплохое, сейчас добавим! Тут, наверное, та же ситуация, что и с другими библиотеками – не хочется тащить ее в код, если в проекте регулярок меньше пары десятков, но вполне можно – если их много, а сменяющие друг друга программисты уже стонут.
Даже если вы декомпозируете пример с датой из начала статьи, конструкция
(0[1-9]|[12][0-9]|3[01])
- это плохой и невкусно пахнущий код. Выковыряйте чиселку года вульгарным\d{1,4}
Это как раз-таки вполне здравый подход в данном случае в рамках применения регулярок, а ваши дополнительные проверки в коде как раз и есть плохой и невкусно пахнущий код.
Регулярки на то и регулярки, чтобы ими оставаться. А вы превратили относительно просто читаемое выражение в вырвиглазный псевдоскрипт, который малопереносим, да и сложен в проверке в каком-нибудь regex101. Вы с тем же успехом поучите математиков формулы писать. Вполне достаточно давать понятные имена самое переменной, использовать именованные группы или разбивку по строкам.
Мне не понятна ваша логика. Моя логика такая: проверка того, что выковырянное значение года больше нуля и меньше 3000
- это численная проверка. Натягивать на нее текстовый метод, пользуясь тем, что наш способ записи чисел таков – это применять инструмент не по назначению.
Вот если бы надо было проверить, что 988 год будет записан как 0988
или что 8 марта записывается не как 8.3
, а как 08.03
- тогда да, это явно текстуальная вещь, регэкспы очень в тему.
это не численная проверка а минимизация срабатываний.
Это потому что вы смешали теплое с мягким. Тема была о декомпозиции регулярных выражений, и в рамках их применения, да, регулярка тут лучше, чем какой-то странный код, который по факту занимается валидацией, а не сопоставлением, прикидываясь, что этим и занимается. Валидация же может быть любой сложности и далеко выходить за рамки поиска.
Опять эти войны остроконечников с тупоконечниками... Ни к чему это. Оба подхода имеют право на жизнь. Да, приведенные в статье примеры (статичных по сути) регулярок нет смысла декомпозировать в переменные/функции. Но в некоторых сложных случаях без такой декомпозиции не обойтись, особенно когда некоторые данные для регулярок подтягиваются извне и/или когда построение регулярки зависит от разных условий, т.е. когда регулярку надо формировать динамически. Пример: yargy-парсер, основанный на правилах.
У меня именно в том пойнт, что в обычном коде, без матлингвистики или построения компиляторов все строчки читаются за 0.5 секунд, а регулярка внезапно читается пять минут. Если это так, с ней не все в порядке, и надо искать способы, чтобы она тоже читалась быстрее.
Если рассматривать регулярку как "функцию", коей она и является под капотом, то вполне нормально, что на осмысление может потребоваться более "0.5 секунд"
Осмыслением регулярок должен заниматься профессиональный программист. А на деле, имеем то, что чуть менее чем полностью, этим занимается человек, бывший учителем русского языка и литературы, который отлаживает код с регулярками. Ему этот код, естественно, не понятен. В итоге говнокод превращается в говнокод в квадрате.
А нанять профи компания не может из-за бюджета и нехватки специалистов. И даже если компания наймёт его, есть проблема, что он просто уйдёт и надо снова искать человека, который разберётся с непонятным кодом.
Сугубо личное мнение (обрабатываю много текста регулярными выражениями и вообще их фанат):
Декомпозиция описанная в статье излишня. Она порождает нагромождение высокоуровневого кода над регулярным выражением.
В большинстве случаев даже для сложных регулярных выражений достаточно использовать именованные группы и форматирование отступами для того, чтобы минимизировать проблемы восприятия регулярных выражений.
Есть исключение: иногда нужно переиспользовать отдельные части регулярных выражений и иметь возможность менять шаблон в одной точке кода. Тогда лучше выделить часть шаблона в отдельную переменную и после этого включать эту часть в другое регулярное выражение через replace, чтобы не смешивать высокоуровневый синтаксис с регулярным выражением:
userID = "user\d+";
regex = "^USERID$";
regex = regex.replace( "USERID", userID );
Для питона есть похожая библиотека
https://github.com/manoss96/pregex
Узнал о ней из вот этой статьи, хорошее описание как она улучшает читабельность
https://towardsdatascience.com/pregex-write-human-readable-regular-expressions-in-python-9c87d1b1335
Скажите, что вы думаете про такой способ декомпозиции регулярных выражений:

https://github.com/rabestro/exercism.io/blob/master/kotlin/pig-latin/src/main/kotlin/PigLatin.kt
Декомпозируем регулярные выражения