Регулярные выражения (regexp) — основы

Регулярные выражения (их еще называют regexp, или regex) — это механизм для поиска и замены текста. В строке, файле, нескольких файлах… Их используют разработчики в коде приложения, тестировщики в автотестах, да просто при работе в командной строке!

Чем это лучше простого поиска? Тем, что позволяет задать шаблон.

Например, на вход приходит дата рождения в формате ДД.ММ.ГГГГГ. Вам надо передать ее дальше, но уже в формате ГГГГ-ММ-ДД. Как это сделать с помощью простого поиска? Вы же не знаете заранее, какая именно дата будет.

8da358d177ba245525e44e5ba2b43073.png

А регулярное выражение позволяет задать шаблон «найди мне цифры в таком-то формате».

Для чего применяют регулярные выражения?

  1. Удалить все файлы, начинающиеся на test (чистим за собой тестовые данные)

  2. Найти все логи

  3. grep-нуть логи

  4. Найти все даты

А еще для замены — например, чтобы изменить формат всех дат в файле. Если дата одна, можно изменить вручную. А если их 200, проще написать регулярку и подменить автоматически. Тем более что регулярные выражения поддерживаются даже простым блокнотом (в Notepad++ они точно есть).

97044dbe95152da2ff61f0677f54cbb1.png

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

Содержание

  1. Где пощупать

  2. Поиск текста

  3. Поиск любого символа

  4. Поиск по набору символов

  5. Перечисление вариантов

  6. Метасимволы

  7. Спецсимволы

  8. Квантификаторы (количество повторений)

  9. Позиция внутри строки

  10. Использование ссылки назад

  11. Просмотр вперед и назад

  12. Замена

  13. Статьи и книги по теме

  14. Итого

  1. Поиск текста

  2. Поиск любого символа

  3. Поиск по набору символов

  4. Перечисление вариантов

  5. Метасимволы

  6. Спецсимволы

  7. Квантификаторы (количество повторений)

  8. Замена

  9. Итого

Где пощупать

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

  1. Notepad++ (установить Search Mode → Regular expression)

  2. Regex101 (мой фаворит в онлайн вариантах)

  3. Myregexp

  4. Regexr

Инструменты есть, теперь начнём

Поиск текста

Самый простой вариант регэкспа. Работает как простой поиск — ищет точно такую же строку, как вы ввели.

Текст: Море, море, океан

Regex: море

Найдет: Море, море, океан

Выделение курсивом не поможет моментально ухватить суть, что именно нашел regex, а выделить цветом в статье я не могу. Атрибут BACKGROUND-COLOR не сработал, поэтому я буду дублировать регулярки текстом (чтобы можно было скопировать себе) и рисунком, чтобы показать, что именно regex нашел:

96b09095c547fb4cb2027c7c03fd9865.png

Обратите внимание, нашлось именно «море», а не первое «Море». Регулярные выражения регистрозависимые!

Хотя, конечно, есть варианты. В JavaScript можно указать дополнительный флажок i, чтобы не учитывать регистр при поиске. В блокноте (notepad++) тоже есть галка «Match case». Но учтите, что это не функция по умолчанию. И всегда стоит проверить, регистрозависимая ваша реализация поиска, или нет.

А что будет, если у нас несколько вхождений искомого слова?

Текст: Море, море, море, океан

Regex: море

Найдет: Море, море, море, океан

d3956830d7b22051a996ec0d081e8f7d.png

По умолчанию большинство механизмов обработки регэкспа вернет только первое вхождение. В JavaScript есть флаг g (global), с ним можно получить массив, содержащий все вхождения.

А что, если у нас искомое слово не само по себе, это часть слова? Регулярное выражение найдет его:

Текст: Море, 55 мореон, океан

Regex: море

Найдет: Море, 55 мореон, океан

43f2b32a74594ab0fef7b63bf1648330.png

Это поведение по умолчанию. Для поиска это даже хорошо. Вот, допустим, я помню, что недавно в чате коллега рассказывала какую-то историю про интересный баг в игре. Что-то там связанное с кораблем… Но что именно? Уже не помню. Как найти?

Если поиск работает только по точному совпадению, мне придется перебирать все падежи для слова «корабль». А если он работает по включению, я просто не буду писать окончание, и все равно найду нужный текст:

Regex: корабл

Найдет:

На корабле

И тут корабль

У корабля

5e86c0ef9e87df655e660c1de07e299e.png

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

Поиск любого символа

. — найдет любой символ (один).

Текст:

Аня

Ася

Оля

Аля

Валя

Regex: А.я

Результат:

Аня

Ася

Оля

Аля

Валя

Символ «.» заменяет 1 любой символСимвол ».» заменяет 1 любой символ

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

А6я

А&я

А я

Учтите это при поиске! Точка очень удобный символ, но в то же время очень опасный — если используете ее, обязательно тестируйте получившееся регулярное выражение. Найдет ли оно то, что нужно? А лишнее не найдет?

Точку точка тоже найдет!

Regex: file.

Найдет:

file.txt

file1.txt

file2.xls

aa8c09e2b369d607db917d93c5c01701.png

Но что, если нам надо найти именно точку? Скажем, мы хотим найти все файлы с расширением txt и пишем такой шаблон:

Regex: .txt

Результат:

file.txt

log.txt

file.png

1txt.doc

one_txt.jpg

1d72715ed6a4b8fd65152b555080cdfb.png

Да, txt файлы мы нашли, но помимо них еще и «мусорные» значения, у которых слово «txt» идет в середине слова. Чтобы отсечь лишнее, мы можем использовать позицию внутри строки (о ней мы поговорим чуть дальше).

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

Regex: \.txt

Результат:

file.txt

log.txt

file.png

1txt.doc

one_txt.jpg

1ddbbf23d932ca0a645e3201a520e119.png

Также мы будем поступать со всеми спецсимволами. Хотим найти именно такой символ в тексте? Добавляем перед ним обратный слеш.

Правило поиска для точки:

. — любой символ

\. — точка

15aa7559debdd37d4fb63a8f2bcd8dc9.png

Поиск по набору символов

Допустим, мы хотим найти имена «Алла», «Анна» в списке. Можно попробовать поиск через точку, но кроме нормальных имен, вернется всякая фигня:

Regex: А…а

Результат:

Анна

Алла

аоикА74арплт

Аркан

А^&а

Абба

9c6d34c9c1890f36f94015ec91f3c761.png

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

Regex: А[нл][нл]а

Результат:

Анна

Алла

аоикА74арплт

Аркан

А^&а

Абба

74aa14ce69cbeb8301e51a7af20b5efd.png

Вот теперь результат уже лучше! Да, нам все еще может вернуться «Анла», но такие ошибки исправим чуть позже.

Как работают квадратные скобки? Внутри них мы указываем набор допустимых символов. Это может быть перечисление нужных букв, или указание диапазона:

[нл] — только «н» и «л»

[а-я] — все русские буквы в нижнем регистре от «а» до «я» (кроме «ё»)

[А-Я] — все заглавные русские буквы

[А-Яа-яЁё] — все русские буквы

[a-z] — латиница мелким шрифтом

[a-zA-Z] — все английские буквы

[0–9] — любая цифра

[В-Ю] — буквы от «В» до «Ю» (да, диапазон — это не только от А до Я)

[А-ГО-Р] — буквы от «А» до «Г» и от «О» до «Р»

Обратите внимание — если мы перечисляем возможные варианты, мы не ставим между ними разделителей! Ни пробел, ни запятую — ничего.

[абв] — только «а», «б» или «в»

[а б в] — «а», «б», «в», или пробел (что может привести к нежелательному результату)

[а, б, в] — «а», «б», «в», пробел или запятая

c3106f0b5a57b3f34086af82c38226c5.png

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

  • Символ до дефиса — начало диапазона

  • Символ после — конец

Один символ! Не два или десять, а один! Учтете это, если захотите написать что-то типа [1–31]. Нет, это не диапазон от 1 до 31, эта запись читается так:

  • Диапазон от 1 до 3

  • И число 1

Здесь отсутствие разделителей играет злую шутку с нашим сознанием. Ведь кажется, что мы написали диапазон от 1 до 31! Но нет. Поэтому, если вы пишете регулярные выражения, очень важно их тестировать. Не зря же мы тестировщики! Проверьте то, что написали! Особенно, если с помощью регулярного выражения вы пытаетесь что-то удалить =)) Как бы не удалили лишнее…

Указание диапазона вместо точки помогает отсеять заведомо плохие данные:

Regex: А.я или А[а-я]я

Результат для обоих:

Аня

Ася

Оля

Аля

Результат для «А.я»:

А6я

А&я

А я

^ внутри [] означает исключение:

[^0–9] — любой символ, кроме цифр

[^ёЁ] — любой символ, кроме буквы «ё»

[^а-в8] — любой символ, кроме букв «а», «б», «в» и цифры 8

ae4f41680a94723921c6c9c0e98ee9c6.png

Например, мы хотим найти все txt файлы, кроме разбитых на кусочки — заканчивающихся на цифру:

Regex: [^0–9]\.txt

Результат:

file.txt

log.txt

file_1.txt

1.txt

4f272b03b5567a62e5f6e642f2b00179.png

Так как квадратные скобки являются спецсимволами, то их нельзя найти в тексте без экранирования:

Regex: fruits[0]

Найдет: fruits0

Не найдет: fruits[0]

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

688502df3f133bfde61558595f188d00.png

Если мы хотим найти именно 0-левой элемент массива фруктов, надо записать так:

Regex: fruits\[0\]

Найдет: fruits[0]

Не найдет: fruits0

А если мы хотим найти все элементы массива фруктов, мы внутри экранированных квадратных скобок ставим неэкранированные!

Regex: fruits\[[0–9]\]

Найдет:

fruits[0] = «апельсин»;

fruits[1] = «яблоко»;

fruits[2] = «лимон»;

Не найдет:

cat[0] = «чеширский кот»;

Конечно, «читать» такое регулярное выражение становится немного тяжело, столько разных символов написано…

56a13d4014d8252573d0307ee93649fd.png

Без паники! Если вы видите сложное регулярное выражение, то просто разберите его по частям. Помните про основу эффективного тайм-менеджмента? Слона надо есть по частям.

Допустим, после отпуска накопилась гора писем. Смотришь на нее и сразу впадаешь в уныние:

— Ууууууу, я это за день не закончу!

f3a65eb0ee563f6a65bc341c8be8e68d.png

Проблема в том, что груз задачи мешает работать. Мы ведь понимаем, что это надолго. А большую задачу делать не хочется… Поэтому мы ее откладываем, беремся за задачи поменьше. В итоге да, день прошел, а мы не успели закончить.

А если не тратить время на размышления «сколько времени это у меня займет», а сосредоточиться на конкретной задаче (в данном случае — первом письме из стопки, потом втором…), то не успеете оглянуться, как уже всё разгребли!

ae95fe46c2502821b498a43f16f657d7.png

Разберем по частям регулярное выражение — fruits\[[0–9]\]

Сначала идет просто текст — «fruits».

27c5e3222a38dbd5fc231fa524a8dee2.png

Потом обратный слеш. Ага, он что-то экранирует.

e6d0b21c897e0b45df8183e1a5e89a0a.png

