2025, Oct 31 04:47
Исключаем совпадение ABC на 4-й позиции с помощью отрицательного просмотра назад
Разбираем, как в регулярных выражениях исключить совпадение ABC на 4-й позиции строки с помощью отрицательного просмотра назад (lookbehind). Пример на Python.
Исключить конкретную позицию из совпадения регулярного выражения может казаться непростой задачей, пока не понимаешь, что проверки контекста (lookaround) позволяют выразить мысль: «совпадение нужно, но не тогда, когда оно находится ровно здесь». Задача: найти все вхождения ABC в строке, кроме случая, когда оно начинается с четвертого символа.
Чего хотим избежать
ABC должно совпадать почти везде, но не тогда, когда его начальный индекс — четвертый символ строки. Иначе говоря, варианты вроде FGHABCDE нужно исключить, тогда как ABCDEFGH, HABCDEFG, GHABCDEF и EFGHABCD должны совпасть.
Наивный подход: совпадает везде
Простой поиск по ABC найдет каждое вхождение, включая нежелательное на четвертой позиции. Это наглядно показывает проблему:
import re
samples = [
    "ABCDEFGH",
    "HABCDEFG",
    "GHABCDEF",
    "FGHABCDE",
    "EFGHABCD"
]
plain = re.compile(r"ABC")
for text in samples:
    print(text, bool(plain.search(text)))
Как и ожидалось, такой подход не дает контроля над тем, где разрешено совпадение.
Почему так происходит
Движки регулярных выражений охотно сопоставляют литеральный шаблон в любом месте, где он встречается. Чтобы исключить конкретный индекс, шаблону нужна осведомленность о контексте. Здесь выручает отрицательный просмотр назад: он позволяет утвердить, что сразу перед текущей позицией какое-то условие не выполняется, при этом символы не потребляются.
Решение: отрицательный просмотр назад с привязкой к позиции
Шаблон (?<!^.{3})ABC точно выражает правило «совпадать с ABC, если оно не стоит сразу после ровно трех символов от начала строки». Фрагмент ^.{3} привязывается к началу строки и соответствует трем символам; отрицательный просмотр назад (?<!...) гарантирует, что этот контекст отсутствует непосредственно перед ABC. В результате вхождение на четвертой позиции исключается, все остальные — допускаются.
import re
items = [
    "ABCDEFGH",
    "HABCDEFG",
    "GHABCDEF",
    "FGHABCDE",
    "EFGHABCD"
]
rx = re.compile(r"(?<!^.{3})ABC")
for row in items:
    print(row, bool(re.search(rx, row)))
Это дает ровно желаемое поведение: совпадает все, кроме случая, когда ABC начинается с четвертого символа.
Зачем это нужно
Иногда требуется сохранить все валидные совпадения, исключив точечную позицию или контекст. Отрицательный просмотр назад с закрепленным подшаблоном дает компактный и точный способ выразить это правило прямо в regex. Это особенно полезно, когда хочется «сделать все в регулярке», без дополнительной фильтрации где-то еще.
Выводы
Если нужно разрешить шаблон везде, кроме определенного стартового индекса, привяжите отрицательный просмотр назад к началу строки и «посчитайте» запрещаемые символы. Для случая «все, кроме четвертой позиции» (?<!^.{3})ABC — лаконичный и надежный вариант. Держите этот прием под рукой, когда встречаются позиционные исключения в задачах парсинга, валидации или обработки текста.
Статья основана на вопросе с StackOverflow от Branden Keck и ответе пользователя ruohola.