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

Привет, Хабр.

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

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

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

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

Поехали!

Вступление

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

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

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

  • preg_match (http://php.net/manual/en/function.preg-match.php)
  • preg_match_all (http://php.net/manual/en/function.preg-match-all.php)
  • preg_replace (http://php.net/manual/en/function.preg-replace.php)

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

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

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

  • match (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match)
  • test (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)
  • replace (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)

Все дальнейшие примеры предлагаю смотреть в https://regex101.com/. Это удобный и наглядный интерфейс для работы с регулярными выражениями.

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

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

Часто используемыми разделителями являются косые черты “/”, знаки решетки “#” и тильды “~”. Ниже представлены примеры шаблонов с корректными разделителями:

  • /foo bar/
  • #^[^0-9]$#
  • %[a-zA-Z0-9_-]%

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

  • /http:///
  • #http://#

В JavaScript регулярные выражения реализованы отдельным объектом RegExp и интегрированы в методы строк.

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

let regexp = new RegExp("шаблон", "флаги");

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

let regexp = /шаблон/; // без флагов

let regexp = /шаблон/gmi; // с флагами gmi (изучим их дальше)

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

RegExp: /o/
Text: hello world

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

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

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

<?php
$text = ‘hello world’;
$regexp = ‘/o/’;
$result = preg_match($regexp, $text, $match);
var_dump(
    $result,
    $match
);

int(1)  // нам вернулось одно вхождение, т.к. после функция заканчивает работу
array(1) {
  [0]=>
  string(1) "o" // нам вернулось вхождение, аналогичное запросу, так как метасимволов мы пока не использовали
}

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

<?php
$text = ‘hello world’;
$regexp = ‘/o/’;
$result = preg_match_all($regexp, $text, $match);
var_dump(
    $result,
    $match
);

int(2)
array(1) {
  [0]=>
  array(2) {
    [0]=>
    string(1) "o"
    [1]=>
    string(1) "o"
  }
}

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

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

let str = 'Hello world';
let result = str.match(/o/);
console.log(result);

["o", index: 4, input: "Hello world"]

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

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

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

Об остальных модификаторах, используемых в PHP, можно почитать тут.

В JavaScript — тут.

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

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

let str = "hello world 
              How is it going?"
let result = str.match(/o/g);
console.log(result);

["o", "o", "o", "o"]

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

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

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

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

Текст: “Привет, твой номер 1528. Запомни его.”

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

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

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

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

Текст: “Я хочу ходить на работу 2 раза в неделю.”
Надо получить цифру из тексте.
RegExp: “/d/”

Текст: “Ваш пинкод: 24356” или “У вас нет пинкода.”
Надо получить пинкод или ничего, если его нет.
RegExp: “/d*/”

Текст: “Номер телефона 89091534357”
Надо получить первые 11 символов, или FALSE, если их меньше.
RegExp: “/d{11}/”

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

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

C кириллицей указанный диапазон работает по-разному для разных кодировок. В юникоде, например, в этот диапазон не входит буква “ё”. Подробнее об этом тут.

Пара примеров:

Текст: “Вот бежит олень” или “Вот ваш индюк”
Надо выбрать либо слово “олень”, либо слово “индюк”.
RegExp: “/[а-яА-Я]+/”

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

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

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

Предыдущий пример стал проще:

Текст: “Вот бежит олень” или “Вот бежит индюк”
Надо выбрать либо “олень”, либо “индюк”.
RegExp: “/[а-яА-Я]+$/”

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

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

Текст: “Олень вкусный” или “Индюк вкусный”
Надо выбрать либо “олень”, либо “индюк”.
RegExp:  “/^[а-яА-Я]+/”

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

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

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

Пример:

Текст: “Я люблю кушать суп”
Надо выбрать все слова.
RegExp: “[^s]+”

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

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

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

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

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

Пример:

Текст: “Не могу перевести I dont know, помогите!”
Надо получить весь английский текст.
RegExp: “/[A-Za-zs]{2,}/”

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

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

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

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

Пример:

Текст: ‘Email you sent was ololo@example.com Is it correct?’
Нам надо выбрать email.

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

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

Итак, поехали:

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

Оказалось не так сложно. Теперь у нас есть email, собранный по частям. Рассмотрим на примере результата работы preg_match в PHP:

<?php
$text = ‘Email you sent was ololo@example.com. Is it correct?’;
$regexp = ‘/[^s]+@[^s.]+.[a-z]+/’;
$result = preg_match_all($regexp, $text, $match);
var_dump(
    $result,
    $match
);

int(1)
array(1) {
  [0]=>
  array(1) {
    [0]=>
    string(13) "ololo@example.com"
  }
}

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

Было:

/[^s]+@[^s.]+.[a-z]+/

Стало:

/([^s]+)@([^s.]+.[a-z]+)/

Пробуем:

<?php
$text = ‘Email you sent was ololo@example.com. Is it correct?’;
$regexp = ‘/([^s]+)@([^s.]+.[a-z]+)/’;
$result = preg_match_all($regexp, $text, $match);
var_dump(
    $result,
    $match
);

int(1)
array(3) {
  [0]=>
  array(1) {
    [0]=>
    string(13) "ololo@example.com"
  }
  [1]=>
  array(1) {
    [0]=>
    string(5) "ololo"
  }
  [2]=>
  array(1) {
    [0]=>
    string(7) "example.com"
  }
}

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

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

/(?<mail>[^s]+)@(?<domain>[^s.]+.[a-z]+)/

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

<?php
$text = ‘Email you sent was ololo@example.com. Is it correct?’;
$regexp = ‘/(?<mail>[^s]+)@(?<domain>[^s.]+.[a-z]+)/’;
$result = preg_match_all($regexp, $text, $match);
var_dump(
    $result,
    $match
);

int(1)
array(5) {
  [0]=>
  array(1) {
    [0]=>
    string(13) "ololo@example.com"
  }
  ["mail"]=>
  array(1) {
    [0]=>
    string(5) "ololo"
  }
  ["domain"]=>
  array(1) {
    [0]=>
    string(7) "example.com"
  }
}

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

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

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

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

Текст: “пароль: <b>f23f43tgt4</b>” или “password: <b>wh4k38f4</b>”
RegExp: “(password|пароль):s<b>([^<]+)</b>”

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

(password|пароль)

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

:s

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

<b>

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

([^<]+)

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

</b>

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

Парсим URL:

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

<?php
$URL = "https://hello.world.ru/uri/starts/here?get_params=here#anchor";
$parsed = parse_url($URL);
var_dump($parsed);

array(5) {
  ["scheme"]=>
  string(5) "https"
  ["host"]=>
  string(14) "hello.world.ru"
  ["path"]=>
  string(16) "/uri/starts/here"
  ["query"]=>
  string(15) "get_params=here"
  ["fragment"]=>
  string(6) "anchor"
}

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

Любой урл начинается со схемы. Для нас это протокол http/https. Можно было бы сделать логическое “или”:

(http|https)

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

http[s]?

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

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

“://”

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

[w.-]+

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

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

[^?$]+

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

[?]?

Далее все до конца строки или начала якоря (символ #) — не забываем о том, что этой части тоже может не быть:

[^#$]+

Далее может быть #, а может не быть:

[#]?

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

[^$]+

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

/(?<scheme>http[s]?)://(?<domain>[w.-]+)(?<path>[^?$]+)?(?<query>[^#$]+)?[#]?(?<fragment>[^$]+)?/

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

<?php
$URL = "https://hello.world.ru/uri/starts/here?get_params=here#anchor";
$regexp = “/(?<scheme>http[s]?)://(?<domain>[w.-]+)(?<path>[^?$]+)?(?<query>[^#$]+)?[#]?(?<fragment>[^$]+)?/”;
$result = preg_match($regexp, $URL, $match);
var_dump(
    $result,
    $match
);

array(11) {
  [0]=>
  string(61) "https://hello.world.ru/uri/starts/here?get_params=here#anchor"
  ["scheme"]=>
  string(5) "https"
  ["domain"]=>
  string(14) "hello.world.ru"
  ["URI"]=>
  string(16) "/uri/starts/here"
  ["params"]=>
  string(15) "get_params=here"
  ["anchor"]=>
  string(6) "anchor"
}

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

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

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

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

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

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

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

<span>
    <span>ololo1</span>
    <span>ololo2</span>
    <span>ololo3</span>
    <span>ololo4</span>
    <span>ololo5</span>
    <...>
</span>

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

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

То же самое с кодом и символами {}.

Например:

function methodA() {
    function() {<...>}
    if () { if () {<...>} }
}

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

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

Заключение

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

Спасибо за внимание.

#статьи

  • 5 окт 2022

  • 0

Исчерпывающий гайд по работе с мощным инструментом для анализа и обработки строк.

Иллюстрация: Оля Ежак для SKillbox Media

Иван Стуков

Журналист, изучает Python. Любит разбираться в мелочах, общаться с людьми и понимать их.

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

Содержание:

  • Что такое регулярные выражения
  • Синтаксис регулярок
  • Как ведётся поиск
  • Квантификаторы и логическое ИЛИ при группировке
  • Регулярные выражения в Python: модуль re и Match-объекты
  • Жадный и ленивый пропуск
  • Примеры и задачи

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

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

К счастью, вы — человек неглупый (не зря же пошли в IT), тренированный и быстро соображающий. Поэтому моментально замечаете основные закономерности:

  • даты обозначаются цифрами: арабскими, если это год и месяц, и римскими, если век;
  • учебник — по истории позднего Средневековья и Нового времени, поэтому все даты, написанные арабскими цифрами, — четырёхсимвольные;
  • после римских цифр всегда идёт слово «век».

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

По такому же принципу работают и регулярные выражения: они ведут поиск фрагментов текста по определённому шаблону. Если фрагмент совпадает с шаблоном — с ним можно работать.

Запишем логику поиска исторических дат в виде регулярных выражений (они ещё называются Regular Expressions, сокращённо regex или regexp). Выглядеть он будет так:

(?:d{4})|(?:[IVX]+ век)

Приятные новости: regex — настолько полезный и мощный инструмент, что поддерживается почти всеми современными языками программирования, в том числе и Python. Причём соответствующий синтаксис в разных языках очень схож. Так что, выучив его в одном языке, можно пользоваться им в других, практически не переучиваясь. Поехали.

С помощью regex можно искать как вполне конкретные выражения (например, слово «век» — последовательность букв «в», «е» и «к»), так и что-то более общее (например, любую букву или цифру).

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

Символ Что означает Пример использования шаблона Пример вывода
. Любой символ, кроме новой строки (n) H.llo, .orld

20.. год

Hello, world; Hallo, 2orld

2022 год, 2010 год

[…] Любой символ из указанных в скобках. Символы можно задавать как перечислением, так и указывая диапазон через дефис [abc123]

[A-Z]

[A-Za-z0-9]

[А-ЯЁа-яё]

а; 1

B; T

A; s; 1

А; ё

[^…] Любой символ, кроме указанных в скобках [^A-Za-z] з, 4
^ Начало строки ^Добрый день, 0
$ Конец строки До свидания!$ 0
| Логическое ИЛИ. Регулярное выражение будет искать один из нескольких вариантов [0-9]|[IVXLCDM] — регулярное выражение будет находить совпадение, если цифра является либо арабской, либо римской 5; V
Экранирование. Помогает регулярным выражениям ориентироваться, является ли следующий за символ обычным или специальным AdwZ — экранирование превращает буквы алфавита в спецсимволы.

[.] — экранирование превращает спецсимволы в обычные

0

Важное замечание 1. Регулярные выражения зависимы от регистра, то есть «А» и «а» при поиске будут считаться разными символами.

Важное замечание 2. Буквы «Ё» и «ё» не входят в диапазон «А — Я» и «а — я». Так что, задавая русский алфавит, их нужно выписывать отдельно.

На экранировании остановимся подробнее. По умолчанию символы .^$*+? {}[]|() являются спецсимволами — то есть они выполняют определённые функции. Чтобы сделать спецсимволы обычными, их нужно экранировать .

Таким образом, . будет обозначать любой символ, а . — знак точки. Чтобы написать обратный слеш, его тоже нужно экранировать, то есть в регулярных выражениях он будет выглядеть так: \.

Обратная ситуация с некоторыми алфавитными символами. По умолчанию они считаются просто буквами, но при экранировании начинают играть роль спецсимволов.

Символ Что означает
d Любая цифра. То же самое, что [0-9]
D Любой символ, кроме цифры. То же самое, что [^0-9]
w Любая буква, цифра и нижнее подчёркивание
W Любой символ, кроме буквы, цифры и нижнего подчёркивания
s Любой пробельный символ (пробел, новая строка, табуляция, возврат каретки и тому подобное)
S Любой символ, кроме пробельного
A Начало строки. То же самое, что ^
Z Конец строки. То же самое, что $
b Начало или конец слова
B Середина слова
n, t, r Стандартные строковые обозначения: новая строка, табуляция, возврат каретки

Важное замечание. A, Z, b и B указывают не на конкретный символ, а на положение других символов относительно друг друга. Можно сказать, что они указывают на пространство между символами.

Например, регулярное выражение b[А-ЯЁаяё]b будет искать только те буквы, которые отделены друг от друга пробелами или знаками препинания.

Часто при записи регулярного выражения какая-то часть шаблона должна повторяться определённое количество раз. Число вхождений в синтаксисе regex задают с помощью квантификаторов. Они всегда помещаются после той части шаблона, которую нужно повторить.

Символ Что означает Примеры шаблона Примеры вывода
{} Указывает количество вхождений, можно задавать единичным числом или диапазоном d{4} — цифра, четыре подряд

d{1,4} — цифра, от одного до четырёх раз подряд

d{2,} — цифра, от двух раз подряд

d{,4} — цифра, от 0 до 4 раз подряд

1243, 1876

1, 12, 176, 1589

22, 456, 988888

5, 15, 987, 1234

? От нуля до одного вхождения. То же самое, что {0,1} d? 0
* От нуля вхождений. То же самое, что {0,} d* 0
+ От одного вхождения. То же самое, что {1,} d+ 0

Теперь давайте ещё раз посмотрим на наше регулярное выражение для поиска дат по учебнику истории:

(?:d{4})|(?:[IVX]+ век)

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

  • d{4}цифра, четыре подряд
  • | — логическое ИЛИ
  • [IVX]+ вексимвол I, V или X, одно или более вхождений, пробел, слово «век»

Попрактиковаться в составлении регулярных выражений можно на сайте regex101.com. А мы разберём основные приёмы их использования и решим несколько задач.

Уточним ещё несколько терминов regex.

Регулярные выражения — это инструмент для работы со строками, которые и являются основной их единицей.

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

Найденные в тексте совпадения с шаблоном называются подстроками. Например, у нас есть регулярное выражение м. (буква «м», затем любой символ) и текст «Мама мыла раму». Применяя регулярное выражение к тексту, мы найдём подстроки «ма», «мы» и «му». Подстроку «Ма» наше выражение пропустит из-за разницы в регистре.

Есть и более мелкая единица, чем подстрока, — группа. Она представляет собой часть подстроки, которую мы попросили выделить специально. Группы выделяются круглыми скобками (…).

Возьмём ту же строку «Мама мыла раму» и применим к ней следующее регулярное выражение:

(w)(w{3})

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

В нашем тексте это выражение найдёт три совпадения, в каждом из которых выделит две группы:

Подстрока Группа 1 Группа 2
Мама М ама
мыла м ыла
раму р аму

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

Группам можно давать имена с помощью такой формы: (? P<name>…)

Вот так будет выглядеть наш шаблон, ищущий четырёхбуквенные слова, если мы дадим имена группам:

?P<first_letter>w)(?P<rest_letters>w{3})

Уберём группы и упростим регулярное выражение, чтобы оно искало только подстроку:

w{4}

Немного изменим текст, по которому ищем совпадения: «Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке».

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

Исправьте регулярное выражение так, чтобы оно находило только четырёхбуквенные слова. То есть оно должно найти подстроки «мама», «мыла», «раму» и «папа» — и ничего больше.

Подсказка, если не можете решить задачу

Используйте символ b.

Важное замечание. При написании regex нужно помнить, что они ищут только непересекающиеся подстроки. Под шаблон w{4} в слове «работает» подходят не только подстроки «рабо» и «тает», но и «абот», «бота», «отае». Их регулярное выражение не находит, потому что тогда бы эти подстроки пересеклись с другими — а в regex так нельзя.

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

(?:d{4})|(?:[IVX]+ век)

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

Важное замечание.? : в начале группы означает, что мы просим regex не запоминать эту группу. Если все группы открываются символами? :, то регулярные выражения вернут только подстроку и ни одной группы.

В Python это может быть полезно, потому что некоторые re-функции возвращают разные результаты в зависимости от того, запомнили ли регулярные выражения какие-то группы или нет.

Также к группам удобно применять квантификаторы. Например, имена многих дроидов в «Звёздных войнах» построены по принципу: буква — цифра — буква — цифра.

Вот так это выглядит без групп:

[A-Z]d[A-Z]d

И вот так с ними:

(?:[A-Z]d){2}

Особенно полезно использовать незапоминаемые группы со сложными шаблонами.

Чтобы работать с регулярными выражениями в Python, необходимо импортировать модуль re:

import re

Это даёт доступ к нескольким функциям. Вот их краткое описание.

Функция Что делает Если находит совпадение Если не находит совпадение
re.match (pattern, string) Ищет pattern в начале строки string Возвращает Match-объект Возвращает None
re.search (pattern, string) Ищет pattern по всей строке string Возвращает Match-объект с первым совпадением, остальные не находит Возвращает None
re.finditer (pattern, string) Ищет pattern по всей строке string Возвращает итератор, содержащий Match-объекты для каждого найденного совпадения Возвращает пустой итератор
re.findall (pattern, string) Ищет pattern по всей строке string Возвращает список со всеми найденными совпадениями Возвращает None
re.split (pattern, string, [maxsplit=0]) Разделяет строку string по подстрокам, соответствующим pattern Возвращает список строк, на которые разделила исходную строку Возвращает список строк, единственный элемент которого — неразделённая исходная строка
re.sub (pattern, repl, string) Заменяет в строке string все pattern на repl Возвращает строку в изменённом виде Возвращает строку в исходном виде
re.compile (pattern) Собирает регулярное выражение в объект для будущего использования в других re-функциях Ничего не ищет, всегда возвращает Pattern-объект 0

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

Для написания регулярных выражений в Python используют r-строки (их называют сырыми, или необработанными). Это связано с тем, что написание знака требует экранирования не только в регулярных выражениях, но и в самом Python тоже.

Чтобы программистам не приходилось экранировать экранирование и писать нагромождения обратных слешей, и придумали r-строки. Синтаксически они обозначаются так:

r'...'

Перечислим самые популярные из них.

Находит совпадение только в том случае, если соответствующая шаблону подстрока находится в начале строки, по которой ведётся поиск:

print (re.match (r'Мама', 'Мама мыла раму'))
>>> <re.Match object; span=(0, 4), match='Мама'>

print (re.match (r'мыла', 'Мама мыла раму'))
>>> None

Как видим, поиск по шаблону «Мама» нашёл совпадение и вернул Match-объект. Слово же «мыла», хотя и есть в строке, находится не в начале. Поэтому регулярное выражение ничего не находит и возвращается None.

Ищет совпадения по всему тексту:

print (re.search (r'Мама', 'Мама мыла раму'))
>>> <re.Match object; span=(0, 4), match='Мама'>

print (re.search (r'мыла', 'Мама мыла раму'))
>>> <re.Match object; span=(5, 9), match='мыла'>

При этом re.search возвращает только первое совпадение, даже если в строке, по которой ведётся поиск, их больше. Проверим это:

print (re.search (r'мыла', 'Мама мыла раму, а потом ещё раз мыла, потому что не домыла'))
>>> <re.Match object; span=(5, 9), match='мыла'>

Возвращает итератор с объектами, к которым можно обратиться через цикл:

results = re.finditer (r'мыла', 'Мама мыла раму, а потом ещё раз мыла, потому что не домыла')
print (results)
>>> <callable_iterator object at 0x000001C4CDE446D0>

for match in results:
    print (match)
>>> <re.Match object; span=(5, 9), match='мыла'>
>>> <re.Match object; span=(32, 36), match='мыла'>
>>> <re.Match object; span=(54, 58), match='мыла'>

Эта функция очень полезна, если вы хотите получить Match-объект для каждого совпадения.

В Match-объектах хранится много всего интересного. Посмотрим внимательнее на объект с подстрокой «Мама», который нашла функция re.match:

<re.Match object; span=(0, 4), match='Мама'>

span — это индекс начала и конца найденной подстроки в тексте, по которому мы искали совпадение. Обратите внимание, что второй индекс не включается в подстроку.

match — это собственно найденная подстрока. Если подстрока длинная, то она будет отображаться не целиком.

Это, конечно же, не всё, что можно получить от Match-объекта. Рассмотрим ещё несколько методов.

Возвращает найденную подстроку, если ему не передавать аргумент или передать аргумент 0. То же самое делает обращение к объекту по индексу 0:

match = re.match (r'Мама', 'Мама мыла раму')

print (match.group())
>>> Мама

print (match.group(0))
>>> Мама

print (match[0])
>>> Мама

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

match = re.match (r'(М)(ама)', 'Мама мыла раму')

print (match.group(1))
print (match.group(2))
>>> М
>>> ама

print (match[1])
print (match[2])
>>> М
>>> ама

#Методом group также можно получить кортеж из нужных групп.
print (match.group(1,2))
>>> ('М', 'ама')

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

match = re.match (r'(?P<first_letter>М)(?P<rest_letters>ама)', 'Мама мыла раму')

print (match.group('first_letter'))
print (match.group('rest_letters'))
>>> М
>>> ама

Если одна и та же группа соответствует шаблону несколько раз, то в группу запишется только последнее совпадение:

#Помещаем в группу один буквенный символ, при этом шаблон представляет собой четыре таких символа.
match = re.match (r'(w){4}', 'Мама мыла раму')

print (match.group(0))
>>> Мама
print (match.group(1))
>>> а

Возвращает кортеж с группами:

match = re.match (r'(М)(ама)', 'Мама мыла раму')

print (match.groups())
>>> ('М', 'ама')

Возвращает кортеж с индексом начала и конца подстроки в исходном тексте. Если мы хотим получить только первый индекс, можно использовать метод start, только последний — end:

match = re.search (r'мыла', 'Мама мыла раму')

print (match.span())
>>> (5, 9)
print (match.start())
>>> 5
print (match.end())
>>> 9

Возвращает просто список совпадений. Никаких Match-объектов, к которым нужно дополнительно обращаться:

#В этом примере в качестве регулярного выражения мы используем правильный ответ на задание 0.
match_list = re.findall (r'bw{4}b', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (match_list)
>>> ['Мама', 'мыла', 'раму', 'папа']

Функция ведёт себя по-другому, если в регулярном выражении есть деление на группы. Тогда функция возвращает список кортежей с группами:

match_list = re.findall (r'b(w{1})(w{3})b', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (match_list)
>>> [('М', 'ама'), ('м', 'ыла'), ('р', 'аму'), ('п', 'апа')]

Аналог метода str.split. Делит исходную строку по шаблону, а сам шаблон исключает из результата:

#Поделим строку по запятой и пробелу после неё.
split_string = re.split (r', ', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (split_string)
>>> ['Мама мыла раму', 'а папа был на пилораме', 'потому что работает на лесопилке.']

re.split также имеет дополнительный аргумент maxsplit — это максимальное количество частей, на которые функция может поделить строку. По умолчанию maxsplit равен нулю, то есть не устанавливает никаких ограничений:

#Приравняем аргумент maxsplit к единице.
split_string = re.split (r', ', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.', maxsplit=1)
print (split_string)
>>> ['Мама мыла раму', 'а папа был на пилораме, потому что работает на лесопилке.']

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

#Помещаем буквы «п» и «а» в одну группу.
split_string = re.split (r'(па)', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (split_string)
>>> ['Мама мыла раму, а ', 'па', '', 'па', ' был на пилораме, потому что работает на лесопилке.']

#Помещаем буквы «п» и «а» в разные группы.
split_string = re.split (r'(п)(а)', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (split_string)
>>> ['Мама мыла раму, а ', 'п', 'а', '', 'п', 'а', ' был на пилораме, потому что работает на лесопилке.']

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

new_string = re.sub (r'Мама', 'Дочка', 'Мама мыла раму, а папа был на пилораме, потому что работает на лесопилке.')
print (new_string)
>>> Дочка мыла раму, а папа был на пилораме, потому что работает на лесопилке.

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

new_string = re.sub (r'(w+) (w+) (w+),', r'2 3 1 –', 'Бендер Остап Ибрагимович, директор ООО "Рога и копыта"')
print (new_string)
>>> Остап Ибрагимович Бендер — директор ООО "Рога и копыта"

Используется для ускорения и упрощения кода, когда одно и то же регулярное выражение применяется в нём несколько раз. Её синтаксис выглядит так:

pattern = re.compile (r'Мама')

print (pattern.search ('Мама мыла раму'))
>>> <re.Match object; span=(0, 4), match='Мама'>

print (pattern.sub ('Дочка', 'Мама мыла раму'))
>>> Дочка мыла раму

Нередко в регулярных выражениях нужно учесть сразу много вариантов и опций, из-за чего их структура усложняется. А regex даже простые и короткие читать нелегко, что уж говорить о длинных.

Чтобы хоть как-то облегчить чтение регулярок, в Python r-строки можно делить точно так же, как и обычные. Возьмём наше выражение для поиска дат по учебнику истории:

re.findall (r'(?:d{4})|(?:[IVX]+ век)', text)

Его же можно написать вот в таком виде:

re.findall (r'(?:d{4})'
            r'|'
            r'(?:[IVX]+ век)', text)

Часто при написании регулярных выражений приходится использовать квантификаторы, охватывающие диапазон значений. Например, d{1,4}. Как регулярные выражения решают, сколько цифр им захватить, одну или четыре? Это определяется пропуском квантификаторов.

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

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

Фрязево — 61-й километр……….64 61-й километр — 65-й километр…68 65-й километр — Павлово-Посад…71 Павлово-Посад — Назарьево……..73 Назарьево — Дрезна……………77 Дрезна — 85-й километр………..80

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

  • Каждый пункт начинается с буквы или цифры (для этого используем шаблон w).
  • Он может содержать внутри себя любой набор символов: буквы, цифры, знаки препинания (для этого используем шаблон .+).
  • Он заканчивается на точку, после которой следует от одной до трёх цифр (для этого используем шаблон .d{1,3}).

Посмотрим в конструкторе, как работает наше выражение:

Скриншот: Skillbox Media

Что же произошло? Почему найдено только одно совпадение, причем за него посчитали весь текст сразу? Всё дело в жадности квантификатора +, который старается захватить максимально возможное количество подходящих символов.

В итоге шаблон w находит совпадение с буквой «Ф» в начале текста, шаблон .d{1,3} находит совпадение с «.80» в конце текста, а всё, что между ними, покрывается шаблоном .+.

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

Если нет, то функция будет искать следующее совпадение с .. А если да, то . закончит свою работу и следующие символы строки будут сравниваться со следующей частью регулярного выражения: .d{1,3}.

Чтобы объявить квантификатор ленивым, после него надо поставить символ ?. Сделаем ленивым квантификатор + в нашем регулярном выражении для поиска строк в оглавлении:

Скриншот: Skillbox Media

Теперь, когда мы уверены в правильности работы нашего регулярного выражения, используем функцию re.findall, чтобы выписать оглавление построчно:

content = 'Фрязево — 61-й километр..........64 61-й километр — 65-й километр....68 65-й километр — Павлово-Посад....71 Павлово-Посад — Назарьево........73 Назарьево — Дрезна...............77 Дрезна — 85-й километр...........80'

strings = re.findall (r'w.+?.d{1,3}', content)
for string in strings:
    print (string)

#Результат на экране.
>>> Фрязево — 61-й километр..........64
>>> 61-й километр — 65-й километр....68
>>> 65-й километр — Павлово-Посад....71
>>> Павлово-Посад — Назарьево........73
>>> Назарьево — Дрезна...............77
>>> Дрезна — 85-й километр...........80

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

При обнародовании судебных решений из них извлекают персональные данные участников процесса — фамилии, имена и отчества. Каждое слово в Ф. И. О. начинается с заглавной буквы, при этом фамилия может быть двойная.

Напишите программу, которая заменит в тексте Ф. И. О. подсудимого на N.

Подсудимая Эверт-Колокольцева Елизавета Александровна в судебном заседании вину инкриминируемого правонарушения признала в полном объёме и суду показала, что 14 сентября 1876 года, будучи в состоянии алкогольного опьянения от безысходности, в связи с состоянием здоровья позвонила со своего стационарного телефона в полицию, сообщив о том, что у неё в квартире якобы заложена бомба. После чего приехали сотрудники полиции, скорая и пожарные, которым она сообщила, что бомба — это она.

«Подсудимая N в судебном заседании» и далее по тексту.

Подсказка

Используйте незапоминаемую опциональную группу вида (? : …)? , чтобы обозначить вторую часть фамилии после дефиса.

Решение

#Сначала кладём в переменную string текст строки, по которой ведём поиск.
print (re.sub (r'[А-ЯЁ]w*'
          r'(?:-[А-ЯЁ]w*)?'
          r'(?: [А-ЯЁ]w*){2}', 'N', string))

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

Перед названием улицы может быть написано «Улица», «улица», «Ул.» или «ул.», перед номером дома — «дом» или «д.», перед номером квартиры — «квартира» или «кв.». Также номер дома и номер квартиры могут быть разделены дефисом без пробелов.

Дан текст, в нём нужно найти все адреса и вывести их в виде «Пушкина 32-135».

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

Добрый день!

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

Петрову П. П. попросили выехать по адресам ул. Культуры 78 кв. 6, улица Мира дом 12Б квартира 144. Смирнова С. С. просят подъехать только по адресу: Восьмого Марта 106-19. Без предпочтений по специалистам пришли запросы с адресов: улица Свободы 54 6, Улица Шишкина дом 9 кв. 15, ул. Лермонтова 18 кв. 93.

Все адреса скопированы из заявок, корректность подтверждена.

Культуры 78-6

Мира 12Б-144

Восьмого Марта 106-19

Свободы 54-6

Шишкина 9-15

Лермонтова 18-93

Подсказка

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

Решение

#Сначала кладём в переменную string текст строки, по которой ведём поиск.
pattern = re.compile (r'(?:[Уу]л(?:.|ица) )?'
                      r'((?:[А-ЯЁ]w+)(?: [А-ЯЁ]w+)*)'
                      r' (?:дом |д. )?'
                      r'(d+w?)'
                      r'[ -](?:квартира |кв. )?'
                      r'(d+)')

addresses = pattern.findall (text)
for address in addresses:
    print (f'{address[0]} {address[1]}-{address[2]}')

Структура этого регулярного выражения довольно сложная. Чтобы в нём разобраться, посмотрите на схему. Прямоугольники обозначают обязательные элементы, овалы — опциональные. Развилки символизируют разные варианты, которые допускает наш шаблон. Красным цветом очерчены группы, которые мы запоминаем.

Инфографика: Майя Мальгина для Skillbox Media

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

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

Важно. Чтобы регулярные выражения не рассматривали заглавные и прописные буквы как разные символы, передайте re-функции дополнительный аргумент flags=re.I или flags=re.IGNORECASE.

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

А в этом предложении есть слова, которые всё-таки начинаются на одну и ту же букву.

А здесь совсем интересно: символ «а» однобуквенный.

Метод Довлатова соблюдён

Вы расстроили Сергея Донатовича

Вы расстроили Сергея Донатовича

Подсказка

Чтобы указать на начало слова, используйте символ b.

Чтобы в каждом совпадении regex не старалось захватить максимум, используйте ленивый пропуск.

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

Решение

#Сначала кладём в переменную string текст строки, по которой ведём поиск.
pattern = r'b(w)w*.*?b1'

match = re.search (pattern, string, flags=re.I)
if match is None:
    print ('Метод Довлатова соблюдён')
else:
    print ('Вы расстроили Сергея Донатовича')

Вернёмся к регулярному выражению, которое ищет даты в учебнике истории: (? :d{4})|(? : [IVX]+ век).

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

Чтобы не получать лишние результаты, обратим внимание на то, как именно могут быть записаны годы. Есть несколько вариантов записи: 1400 год, 1400 г., 1400–1500 годы, 1400–1500 гг., (1400), (1400–1500).

Чтобы немного упростить задачу и не раздувать регулярное выражение, мы не будем искать конструкции «с такого-то по такой-то год» и «между таким-то и таким-то годом».

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

Началом Реформации принято считать 31 октября 1517 г. — день, когда Мартин Лютер (1483–1546) прибил к дверям виттенбергской Замковой церкви свои «95 тезисов», в которых выступил против злоупотреблений Католической церкви. Реформация охватила практически всю Европу и продолжалась в течение всего XVI века и первой половины XVII века. Одно из самых известных и кровавых событий Реформации — Варфоломеевская ночь во Франции, произошедшая в ночь на 24 августа 1572 года.

Точное число жертв так и не удалось установить достоверно. Погибли по меньшей мере 2000 гугенотов в Париже и 3000 — в провинциях. Герцог де Сюлли, сам едва избежавший смерти во время резни, говорил о 70 000 жертв. Для Парижа единственным точным числом остаётся 1100 погибших во время Варфоломеевской ночи.

Этому событию предшествовали три других, произошедшие в 1570–1572 годах: Сен-Жерменский мирный договор (1570), свадьба гугенота Генриха Наваррского и Маргариты Валуа (1572) и неудавшееся покушение на убийство адмирала Колиньи (1572).

[‘1517 г.’, ‘(1483–1546)’, ‘XVI век’, ‘XVII век’, ‘1572 год’, ‘1570–1572 годах’, ‘(1570)’, ‘(1572)’, ‘(1572)’]

Решение

#Сначала кладём в переменную string текст строки, по которой ведём поиск.
pattern = re.compile (r'(?:(d{4}(?:-d{4})?))'
                      r'|'
                      r'(?:'
                          r'(?:d{4}-)?d{4} '
                          r'(?:'
                              r'(?:год(?:ы|ах|ов)?)'
                              r'|'
                              r'(?:гг?.)'
                          r')'
                      r')'
                      r'|'
                      r'(?:[IVX]+ век)')

print (pattern.findall (string))

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

Инфографика: Майя Мальгина для Skillbox Media

Научитесь: Профессия Python-разработчик
Узнать больше

Apache Commons Lang provides a host of helper utilities for the java.lang API, most notably String manipulation methods.
In your case, the start and end substrings are the same, so just call the following function.

StringUtils.substringBetween(String str, String tag)

Gets the String that is nested in between two instances of the same
String
.

If the start and the end substrings are different then use the following overloaded method.

StringUtils.substringBetween(String str, String open, String close)

Gets the String that is nested in between two Strings.

If you want all instances of the matching substrings, then use,

StringUtils.substringsBetween(String str, String open, String close)

Searches a String for substrings delimited by a start and end tag,
returning all matching substrings in an array.

For the example in question to get all instances of the matching substring

String[] results = StringUtils.substringsBetween(mydata, "'", "'");

Шпаргалка по регулярным выражениям

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

  Аналог Пример Описание
? {0,1} a? одно или ноль вхождений «а»
+ {1,} a+ одно или более вхождений «а»
* {0,} a* ноль или более вхождений «а»

Модификаторы

Символ «минус» (-) меред модификатором (за исключением U) создаёт его отрицание.

  Описание
g глобальный поиск (обрабатываются все совпадения с шаблоном поиска)
i игнорировать регистр
m многострочный поиск. Поясню: по умолчанию текст это одна строка, с модификатором есть отдельные строки, а значит ^– начало строки в тексте, $– конец строки в тексте.
s текст воспринимается как одна строка, спец символ «точка» (.) будет вкючать и перевод строки
u используется кодировка UTF-8
U инвертировать жадность
x игнорировать все неэкранированные пробельные и перечисленные в классе символы

Спецсимволы

  Аналог Описание
()   подмаска, вложенное выражение
[]   групповой символ
{a,b}   количество вхождений от «a» до «b»
|   логическое «или», в случае с односимвольными альтернативами используйте []
  экранирование спец символа
.   любой сивол, кроме перевода строки
d [0-9] десятичная цифра
D [^d] любой символ, кроме десятичной цифры
f   конец (разрыв) страницы
n   перевод строки
pL   буква в кодировке UTF-8 при использовании модификатора u
r   возврат каретки
s [ tvrnf] пробельный символ
S [^s] любой символ, кроме промельного
t   табуляция
w [0-9a-z_] любая цифра, буква или знак подчеркивания
W [^w] любой символ, кроме цифры, буквы или знака подчеркивания
v   вертикальная табуляция

Спецсимволы внутри символьного класса

  Пример Описание
^ [^da] отрицание, любой символ кроме «d» или «a»
[a-z] интервал, любой симво от «a» до «z»

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

  Пример Соответствие Описание
^ ^a aaa aaa начало строки
$ a$ aaa aaa конец строки
A Aa aaa aaa
aaa aaa
начало текста
z az aaa aaa
aaa aaa
конец текста
b ab
ba
aaa aaa
aaa aaa
граница слова, утверждение: предыдущий символ словесный, а следующий – нет, либо наоборот
B BaB aaa aaa отсутствие границы слова
G Ga aaa aaa Предыдущий успешный поиск, поиск остановился на 4-й позиции — там, где не нашлось a

Скачать в PDF, PNG.

Якоря

Якоря в регулярных выражениях указывают на начало или конец чего-либо. Например, строки или слова. Они представлены определенными символами. К примеру, шаблон, соответствующий строке, начинающейся с цифры, должен иметь следующий вид:

^[0-9]+

Здесь символ ^обозначает начало строки. Без него шаблон соответствовал бы любой строке, содержащей цифру.

Символьные классы

Символьные классы в регулярных выражениях соответствуют сразу некоторому набору символов. Например, dсоответствует любой цифре от 0 до 9 включительно, wсоответствует буквам и цифрам, а
W— всем символам, кроме букв и цифр. Шаблон, идентифицирующий буквы, цифры и пробел, выглядит
так:

ws

POSIX

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

Утверждения

Поначалу практически у всех возникают трудности с пониманием утверждений, однако познакомившись с ними ближе, вы
будете использовать их довольно часто. Утверждения предоставляют способ сказать: «я хочу найти в этом документе
каждое слово, включающее букву “q”, за которой не следует “werty”».

[^s]*q(?!werty)[^s]*

Приведенный выше код начинается с поиска любых символов, кроме пробела ([^s]*), за которыми следует
q. Затем парсер достигает «смотрящего вперед» утверждения. Это автоматически делает предшествующий
элемент (символ, группу или символьный класс) условным — он будет соответствовать шаблону, только если
утверждение верно. В нашем случае, утверждение является отрицательным (?!), т. е. оно будет верным,
если то, что в нем ищется, не будет найдено.

Итак, парсер проверяет несколько следующих символов по предложенному шаблону (werty). Если они найдены,
то утверждение ложно, а значит символ qбудет «проигнорирован», т. е. не будет соответствовать шаблону.
Если же wertyне найдено, то утверждение верно, и с qвсе в порядке. Затем продолжается
поиск любых символов, кроме пробела ([^s]*).

Кванторы

Кванторы позволяют определить часть шаблона, которая должна повторяться несколько раз подряд. Например, если вы
хотите выяснить, содержит ли документ строку из от 10 до 20 (включительно) букв «a», то можно использовать этот
шаблон:

a{10,20}

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

".*"

Этот шаблон соответствует тексту, заключенному в двойные кавычки. Однако, ваша исходная строка может быть вроде
этой:

<a href="helloworld.htm" title="Привет, Мир">Привет, Мир</a>

Приведенный выше шаблон найдет в этой строке вот такую подстроку:

"helloworld.htm" title="Привет, Мир"

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

".*?"

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

"helloworld.htm" "Привет, Мир"

Экранирование в регулярных выражениях

Регулярные выражения используют некоторые символы для обозначения различных частей шаблона. Однако, возникает
проблема, если вам нужно найти один из таких символов в строке, как обычный символ. Точка, к примеру, в регулярном
выражении обозначает «любой символ, кроме переноса строки». Если вам нужно найти точку в строке, вы не можете просто
использовать «.» в качестве шаблона — это приведет к нахождению практически всего. Итак, вам
необходимо сообщить парсеру, что эта точка должна считаться обычной точкой, а не «любым символом». Это делается с
помощью знака экранирования.

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

Шаблон для нахождения точки таков:

.

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

Спецсимволы экранирования в регулярных выражениях

Выражение Соответствие
не соответствует ничему, только экранирует следующий за ним символ. Это нужно, если вы хотите ввести метасимволы !$()*+.<>?[]^{|}в качестве их буквальных значений.
Q не соответствует ничему, только экранирует все символы вплоть до E
E не соответствует ничему, только прекращает экранирование, начатое Q

Подстановка строк

Подстановка строк подробно описана в следующем параграфе «Группы и диапазоны», однако здесь следует упомянуть о
существовании «пассивных» групп. Это группы, игнорируемые при подстановке, что очень полезно, если вы хотите
использовать в шаблоне условие «или», но не хотите, чтобы эта группа принимала участие в подстановке.

Группы и диапазоны

Группы и диапазоны очень-очень полезны. Вероятно, проще будет начать с диапазонов. Они позволяют указать набор
подходящих символов. Например, чтобы проверить, содержит ли строка шестнадцатеричные цифры (от 0 до 9 и от A до F),
следует использовать такой диапазон:

[A-Fa-f0-9]

Чтобы проверить обратное, используйте отрицательный диапазон, который в нашем случае подходит под любой символ, кроме
цифр от 0 до 9 и букв от A до F:

[^A-Fa-f0-9]

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

Использовать «или» очень просто: следующий шаблон ищет «ab» или «bc»:

(ab|bc)

Если в регулярном выражении необходимо сослаться на какую-то из предшествующих групп, следует использовать
n, где вместо nподставить номер нужной группы. Вам может понадобиться шаблон,
соответствующий буквам «aaa» или «bbb», за которыми следует число, а затем те же три буквы. Такой шаблон реализуется
с помощью групп:

(aaa|bbb)[0-9]+1

Первая часть шаблона ищет «aaa» или «bbb», объединяя найденные буквы в группу. За этим следует поиск одной или более
цифр ([0-9]+), и наконец 1. Последняя часть шаблона ссылается на первую группу и ищет то
же самое. Она ищет совпадение с текстом, уже найденным первой частью шаблона, а не соответствующее ему. Таким
образом, «aaa123bbb» не будет удовлетворять вышеприведенному шаблону, так как 1будет искать «aaa»
после числа.

Одним из наиболее полезных инструментов в регулярных выражениях является подстановка строк. При замене текста можно
сослаться на найденную группу, используя $n. Скажем, вы хотите выделить в тексте все слова «wish»
жирным начертанием. Для этого вам следует использовать функцию замены по регулярному выражению, которая может
выглядеть так:

replace(pattern, replacement, subject)

Первым параметром будет примерно такой шаблон (возможно вам понадобятся несколько дополнительных символов для этой
конкретной функции):

([^A-Za-z0-9])(wish)([^A-Za-z0-9])

Он найдет любые вхождения слова «wish» вместе с предыдущим и следующим символами, если только это не буквы или цифры.
Тогда ваша подстановка может быть такой:

$1<b>$2</b>$3

Ею будет заменена вся найденная по шаблону строка. Мы начинаем замену с первого найденного символа (который не буква
и не цифра), отмечая его $1. Без этого мы бы просто удалили этот символ из текста. То же касается конца
подстановки ($3). В середину мы добавили HTML тег для жирного начертания (разумеется, вместо него вы
можете использовать CSS или <strong>), выделив им вторую группу, найденную по шаблону
($2).

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

Модификаторы шаблонов используются в нескольких языках, в частности, в Perl. Они позволяют изменить работу парсера.
Например, модификатор iзаставляет парсер игнорировать регистры.

Регулярные выражения в Perl обрамляются одним и тем же символом в начале и в конце. Это может быть любой символ (чаще
используется «/»), и выглядит все таким образом:

/pattern/

Модификаторы добавляются в конец этой строки, вот так:

/pattern/i

Мета-символы

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

(

Шпаргалка представляет собой общее руководство по шаблонам регулярных выражений без учета специфики какого-либо языка.
Она представлена в виде таблицы, помещающейся на одном печатном листе формата A4.
Создана под лицензией Creative Commons на базе шпаргалки, автором которой является Dave Child.
Скачать в PDF, PNG.

  1. Регулярные выражения

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

Содержание

  • Регулярные выражения и их синтаксис
  • Модуль стандартной библиотеки re и функции re.search
  • Домашнее задание

Регулярные выражения и их синтаксис

Регулярные выражения (RE, regexp) нужны, чтобы находить в строках подстроки не по точному вхождению, а описываемые правилами-шаблонами.

! Если нужно найти точное вхождение, лучше использовать стандартные методы строк, а не re.

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

  • . один любой символ
  • ? 0 или 1 вхождение предыдущего символа
  • * предыдущий символ повторяется ≥ 0 раз (0, 1, 2, 3 и т. д.)
  • + предыдущий символ повторяется ≥ 1 раз (1, 2, 3 и т. д.)
  • ^ начало строки
  • $ конец строки
  • [abc] «или»: любой из символов а, b, c
  • [а-я] любая буква русского алфавита от «а» до «я» Внутри квадратных скобок большинство специальных символ не действуют: . обозначает точку, ? — вопросительный знак. Вне квадратных скобок, чтобы получить точку или, например, плюс, специальные символы надо экранировать с помощью (. обозначает точку, + обозначает плюс).
  • [^abc] — отрицание: любой символ, кроме a, b, c.
  • d любая цифра, аналогично [0-9]
  • D — любой символ, кроме цифр (отрицание d или [^0-9])
  • w — буквы, цифры, _ (то же, что [a-zA-Z0-9_]), W — всё кроме букв, цифр, _.
  • s — любой пробелоподбный символ ([ tnrfv]), S — любой непробелоподбный символ

Регулярные выражения в питоне

Официальный HOWTO

Мы будем использовать модуль re:

Функция re.search(pattern, string) возвращает первое вхождение подстроки, которая подходит под регулярное выражение. Обратите внимание на порядок аргументов: первый — это регулярное выражение, второй — исходная строка, в которой мы ищем.

first_match = re.search('кот.', 'Кот пришёл к коту и спросил кота: «Бойкот, котелок или скотч?»')

re.search возвращает объект match (или None, если ничего не нашлось), из которого затем можно извлечь результат методом group():

if first_match:
    first_match.group()    # 'кот '
else:
    print('Nothing found.')

Ещё одна полезная функция re.findall(pattern, string) находит все вхождения подходящих строк:

all_results = re.findall('кот.', 'кот пришёл к коту и спросил кота: «Бойкот, котелок или скотч?»')
all_results    # ['кот ', 'коту', 'кота', 'кот,', 'коте', 'котч']

“Сырые” (raw) строки

Для регулярных выражений в питоне лучше использовать синтаксис для «сырых» (raw) строк. Если добавить r в начале строки (r'd'), то питон будет обрабатывать эту строку не как обычную, а как сырую.

Зачем это нужно? В регулярных выражениях для экранирования специальных символов используется – например, . обозначает точку, + – плюс, и т. д. Но когда питон читает обычные, не “сырые” строки (ещё до того, как мы с этой строкой что-то сделали), он тоже использует для экранирования символов, но по-другому (например n – это перенос строки, t – табуляция, а \ – буквально символ ). Из-за различий в правилах экранирования может возникнуть путаница – например, при попытке обработать строку d как обычную, не как “сырую”, питон выдаст синтаксическую ошибку, потому что он не знает, во что преобразовать d. Другой пример – в синтаксисе регулярных выражений b обозначает границу слова, а в синтаксисе питоновских строк – символ backspace (поэтому, когда мы не используем “сырые” строки, b преобразуется в backspace).

Предположим, нам нужно написать регулярное выражение, соответствующее подстроке section. В синтаксисе регулярных выражений оно будет выглядеть так: \section (символ должен быть экранирован). Но, если мы не используем “сырые” строки, то нам придётся записать его как \\section – оба символа должны быть экранированы ещё раз, чтобы питон их правильно прочитал. Довольно неудобно.

В “сырых” строках питон ничего не экранирует, а читается как обычный символ и экранируется только в регулярном выражении, поэтому путаницы не возникает. Например, на print('Hello!nHi!') мы получим

a на print(r'Hello!nHi!')

Соответственно, “сырая” строка с регулярным выражением для поиска подстроки section будет выглядеть просто как r'\section'.

Кроме того, “сырые” строки удобно использовать для хранения пути к файлам в Windows (например r'C:Documentsmyfile.txt'), так как там тоже используется .

Ограничение: “сырые” строки не могут содержать нечётное количество символов в конце.

Найдем все числа в строке:

digits_list = re.findall(r'd', 'Я родился 30 февраля 1930 года')
digits_str = ''.join(digits)    # '301930'

Домашнее задание

Задача для всех вариантов – написать 10 функций, использующих регулярные выражения. Решение не должно содержать в себе ничего, кроме функций. Для того, чтобы удалять из текста подстроки, соответствующие определённому паттерну, воспользуйтесь функцией re.sub.

Скелет программы для работы вот такой:

def phone_num_validation(string):
    pass


def date_validation(string):
    pass


def roman_num_validation(string):
    pass


def ipv4_validation(string):
    pass


def whitespace_cleaning(filename):
    pass


def named_entitites_counter(filename):
    pass


def numbering_cleaning(filename):
    pass


def loanwords_extraction(filename):
    pass


def html_tag_extraction(filename):
    pass


def html_tag_cleaning(filename):
    pass

Первые 4 функции принимают на вход строку и возвращают True или False в зависимости от того, является ли строка:

  • номером мобильного телефона формата +7 (9ХХ) ХХХ-ХХ-ХХ
  • датой формата DD.MM.YYYY в диапазоне от 1000 до 1999 года включительно
  • числом в римской системе счисления
  • валидным IPv4 адресом в десятичной с точками форме записи (например, 127.0.0.1)

Следующие 4 функции принимают на вход название текстового файла с русским текстом в кодировке UTF-8 и:

  • удаляет из текста все лишние пробелы и переносы строк – заменяет все последовательности из двух и более пробелов или переносов строк на один пробел или перенос строки соотвественно, и возвращает очищенный текст.
  • возвращает частотный словарь (воспользуйтесь классом collections.Counter) всех словоформ имён собственных, кроме тех, которые стоят в начале предложения
  • удаляет из текста нумерацию глав и возвращает очищенный текст. Нумерацией глав считать любые арабские или римские числа, находящиеся на отдельной строке (после числа может стоять одна точка).
  • возвращает список заимствований в тексте. Заимствованиями считать любые последовательности букв латинского алфавита (кроме римских цифр), разделённых пробелами и переносами строк (примеры из тестового текста: 'Hollywood Canteen', 'Black and Tan Fantasy'). Подсказка: пользуйтесь функциями, написанными ранее.

Последние 2 функции принимают на вход название HTML-файла в кодировке UTF-8 и:

  • возвращает список всех HTML-тэгов.
  • чистит текст файла от HTML-тэгов и возвращает очищенный текст.

Тестировать можно, например, на “Пене дней” (txt, html).

Добавить комментарий