Artur
40 уровень
Tallinn

RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4

Статья из группы Random
RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 1 RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 2 20 коротких шагов для освоения регулярных выражений. Часть 3 Эта, заключительная часть, в ее середине коснется таких вещей, которыми пользуются в основном мастера регулярных выражений. Но вам же легко давался материал из предыдущих частей, ведь правда? Значит и с этим материалом вы справитесь с той же легкостью! Оригинал здесь RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 1 <h2>Шаг 16: группы без захвата (?:)</h2> RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 2В двух примерах на предыдущем шаге мы захватывали текст, который в действительности нам не нужен. В задаче "Размеры файлов" мы захватили пробелы перед первой цифрой размеров файлов, а в задаче "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> RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 3Хоть я и предупреждал вас во введении, что попытка создать 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> RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 4Сейчас мы углубимся в некоторые расширенные функции регулярных выражений. Всё, вплоть до шага 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> RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 5Сейчас перешли к тому этапу, когда большинство людей уже не станут использовать регулярные выражения. Мы рассмотрели, вероятно, 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> RegEx: 20 коротких шагов для освоения регулярных выражений. Часть 4 - 6На самом деле, есть очень много всего, что можно втиснуть в 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    
(Пример) Можете ли вы сказать, почему это регулярное выражение захватило только вложенный комментарий, а не внешний комментарий? Одно можно сказать наверняка: при написании сложных регулярных выражений всегда проверяйте их, чтобы убедиться, что они работают так, как вы думаете. Вот и подошло к концу это скоростное ралли по дорогам регулярных выражений. Надеюсь, вам понравилось это путешествие. Ну, и напоследок, я оставлю здесь, как и обещал в начале, несколько полезных ссылок для более углубленного изучения материала:
Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Никита Уровень 22, Вологда, Россия
17 июня 2021
Всем привет. Подскажите как можно реализовать поиск любых слов в тексте, но в которых нет дефиса. Голову уже сломал ))
Кирияк Максим Уровень 26, Tiraspol, Молдова
29 января 2021
Огромное спасибо за перевод статьи. В духе JR, теория перемежает практику. Наконец-то стало более менее ясно как обращаться с RegExp, но это не точно)
MermaidMan Уровень 41, Bikini Bottom
16 мая 2020
Отличная статья, интересно и понятно. Спасибо всем, кто к ней причастен. Раньше я бесился, когда видел, что люди решают задачи с помощью регулярок, в то время, когда можно обойтись без них. Но похоже, теперь я сам буду все решать через них)
Андрей Шевченко Уровень 41, Москва, Россия
10 мая 2020
В шаге 19 в задача про клоунов и облака опечатка, Напишите отрицательное lookahead нужно использовать отрицательный lookbehind. А также в предпоследней задаче 19 шага Jones Smith 9sfjn Hobbes 23r4tgr9h CSV Csv vVv сломал мозг, так и не понял задачу, даже после просмотра решения. Если кто-то объяснить, что там происходить - буду очень благодарен
Artur Уровень 40, Tallinn, Эстония Expert
7 мая 2020
Спасибо друзья, что нашли помарки в статье! А также, выражаю особенную благодарность редактору Hanna Moruga за то, что она очень сильно помогла мне в редактировании и форматировании данного материала! Последнюю правку делал сам, поэтому у меня послетали некоторые теги. Видимо, какой-то косяк у меня в браузере.
Arseny Vinogradow Уровень 35, Санкт-Петербург
4 мая 2020
В 18 и 19 шаге много косяков с переводом. Перепутаны lookbehind и lookahead, выражения для них, ссылка на решение задачи про клоунов и облака неверна. В целом полезная статья, но пришлось лезть в оригинал по итогу.
Кирилл Уровень 35, Москва, Россия
4 мая 2020
Наикрутейшая статья! Спасибо за перевод! Информация начинающаяся с 16-го шага оказалась весьма полезной. В рунете более подробного их описания не находил.
Кирилл Уровень 35, Москва, Россия
4 мая 2020
В шаге 18 нашлись опечатки: