Некоторые ошибки при написании регэкспов

  • Tutorial
По мотивам переведенной статьи

Впервые я увидел регэкспы еще в школе в перле, и в общем полюбил их с первого взгляда, конечно после того как разобрался что это такое :). И с превеликим энтузиазмом принялся все регэкспить. Конечно, при этом, понабивал себе шишек на мозг, но любить их не перестал. Со временем любая искренняя любовь взрослеет и перерастает в глубокую привязанность, с понимаем того, что объект твоего чувства может быть несовершенным, но от этого не менее любимым.

Итак, несколько способов предохраниться от разочарования в этом мощном и прекрасном инструменте…



Ancient Egyptian Regexp
  • Используйте только те регэкспы, которые вы написали сами

    .

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

  • Запомните значения мета-символов в контексте



    Очень важно знать и помнить что означают основные символы и в каком контексте их применять. Например знак "^" в начале выражения и в перечислении "[..]" имеет разное значение.
    К сожалению усидчивость при заучивании таблиц исчезает после таблицы умножения и вбить себе в голову несколько таблиц из хелпа получается только в виде набитых шишек.

    Как признак того что вы помните и успешно применяете данное правило это правильное экранирование мета-символов. Такое [a-z0-9\.-] матерый регэкспщик не напишет. Потому что точка в перечислении всего лишь точка и ее не надо экранировать, зато дефис в перечислении — мета-символ. В данном примере дефис стоит в конце и правильно распознается как обычный символ, но достаточно другому копи-пастеру «улучшить» его, добавив еще один символ в перечисление, то в лучшем случае будет ошибка интервала, а в худшем получим непростой баг: например вот в этом [a-z0-9\.-\/] парсер воспримет "\.-\/" как интервал а не как 3 символа, как вроде-бы ожидалось.

  • Думайте как работает парсер. Старайтесь ему помочь.



    Конечно современные парсеры достаточно сложны что бы быть предметом общественного внимания, но в принципе они работают логично. Если вы написали подряд несколько паттернов то они и будут искаться подряд. И лучше всего так именно и делать — вписывать паттерны друг за дружкой и не забывать про разделители. Типа этого:
    /^<[^>]+>$/
    где в реальности три шаблона:
    • символ "<"
    • не ">" символов один или больше одного
    • символ ">"

    И вы должны давать парсеру как можно больше островков стабильности — конкретных символов «http», в крайнем случае ограничивайте перечислением "[\w\s]+" или давайте несколько вариантов "(http)|(ftp)". И чем меньше будет ".+" и "*" тем стабильнее и быстрее будет результат.

    если вдруг вы решите что так будет лучше:
    /^<.+>$/
    то скорее всего нарветесь на забавные проблемы. Поскольку стандартные имплементации по умолчанию «жадные» то внутри тега окажется все от начала первого тега, до конца последнего. Подробнее об этом чуть позже.

    Да и старайтесь избегать неопределенностей. Например
    /([a-z]+)([^<]+)*>/
    может разбить последовательность букв любым способом. Опять же, поскольку по-умолчанию квантификаторы «жадные» то в первый шаблон «правильно» попадет слово до первой не-буквы и остальное до ">" во-второй. Наверно правильнее было-бы написать
    /([a-z]+)([^a-z<>]*)>/
    но возвращаясь к первому правилу — это зависит от задачи.

  • Не используйте регулярные выражения для разбора с вложенностью



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

    Для примера возмем кусочек html:
    <span>text</span><span>text<span>text</span></span>
    как видно могут быть
    • вложенные теги
    • последовательные теги

    если с последовательными тегами можно разобраться с помощью «не жадных» квантификаторов или стопив выражение на начале следующего ( было выше типа "[^<]+" ), то добиться вложенности практически нереально. То есть для конкретных случаев это реализуемо, но всегда найдется вариант, который поломает нашу стройную картину.

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

  • Регулярные выражения — это разбор строки. Все прочее лучше делается по-другому



    Возмем код «валидации» IP
    /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/

    зачем по-символьно разбирать числа? в данном примере будут «валидны» адреса 0.0.0.0 и 255.255.255.255, не говоря уже о прочих multicast-ах.
    В таких случаях лучше делать простой фильтр на откровенно невалидный ввод ( типа SQL injection ) /^\d+\.\d+\.\d+\.\d+$/ а уж что там за IP — разобрать отдельно



