Основы: поиск по шаблону вместо LIKE

Метасимволы: точка и экранирование

26 min
O que você vai aprender
  • замечать знаки, которые регулярка воспринимает как команды;
  • читать точку . как место ровно для одного символа;
  • искать настоящие точку, плюс и вопросительный знак;
  • экранировать служебные знаки обратной косой чертой;
  • разбираться, почему одна и та же косая черта по-разному записывается в обычной SQL-строке и в E'...'.

Ты открываешь в Сортировочной обращение 21. Покупатель прислал подозрительную ссылку: kotomarket-ru.shop. Отправитель просит ввести данные карты.

Комендант кладёт рядом настоящий адрес магазина — kotomarket.ru — и просит найти его упоминания в архиве. После прошлого урока рука сама пишет шаблон: kotomarket.ru, оператор ~, запуск.

Но присмотрись к двум адресам. Они расходятся как раз на месте точки: у магазина после kotomarket стоит ., а в подозрительной ссылке — -.

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

КВЕРИ: Адрес переписан точно. Теперь проверим, что именно регулярка понимает под точкой.

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

Одна свободная клетка

Возьмём короткую строку, где всё видно без таблиц и логов: кот.

Шаблон к.т читаем слева направо. Сначала нужна буква к. Затем идёт точка . — на этом месте подойдёт один любой символ. После него обязательно должна стоять т.

На строке кот получаем: к → о → т. Все три позиции совпали.

Проверим кит: к → и → т. Тоже подходит. Точка не требует букву о; ей всё равно, какой именно один символ окажется посередине.

Теперь к-т: к → - → т. Дефис занимает ровно одну позицию, поэтому и эта строка подходит.

С кт уже не выйдет. После к шаблону нужен отдельный символ для точки, а строка сразу дошла до т. Пропустить эту позицию нельзя. У коот обратная проблема: между к и последней т стоят два символа, а точка забирает только один.

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

Теперь вернись к kotomarket.ru. Буквы kotomarket идут буквально. На месте точки движок оставляет одну свободную позицию. После неё ждёт ru.

В kotomarket.ru эту позицию занимает точка из строки. В kotomarket-ru туда встаёт дефис. Поэтому оба фрагмента удовлетворяют одному шаблону.

То же поведение можно увидеть на цене из девятого обращения: 5 990. Шаблон 5.990 найдёт её, потому что пробел занимает место точки. Строка 5-990 подошла бы по той же причине.

У PostgreSQL есть ещё одна особенность: точка по умолчанию может совпасть с переводом строки. В наших коротких однострочных примерах это ничего не меняет. Но если ты привык к движку, где . обычно не переходит через перенос строки, эту разницу лучше держать в голове.

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

Посмотрим на тот же механизм ещё раз, но буквально по шагам.

Строка — A-B, шаблон — A.B.

Сначала A из шаблона сравнивается с A в строке. Совпало. Следом идёт точка. Сейчас в строке под курсором стоит -; точке нужен один любой символ, поэтому дефис подходит. Осталась последняя B, и она тоже совпадает. В результате движок нашёл A-B.

Со строкой A.B всё пройдёт так же. Просто среднюю позицию займёт уже настоящая точка.

На AB поиск развалится после первой буквы. Точка заберёт B как свой один символ, а для последней B из шаблона символов в строке уже не останется.

Короткие регулярки я бы именно так и читал: слева направо, по одному элементу. Не пытайся охватить A.B целиком. Спроси, чего требует A, затем ., затем B.

Здесь подсказок уже меньше. До запуска выбери три строки, где маркер сможет собрать подряд A, ещё один символ и B. Отдельно проверь AB и A==B: одной строке не хватает средней позиции, у другой таких позиций между A и B две.
Теперь тот же эффект видно на данных из архива. В обращении 8 маркер выделит kotomarket.ru. В обращении 21 он найдёт kotomarket-ru; хвост .shop останется за пределами совпадения. Запросу достаточно найти подходящий фрагмент внутри текста, поэтому весь подозрительный адрес целиком совпадать не обязан.

Служебный смысл зависит от того, где стоит знак. Внутри шаблона точка . означает один любой символ. А звёздочка в знакомом операторе ~* вообще не входит в строку регулярного выражения: это часть SQL-оператора, которая включает поиск без учёта регистра.

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

Как потребовать настоящую точку

Коменданту нужен именно kotomarket.ru. Свободная позиция от точки нам мешает: из-за неё проходит и адрес с дефисом.

Чтобы точка означала саму себя, перед ней ставят обратную косую черту: \.. После такой черты служебное значение точки выключается. Этот приём называется экранированием.

Прочитай kotomarket\.ru слева направо. Сначала идут обычные буквы kotomarket. Затем \. требует настоящий символ точки. После него должны встретиться ru.

kotomarket.ru теперь проходит. А kotomarket-ru — уже нет: там, где шаблон ждёт ., в строке лежит -.

С ценой получится тот же контраст. 5.990 совпадает с 5 990, потому что пробел занимает свободную позицию. А 5\.990 к этой записи уже не подходит: он требует именно точку. Если в тексте есть 5.990, такой шаблон найдёт её буквально.

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

Сравни результат с предыдущей ячейкой. Обращение 8 останется, а 21 исчезнет, потому что \. больше не принимает дефис. Пока мы ищем адрес как фрагмент внутри текста; совпадение всей строки целиком появится позже в курсе.

Тот же способ работает и с другими служебными знаками. В четвёртом обращении, например, есть фраза Это нормально?.

Если нужен сам вопросительный знак, пишем \?. Обратная косая черта заставляет движок воспринимать ? как обычный символ текста.

Проверь на трёх строках. В Готово? шаблон \? найдёт последний символ. В Готово! не найдёт ничего. В Что? Правда? будут два совпадения, если замена идёт с флагом g.

Позже ? получит собственное служебное значение, рядом со звёздочкой и плюсом. Пока достаточно различать две ситуации: нужен знак вопроса из текста — экранируем его.

Сначала предскажи результат. У Готово? выделится один ?, у Готово! строка останется как была, а в Что? Правда? маркер покажет оба вопросительных знака. Здесь хорошо видно буквальное чтение: \? ищет ровно символ ?.

Почему число косых черт меняется

В выражении body ~ 'kotomarket\.ru' участвуют два разных разбора. Сначала PostgreSQL читает строковый между кавычками. Уже после этого получившийся текст попадает в движок регулярных выражений.

В обычной строке PostgreSQL 16 при стандартной настройке standard_conforming_strings обратная косая черта не работает как специальная SQL-команда. Поэтому 'kotomarket\.ru' передаёт регулярке одну обратную косую черту перед точкой. Движок получает kotomarket\.ru и читает \. как буквальную точку.

С E'...' всё иначе. Буква E включает обработку обратных косых черт самим SQL. Если оставить там одну черту, PostgreSQL попытается разобрать её ещё до запуска регулярки. Поэтому для одной черты на входе движка в E-строке пишут две: E'kotomarket\\.ru'.

Полезно временно забыть про SQL и спросить: какой текст должна получить регулярка? Здесь ответ один — kotomarket\.ru. Обычная строка передаст его через 'kotomarket\.ru', а E-строка — через E'kotomarket\\.ru'.

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

В ячейках курса мы используем обычные SQL-строки без E. Поэтому дополнительную обратную косую черту «для надёжности» добавлять не нужно. Подробности есть в правилах строковых констант PostgreSQL.

Плюс в телефонном номере

Комендант закрывает письмо и открывает выгрузку контактов. У Анны Ивановой телефон записан как +7 (912) 345-67-89. Задача простая: найти записи, где есть буквальный фрагмент +7.

Первая версия напрашивается сама: phone ~ '+7'. Только запрос не возвращает контакты — он падает. Плюс в регулярном выражении служебный и связан с повторами, которые появятся в следующей главе. В начале шаблона ему не к чему примениться, поэтому PostgreSQL отвечает invalid regular expression: quantifier operand invalid.

С точкой было опаснее: запрос спокойно выполнялся и давал лишнее совпадение. Плюс хотя бы сразу шумит ошибкой.

Для буквального плюса нужна запись \+7. Читаем её так: \+ требует символ +, после него должна идти цифра 7.

Проверь в уме три строки. +7 подходит. В +7 727 312 45 67 совпадение находится в самом начале. А 7 912 222 33 44 не содержит плюса, поэтому \+7 там не найдётся.

По той же схеме \? ищет вопросительный знак. С самой обратной косой чертой интереснее: регулярке тоже приходится экранировать её, то есть движок должен получить две косые черты, чтобы одну из них прочитать буквально. Вот здесь особенно легко перепутать синтаксис регулярки и синтаксис SQL-строки. Я бы сначала выписал нужный текст шаблона для движка и только потом заворачивал его в SQL.

КВЕРИ: Упавший запрос хотя бы сообщил о проблеме. Слишком широкая регулярка обычно работает молча.

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

Check yourself
В девятом обращении цена записана как «5 990 руб.». Найдёт ли эту запись шаблон 5.990?
Find and fix the bug
Найди контакты, где в телефоне встречается буквальный фрагмент «+7». Сейчас запрос падает ещё до вывода строк. Меняй только шаблон: весь формат телефона и его длину проверять здесь не нужно.
Плюс в регулярке служебный. Если нужен настоящий +, поставь перед ним обратную косую черту.
Practice: solve the tasks
Solved 0 of 2 · any 1 is enough to pass

Частые ошибки

Копировать домен как обычный текст

Строка: kotomarket-ru.shop.

Ожидание: kotomarket.ru найдёт только адрес, где между kotomarket и ru действительно стоит точка.

На деле точка принимает дефис, поэтому маркер показывает kotomarket-ru.shop.

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

Если нужна буквальная точка, пиши kotomarket\.ru.

Считать точку пропуском произвольной длины

Строки: кт и коот.

Ошибка здесь обычно звучит так: «точка — это что угодно между к и т, значит обе строки подойдут».

Но не подходит ни одна. В кт между буквами нет отдельного символа. В коот их два. Для . нужен ровно один.

Разложи к.т на три позиции: к, любой один символ, т. После этого обе строки легко проверить глазами.

Средства для повторов будут в следующей главе. Пока правило жёсткое: одна точка занимает одно место.

Экранировать точку там, где как раз нужен любой символ

Строка: 5 990.

Можно машинально решить, что 5\.990 выглядит «правильнее», потому что в нём есть экранирование.

Только совпадения не будет. \. ждёт буквальную точку, а между 5 и 990 стоит пробел.

Перед запуском сформулируй условие словами. «Между 5 и 990 может быть любой один символ» превращается в 5.990. «Между 5 и 990 должна стоять точка» — в 5\.990.

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

Переносить число косых черт из другого языка

Регулярка должна получить текст kotomarket\.ru.

Если где-то в Python или JavaScript ты видел две косые черты, легко перенести их в SQL автоматически. Так делать опасно: количество черт зависит от синтаксиса строки, через которую проходит шаблон. Обычная строка PostgreSQL при standard_conforming_strings и E-строка обрабатывают их по-разному.

Раздели задачу. Сначала определи, какой текст должен увидеть движок регулярных выражений. Затем запиши этот текст в нужном виде SQL-строки.

В ячейках этого курса для буквальной точки используется обычная строка '\.'. В E'...' появится дополнительный слой обработки.

Искать +7 неэкранированным плюсом

Строка: +7 (912) 345-67-89.

Кажется естественным написать +7 и ждать первые два символа.

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

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

Исправленная запись: \+7.

Перед запуском чужого шаблона

В рабочих запросах такая ошибка часто появляется при копировании реального значения из логов, а не при написании регулярки с чистого листа.

Допустим, в логе есть хост api.example.com, и хочется быстро найти остальные строки с тем же адресом. Очень легко написать line ~ 'api.example.com'. Пока в данных встречается только нужный хост, запрос выглядит исправным. Проблема всплывёт позже, когда рядом появится что-нибудь вроде api-example.com: обе точки в шаблоне оставляют свободные позиции.

Для таких случаев у меня простая проверка: точное значение, похожая строка, которую нужно отвергнуть, и заведомо чужая строка. Здесь это могут быть api.example.com, api-example.com и shop.example.com. Три примера сразу показывают, где нужны \..

С вопросительным знаком всё так же. Ищешь сам знак пунктуации в сообщении поддержки — используй \?. Когда ? станет частью синтаксиса более сложной регулярки, чтение поменяется; это будет дальше.

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

Если результат вызывает сомнение, полезнее вывести маркер, чем ограничиться true или false. В kotomarket-ru.shop ошибка видна сразу: движок сам показывает, какой фрагмент он посчитал совпадением.

Principais pontos
  • Точка . в регулярке занимает ровно одну позицию и принимает любой символ.
  • Поэтому kotomarket.ru совпадает и с фрагментом kotomarket-ru: дефис занимает место точки.
  • Если нужен сам служебный знак, его экранируют: \. ищет точку, \+ — плюс, \? — вопросительный знак.
  • SQL сначала разбирает строковый , и только потом получившийся текст читает движок регулярных выражений. Поэтому запись обратных косых черт зависит от вида строки.
  • true сообщает, что совпадение существует. Маркер показывает конкретный найденный фрагмент и быстро выдаёт слишком широкий шаблон.
  • Когда переносишь в регулярку адрес, телефон, цену или другую строку с пунктуацией, проверь служебные знаки до запуска.

Теперь kotomarket-ru больше не выдаёт себя за настоящий адрес магазина. Следующее обращение связано с Алёной: одной свободной позиции там уже не хватит, придётся указать, какие символы вообще разрешены на этом месте.