Что именно? Квадратную скобку. Значит, это просто квадратная скобка в моем тексте — «fruits[»

df33e783753b05455c68800079749dd1.png

Дальше снова квадратная скобка. Она не экранирована — значит, это набор допустимых значений. Ищем закрывающую квадратную скобку.

792f1dab5cf3794960818e1b47db099c.png

Нашли. Наш набор: [0–9]. То есть любое число. Но одно. Там не может быть 10, 11 или 325, потому что квадратные скобки без квантификатора (о них мы поговорим чуть позже) заменяют ровно один символ.

Пока получается: fruits[«любое однозназначное число»

1ad2449d768404f834e8dc5d30da2cdb.png

Дальше снова обратный слеш. То есть следующий за ним спецсимвол будет просто символом в моем тексте.

2690b62e7c91321968144d2da854d007.png

А следующий символ — ]

Получается выражение: fruits[«любое однозназначное число»]

8d38e83fb2ce1e5a214c303af7834774.png

Наше выражение найдет значения массива фруктов! Не только нулевое, но и первое, и пятое… Вплоть до девятого:

Regex: fruits\[[0–9]\]

Найдет:

fruits[0] = «апельсин»;

fruits[1] = «яблоко»;

fruits[9] = «лимон»;

Не найдет:

fruits[10] = «банан»;

fruits[325] = » абрикос »;

Как найти вообще все значения массива, см дальше, в разделе «квантификаторы».

А пока давайте посмотрим, как с помощью диапазонов можно найти все даты.

a41fd79ea4164158b258332adf5bb39b.png

Какой у даты шаблон? Мы рассмотрим ДД.ММ.ГГГГ:

  • 2 цифры дня

  • точка

  • 2 цифры месяца

  • точка

  • 4 цифры года

Запишем в виде регулярного выражения: [0–9][0–9]\.[0–9][0–9]\.[0–9][0–9][0–9][0–9].

Напомню, что мы не можем записать диапазон [1–31]. Потому что это будет значить не «диапазон от 1 до 31», а «диапазон от 1 до 3, плюс число 1». Поэтому пишем шаблон для каждой цифры отдельно.

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

Давайте его протестируем! Как насчет 8888 года или 99 месяца, а?

Regex: [0–9][0–9]\.[0–9][0–9]\.[0–9][0–9][0–9][0–9]

Найдет:

01.01.1999

05.08.2015

Тоже найдет:

08.08.8888

99.99.2000

39d8eebc8cc159e1324e8ea0dcdced24.png

Попробуем ограничить:

  • День недели может быть максимум 31 — первая цифра [0–3]

  • Максимальный месяц 12 — первая цифра [01]

  • Год или 19…, или 20… — первая цифра [12], а вторая [09]

2398aa83562b932f5b0aa70c39d7cd66.png

Вот, уже лучше, явно плохие данные регулярка отсекла. Надо признать, она отсечет довольно много тестовых данных, ведь обычно, когда хотят именно сломать, то фигачат именно »9999» год или »99» месяц…

Однако если мы присмотримся внимательнее к регулярному выражению, то сможем найти в нем дыры:

Regex: [0–3][0–9]\.[0–1][0–9]\.[12][09][0–9][0–9]

Не найдет:

08.08.8888

99.99.2000

Но найдет:

33.01.2000

01.19.1999

05.06.2999

5e1938194ffacb5480e7134e12168fa8.png

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

Перечисление вариантов

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

Regex: Оля|Олечка|Котик

Найдет:

Оля

Олечка

Котик

Не найдет:

Оленька

Котенка

Можно использовать вертикальную черту и для одного символа. Можно даже внутри слова — тогда вариативную букву берем в круглые скобки

Regex: А (н|л)я

Найдет:

Аня

Аля

Круглые скобки обозначают группу символов. В этой группе у нас или буква «н», или буква «л». Зачем нужны скобки? Показать, где начинается и заканчивается группа. Иначе вертикальная черта применится ко всем символам — мы будем искать или «Ан», или «ля»:

Regex: Ан|ля

Найдет:

Аня

Аля

Оля

Малюля

608616064a02f1f0a6d03ac1f2ca3ffc.png

А если мы хотим именно «Аня» или «Аля», то перечисление используем только для второго символа. Для этого берем его в скобки.

Эти 2 варианта вернут одно и то же:

  • А (н|л)я

  • А[нл]я

Но для замены одной буквы лучше использовать [], так как сравнение с символьным классом выполняется проще, чем обработка группы с проверкой на все её возможные модификаторы.

Давайте вернемся к задаче «проверить введенную пользователем дату с помощью регулярных выражений». Мы пробовали записать для дня диапазон [0–3][0–9], но он пропускает значения 33, 35, 39… Это нехорошо!

Тогда распишем ТЗ подробнее. Та-а-а-ак… Если первая цифра:

  • 0 — вторая может от 1 до 9 (даты 00 быть не может)

  • 1, 2 — вторая может от 0 до 9

  • 3 — вторая только 0 или 1

Составим регулярные выражения на каждый пункт:

  • 0[1–9]

  • [12][0–9]

  • 3[01]

А теперь осталось их соединить в одно выражение! Получаем: 0[1–9]|[12][0–9]|3[01]

9b5da6636e01a9578b61abe4bee54907.png

По аналогии разбираем месяц и год. Но это остается вам для домашнего задания ?

Потом, когда распишем регулярки отдельно для дня, месяца и года, собираем все вместе:

(<день>)\.(<месяц>)\.(<год>)

Обратите внимание — каждую часть регулярного выражения мы берем в скобки. Зачем? Чтобы показать системе, где заканчивается выбор. Вот смотрите, допустим, что для месяца и года у нас осталось выражение:

[0–1][0–9]\.[12][09][0–9][0–9]

Подставим то, что написали для дня:

0[1–9]|[12][0–9]|3[01]\.[0–1][0–9]\.[12][09][0–9][0–9]

Как читается это выражение?

  • ИЛИ 0[1–9]

  • ИЛИ [12][0–9]

  • ИЛИ 3[01]\.[0–1][0–9]\.[12][09][0–9][0–9]

Видите проблему? Число »19» будет считаться корректной датой. Система не знает, что перебор вариантов | закончился на точке после дня. Чтобы она это поняла, нужно взять перебор в скобки. Как в математике, разделяем слагаемые.

Так что запомните — если перебор идет в середине слова, его надо взять в круглые скобки!

Regex: А (нн|лл|лин|нтонин)а

Найдет:

Анна

Алла

Алина

Антонина

Без скобок:

Regex: Анн|лл|лин|нтонина

Найдет:

Анна

Алла

Аннушка

Кукулинка

08df1c697d7ef839254b02b70a3a9486.png

Итого, если мы хотим указать допустимые значения:

  • Одного символа — используем []

  • Нескольких символов или целого слова — используем |

4f6f3ae039297e7e996d5960eb025d5b.png

Метасимволы

Если мы хотим найти число, то пишем диапазон [0–9].

Если букву, то [а-яА-ЯёЁa-zA-Z].

А есть ли другой способ?

976f104b39585e122988b717d328fce8.png

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

Символ

Эквивалент

Пояснение

\d

[0–9]

Цифровой символ

\D

[^0–9]

Нецифровой символ

\s

[ \f\n\r\t\v]

Пробельный символ

\S

[^ \f\n\r\t\v]

Непробельный символ

\w

[[: word:]]

Буквенный или цифровой символ или знак подчёркивания

\W

[^[: word:]]

Любой символ, кроме буквенного или цифрового символа или знака подчёркивания

.

Вообще любой символ

Это самые распространенные символы, которые вы будете использовать чаще всего. Но давайте разберемся с колонкой «эквивалентн». Для \d все понятно — это просто некие числа. А что такое «пробельные символы»? В них входят:

Символ

Пояснение

Пробел

\r

Возврат каретки (Carriage return, CR)

\n

Перевод строки (Line feed, LF)

\t

Табуляция (Tab)

\v

Вертикальная табуляция (vertical tab)

\f

Конец страницы (Form feed)

[\b]

Возврат на 1 символ (Backspace)

Из них вы чаще всего будете использовать сам пробел и перевод строки — выражение »\r\n». Напишем текст в несколько строк:

Первая строка

Вторая строка

Для регулярного выражения это:

Первая строка\r\nВторая строка

А вот что такое backspace в тексте? Как его можно увидеть вообще? Это же если написать символ и стереть его. В итоге символа нет! Неужели стирание хранится где-то в памяти? Но тогда это было бы ужасно, мы бы вообще ничего не смогли найти — откуда нам знать, сколько раз текст исправляли и в каких местах там теперь есть невидимый символ [\b]?

0ed4ccef94576ac781a5e6dc9ba57cc6.png

Выдыхаем — этот символ не найдет все места исправления текста. Просто символ backspace — это ASCII символ, который может появляться в тексте (ASCII code 8, или 10 в octal). Вы можете «создать» его, написать в консоли браузера (там используется JavaScript):

console.log("abc\b\bdef");

Результат команды:

adef

Мы написали «abc», а потом стерли «b» и «с». В итоге пользователь в консоли их не видит, но они есть. Потому что мы прямо в коде прописали символ удаления текста. Не просто удалили текст, а прописали этот символ. Вот такой символ регулярное выражение  [\b] и найдет.

См также:

What’s the use of the [\b] backspace regex? — подробнее об этом символе

Но обычно, когда мы вводим \s, мы имеем в виду пробел, табуляцию, или перенос строки.

Ок, с этими эквивалентами разобрались. А что значит [[: word:]]? Это один из способов заменить диапазон. Чтобы запомнить проще было, написали значения на английском, объединив символы в классы. Какие есть классы:

Класс символов

Пояснение

[[: alnum:]]

Буквы или цифры: [а-яА-ЯёЁa-zA-Z0–9]

[[: alpha:]]

Только буквы: [а-яА-ЯёЁa-zA-Z]

[[: digit:]]

Только цифры: [0–9]

[[: graph:]]

Только отображаемые символы (пробелы, служебные знаки и т. д. не учитываются)

[[: print:]]

Отображаемые символы и пробелы

[[: space:]]

Пробельные символы [ \f\n\r\t\v]

[[: punct:]]

Знаки пунктуации: !» # $ % & ' () * + , \ -. / : ; < = >? @ [ ] ^ _ ` { | }

[[: word:]]

Буквенный или цифровой символ или знак подчёркивания: [а-яА-ЯёЁa-zA-Z0–9_]

Теперь мы можем переписать регулярку для проверки даты, которая выберет лишь даты формата ДД.ММ.ГГГГГ, отсеяв при этом все остальное:

[0–9][0–9]\.[0–9][0–9]\.[0–9][0–9][0–9][0–9]

\d\d\.\d\d.\d\d\d\d

Согласитесь, через метасимволы запись посимпатичнее будет =))

Спецсимволы

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

[ ] \ / ^ $ . | ? * + () { }

Эти символы нужны, чтобы обозначить диапазон допустимых значений или границу фразы, указать количество повторений, или сделать что-то еще. В разных типах регулярных выражений этот набор различается (см «разновидности регулярных выражений»).

Если вы хотите найти один из этих символов внутри вашего текста, его надо экранировать символом \ (обратная косая черта).

Regex: 2\^2 = 4

Найдет: 2^2 = 4

Можно экранировать целую последовательность символов, заключив её между \Q и \E (но не во всех разновидностях).

Regex: \Q{кто тут?}\E

Найдет: {кто тут?}

Квантификаторы (количество повторений)

Усложняем задачу. Есть некий текст, нам нужно вычленить оттуда все email-адреса. Например:

7cc7ae412933ec91f353c2c0c192c5cd.png

Как составляется регулярное выражение? Нужно внимательно изучить данные, которые мы хотим получить на выходе, и составить по ним шаблон. В email два разделителя — собачка »@» и точка ».».

59ecb1feca5de8a861665e80df36b17c.png

Запишем ТЗ для регулярного выражения:

  • Буквы / цифры / _

  • Потом @

  • Снова буквы / цыфры / _

  • Точка

  • Буквы

Так, до собачки у нас явно идет метасимвол »\w», туда попадет и просто текст (test), и цифры (olga31), и подчеркивание (pupsik_99). Но есть проблема — мы не знаем, сколько таких символов будет. Это при поиске даты все ясно — 2 цифры, 2 цифры, 4 цифры. А тут может быть как 2, так и 22 символа.

И тут на помощь приходят квантификаторы — так называют специальные символы в регулярных выражениях, которые указывают количество повторений текста.

Символ »+» означает «одно или более повторений», это как раз то, что нам надо! Получаем: \w+@

309bc82ea4ea3397e94967e90778d322.png

После собачки и снова идет \w, и снова от одного повторения. Получаем: \w+@\w+\.

После точки обычно идут именно символы, но для простоты можно снова написано \w. И снова несколько символов ждем, не зная точно сколько. Итого получилось выражение, которое найдет нам email любой длины:

Regex: \w+@\w+\.\w+

Найдет:

test@mail.ru

olga31@gmail.com

pupsik_99_and_slonik_33_and_mikky_87_and_kotik_28@yandex.megatron

Какие есть квантификаторы, кроме знака »+»?

Квантификатор

Число повторений

?

Ноль или одно

*

Ноль или более

+

Один или более

Символ * часто используют с точкой — когда нам неважно, какой идет текст до интересующей нас фразы, мы заменяем его на ».*» — любой символ ноль или более раз.

Regex: .*\d\d\.\d\d\.\d\d\d\d.*

Найдет:

01.01.2000

Приходи на ДР 09.08.2015! Будет весело!

Но будьте осторожны! Если использовать ».*» повсеместно, можно получить много ложноположительных срабатываний:

Regex: .*@.*\…*

Найдет:

test@mail.ru

olga31@gmail.com

pupsik_99@yandex.ru

Но также найдет:

@yandex.ru

test@.ru

test@mail.

57ebd632585b27d91f6defd29a7c6d32.png

Уж лучше \w, и плюсик вместо звездочки.

А вот есть мы хотим найти все лог-файлы, которые нумеруются — log,  log1, log2… log133, то * подойдет хорошо:

Regex: log\d*\.txt

Найдет:

log.txt

log1.txt

log2.txt

log3.txt

log33.txt

log133.txt

А знак вопроса (ноль или одно повторение) поможет нам найти людей с конкретной фамилией — причем всех, и мужчин, и женщин:

Regex: Назина?

Найдет:

Назин

Назина

Если мы хотим применить квантификатор к группе символов или нескольким словам, их нужно взять в скобки:

Regex: (Хихи)*(Хаха)*

Найдет:

ХихиХаха

ХихиХихиХихи

Хихи

Хаха

ХихиХихиХахаХахаХаха

(пустота — да, её такая регулярка тоже найдет)

Квантификаторы применяются к символу или группе в скобках, которые стоят перед ним.

А что, если мне нужно определенное количество повторений? Скажем, я хочу записать регулярное выражение для даты. Пока мы знаем только вариант «перечислить нужный метасимвол нужное количество раз» — \d\d\.\d\d\.\d\d\d\d.

Ну ладно 2–4 раза повторение идет, а если 10? А если повторить надо фразу? Так и писать ее 10 раз? Не слишком удобно. А использовать * нельзя:

Regex: \d*\.\d*\.\d*

Найдет:

.0.1999

05.08.20155555555555555

03444.025555.200077777777777777

Чтобы указать конкретное количество повторений, их надо записать внутри фигурных скобок:

Квантификатор

Число повторений

{n}

Ровно n раз

{m, n}

От m до n включительно

{m,}

Не менее m

{, n}

Не более n

Таким образом, для проверки даты можно использовать как перечисление \d n раз, так и использование квантификатора:

\d\d\.\d\d\.\d\d\d\d

\d{2}\.\d{2}.\d{4}

Обе записи будут валидны. Но вторая читается чуть проще — не надо самому считать повторения, просто смотрим на цифру.

Не забывайте — квантификатор применяется к последнему символу!

Regex: test{2}

Найдет: testt

Не найдет: testtest

Или группе символов, если они взяты в круглые скобки:

Regex: (test){2}

Найдет: testtest

Не найдет: testt

1d2ffb06d9eebcbfd122b0fb6c0608bc.png

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

Regex: x\{3\}

Найдет: x{3}

Иногда квантификатор находит не совсем то, что нам нужно.

Regex: <.*>

Ожидание:


Ан
FEMALE

Реальность:

 Ан FEMALE

Мы хотим найти все теги HTML или XML по отдельности, а регулярное выражение возвращает целую строку, внутри которой есть несколько тегов.

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

5a0cf39b145dbae5a5e0c5b2272ee604.png

Если мы понимаем, что нашли не то, что хотели, можно пойти двумя путями:

  1. Учитывать символы, не соответствующие желаемому образцу

  2. Определить квантификатор как нежадный (ленивый, англ. lazy) — большинство реализаций позволяют это сделать, добавив после него знак вопроса.

Как учитывать символы? Для примера с тегами можно написать такое регулярное выражение:

<[^>]*>

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

Жадный

Ленивый

*

*?

+

+?

{n,}

{n,}?

b4c23f066061280657a31ebc7d9ba4f0.png

Есть еще и сверхжадная квантификация, также именуемая ревнивой. Но о ней почитайте в википедии =)

Позиция внутри строки

По умолчанию регулярные выражения ищут «по включению».

Regex: арка

Найдет:

арка

чарка

аркан

баварка

знахарка

Это не всегда то, что нам нужно. Иногда мы хотим найти конкретное слово.

9d58267c669aabfd6b4f128f987bd3c0.png

Если мы ищем не одно слово, а некую строку, проблема решается в помощью пробелов:

Regex: Товар №\d+ добавлен в корзину в \d\d:\d\d

Найдет: Товар №555 добавлен в корзину в 15:30

Не найдет: Товарный чек №555 добавлен в корзину в 15:30

217b3d5edab7e8a9c76a68067e972ffa.png

Или так:

Regex: .* арка .*

Найдет: Триумфальная арка была…

Не найдет: Знахарка сегодня…

А что, если у нас не пробел рядом с искомым словом? Это может быть знак препинания: «И вот перед нами арка.», или »…арка:».

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

Regex: \bарка\b

Найдет:

арка

Не найдет:

чарка

аркан

баварка

знахарка

84fc991b0454248218489a7d4f9c4f84.png

Можно ограничить только спереди — «найди все слова, которые начинаются на такое-то значение»:

Regex: \bарка

Найдет:

арка

аркан

Не найдет:

чарка

баварка

знахарка

Можно ограничить только сзади —  «найди все слова, которые заканчиваются на такое-то значение»:

Regex: арка\b

Найдет:

арка

чарка

баварка

знахарка

Не найдет:

аркан

Если использовать метасимвол \B, он найдем нам НЕ-границу слова:

Regex: \Bакр\B

Найдет:

закройка

Не найдет:

акр

акрил

Если мы хотим найти конкретную фразу, а не слово, то используем следующие спецсимволы:

^ — начало текста (строки)

$ — конец текста (строки)

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

Regex: ^Я нашел!$

Найдет:

Я нашел!

Не найдет:

Смотри! Я нашел!

Я нашел! Посмотри!

© Habrahabr.ru