Обычно такие статьи называются типа «10 поз которые должен знаться каждый»… я намеренно заменил <ol> на <ul> потому что я не Бог, я только учусь.
Я написал исключительно то что помню и на что нарывался. Думаю что каждый со сравнимым опытом может продемонстрировать свои шишки.
Поделиться публикацией

Комментарии 45

    0
    Регекспы — это черная магия
    Прикольно, что на Хабре живет черный маг :)
      +22
      Регекспы — это рядовой рабочий инструмент.
        +19
        Рядовой рабочий инструмент черного мага ;)
          +1
          для обычных людей все программисты — черные маги, а программисты умеющие эффективно и правильно решать задачи регулярными выражениями видимо должны быть чернейшими магами =)

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

          Стоит наверное еще добавить один немаловажный пункт:
          «Не используйте регулярные выражения для решения простых задач, которые можно решить другими, более простыми и быстрыми средствами» или «Пользуйтесь, но без фанатизма»
          0
          Если у тебя есть проблема, и ты подумал «сейчас я ее решу с помощью регэксов» — значит, у тебя уже две проблемы :)) ©
          Если есть возможность, лучше использовать генератор парсеров по EBNF или PEG грамматикам.
          Но если обойтись без регэксов всё-таки нельзя, то надо хотя бы использовать правильные инструменты. Например, RegexBuddy — подсвечивает синтаксис, позволяет просмотреть структуру выражения в виде дерева. Но самое главное — там есть отладчик. Упрощает работу с регэксами на порядок, рекомендую.
        +1
        Насчёт вложенности:
        my $rem;
        $rem = qr/ (?>[^()]+|\((??{$rem})\))*/;
        $str =~ /\($rem\)/;
        Мысль я думая понята. Так с помощью динамич регекспов можно решать различные проблемы вложенности :) В перле есть их поддержка, Интерестно как обстоят дела в др языках.
          +1
          Вы не поняли. Регулярные выражения они на то и регулярные, что описывают регулярные языки (ну, по идее — в Perl/и др. можно распознавать и более «сложные» языки). А регулярные языки не могут выразить вложенность!

          Для более сложного есть контекстно-независимые грамматики, которые удобнее для таких задач. Попробуйте написать парсер XML (или же S-выражений, что проще) на регулярках и на CFG и как говорится, почувствуйте разницу.
            +1
            Не понял философию про регулярные/нерегулярные языки. Если у вам встречается рекурсивные данные, то от рекурсии не уйти, если только не задать максимальную глубину рекурсии. А в некоторых задачах глубина рекурсии данных может быть большой. В этом случае вам все равно придется рекурсивно парсить. Вопрос только чем — рекурсивными регулярками или рекурсивным кодом.

            На практике в PHP очень часто использую рекурсивные регулярки (?R). Один из примеров — парсер уравнения (может быть бесконечное кол-во вложенных скобок). Среди известных примеров, библиотека DbSimple (http://dklab.ru/lib/DbSimple/), ф-ция _expandPlaceholdersFlow — осуществляет подстановку плейсхолдеров. По-моему правильно и удобно.
              +2
              > Вопрос только чем — рекурсивными регулярками или рекурсивным кодом.

              Парсером LL, LR или там GLR (в общем — даешь ему BNF, а он делает свое черное дело). Самое главное — *парсером*. На входе строка (или последовательность token'ов), на выходе — AST.

              > На практике в PHP очень часто использую рекурсивные регулярки (?R).

              И абсолютно зря. Нормальные люди описывают грамматики в BNF, а затем используют либо Yacc+Flex, либо комбинаторные парсеры.
                0
                Не уверен, что Yacc+Flex выйграет у регулярок PHP по сумме коэффициентов
                время, потраченное на основение + время потраченное настройку + кроссплатформенность ;)
                  +1
                  Еще как выиграет. Все дело в том, что именно *фундментальные ограничения* регулярных выражений и *практическая необходимость* в разборе более широкого класса грамматик, кроме всего прочего, мотивировали создание BNF и таких инструментов, как Yacc/Flex.
          +4
          в komodo IDE есть отличная тулза — rx toolkit — пишешь текст, пишешь постепенно регексп и оно сразу подвесчивает что нашло. конечно такое можно и самому сообразить…

          советую по регулярным выражениям почитать «регулярные выражения. 2-е изд. / Дж. Фридл. — СПб.: Питер, 2003. — 464с (ISBN 5-272-00331-4)»
            +4
            Кстати, книга уже давно в свободном доступе в электронном виде. www.softtime.ru/info/fridl.php
              0
              Хм, не думаю, что издательство «Питер» эту книгу «освободило». Но ввиду мизерного тиража (3000 экземпляров) ничего другого не остается, кроме как скачивать из альтернативных источников :(
                0
                Последние русское издание, если не ошибаюсь выпустило издательство Слово (которые букс, ру)
                  0
                  Регулярные выражения, 3-е издание / Дж. Фридл. — СПб.: Символ-Плюс, 2008 (ISBN-10: 5-93286-121-5)
                  Тираж 2000 экз. :(
                    0
                    Блин, да, Символ Плюс, в мозгу чего то клинануло, букс.ру эт их лавочка :)
              0
              Лучше 3 издание, Питер 2008, Орейли 2006.
                +1
                В EPIC на эклипс есть подобное, но там кнопочку надо жать :)
                  0
                  Я по наводке поставил и радуюсь ( пока глюков не было )
                  RegexUtil, ставиться regex-util.sourceforge.net/update/
                  +2
                  Есть и отдельная утилита

                  weitz.de/regex-coach/

                  и не одна
                  www.regular-expressions.info/regexbuddy.html
                    +1
                      +2
                      + 1000 за, чтение фридела, статья спорная,

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

                      символ «-» не всегда указывает на диапазон (на пример если он будет в самом начале)

                      если говорить, что давайте писать не так «…» а вот так «…» то хорошо бы говорить почему именно, довод, «выглядит проще», «красивее» — это не правильно
                        0
                        Я пишу из своей практики. Если кому-то повезло с чужими выражениями, я рад за него. Но в реальности приходиться менять условия и тут придется разобрать чужой регэксп, по-любому.

                        Скорость работы… пишите проще, меньше шаблонов и больше конкретики — я писал об этом в статье и будут Ваши выражения быстры и верны.

                        Да, симовол "-" и в конце и в начале диапазона воспринимается как просто символ. Можно это использовать, главное не нарваться на описанную выше проблему. Я регулярно наблюдаю эту ошибку, поэтому решил что это важно.

                        Мне казалось я написал причины… но все же аргумент за простоту и красоту:
                        Код, имеет свойство переписываться и для меня важно быстро и безошибочно определить что он делает, поменять его, оставив простоту и ясность. Я всегда могу усложнить код, но упростить, как правило, очень сложно.
                      +3
                      Статья полезная!
                      Но забыл написать про «ленивые» и «жадные» регэпсы.
                      Часто на них ошибки делают.
                        +4
                        * «ленивые» и «жадные» квантификаторы
                          0
                          Стоит об это написать?
                            +2
                            Думаю, что про это надо написать. Плюс стоит описать принцип работы, т.е. каким образом обрабатывается строка по регулярному выражению. Сейчас пример не вспомню, но в какой-то книге было это объяснение и в конце приведён регексп типа b*b*b*b*b*b*b*b*b*b*b*$ применительно к строке bbbbbbbbbb, ну и пояснения, что такая регулярка будет выполняться очень долго, так как будет сперва хватать всю строку, потом посимвольно откатываться назад и так для каждого символа (не квантификатора) в регулярке.
                          +2
                          С вложенность (как раз парсинг html) строили динамеческие регэкспы, исходя из максимальной длины вложенности. Для большей части html который нам попадался (популярные интернет порталы) хватало вложенности 2-3, иногда в частных случаях 4-5 ( например тэги div вконтакте :) ).
                          На такой вложенности скорость работы вполне терпима, а вот альтернатива в виде посимвольный разбора строки с составлением объекта-прокси (использовали open source библиотеку на AS3), на больших страницах потребляла на порядок больше системных ресурсов.

                          Спасибо за статью )
                            +1
                            немного оффтоп, по поводу IP — недавно обнаружил (что в общем не секрет, просто я как-то пропустил этот момент) что IP резолвятся как в обычном и десятичном представлении, так и в хексе и восьмеричном. То есть взяв скажем ya.ru и один из адресов 77.88.21.8, можем достучаться по нему как 1297618184, 0x4D581508 и 011526012410. Опять же, ничего нового для тех кто знает/помнит особенности IPv4, просто от парсинга IP-адреса вспомнилось.
                              0
                              послесловие порадовало
                                +2
                                >Опять же, поскольку по-умолчанию парсеры «жадные» то в первый шаблон...

                                Хотелось бы поправить. Жадные не парсеры, а квантификаторы в регулярных выражениях ;)
                                  +1
                                  Спасибо за уточнение, некоторые вещи вылетают из головы когда тебя и так понимают. А термины должны быть точными.
                                  +5
                                  > В таких случаях лучше делать простой фильтр на откровенно невалидный ввод ( типа SQL injection )
                                  Вы по книгам Фленова учились?
                                  НА SQL-inj НЕ НУЖНО проверять. Нужно просто правильно работать с данными.

                                  Ну и дополнение: не стоит делать BB-теги на регекспах, за исключением тех случаев, когда вам не страшны XSS.
                                  Чтобы сделать BB-коды безопасными, надо использовать алгоритм State Machine. И есть готовые реализации.
                                    0
                                    Не могу не согласиться. Вопрос в том как проверять.

                                    PS. Без понятия кто такой Фленов :) Учился в универе. Давно. На физика.

                                    +2
                                    А ещё часто народ забывает экранировать динамические части regexp-ов.
                                    В PHP для этого есть функция php.net/preg_quote

                                      +1
                                      >> Втаких случаях лучше делать простой фильтр на откровенно невалидный ввод
                                      >> /^\d+\.\d+\.\d+\.\d+$/

                                      Смотря откуда ip берётся, и куда. Все прекрасно помнят дырку в IPB с заголовком HTTP_CLIENT_IP…

                                      +2
                                      Никогда небыло таких проблем с рэгэкспами только с буквами вконце выражения типо x,m, s всегда забывал предназначения, приходилось гуглить ;)
                                        0
                                        $_='192.168.0.1';
                                        $ip_is_valid = do{my($r,$x)=('^(\d+)'.('\.(\d+)'x3).'$',1);for(m/$r/gx){$x&=($_>=0&&$_<=255);}$x;};
                                          0
                                          А зачем, если не секрет, используете модификатор gx?
                                          Какой смысл в переменной $r?
                                          0
                                          \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
                                            0
                                            а если 256.256.256.256?
                                              0
                                              тогда 999.999.999.999 и нет проблем
                                            0
                                            Для .NET-чиков рекомендую:
                                            Regular Expression Recipes for Windows Developers. A Problem-Solution Approach. NATHAN A. GOOD

                                            www.amazon.com/Regular-Expression-Recipes-Windows-Developers/dp/1590594975
                                            Посимвольный разбор типичных задач.
                                            Хотя лично я учился по Python-овским regexp-ам и хелпам от Питона.

                                              0
                                              хотел бы заказать хабре статью про I2P

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое