Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 12 Средний 40 мин 130 XP

Якоря и границы слова в регулярках: ^, $, \b

Якоря ^ и $ не ищут символы — они проверяют позицию, а \b находит слово целиком и не трогает подстроки. Три приёма, после которых шаблоны становятся точными.

Редакция Питоники

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

Для этого в регулярках есть якоря: ^ привязывает совпадение к началу строки, $ — к концу, а граница слова \b работает точнее — на стыке слова и всего остального. В этом уроке ты разберёшь их все, поймёшь, чем ^ в шаблоне отличается от ^ в наборе, и увидишь ловушку, где $ пропускает невидимый перевод строки.

Что делают якоря ^ и $?

Якоря не находят символы — они проверяют позицию: можно ли здесь стоять, чтобы совпадение началось или закончилось. Ноль символов расходуется, поэтому якорь не виден в результате — но без него совпадения просто не происходит. Самый частый шаблон с якорями — «вся строка состоит из того-то»: ^\d+$ читается «от начала строки до её конца — только цифры».

вся строка - только цифры
import re

inputs = ["4500", "  4500", "4500  ", "сумма 4500"]

for s in inputs:
    ok = bool(re.search(r"^\d+$", s))
    print(repr(s), "->", ok)
Вывод
'4500' -> True
'  4500' -> False
'4500  ' -> False
'сумма 4500' -> False

Без якорей re.search(r"\d+", s) нашёл бы цифры во всех четырёх строках — и валидация превратилась бы в решето. С якорями проходит только строка, где цифры — от первого до последнего символа. Пробелы в начале и в конце ломают совпадение, слово перед числом — тоже. Именно так пишут проверки полей формы: не «есть ли в поле число», а «является ли поле числом».

Второй частый случай — якоря как фильтр против слишком щедрого поиска. findall и search видят куски, якоря требуют целое. Сравни на одной строке:

findall без якорей против search с якорями
import re

text = "id: 12, id: 34"

print(re.findall(r"\d+", text))
print(re.search(r"^\d+$", text))
Вывод
['12', '34']
None

findall честно нашёл оба числа — он и должен находить куски. Но search с якорями вернул None: строка «id: 12, id: 34» — не число целиком. Выбор между «найти в тексте» и «убедиться, что весь текст такой» — это выбор между поиском без якорей и с ними. Полную карту методов поиска — search против fullmatch — мы уже рисовали в уроке про match, search и findall; якоря дают тот же эффект для search, плюс возможность привязать совпадение только с одной стороны, например ^ОШИБКА — «строка начинается со слова ОШИБКА».

Чем ^ в наборе [^abc] отличается от ^ в начале шаблона?

Здесь новички теряются чаще всего: у крышки два значения, и зависят они от места. Первым символом шаблона ^ — это якорь начала строки. Первым символом набора [^abc] — это отрицание: «любой символ, кроме a, b, c». Один и тот же знак, две противоположные работы.

якорь и отрицание - одна крышка
import re

words = ["бар", "жар", "шар", "арбуз"]

print([w for w in words if re.search(r"[бж]ар", w)])
print([w for w in words if re.search(r"[^бж]ар", w)])
Вывод
['бар', 'жар']
['шар']

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

Что такое граница слова \b?

Якоря ^ и $ знают только края всей строки. Но слова живут внутри текста, и для них есть свой якорь — \b, граница слова. Граница — это невидимый переход между символом \w (буква, цифра, подчёркивание) и не-\w (пробел, запятая, конец строки). Шаблон \bкот\b требует границу перед «кот» и после него: слово целиком, не кусок другого слова.

подстрока против целого слова
import re

text = "кот, котёнок и скот"

print(re.findall(r"кот", text))
print(re.findall(r"\bкот\b", text))
Вывод
['кот', 'кот', 'кот']
['кот']

Без границ шаблон нашёл все три «кота»: в «котёнок», в «скоте» и отдельно стоящий. С \b по краям остался один — потому что только у него слева пробел, а справа запятая: обе позиции — границы слова. В «котёнке» после «кот» идёт «ё» — буква, границы нет; в «скоте» перед «кот» стоит «с» — и здесь границы нет. Сам \b не съедает ни одного символа: он лишь разрешает или запрещает совпадение в этой точке.

замена целых слов без задевания подстрок
import re

text = "кот вышел из скотча, но не из кота"

print(re.sub(r"\bкот\b", "пёс", text))
Вывод
пёс вышел из скотча, но не из кота

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

Чем \b отличается от \s?

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

И вот где нулевая ширина бьёт по рукам: дефис — не \w, поэтому он создаёт границу слова. Слова с дефисом регулярка считает несколькими словами.

дефис - это граница слова
import re

text = "какой-то день"

print(re.findall(r"\b\w+\b", text))
print(re.search(r"\bкакой\b", text).span())
Вывод
['какой', 'то', 'день']
(0, 5)

Что такое \B, \A и \Z?

У якорей есть младшие братья. \B — отсутствие границы слова: совпадение разрешено только внутри слова, где оба соседа — \w. Пара \A и \Z дублирует ^ и $ — начало и конец строки, — но с одним честным отличием, о котором сразу после примера.

\B, \A и \Z
import re

print(bool(re.search(r"\Aкот\Z", "кот")))
print(bool(re.search(r"\Aкот\Z", " кот")))
print(re.findall(r"\Bкот", "скот кот"))
Вывод
True
False
['кот']
ловушка $ перед переводом строки
import re

line = "4500\n"

print(bool(re.search(r"^\d+$", line)))
print(bool(re.search(r"\A\d+\Z", line)))
Вывод
True
False

Во втором блоке и вся соль: $ совпадает не только в самом конце строки, но и прямо перед финальным переводом строки \n. Строка «4500\n» прошла проверку ^\d+$, хотя в конце висит невидимый символ. \Z строже — конец есть конец. Для валидации пользовательского ввода, где хвостовой \n — обычное дело после split и чтения файлов, выбирай \Z или re.fullmatch, а \A — полный аналог ^ без сюрпризов в многострочных режимах.

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

якоря на двух строках текста
import re

log = "2026-09-20 ERROR disk\n2026-09-21 OK"

print(re.search(r"^\d{4}-\d{2}-\d{2}$", log))
Вывод
None

Якоря в реальной жизни: маршруты сайта

Где якоря стоят каждый день — в маршрутизации веб-фреймворков. Роутер должен отличить «/products/» от «/products/17/» и не принять «/products/17/extra/»: без якорей шаблон /products/ совпал бы с началом любого из трёх путей. Посмотри, как пара ^...$ решает задачу начисто.

роутер на якорях
import re

ROUTES = [
    (r"^/products/$", "список товаров"),
    (r"^/products/(?P<id>\d+)/$", "карточка товара"),
]

def resolve(path):
    for pattern, name in ROUTES:
        m = re.search(pattern, path)
        if m:
            return name, m.groupdict()
    return "страница не найдена", {}

print(resolve("/products/17/"))
print(resolve("/products/"))
print(resolve("/products/17/extra/"))
Вывод
('карточка товара', {'id': '17'})
('список товаров', {})
('страница не найдена', {})

Первый путь попал в маршрут с номером и попутно отдал его через именованную группу — приёмы урока про группы работают и здесь. Третий путь не совпал ни с одним шаблоном: после «17/» идёт «extra/», а $ требует конца строки ровно после слэша. Так же устроены URL-роутеры внутри Django и Flask — только шаблоны там компилируются при старте приложения через re.compile, как в уроке про методы поиска.

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

фильтр ошибок в файле лога
import re

pattern = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} .* (?P<code>50\d) \d+$")

with open("access.log", encoding="utf-8") as f:
    for line in f:
        m = pattern.search(line)
        if m:
            print(m.group("code"), line.split()[0])
Вывод
500 10.0.0.5
503 172.16.0.9
Песочница не открывает файлы, поэтому блок не запускается: это заготовка локального скрипта. Якоря здесь на вес золота: ^ привязывает шаблон к началу строки лога, $ — к её концу, и строки, где 500 — просто размер ответа, а не код, мимо шаблона не пройдут.

Что дальше

Ты собрал полный набор привязок: ^ и $ для краёв строки, \b для краёв слов, \B для «внутри слова», строгие \A и \Z без сюрпризов с переводом строки. Шаблоны перестали быть ситом — теперь они умеют требовать точности. Следующий уровень избирательности — опережающие и ретроспективные проверки: они проверяют, что стоит ПОСЛЕ и ДО совпадения, не съедая ни символа. А если хочется навести порядок в регистре и многострочных текстах — флаги re.IGNORECASE и re.MULTILINE, о которых сегодня был только анонс.

Шаблон без якорей находит куски, шаблон с якорями принимает решения: это число или нет, это слово или подстрока.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import re

print(re.findall(r"\bкот\b", "кот, скот и котёнок"))
import re

line = "4500\n"
print(bool(re.search(r"^\d+$", line)), bool(re.search(r"\A\d+\Z", line)))
import re

print(re.sub(r"\bмир\b", "свет", "мирный мир"))
Проверь себя
0 / 5

1. Что делает ^ в начале шаблона?

2. Что означает ^ первым символом внутри набора [^abc]?

3. Что вернёт re.search(r"\bкот\b", "котёнок")?

4. Чем \b отличается от \s?

5. Чем \Z отличается от $?

Карточки терминов
Запомнено: 0 / 6
Практика

В редакторе — три сообщения чата. Замени целое слово «спам» на «***», не задевая слово «спамер», и выведи каждую обработанную строку. Формат вывода — из заготовки.

practice.py
Вопросы и ответы по уроку

Как найти слово целиком, а не его вхождение в другое слово?

Поставить границы слова по краям: шаблон \bкот\b находит «кот» как отдельное слово и не срабатывает внутри «котёнка» и «скота». Граница слова — это невидимый переход между буквенно-цифровым символом \w и всем остальным; сам \b ни одного символа не съедает. Для замены целых слов тот же шаблон передаётся в re.sub.

Почему \b находит слово «какой» внутри «какой-то»?

Потому что дефис не является буквенно-цифровым символом \w, а значит, образует границу слова. Для \b «какой-то» — это два слова: «какой» и «то». Если составные слова нужно держать вместе, включи дефис в шаблон явно: \b[\w-]+\b вернёт «какой-то» одним куском.

Чем ^ отличается от \A, а $ от \Z?

Без флагов ^ и \A работают одинаково — привязка к началу строки. Разница на конце: $ совпадает ещё и прямо перед финальным переводом строки, поэтому «4500\n» проходит проверку ^\d+$. \Z требует настоящего конца строки без исключений. Для валидации ввода из файлов и форм выбирай \Z или re.fullmatch.

Что означает запись [^abc] в регулярном выражении?

Отрицание набора: любой один символ, кроме a, b и c. Крышка работает как отрицание только первой внутри квадратных скобок; в начале шаблона ^ — это якорь начала строки, а внутри набора не на первом месте — просто литеральная крышка. Проверяй позицию крышки, прежде чем читать шаблон вслух.

Понравился урок? Сошлитесь на него

«Якоря не находят символы — они проверяют позицию: можно ли здесь стоять, чтобы совпадение началось или закончилось.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

re · Урок 6

re.match, re.search, re.findall и re.fullmatch: как выбрать метод

Четыре метода поиска на одной и той же строке дают четыре разных результата. Разбираем, где match останавливается, зачем нужен fullmatch и когда компилировать шаблон.

re · Урок 7

re.sub и re.split: замена и разрезание текста по шаблону

Замена по шаблону со ссылками на группы, функция вместо строки замены и разрезание строки по любому разделителю — два инструмента модуля re, которые заменяют десятки строк с if.

re · Урок 9

Опережающие и ретроспективные проверки: lookahead и lookbehind

Проверки lookahead и lookbehind добавляют условие «только если рядом есть такой-то текст», не включая его в совпадение: пароль с тремя требованиями — одной регуляркой, разряды 1 000 000 — одной заменой.

re · Урок 10

Флаги в регулярных выражениях: re.IGNORECASE, MULTILINE, DOTALL, VERBOSE

Четыре флага, которые меняют поведение целого движка: поиск в любом регистре, якоря на каждой строке, точка с переводом строки и шаблоны с комментариями вместо «полотна».

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.