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

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



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

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



    Не секрет, что многие при упоминании о регулярных выражениях вспоминают язык 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.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 68
    • +3
      некоторые бояться их как огня
      Это про меня :)

      Регулярок ужасно боюсь, а часто нужны… Спасибо большое, жду следующей части!
      • +9
        Есть хорошая книга «Дж. Фридл — Регулярные Выражения».
        Поверьте, они не кусаются)
        • 0
          Причем перевод тоже очень хорош.
          • 0
            Прочитал когда-то эту книгу, после прочтения стало все на свои места.
            Всем рекомендую)
            • 0
              Яростно подтверждаю. Ссался и боялся до уссачки. Яндекс выдал первым мануалом — Фридла. Сейчас я не знаю как я жил без регулярок — работа в шелле, скрипты, веб-скрипты — всё с регулярками.

              Даже работа в блокноте и та — только с регулярками.
          • 0
            Если с ними поработать на каком нибудь практическом примере, то они совсем и не такие страшные окажуться, а через некоторое время возникнет вопрос: «Как же я раньше без них обходился???».
            • –1
              парсер чужих страниц с совершенно разной семантикой и различными данными, которые нужно получить. HTML структурирован и идеально подходит не просто для тренировки в регулярных выражениях, но и поиска закономерностей и опорных точек для выделения нужного результирующего литерала
              • 0
                XPath для этого подходит гораздо лучше
                • 0
                  мой пример — парсинг заранее заданных 100 страниц с выдёргиванием не только «нужных» ссылок, но и текста в них (а если не текст, но картинка — скачать её к себе) — всё это в условиях незакрытых тегов и т.д. XPath первым рассматривался, но не взлетело. Зато какой простор для упражнений с регулярками, ммм!
                  • 0
                    простор для творчества это хорошо и полезно, но не закрытые теги — это ЗЛО с которым нужно бороться!
                    • +2
                      Вебмастера, не закрывающие теги, совсем не думают о тех, кто ворует их контент.
                    • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Для .NET для этой цели есть HtmlAgilityPack. Субъективно — намного удобнее, чем регулярки. Хотя регулярные выражения я люблю.
                      • –1
                        Это был сферический пример в вакууме для освоения. Проект сдан и я могу вздохнуть :)
                  • +3
                    А в них нет ничего страшного :)
                    Если дойдут руки, то хочу поверхностно описать как устроены эти самые регэкспы внутри. Очень важно для понимания регэкспов в целом.
                    • НЛО прилетело и опубликовало эту надпись здесь
                    • 0
                      У Вас неплохо получается, тема интересная, ждем еще статей! По ходу чтения возникло несколько мелких замечаний, но я бы предпочел отправить их в личную почту (если Вам это интересно), а не озвучивать здесь.
                      • 0
                        Да, конечно интересно, отправляйте.
                      • +1
                        Кстати, среди языков, поддерживающих регулярки на уровне синтаксиса, есть и Groovy. Примеры:

                        def source = 'this is some string just string plain string'

                        // во-первых, есть спец. форма записи строки, в которой не требуется квотирование
                        word = /\w+/

                        // оператор ~ позволяет прекомпилировать объект паттерна (так же поддерживает подстановку подстрок)
                        wholePattern = ~/($word $word)*/

                        // это проверка (оператором ==~) что вся строка содержит только слова, разделенные пробелами.
                        assert source ==~ wholePattern

                        // А это матчинг, операторв =~ создает объект матчера. Проверяем, что данному паттерну сооттствует три подстроки
                        def finder = (source =~ "st..ng*")
                        assert finder.getCount() == 3
                        • –1
                          За такими языками даже из бразуера выходить не надо :) JavaScript
                        • +12
                          First you have a problem, and then you think: «I know, I'll use regular expressions». Now you've got two problems.

                          источника уже не вспомню, извините.
                          • +2
                            Jamie Zawinski
                            • 0
                              Вот именно поэтому я написал эту статью :)
                            • 0
                              Фридл вроде делит их на детерменированные и недетерменированные. Первые используются в программировании и поддерживают сохранение состояний, но за счет этого более медленные, а вторые быстрые, но только для поиска.
                              • 0
                                Да, я хочу об этом написать в следующих статьях подробно.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                • 0
                                  Конечно, я это и имел в виду, глупо парсить регэкспами TCP пакеты.
                                • 0
                                  При работе с кириллицей используйте [А-Яа-яЁё] — а то на этом часто спотыкаются
                                  • 0
                                    Почему то у меня в PHP [А-Яа-яЁё] не работает — использую [\x7F-\xFF], а буква «ё» — остаётся для меня загадкой.
                                    • 0
                                      Всё зависит от кодировки кода и «испытуемого» текста
                                      • 0
                                        В общем совет новичкам — надо быть осторожней с кириллицей
                                    • 0
                                      У меня в Notepad++ для выражения [A-Я] не совпадает буква «Р». Вот такие вот фокусы.
                                      • 0
                                        Какие плагины для регулярок юзаешь?
                                    • 0
                                      Дамc, а у меня бука Х не работает :) Чудеса :)
                                      • +1
                                        При работе с кириллицой используйте юникод и модификатор \p. Не надо изобретать всякую фигню!
                                    • 0
                                      боян, но в тему:
                                      если вы программист и решили решить проблему с помощью регулярных выражений — то теперь у вас ДВЕ проблемы!
                                      • +1
                                        а если не программист — то нет проблем :)
                                      • 0
                                        [\.\(\)\{\^]
                                        Лишне так писать или нет, зависит исключительно от движка регулярных выражений.

                                        Например, в движке Oniguruma выражение [a-w&&[^c-g]z] несёт вполне определённый смысл: диапазон c-g исключается из a-w. Так что символ «&» нужно экранировать. Таких расширений синтаксиса много.

                                        В Perl6 регулярки вообще ни на что не похожи.
                                        • 0
                                          во! надо будет написать про регекспы в Perl6. только сначала справиться с собственным разрывом шаблона по этому поводу
                                          • 0
                                            Да, вы правы, поэтому я и начал с диалектов. Каждый раз надо смотреть как то сделано в конкретном языке или утилите.
                                          • 0
                                            Кстати! Вот мне приходилось сталкиваться с задачей нахождения НЕсовпадения части шаблона. Щас какой — нить синтетический пример придумаю…

                                            Есть строки «Мама мыла раму» «Мама мыла папу» «Мама мыла мыло» «Мама мыла маму» и т.п. Они попадают под выражение "^Мама мыла (\w+)$"
                                            А теперь мне понадобилось заматчить все строки, в которых мама моет все что угодно кроме рамы. Как? (Вариант с «заматчить „^Мама мыла раму$“ и выкинуть, а остальное пропустить» мне поему-то не подошел, не помню почему.)
                                            • 0
                                              Мама мыла [^раму]
                                              ?
                                              • 0
                                                Вроде это выделит «Мама мыла „+1 какой то любой символ, кроме р, а, м, у?
                                                В таком варианте “^Мама мыла [^раму]+$» выделит «Мама мыла „+ что угодно, где нет символов р, а, м, у
                                                • +1
                                                  вот держите

                                                  Мама мыла (?! раму)
                                                  если что
                                                  • +1
                                                    Забыли заматчить «не раму». Negative lookahead совпадает (правильно, конечно, «не совпадает», то так можно мозг сломать) с позицией, а не подвыражением, соответственно захватится только «Мама мыла » в строке «Мама мыла Ваню».
                                                    • 0
                                                      Работает, спасибо!
                                                      PS: по ссылке нужно убрать и снова поставить галку на «g» чтобы заработало как ожидается.
                                                • +2
                                                  Можно использовать поисковые конструкции чтобы исключить совпадение рамы.

                                                  Что-то типа ^Мама мыла (\w+)(?<! раму)$. Вторая часть (?! раму) говорит что в этой позиции подвыражение не должно совпадать назад (то что уже захватили в \w+). Но опять же надо понимать, что текст должен совпадать с НЕ рамой. В данном случае это не критично, потому что с \w идет квантификатор +, который требует хотя бы одной этой самой \w. Если бы стояла *, то были бы отличия в поведении.
                                                  • 0
                                                    Парсер наставил пробелов, но общий смысл должен быть понятен.
                                                    • 0
                                                      Спасибо, то что надо!
                                                      А поисковые конструкции совместимы с какими движками регулярных выражений? Меня интересуют Python, JS, PHP хотябы
                                                      • +1
                                                        Гуглите в сторону «negative lookahead» (как в примере Terion), либо «negative lookbehind» (как в моем) для своего языка. Обычно синтаксис этой части совпадает.
                                                        • +1
                                                          В джаваскрипте нет обратного поиска (lookbehind), а прямой есть.

                                                          В PHP есть всё.

                                                          Питона не знаю.
                                                    • –1
                                                      tsya.ru
                                                      • +1
                                                        Всегда жутко завидовал тем, кто понимает регулярки. Я когда вижу эту кашу из символов, мой мозг отказывается работать и тактично уходит в сторону заката.
                                                        • 0
                                                          Читайте Фридля, просвещайтеся.
                                                          • –1
                                                            У меня нет цели изучить регулярки — я просто ужасаюсь им :-)
                                                        • 0
                                                          Как раз сегодня открыл Фридла, поэтому большая часть вещей показалась очевидной, хотя дошел только до 42 страницы. Но поясните пожалуйста, что такое «квотирование». Гуглил — не нашёл, или в книге это понятие дальше рассматривается?
                                                          • +3
                                                            По-видимому, Lattyf переводит словом «квотировать» англоязычный термин «escape», означающий в данном случае «предпринять усилия для устранения служебного смысла некоторого символа».

                                                            Например, символ точки («.») в регулярных выражениях имеет служебный смысл («найти на этом месте любой символ»). Поэтому, чтобы искать точку в буквальном смысле («найти на этом месте точку, только точку и ничего, кроме точки»), в регулярном выражении перед точкою надо поставить обратную косую черту («\.»).
                                                            • +3
                                                              Еще иногда «escaping» переводят как «экранирование».
                                                            • +1
                                                              Ну здесь автор имеет ввиду написание "\" перед метасимволом, чтобы метасимвол читался как обычный символ.

                                                              ".jpg$" => ".jpg", «ajpg», «0jpg»
                                                              "\.jpg$" => ".jpg"
                                                              • 0
                                                                Выше уже пояснили, но хочу лобавить, что как раз один из тех терминов, для которого все переводы для непосвященного не очевидны.
                                                                Когда я первый раз увидел перевод «экранировать» тоже не смог сразу найти.
                                                              • +5
                                                                [xkcd: Regular Expressions]
                                                                • +1
                                                                  ться-тся
                                                                  • 0
                                                                    Объясните, пожалуйста, что означает «квотирование»? Это экранирование бэкслешем?
                                                                    • +1
                                                                      Да, Вы правы, это экранирование, escaping.

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