Якоря и границы слова в регулярках: ^, $, \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 видят куски, якоря требуют целое. Сравни на одной строке:
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 дублирует ^ и $ — начало и конец строки, — но с одним честным отличием, о котором сразу после примера.
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
Что дальше
Ты собрал полный набор привязок: ^ и $ для краёв строки, \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", "свет", "мирный мир"))
1. Что делает ^ в начале шаблона?
2. Что означает ^ первым символом внутри набора [^abc]?
3. Что вернёт re.search(r"\bкот\b", "котёнок")?
4. Чем \b отличается от \s?
5. Чем \Z отличается от $?
В редакторе — три сообщения чата. Замени целое слово «спам» на «***», не задевая слово «спамер», и выведи каждую обработанную строку. Формат вывода — из заготовки.
Как найти слово целиком, а не его вхождение в другое слово?
Поставить границы слова по краям: шаблон \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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
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
Четыре флага, которые меняют поведение целого движка: поиск в любом регистре, якоря на каждой строке, точка с переводом строки и шаблоны с комментариями вместо «полотна».
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 2
json.loads: чтение JSON из строки
Обратная дорога: текст JSON становится словарём Python одной командой json.loads — и по ключам можно ходить, считать и менять значения.
из строки в словарь python
openpyxl · Урок 5
Форматирование ячеек: шрифт, заливка и границы в openpyxl
Шапка прайса — жирный белый на синем, границы на всю таблицу: Font, PatternFill и Border, и главное правило — один стиль на весь диапазон.
openpyxl шрифт заливка границыграницы таблицы excel python
re · Урок 2
Символы и классы символов в регулярках: \d, \w, \s и свои наборы
Цифры, буквы, пробелы и свои наборы в квадратных скобках: полный словарь символьных классов, точка и её аппетиты, экранирование точки в ценах и датах.
спецсимволы регулярных выражений\d регулярка