Выражение всегда начинается с символа

Обновлено: 04.10.2024

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

Некоторые из них для наглядности будут показаны на примере языков программирования PHP или JavaScript, но в целом они работают независимо от ЯП.

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

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




Вступление

Регулярные выражения — язык поиска подстроки или подстрок в тексте. Для поиска используется паттерн (шаблон, маска), состоящий из символов и метасимволов (символы, которые обозначают не сами себя, а набор символов).

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

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

Функции на match возвращают число найденных подстрок или false в случае ошибок. Функция на replace возвращает измененную строку/массив или null в случае ошибки. Результат можно привести к bool (false, если не было найдено значений и true, если было) и использовать вместе с if или assertTrue для обработки результата работы.

В JS чаще всего мне приходится использовать:

Пример использования функций

В PHP регулярное выражение — это строка, которая начинается и заканчивается символом-разделителем. Все, что находится между разделителями и есть регулярное выражение.

”. Ниже представлены примеры шаблонов с корректными разделителями:

Создать регулярное выражение можно так:


Или более короткий вариант:

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


В этом примере мы просто ищем все символы “o”.

В PHP разница между preg_match и preg_match_all в том, что первая функция найдет только первый match и закончит поиск, в то время как вторая функция вернет все вхождения.

Пример кода на PHP:

Пробуем то же самое для второй функции:

В последнем случае функция вернула все вхождения, которые есть в нашем тексте.

Тот же пример на JavaScript:

Модификаторы шаблонов

Для регулярных выражений существует набор модификаторов, которые меняют работу поиска. Они обозначаются одиночной буквой латинского алфавита и ставятся в конце регулярного выражения, после закрывающего “/”.

  • i — символы в шаблоне соответствуют символам как верхнего, так и нижнего регистра.
  • m — по умолчанию текст обрабатывается, как однострочная символьная строка. Метасимвол начала строки '^' соответствует только началу обрабатываемого текста, в то время как метасимвол конца строки '$' соответствует концу текста. Если этот модификатор используется, метасимволы «начало строки» и «конец строки» также соответствуют позициям перед произвольным символом перевода и строки и, соответственно, после, как и в самом начале, и в самом конце строки.

О том, какие вообще бывают модификаторы, можно почитать тут.

Пример предыдущего регулярного выражения с модификатором на JavaScript:

Метасимволы в регулярных выражениях

Примеры по началу будут довольно примитивные, потому что мы знакомимся с самыми основами. Чем больше мы узнаем, тем ближе к реалиям будут примеры.

Чаще всего мы заранее не знаем, какой текст нам придется парсить. Заранее известен только примерный набор правил. Будь то пинкод в смс, email в письме и т.п.

Первый пример, нам надо получить все числа из текста:


Чтобы выбрать любое число, надо собрать все числа, указав “[0123456789]”. Более коротко можно задать вот так: “8”. Для всех цифр существует метасимвол “\d”. Он работает идентично.

Но если мы укажем регулярное выражение “/\d/”, то нам вернётся только первая цифра. Мы, конечно, можем использовать модификатор “g”, но в таком случае каждая цифра вернется отдельным элементом массива, поскольку будет считаться новым вхождением.

Для того, чтобы вывести подстроку единым вхождением, существуют символы плюс “+” и звездочка “*”. Первый указывает, что нам подойдет подстрока, где есть как минимум один подходящий под набор символ. Второй — что данный набор символов может быть, а может и не быть, и это нормально. Помимо этого мы можем указать точное значение подходящих символов вот так: “”, где N — нужное количество. Или задать “от” и “до”, указав вот так: “”.

Сейчас будет пара примеров, чтобы это уложилось в голове:


Примерно так же мы работает с буквами, не забывая, что у них бывает регистр. Вот так можно задавать буквы:

  • [a-z]
  • [a-zA-Z]
  • [а-яА-Я]


Такое выражение выберет все слова, которые есть в предложении и написаны кириллицей. Нам нужно третье слово.

Помимо букв и цифр у нас могут быть еще важные символы, такие как:

  • \s — пробел
  • ^ — начало строки
  • $ — конец строки
  • | — “или”


Если мы точно знаем, что искомое слово последнее, мы ставим “$” и результатом работы будет только тот набор символов, после которого идет конец строки.

То же самое с началом строки:


Прежде, чем знакомиться с метасимволами дальше, надо отдельно обсудить символ “^”, потому что он у нас ходит на две работы сразу (это чтобы было интереснее). В некоторых случаях он обозначает начало строки, но в некоторых — отрицание.

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

Допустим, мы собрали набор символов, которые нам подходят: “[a-z0-9]” (нас устроит любая маленькая латинская буква или цифра). А теперь предположим, что нас устроит любой символ, кроме этого. Это будет обозначаться вот так: “[^a-z0-9]”.


Выбираем все “не пробелы”.

Итак, вот список основных метасимволов:

  • \d — соответствует любой цифре; эквивалент 8
  • \D — соответствует любому не числовому символу; эквивалент [^0-9]
  • \s — соответствует любому символу whitespace; эквивалент [ \t\n\r\f\v]
  • \S — соответствует любому не-whitespace символу; эквивалент [^ \t\n\r\f\v]
  • \w — соответствует любой букве или цифре; эквивалент [a-zA-Z0-9_]
  • \W — наоборот; эквивалент [^a-zA-Z0-9_]
  • . — (просто точка) любой символ, кроме перевода “каретки”

Операторы [] и ()

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


Тут мы собрали в группу (между символами []) все латинские буквы и пробел. При помощи <> указали, что нас интересуют вхождения, где минимум 2 символа, чтобы исключить вхождения из пустых пробелов.

Аналогично мы могли бы получить все русские слова, сделав инверсию: “[^A-Za-z\s]”.

В отличие от [], символы () собирают отмеченные выражения. Их иногда называют “захватом”.

Они нужны для того, чтобы передать выбранный кусок (который, возможно, состоит из нескольких вхождений [] в результат выдачи).


Существует много решений. Пример ниже — это приближенный вариант, который просто покажет возможности регулярных выражений. На самом деле есть RFC, который определяет правильность email. И есть “регулярки” по RFC — вот примеры.

Мы выбираем все, что не пробел (потому что первая часть email может содержать любой набор символов), далее должен идти символ @, далее что угодно, кроме точки и пробела, далее точка, далее любой символ латиницы в нижнем регистре…

  • мы выбираем все, что не пробел: “[^\s]+”
  • мы выбираем знак @: “@”
  • мы выбираем что угодно, кроме точки и пробела: “[^\s\.]+”
  • мы выбираем точку: “\.” (обратный слеш нужен для экранирования метасимвола, так как знак точки описывает любой символ — см. выше)
  • мы выбираем любой символ латиницы в нижнем регистре: “[a-z]+”

Получилось! Но что, если теперь нам надо по отдельности получить домен и имя по email? И как-то использовать дальше в коде? Вот тут нам поможет “захват”. Мы просто выбираем, что нам нужно, и оборачиваем знаками (), как в примере:

В массиве match нулевым элементом всегда идет полное вхождение регулярного выражения. А дальше по очереди идут “захваты”.

В PHP можно именовать “захваты”, используя следующий синтаксис:


Тогда массив матча станет ассоциативным:

Это сразу +100 к читаемости и кода, и регулярки.

Примеры из реальной жизни

Парсим письмо в поисках нового пароля:

Есть письмо с HTML-кодом, надо выдернуть из него новый пароль. Текст может быть либо на английском, либо на русском:


Сначала мы говорим, что текст перед паролем может быть двух вариантов, использовав “или”.
Вариантов можно перечислять сколько угодно:


Далее у нас знак двоеточия и один пробел:


Далее знак тега b:


А дальше нас интересует все, что не символ “<”, поскольку он будет свидетельствовать о том, что тег b закрывается:


Мы оборачиваем его в захват, потому что именно он нам и нужен.
Далее мы пишем закрывающий тег b, проэкранировав символ “/”, так как это спецсимвол:


Все довольно просто.

Парсим URL:

В PHP есть клевая функция, которая помогает работать с урлом, разбирая его на составные части:

Давай сделаем то же самое, только регуляркой? :)


Но можно схитрить и сделать вот так:


В данном случае символ “?” означает, что “s” может есть, может нет…

Далее у нас идет “://”, но символ “/” нам придется экранировать (см. выше):


Далее у нас до знака “/” или до конца строки идет домен. Он может состоять из цифр, букв, знака подчеркивания, тире и точки:


Тут мы собрали в единую группу метасимвол “\w”, точку ”\.” и тире ”-”.

Далее идет URI. Тут все просто, мы берем все до вопросительного знака или конца строки:


Теперь знак вопроса, который может быть, а может не быть:


Дальше все до конца строки, если есть:


Вся красота в итоге выглядит так (к сожалению, я не придумал, как вставить эту часть так, чтобы Habr не считал часть строки — комментарием):


Главное не моргать! :)

Получилось примерно то же самое, только своими руками.


Какие задачи не решаются регулярными выражениями

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

Регулярные выражении — это подвид формальных языков, который в иерархии Хомского принадлежат 3-ому типу, самому простому. Об этом тут.

При помощи этого языка мы не можем, например, парсить синтаксис языков программирования с вложенной грамматикой. Или HTML код.

Примеры задач:

У нас есть span, внутри которых много других span и мы не знаем сколько. Надо выбрать все, что находится внутри этого span:


Само собой, если мы парсим HTML, где есть не только этот span. :)

Суть в том, что мы не можем начать с какого-то момента “считать” символы span и /span, подразумевая, что открывающих и закрывающих символов должно быть равное количество. И “понять”, что закрывающий символ, для которого ранее не было пары — тот самый закрывающий, который обосабливает блок.

То же самое с кодом и символами <>.


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

Для решение таких задач используются языки более высокого уровня.

Заключение

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

Читайте также: