Webrium
Подписаться

Регулярное выражение, которое можно поддерживать

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

Выражение не сокращают, а разбирают на именованные части — как кодстрокисовпало

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

Почему они становятся нечитаемыми

Не потому, что синтаксис плохой. Потому что он поощряет уплотнение: любое усложнение задачи добавляет несколько символов в ту же строку, и строка растёт, не меняя формы. Код при усложнении разбухает и требует разбиения на функции — выражение просто становится длиннее.

Второе свойство: у выражения нет промежуточных состояний. Функцию можно читать по шагам, выражение приходится удерживать целиком. Чем оно длиннее, тем больше нужно удерживать, и в какой-то момент этот объём перестаёт помещаться.

Отсюда практический вывод, из которого следует всё остальное: выражение надо не сокращать, а разбирать на части — так же, как код.

Тупик: добавить символов, пока не заработает

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

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

Есть и обратный вариант той же ошибки: сузить выражение под конкретный неудачный пример. Оно перестаёт ловить и половину правильных случаев, но их в тестовом наборе не оказалось.

Общая причина в обоих случаях — правка вслепую: меняют выражение, не имея набора примеров, на котором видно и что должно ловиться, и что не должно.

Что делает выражение поддерживаемым

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

Многострочная запись с комментариями. Большинство языков умеет игнорировать пробелы и переводы строк внутри выражения. Тогда его можно записать столбиком, по смысловому куску на строку, с пояснением рядом. Это то же самое выражение, только читаемое.

Разбиение на части. Выражение можно собирать из именованных кусков, объявленных отдельно. Кусок «дата», кусок «идентификатор», кусок «разделитель» — каждый читается и проверяется сам по себе, а целое становится их сборкой.

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

Осторожность с ненасытностью. Конструкция, которая берёт «сколько получится», по умолчанию берёт слишком много и останавливается на последнем совпадении, а не на первом. Это не ошибка, это документированное поведение, но оно противоречит интуиции почти всех, кто читает выражение впервые.

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

Пробелы, которых не видно. Табуляция, неразрывный пробел, символ возврата каретки в конце строки — всё это выглядит одинаково в редакторе и по-разному для выражения. Значительная часть случаев «не совпадает, хотя должно» объясняется именно этим, и лечится не правкой выражения, а нормализацией входа до него.

Где регулярные выражения — неправильный инструмент

Вложенные структуры. Разметка, скобки, вложенные кавычки. Выражение по своей природе не умеет считать вложенность, и попытка обойти это даёт конструкцию, которая работает на трёх уровнях и ломается на четвёртом. Для таких данных берут разбор, а не поиск.

Форматы, у которых есть библиотека. Адреса электронной почты, номера телефонов, даты, ссылки. Все они выглядят простыми и все имеют пограничные случаи, которых не ожидают. Готовая библиотека уже наступила на них, ваше выражение — ещё нет.

Проверка того, что важно проверить по-настоящему. Выражение говорит, что строка похожа на адрес. Оно не говорит, что адрес существует. Для второго нужен другой механизм, и подменять одно другим — типичный источник ложной уверенности.

Как их проверять

Так же, как код: набором примеров, где записано и что должно совпасть, и что не должно. Второй список важнее первого — именно он ловит расширение при правках.

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

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

Отдельно стоит проверить поведение на длинной строке, которая почти совпадает. Некоторые конструкции при таком входе начинают перебирать варианты, и время растёт непропорционально длине. Это не теоретическая опасность: если выражение применяется к данным, пришедшим снаружи, это способ уронить обработку.

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

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

Позиция редакции

Мы считаем, что регулярное выражение длиннее одной строки должно записываться столбиком с комментариями и сопровождаться набором примеров — включая примеры того, что совпадать не должно. Практический вывод: если выражение нельзя объяснить вслух за полминуты, его надо не сокращать, а разбирать на именованные части.

Мы неправы, если выражение одноразовое: для разового поиска по файлу в редакторе вся эта обвязка — лишняя работа, там выражение живёт минуту и умирает вместе с задачей. Граница проходит там, где выражение попадает в код и переживает своего автора.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Шесть столбцов потерь времени в терминале: видимые потери — нажатия, опечатки, повторяемые пути — низкие, а невидимые — вспомнить команду, найти каталог, восстаЧасы — не в наборе
Практика

Терминал, который экономит часы, а не минуты

Руками: где в работе с терминалом теряются часы и какие приёмы их возвращают, — скорость набора здесь ни при чём.

Контракт существует, только если записан отдельно от кода обеих сторонполе вне схемы
Усиление

JSON как контракт: где он подводит

Подтверждение с практической стороны: договорённость держится не проверками на приёме, а описанием, вынесенным из кода наружу.

Конфликт — это пересечение двух намерений, а не поломка кодаслилось самоконфликт
Углубление

Конфликт слияния: почему он ваш и что с ним делать

Глубже про конфликт: он означает не сломанный код, а пересечение двух намерений, и выбор своей стороны целиком — плохой ответ.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть