RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 1
RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 2
20 коротких шагов для освоения регулярных выражений. Часть 3
Эта, заключительная часть, в ее середине коснется таких вещей, которыми пользуются в основном мастера регулярных выражений. Но вам же легко давался материал из предыдущих частей, ведь правда? Значит и с этим материалом вы справитесь с той же легкостью!
Оригинал здесь
<h2>Шаг 16: группы без захвата
(?:)
</h2>
В двух примерах на предыдущем шаге мы захватывали текст, который в действительности нам не нужен.
В задаче "Размеры файлов" мы захватили пробелы перед первой цифрой размеров файлов, а в задаче "CSV" мы захватили запятые между каждым токеном. Нам не нужно захватывать эти символы, но нам нужно использовать их для структурирования нашего регулярного выражения. Это идеальные варианты для использования группы без захвата, (?:)
.
Группа без захвата делает именно то, на что это похоже по смыслу - она позволяет группировать символы и использовать их в регулярных выражениях, но не захватывает их в пронумерованной группе:
pattern: (?:")([^"]+)(?:") string: I only want "the text inside these quotes". matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ group: 1111111111111111111111111111(Пример) Теперь регулярное выражение соответствовует тексту в кавычках, а также самим символам кавычек, но группа захвата захватила только текст в кавычках. Зачем нам так делать? Дело в том, что большинство движков регулярных выражений позволяют вам восстанавливать текст из групп захвата, определенных в ваших регулярных выражениях. Если мы сможем обрезать лишние символы, которые нам не нужны, не включив их в наши группы захвата, то это упростит анализ и манипулирование текстом позже. Вот как можно почистить парсер CSV из предыдущего шага:
pattern: (?:^|,)\s*(?:\"([^",]*)\"|([^", ]*)) string: a, "b", "c d",e,f, "g h", dfgi,, k, "", l matches: ^ ^ ^^^ ^ ^ ^^^ ^^^^ ^ ^ group: 2 1 111 2 2 111 2222 2 2(Пример) Здесь есть несколько вещей, на которые стоит <mark>обратить внимание:</mark> Во-первых, мы больше не захватываем запятые, так как мы изменили группу захвата
(^|,)
на группу без захвата (?:^|,)
. Во-вторых, мы вложили группу захвата в группу без захвата. Это полезно, когда, например, вам нужно, чтобы группа символов отображалась в определенном порядке, но вы заботитесь только о подмножестве этих символов.
В нашем случае нам нужно, чтобы символы не кавычек и не запятые [^",]*
отображались в кавычках, но на самом деле нам не нужны сами символы кавычек, поэтому их не нужно было захватывать.
Наконец, <mark>обратите внимание</mark>, что в приведенном выше примере также есть совпадение нулевой длины между символами k
и l
. Кавычки ""
являются искомой подстрокой, но между кавычками нет символов, поэтому соответствующая подстрока не содержит символов (имеет нулевую длину).
<h3>Закрепим знания? Вот две с половиной задачи, которые помогут нам в этом:</h3>
Используя группы без захвата (и группы захвата, и классы символов, и т.д.), напишите регулярное выражение, которое захватывает только правильно отформатированные размеры файлов в строке ниже:
pattern: string: 6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB. matches: ^^^^^ ^^^^^ ^^^^^^ ^^^^ group: 11111 1111 11111 111(Решение) Открывающие HTML-теги начинаются с символа
<
и заканчиваются символом >
. Закрывающие теги HTML начинаются с последовательности символов </
и заканчиваются символом >
. Имя тега содержится между этими символами. Можете ли вы написать регулярное выражение, чтобы захватить только имена в следующих тегах? (Возможно, вам удастся решить эту проблему без использования групп без захвата. Попробуйте решить это двумя способами! Один раз с помощью групп и один раз без них.)
pattern: string: <p> </span> <div> </kbd> <link> matches: ^^^ ^^^^^^ ^^^^^ ^^^^^^ ^^^^^^ group: 1 1111 111 111 1111(Решение с помощью групп без захвата) (Решение без помощи групп без захвата) <h2>Шаг 17: обратные ссылки
\N
и именованные группы захвата</h2>
Хоть я и предупреждал вас во введении, что попытка создать HTML-парсер при помощи регулярных выражений обычно приводит к душевным страданиям, последний пример - хороший переход к другой (иногда) полезной функции большинства регулярных выражений: обратным ссылкам (backreferences).
Обратные ссылки похожи на повторяющиеся группы, в которых вы можете попытаться захватить один и тот же текст дважды. Но они отличаются в одном важном аспекте - они будут захватывать только один и тот же текст, символ за символом.
В то время как повторяющаяся группа позволит нам захватить что-то вроде этого:
pattern: (he(?:[a-z])+) string: heyabcdefg hey heyo heyellow heyyyyyyyyy matches: ^^^^^^^^^^ ^^^ ^^^^ ^^^^^^^^ ^^^^^^^^^^^ group: 1111111111 111 1111 11111111 11111111111(Пример) ... то обратная ссылка будет соответствовать только этому:
pattern: (he([a-z])(\2+)) string: heyabcdefg hey heyo heyellow heyyyyyyyyy matches: ^^^^^^^^^^^ group: 11233333333(Пример) Повторяющиеся группы захвата полезны, когда вы хотите повторно сопоставить один и тот же шаблон, тогда как обратные ссылки хороши, когда вы хотите сопоставить один и тот же текст. Например, мы могли бы использовать обратную ссылку, чтобы попытаться найти подходящие открывающие и закрывающие HTML-теги:
pattern: <(\w+)[^>]*>[^<]+<\/\1> string: <span style="color: red">hey</span> matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ group: 1111(Пример) <mark>Обратите внимание</mark>, что это чрезвычайно упрощенный пример, и я настоятельно рекомендую вам не пытаться писать анализатор HTML на основе регулярных выражений. Это очень сложный синтаксис, и вам, скорее всего, станет плохо. Именованные группы захвата очень похожи на обратные ссылки, поэтому я кратко расскажу о них здесь. Единственная разница между обратными ссылками и именованной группой захвата состоит в том, что... именованная группа захвата имеет имя:
pattern: <(?<tag>\w+)[^>]*>[^<]+<\/(?P=tag)></tag> string: <span style="color: red">hey</span> matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ group: 1111(Пример) Вы можете создать именованную группу захвата с помощью (?<name>...) или (?'name'...) синтаксиса (.NET-совместимое регулярное выражение) или с таким синтаксисом (?P<name>...) или (?P'name'...) (Python-совместимое регулярное выражение). Поскольку мы используем PCRE (Perl-совместимое регулярное выражение), которое поддерживает обе версии, мы можем использовать любой из них здесь. (Java 7 скопировала синтаксис .NET, но только вариант с угловыми скобками. прим. переводчика) Чтобы повторить именованную группу захвата позже в регулярном выражении, мы используем \<kname> или \k'name' (.NET) или (?P=name) (Python). Опять же, PCRE поддерживает все эти различные варианты. Вы можете прочитать больше об именованных группах захвата здесь, но это была большая часть того, что вам действительно нужно знать о них. <h3>Задачка нам в помощь:</h3> Используйте обратные ссылки, чтобы помочь мне вспомнить ... эммм ... имя этого человека.
pattern: string: "Hi my name's Joe." [later] "What's that guy's name? Joe?". matches: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ group: 111(Решение) <h2>Шаг 18: взгляд вперед (lookahead) и взгляд назад (lookbehind)</h2> Сейчас мы углубимся в некоторые расширенные функции регулярных выражений. Всё, вплоть до шага 16, я использую довольно часто. Но эти последние несколько шагов предназначены только для людей, которые очень серьезно используют regex для сопоставления очень сложных выражений. Другими словами, мастера регулярных выражений. "Взгляд вперед" и "взгяд назад" могут показаться довольно сложными, но на самом деле они не слишком сложны. Они позволяют вам сделать что-то похожее на то, что мы делали с группами без захвата ранее - проверять, существует ли какой-либо текст непосредственно перед или сразу после фактического текста, который мы хотим сопоставить. Например, предположим, что мы хотим сопоставлять только названия вещей, которые люди любят, но только если они с энтузиазмом относятся к этому (только если они заканчивают свое предложение восклицательным знаком). Мы могли бы сделать что-то вроде:
pattern: (\w+)(?=!) string: I like desk. I appreciate stapler. I love lamp! matches: ^^^^ group: 1111(Пример) Вы можете видеть, как указанная выше группа захвата
(\w+)
, которая обычно соответствует любому из слов в отрывке, соответствует только слову lamp. Положительный "взгляд вперед" (?=!)
означает, что мы можем сопоставлять только те последовательности, которые заканчиваются на !
но, на самом деле, мы не сопоставляем сам символ восклицательного знака. Это важное различие, потому что с группами без захвата мы сопоставляем символ, но не захватываем его. С помощью lookaheads и lookbehinds мы используем символ для построения нашего регулярного выражения, но затем мы даже не сопоставляем его с ним самим. Мы можем сопоставить его позже в нашем регулярном выражении.
Всего существует четыре вида lookaheads и lookbehinds: положительный взгляд вперед (?=...), отрицательный взгляд вперед (?!...), положительный взгляд назад (?<=...) и отрицательный взгляд назад (?<!...). Они делают то, на что они похожи - положительные lookahead и lookbehind позволяют обработчику регулярных выражений продолжать сопоставление, только когда текст, содержащийся в lookahead / lookbehind, действительно совпадает. Отрицательные lookahead и lookbehind делают противоположное - они позволяют регулярному выражению совпадать только тогда, когда текст, содержащийся в lookahead / lookbehind, не совпадает.
Например, мы хотим сопоставить имена методов только в цепочке последовательностей методов, а не объект, над которым они работают. В этом случае каждому имени метода должен предшествовать символ .
. Здесь может помочь регулярное выражение, использующее простой взгляд назад:
pattern: (?<=\.)(\w+) string: myArray.flatMap.aggregate.summarise.print! matches: ^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^ group: 1111111 111111111 111111111 11111(Пример) В приведенном выше тексте мы сопоставляем любую последовательность символов слова
\w+
, но только в том случае, если им предшествует символ .
. Мы могли бы достичь чего-то подобного, используя группы без захвата, но результат получится немного грязнее:
pattern: (?:\.)(\w+) string: myArray.flatMap.aggregate.summarise.print! matches: ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^ group: 1111111 111111111 111111111 11111(Пример) Несмотря на то, что он короче, он соответствует символам, которые нам не нужны. Хотя этот пример может показаться тривиальным, lookaheads и lookbehinds действительно могут помочь нам очистить наши регулярные выражения. <h3>Осталось совсем немного до финиша! Следующие 2 задачи приблизят нас к нему еще на 1 шаг:</h3> Отрицательный lookbehind (?<!...) позволяет движку регулярных выражений продолжать попытки найти совпадение, только если текст, содержащийся внутри отрицательного lookbehind, не отображается до оставшейся части текста, с которой нужно найти соответствие. Например, мы могли бы использовать регулярное выражение, чтобы найти соответствия только фамилиям женщин, посещающих конференцию. Для этого мы бы хотели убедиться, что фамилии человека не предшествует
Mr.
.
Можете ли вы написать регулярное выражение для этого? (Можно предположить, что фамилии имеют длину не менее четырех символов.)
pattern: string: Mr. Brown, Ms. Smith, Mrs. Jones, Miss Daisy, Mr. Green matches: ^^^^^ ^^^^^ ^^^^^ group: 11111 11111 11111(Решение) Предположим, что мы очищаем базу данных и у нас есть столбец информации, который обозначает проценты. К сожалению, некоторые люди записали числа в виде десятичных значений в диапазоне [0,0, 1,0], в то время как другие написали проценты в диапазоне [0,0%, 100,0%], а третьи написали процентные значения, но забыли литерал знак процента
%
. Используя отрицательный взгляд вперед (?!...), можете-ли вы пометить только те значения, которые должны быть процентами, но в которых отсутствуют знаки %
? Это должны быть значения, строго превышающие 1,00, но без конечного %
. (Ни одно число не может содержать более двух цифр до или после десятичной точки.)
<mark>Обратите внимание</mark>, что это решение чрезвычайно сложно. Если вы сможете решить эту проблему, не заглядывая в мой ответ, то у вас уже есть огромные навыки в регулярных выражениях!
pattern: string: 0.32 100.00 5.6 0.27 98% 12.2% 1.01 0.99% 0.99 13.13 1.10 matches: ^^^^^^ ^^^ ^^^^ ^^^^^ ^^^^ group: 111111 111 1111 11111 1111(Решение) <h2>Шаг 19: условия в регулярных выражениях</h2> Сейчас перешли к тому этапу, когда большинство людей уже не станут использовать регулярные выражения. Мы рассмотрели, вероятно, 95% сценариев использования простых регулярных выражений, и все, что делается на шагах 19 и 20, обычно выполняется более полнофункциональным языком манипулирования текстом, таким как awk или sed (или языком программирования общего назначения). Тем не менее, давайте продолжим, просто чтобы вы знали, на что действительно способно регулярное выражение. Хотя регулярные выражения не являются полными по Тьюрингу, некоторые движки регулярных выражений предлагают функции, которые очень похожи на полный язык программирования. Одна из таких особенностей является "условием". Условные выражения Regex допускают операторы if-then-else, где выбранная ветвь определяется либо "взглядом вперед", либо "взглядом назад", о которых мы узнали на предыдущем шаге. Например, вы можете захотеть сопоставить только действительные записи в списке дат:
pattern: (?<=Feb )([1-2][0-9])|(?<=Mar )([1-2][0-9]|3[0-1]) string: Dates worked: Feb 28, Feb 29, Feb 30, Mar 30, Mar 31 matches: ^^ ^^ ^^ ^^ group: 11 11 22 22(Пример) <mark>Обратите внимание</mark>, что указанные выше группы также индексируются по месяцам. Мы могли бы написать регулярное выражение для всех 12 месяцев и зафиксировать только действительные даты, которые затем были бы объединены в группы, проиндексированные по месяцу года. Выше используется своего рода структура, подобная if, которая будет искать совпадения в первой группе, только если "Feb" предшествует числу (и аналогично для второй). Но что, если бы мы хотели использовать специальную обработку только для февраля? Что-то вроде "если числу предшествует "Feb ", сделайте это, иначе сделайте эту другую вещь". Вот как это делают условные выражения:
pattern: (?(?<=Feb )([1-2][0-9])|([1-2][0-9]|3[0-1])) string: Dates worked: Feb 28, Feb 29, Feb 30, Mar 30, Mar 31 matches: ^^ ^^ ^^ ^^ group: 11 11 22 22(Пример) Структура if-then-else выглядит как (?(If)then|else), где (if) заменяется "взглядом вперед" или "взглядом назад". В приведенном выше примере (if) записан как
(?<=Feb)
. Вы можете видеть, что мы сопоставляли даты больше 29, но только если они не следовали за "Feb ". Использование же lookbehinds ("взглядов назад") в условных выражениях полезно, если вы хотите убедиться, что совпадению предшествует какой-либо текст.
Положительные lookahead условные выражения могут сбивать с толку, потому что само условие не соответствует ни одному тексту. Поэтому, если вы хотите, чтобы условие if когда-либо имело значение, оно должно быть сопоставимым с lookahead, как показано ниже:
pattern: (?(?=exact)exact|else)wo string: exact else exactwo elsewo matches: ^^^^^^^ ^^^^^^(Пример) Это означает, что положительные lookahead условные выражения бесполезны. Вы проверяете, находится ли этот текст впереди, и затем предоставляете шаблон соответствия, чтобы следовать ему, когда он есть. Условное выражение не помогает нам здесь вообще. Вы также можете просто заменить вышеприведенное на более простое регулярное выражение:
pattern: (?:exact|else)wo string: exact else exactwo elsewo matches: ^^^^^^^ ^^^^^^(Пример) Итак, эмпирическое правило для выражений с условиями: тест, тест, и еще раз тест. Иначе решения, которые вы считаете очевидными, потерпят неудачу самыми захватывающими и неожиданными способами :) <h3>Вот мы и подошли к последнему блоку задач, который отделяет нас от завершающего, 20-го шага:</h3> Напишите регулярное выражение, которое использует отрицательное lookahead условное выражение, чтобы проверить, начинается ли следующее слово с заглавной буквы. Если это так, захватите только одну заглавную букву, а затем строчные буквы. Если это не так, захватите любые символы слова.
pattern: string: Jones Smith 9sfjn Hobbes 23r4tgr9h CSV Csv vVv matches: ^^^^^ ^^^^^ ^^^^^ ^^^^^^ ^^^^^^^^^ ^^^ ^^^ group: 22222 22222 11111 222222 111111111 222 111(Решение) Напишите отрицательное lookbehind условное выражение, которое захватывает текст
owns
, только если ему не предшествует текст cl
, и которое захватывает текст ouds
, только когда ему предшествует текст cl
. (Немного надуманный пример, но что поделаешь...)
pattern: string: Those clowns owns some clouds. ouds. matches: ^^^^ ^^^^(Решение) <h2>Шаг 20: рекурсия и дальнейшее обучение</h2> На самом деле, есть очень много всего, что можно втиснуть в 20-шаговое введение в любую тему, и регулярные выражения не являются исключением. Существует множество различных реализаций и стандартов для регулярных выражений, которые можно найти в Интернете. Если вы хотите узнать больше, я предлагаю вам посетить замечательный сайт regularexpressions.info, это фантастический справочник, и я, конечно, многое узнал от туда о регулярных выражениях. Я настоятельно рекомендую его, а также regex101.com для тестирования и публикации ваших творений. На этом завершающем шаге я дам вам еще немного знаний о регулярных выражениях, а именно: как писать рекурсивные выражения. Простые рекурсии довольно просты, но давайте подумаем, что это значит в контексте регулярного выражения. Синтаксис простой рекурсии в регулярном выражении записывается так:
(?R)?
. Но, конечно, этот синтаксис должен появляться внутри самого выражения. То что мы сделаем, это вложим выражение в себя, произвольное количество раз. Например:
pattern: (hey(?R)?oh) string: heyoh heyyoh heyheyohoh hey oh heyhey heyheyheyohoh matches: ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^ group: 11111 1111111111 1111111111(Пример) Поскольку вложенное выражение является необязательным (
(?R)
сопровождается ?
), то самое простое совпадение - просто полностью игнорировать рекурсию. Итак, hey
, а затем oh
совпадает (heyoh
). Чтобы сопоставить любое более сложное выражение, чем это, мы должны найти эту совпадающую подстроку, вложенную внутрь себя в той точке выражения, в которую мы вставили (?R)
последовательность. Другими словами, мы могли бы найти heyheyohoh или heyheyheyohohoh, и так далее.
Одна из замечательных особенностей этих вложенных выражений заключается в том, что, в отличие от обратных ссылок и именованных групп захвата, они не ограничивают вас в соответствии с точным текстом, который вы сопоставляли ранее, символ за символом. Например:
pattern: ([Hh][Ee][Yy](?R)?oh) string: heyoh heyyoh hEyHeYohoh hey oh heyhey hEyHeYHEyohohoh matches: ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^ group: 11111 1111111111 111111111111111(Пример) Вы можете себе представить, что механизм регулярных выражений буквально копирует и вставляет ваше регулярное выражение в себя произвольное количество раз. Конечно, это означает, что иногда оно может делать не то, на что вы могли надеяться:
pattern: ((?:\(\*)[^*)]*(?R)?(?:\*\))) string: (* comment (* nested *) not *) matches: ^^^^^^^^^^^^ group: 111111111111(Пример) Можете ли вы сказать, почему это регулярное выражение захватило только вложенный комментарий, а не внешний комментарий? Одно можно сказать наверняка: при написании сложных регулярных выражений всегда проверяйте их, чтобы убедиться, что они работают так, как вы думаете. Вот и подошло к концу это скоростное ралли по дорогам регулярных выражений. Надеюсь, вам понравилось это путешествие. Ну, и напоследок, я оставлю здесь, как и обещал в начале, несколько полезных ссылок для более углубленного изучения материала:
- Регулярные выражения в Java (Статья от пользователя Alex)
- Регулярные выражения в Java (Перевод статьи Джеффа Фрисена от Эллеоноры Керри)
- Регулярные выражения java примеры (с удобной таблицей по синтакису)
- Жадные и ленивые квантификаторы (примеры на JavaScript)
- 25 самых используемых регулярных выражений в Java
- RegexOne (Задачки с проверялкой)
- Не бойтесь регулярных выражений. Regex за 20 минут! (для тех, кому больше нравятся видеоуроки)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Напишите отрицательное lookaheadнужно использовать отрицательный lookbehind. А также в предпоследней задаче 19 шага Jones Smith 9sfjn Hobbes 23r4tgr9h CSV Csv vVv сломал мозг, так и не понял задачу, даже после просмотра решения. Если кто-то объяснить, что там происходить - буду очень благодарен