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

Наборы символов: диапазоны, классы и «ё»

33 мин
Чему научишься
  • выбирать один символ из нескольких и разбирать такой шаблон по позициям;
  • записывать диапазоны и не забывать про «ё»;
  • находить символы, которых нет в разрешённом наборе;
  • пользоваться короткими обозначениями для цифр, букв и пробельных символов.

Ты открываешь следующее письмо на панели Сортировочной. В обращении 18 Алёна пишет: «автозамена путает: ёлка, ёжик и Ёлкина — это я, Алёна». Комендант просит найти её в выгрузке. Заодно выясняется, что сама Алёна почему-то попала в список «подозрительных имён».

КВЕРИ: Прежде чем подозревать человека, проверь сито. Букву «ё» в таких проверках забывают чаще, чем хотелось бы.

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

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

Круглое голографическое сито сортирует падающие капсулы; одна капсула с двумя светящимися точками сверху проскальзывает в единственную пустую ячейку сетки и падает в темноту, курсант тянется её поймать, кот следит широко раскрытыми глазами.
Диапазон [а-я] — сито с одной дырой: «ё» в него проваливается.

Одно место, несколько разрешённых символов

Возьми слово Кот. Шаблон кот его не найдёт: первая буква отличается регистром. Если в первой позиции подходят и «к», и «К», перечисли обе буквы в квадратных скобках: [кК]от.

Читай шаблон слева направо. [кК] отвечает за первую позицию: здесь подходит либо «к», либо «К». Дальше должна идти строчная о, а после неё строчная т.

Проверим три строки вручную.

кот: первая к входит в [кК], затем совпадают о и т. Получаем кот.

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

КОТ: К проходит первую проверку, но дальше шаблон ждёт строчную о, а встречает О. Совпадения нет. Набор влияет только на ту позицию, где он записан.

Если бы мы использовали оператор ~* из первого урока, регистр игнорировался бы во всём выражении. В [кК]от мы сами разрешили два варианта только для первой буквы.

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

Теперь строка скотч. Шаблон кот найдёт скотч. [кот] отметит отдельные подходящие буквы внутри строки. Квадратные скобки всегда занимают одну позицию.

В письмах 3 и 17 теперь подсвечено заглавное «Кот». В письме 12 по-прежнему выделяется только начало «кота». Мы изменили первую букву шаблона, а правило поиска внутри слов осталось тем же.

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

Сначала предскажи результат. Для [кК]от какие строки из кот, Кот, КОТ, скотч дадут совпадение? У скотч проверяй не всю строку целиком, а наличие подходящего фрагмента внутри.

Совпадение есть у кот, Кот и скотч. В скотч подсвечен только фрагмент кот. КОТ не подходит, потому что заглавную букву мы разрешили только в первой позиции.

Диапазон вместо длинного списка

Комендант открывает журнал сервера. В адресах встречаются коды вроде KM-2025-000311 и KM-2024-002468. Пока задача простая: найти KM- и одну цифру сразу после него.

Можно написать KM-[0123456789]. Такой шаблон сработает, но десять цифр подряд неудобно читать и проверять. [0-9] означает то же самое короче: одну цифру от 0 до 9, включая оба края.

Разберём KM-[0-9] по позициям. Сначала K, потом M, затем обычный дефис. Последняя часть [0-9] занимает одну позицию и принимает одну цифру.

В строке KM-2025-000311 сначала совпадает KM-, затем [0-9] забирает 2. На этом шаблон заканчивается, поэтому получаем KM-2025-000311. Остаток 025-000311 не входит в совпадение: команды взять несколько цифр подряд здесь пока нет.

А что будет со строкой KM-X? K, M и дефис совпадут, но в последней позиции вместо цифры стоит X. Половина шаблона за результат не считается, поэтому совпадения не будет.

Диапазоны можно задавать и для букв. [a-z] выбирает одну строчную латинскую букву, [A-Z] — одну заглавную. Несколько диапазонов помещаются в один набор: [a-zA-Z0-9]. Несмотря на длину записи, совпадение всё ещё состоит из одного символа.

Возьми строку A7_. [a-zA-Z0-9] подходит сначала к A, потом отдельно к 7, а _ пропускает. При глобальном поиске увидишь A7_.

В access_log колонка line хранит текст строки журнала, а line_no — её номер. Найди подсвеченное KM-2 внутри адресов заказов. И 2024, и 2025 начинаются с «2», поэтому такой шаблон ещё не различает годы и не проверяет длину кода.

Перед запуском прикинь результат без SQL. Есть три строки: A, 7, _. Шаблон [a-zA-Z0-9] проверяется на каждой отдельно.

С A всё видно сразу: она входит в диапазон A-Z. Для 7 закончи проверку сам. В какой из трёх диапазонов попадает эта цифра? И что останется для _?

После ответа запускай запрос. Смотри не только на true и false, но и на сам набор: в нём три диапазона, а подчёркивания среди них нет.

A и 7 дают true: первая входит в A-Z, вторая в 0-9. Для _ получаем false.

Почему «ё» остаётся за бортом

Вернёмся к Алёне. В контакте 3 записано Ёлкина Алёна. Кириллица выглядит обычной, поэтому первая идея вполне естественная: строчные русские буквы записать как [а-я], заглавные как [А-Я].

На букве ё эта проверка ломается. В нашей среде PostgreSQL [а-я] не включает ё, а [А-Я] не включает Ё.

Посмотри на Алёна по символам. Заглавную А строчный диапазон сейчас не проверяет. л попадает в [а-я]. Следующая ё уже не попадает. н и а снова подходят. Если заменить каждую найденную строчную букву кружком, получится что-то вроде А·ё··: ё останется видимой посреди замен.

На схеме причина заметнее. [а-я] задаёт отрезок от а до я, а ё находится вне него. Человек воспринимает запись как «русский алфавит», движок видит конкретный диапазон между двумя символами.

В Юникоде кодовые позиции ё и Ё действительно находятся отдельно от непрерывных последовательностей соответствующих букв. Кроме того, поведение диапазонов в разных окружениях может зависеть от правил сортировки. Для нашей песочницы правило простое: ё нужно дописать явно.

Для строчных получится [а-яё], для заглавных [А-ЯЁ]. Если нужны оба регистра, их можно собрать в одном наборе: [а-яёА-ЯЁ].

А если задача уже конкретная и нужны только два варианта имени, лучше сузить шаблон. Ал[её]на читается так: А, л, одна е или ё, затем н, а. Он подходит и к Алена, и к Алёна, не меняя остальные позиции.

[а-я] заканчивается на «я»; ё нужно дописать в набор отдельно.
Найди в результате имя «Ёлкина Алёна». Заглавные «Ё» и «А», пробел и строчная «ё» остаются видимыми. Остальные строчные русские буквы превращаются в кружки. Условие ~* 'ё' при отборе не различает регистр, поэтому строка с заглавной «Ё» тоже подходит.

Добавляй «ё» явно: [а-яё] — русские строчные, [А-ЯЁ] — заглавные, [а-яёА-ЯЁ] — оба регистра. Каждый такой набор выбирает один символ.

Если допустимы только варианты «Алёна» и «Алена», точнее написать Ал[её]на: выбор меняется в одной позиции. В выгрузке контакт 3 записан как Ёлкина Алёна.

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

В прошлом уроке точка была метасимволом: обычную точку приходилось отличать от команды «любой символ». Внутри набора всё проще. [.] выбирает одну точку, [.,] — одну точку или одну запятую.

Возьми строку 5 990,00 ₽. [.,] найдёт в ней запятую: 5 990,00 ₽. Точки в этой строке нет. В mail.ru тот же шаблон нашёл бы точку.

С дефисом надо быть внимательнее. Между границами диапазона он выполняет служебную работу, как в [0-9]. Если нужен сам дефис, один из понятных вариантов — поставить его в конец набора: [0-9-].

Пройдём строку 45-67. 4 входит в 0-9, 5 тоже. Затем встречается дефис, и он отдельно разрешён последним элементом набора. После него подходят 6 и 7. Глобальный маркер для [0-9-] подсветит все пять символов по одному.

А вот + в этот набор не входит. В строке +7 шаблон [0-9-] пропустит плюс и найдёт только 7.

В набор можно добавить и обычный пробел. В [а-яёА-ЯЁ ] перед закрывающей квадратной скобкой стоит пробел, поэтому разрешена русская буква любого регистра или один обычный пробел. Такой пробел легко не заметить глазами. Я бы в реальном запросе особенно внимательно проверил именно это место.

Один символ, которого в списке нет

Телефоны в выгрузке записаны по-разному: 89161234567, 45-67, тел. 8 (912) 111-22-33. Нужно быстро увидеть всё оформление вокруг цифр.

Поставь ^ сразу после открывающей квадратной скобки: [^0-9]. Такой набор выбирает один символ, который не входит в диапазон цифр.

Проверим 45-67. На 4 вопрос такой: «символ вне 0-9?» Нет. На 5 тоже нет. На дефисе — да. 6 и 7 снова не подходят. Получаем 45-67.

В строке 89161234567 совпадений не будет: каждый символ входит в 0-9.

В +7 первым совпадением станет +. Цифра 7 не подходит. По тому же правилу найдутся пробелы, круглые скобки и буквы, если они есть в строке.

Здесь легко спутать две записи с отрицанием. phone !~ '[0-9]' проверяет, нет ли во всей строке ни одной цифры. phone ~ '[^0-9]' проверяет другое: есть ли хотя бы один символ, который не является цифрой.

Для 45-67 первая проверка даст ложь, потому что цифры есть. Вторая даст истину из-за дефиса. Для 89161234567 обе проверки ложны: цифры присутствуют, а нецифровых символов нет.

КВЕРИ: У ^ две работы. Сразу после [ он переворачивает набор, а вне скобок означает начало строки — до этого дойдём в третьей главе.

Ниже запрос удаляет все совпадения [^0-9]. Пока используй его как способ сравнить исходный телефон с тем, что осталось. Саму замену подробно разберём позже.

Сравни phone и digits. Из «45-67» получается «4567», а из строки с «тел.» исчезают буквы, пробелы и разделители. У контакта 19 телефона нет: там NULL, то есть неизвестное значение, а не пустая строка. Поэтому результат тоже остаётся NULL.

Ещё одна короткая проверка. Для каждой строки [^0-9] ищет хотя бы один нецифровой символ.

123 — ответ здесь можно дать сразу.

12-3 — среди цифр стоит один знак.

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

Сначала реши три случая в уме, потом запускай запрос.

123 даёт false, 12-3 — true из-за дефиса, пустая строка — false: набору нужен существующий символ.

Короткие имена для частых классов

[0-9] писать недолго, но цифры, буквы и пробельные символы встречаются в регулярках постоянно. Для таких случаев есть короткие обозначения.

\d выбирает одну цифру. В телефонах нашей песочницы он находит те же цифры, которые мы только что искали через [0-9].

\w выбирает один символ из класса «слово»: букву, цифру или _. В нашей среде русские буквы тоже входят в этот класс, в том числе ё.

\s выбирает один пробельный символ. Обычный пробел подходит, но это не единственный вариант: например, табуляция тоже относится к этому классу.

Разберём KM-\d. K занимает первую позицию, M вторую, дефис третью, а \d четвёртую и требует одну цифру. На KM-2024 совпадением станет KM-2024, как и с KM-[0-9].

Теперь посмотрим на \w. Строка petrov_88 целиком состоит из таких символов: латинских букв, подчёркивания и цифр. Дефис сюда не входит. В фамилии Римская-Корсакова буквы подходят под \w, а - нет.

Буква ё в нашей песочнице тоже подходит под \w. Это заметное отличие от [а-я], где её приходилось добавлять вручную.

У этих классов есть пары с заглавной буквой. \D выбирает один нецифровой символ, \W — один символ вне \w, \S — один непробельный символ.

Проверь _: он входит в \w, поэтому не входит в \W. С - наоборот: \w ложно, \W истинно. Для обычного пробела \s истинно, а \S ложно.

Обратная косая черта вместе со следующей буквой образует команду, а незнакомая пара вроде \q даёт ошибку. Поэтому добавлять её «на всякий случай» не стоит: сочетание уже может что-то означать в этом регулярных выражений.

Длинная запись того же класса

В документации PostgreSQL и чужом SQL тебе встретится [[:digit:]]. Внешние квадратные скобки задают набор, а [:digit:] внутри обозначает готовый класс цифр. В PostgreSQL \d и [[:digit:]] равнозначны. То же верно для \s и [[:space:]].

Эта запись пришла из семейства стандартов POSIX. Например, [[:alpha:]] обозначает буквы. От \w он отличается составом: цифры и подчёркивание в alpha не входят.

С буквенными классами есть ещё одна деталь: их поведение связано с локалью базы. Курс работает в en_US.utf8, и здесь используемые нами классы распознают кириллицу. В локали C русские буквы в такие классы не входят. Если переносишь регулярку в другое окружение, проще один раз проверить ё, _, цифру и дефис коротким запросом, чем гадать по названию класса.

В каждой строке здесь ровно один символ. Сравни ё и _: оба дают true в колонке word_symbol, хотя подчёркивание не буква. Для строки с пробелом смотри на space_symbol, а для дефиса сравни сразу все три колонки. Такой мини-тест удобно запускать при переезде на другую базу.

Сделаем невидимое заметным

Контакт 2 выглядит как аккуратное «Петров Сергей», но вокруг слов спрятано шесть пробелов. В тексте страницы они схлопываются и не видны — ячейка ниже покажет каждый.

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

Для Иванова Анна всё просто: между фамилией и именем один пробел, значит, появится один кружок.

У Петрова сначала стоят два пробела, между словами ещё три, после имени один. Значит, ждём две отметки в начале, три в середине и одну в конце.

Контакт 16, Римская-Корсакова Ирина, помогает не смешать дефис и пробел. Дефис внутри фамилии не относится к \s, поэтому единственное совпадение здесь — пробел перед именем.

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

Сравни контакты 1 и 16 с контактом 2. У Петрова кружки стоят перед фамилией, между словами и после имени. Пока мы только показываем пробелы; приводить такие строки в порядок будем в главе о замене.
Проверь себя
Сколько фрагментов подсветит наш маркер с шаблоном [кот] в слове «кот»?
Найди и исправь ошибку
Коменданту нужен список имён, где встречается хотя бы один символ кроме русских букв и обычного пробела. Посмотри на сломанный набор: отрицание, диапазон строчных, диапазон заглавных и пробел перед закрывающей квадратной скобкой. Сейчас запрос ошибочно считает ё и Ё посторонними символами. Добавь обе буквы в разрешённый набор, не убирая отрицание. Латиница и дефис по условию всё ещё должны отправлять строку в список.
В диапазонах не хватает одной русской буквы в двух регистрах.
Закрепление: реши задачи
Решено 0 из 2 · для зачёта достаточно 1

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

[кот] вместо кот

Строка: кот.

Ожидание: маркер выделит слово целиком.

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

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

Считать [а-я] полным русским алфавитом

Строка: Алёна.

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

Но ё останется снаружи. Если заменить совпадения [а-я] кружками, эта буква останется видимой среди замен.

Для строчных букв в нашей песочнице пиши [а-яё], для обоих регистров — [а-яёА-ЯЁ]. На другой базе я бы не доверял памяти: запустил бы короткий тест на нужных символах и посмотрел результат.

Думать, что набор забирает несколько символов

Строка: KM-2025-000311.

Ожидание: KM-[0-9] выделит весь числовой хвост после KM-.

На деле шаблон заканчивается после первой цифры: KM-2025-000311. [0-9] занимает одну позицию. Повторения появятся в следующей главе.

Перепутать !~ и [^…]

Строка: 45-67.

Ожидание при phone !~ '[0-9]': строка попадёт в результат, потому что в ней есть дефис.

Нет. !~ требует, чтобы совпадений с [0-9] не было вообще, а цифры в строке есть. Если нужно узнать, встречается ли хотя бы один нецифровой символ, ищи [^0-9].

Для проверки возьми три короткие строки: 123, 12-3, пустую строку. На них разница видна без лишнего текста вокруг.

Считать _ обычным разделителем для \w

Строка: petrov_88.

Ожидание: подчёркивание окажется вне класса слова, как дефис.

Но _ входит в \w, поэтому \W его не найдёт. Дефис -, напротив, подходит под \W.

Если правило поля говорит именно «буквы», не заменяй его автоматически на \w: туда входят ещё цифры и _.

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

Допустим, завтра приходит новая выгрузка. В колонке code разрешены латинские буквы, цифры и дефис. Я бы не начинал с большой регулярки. Сначала взял бы одну-две реальные строки и решил, какие символы допустимы в одной позиции.

Для латинской буквы или цифры подойдёт [a-zA-Z0-9]. Если туда же разрешён дефис, добавь его в конец: [a-zA-Z0-9-]. Затем отдельно проверь _, пробел, кириллическую А и дефис. Четыре маленьких примера быстро показывают, что именно разрешено шаблоном.

Если задача обратная и нужно искать посторонние символы, поставь ^ сразу после открывающей квадратной скобки: [^a-zA-Z0-9-]. Тогда пробел и _ станут совпадениями, а буквы, цифры и дефис нет.

С короткими классами проверка такая же. Перед заменой длинного набора на \w посмотри, разрешено ли по смыслу поля подчёркивание. Перед \s реши, нужен обычный пробел или любой пробельный символ. Регулярка знает только записанное условие; смысл колонки ей неизвестен.

Главное из урока
  • Набор выбирает один символ: [кК]от находит «кот» и «Кот», а [кот] выбирает одну из трёх букв.
  • Диапазон заменяет длинное перечисление: [0-9], [a-z]. В нашей песочнице для русских букв явно добавляй ё и Ё: [а-яёА-ЯЁ].
  • ^ сразу после [ отрицает набор: [^0-9] ищет один нецифровой символ. Оператор !~ проверяет другое условие.
  • Внутри набора можно перечислять знаки. [.,] ищет точку или запятую, [0-9-] — цифру или дефис.
  • \d, \w, \s выбирают цифру, символ класса слова и пробельный символ. \D, \W, \S работают наоборот.
  • В PostgreSQL \d равнозначен [[:digit:]]. Поведение буквенных классов зависит от локали, поэтому нужные символы лучше проверять коротким запросом.

Алёна больше не попадает в «подозрительные» из-за буквы ё. На соседнем экране всё ещё светится KM-2: набор выбрал нужную цифру, но только одну. В следующей главе добавим повторения и доберёмся до остальной части кода.