Квантификаторы и метасимволы. Жадность, классы символов
- Жадность, ленивость и «жадность без возврата»
- По умолчанию квантификаторы жадные: *, +, ?, {m,n}
- Ленивые (минимальные): *?, +?, ??, {m,n}?
- «Без возврата» (possessive): *+, ++, ?+, {m,n}+ — не делают бэктрекинг после совпадения.
- Поддержка: PCRE/Java/.NET — да; Python re/JS — нет (аналог в PCRE/.NET: атомарная группа (?>…); в Python — в модуле regex, не re).
- Пример разницы (захват группы):
- Шаблон: ^(.+)\d$ на “ab1cd2” → группа 1 = “ab1cd”
- Ленивый: ^(.+?)\d$ → группа 1 = “ab1c”
- Избегаем катастрофического бэктрекинга:
- Плохо: ^(a+)+$ на “aaaaaaaaaaab” — взрыв по времени
- Лучше: ^(?:a++)+$ (PCRE/.NET), либо просто ^a+$ если логика позволяет
- Альтернатива: атомарная группа ^(?>a+)+$ (PCRE/.NET)
- Классы символов: [A-Za-z0-9] vs \d, \w, \s
- Явные диапазоны безопасны для ASCII: [0-9], [A-Za-z], [A-Za-z0-9_]
- Предопределённые классы зависят от движка/флагов:
- JavaScript: \d = [0-9], \w = [A-Za-z0-9_], даже с флагом u; доступны Unicode-свойства c \p{…} только с флагом u (например, \p{L}, \p{Nd})
- Python 3 re (Unicode по умолчанию): \d — любые Unicode-цифры; \w — «слово» из многих алфавитов + _; для ASCII-режима используйте явные диапазоны или флаг re.ASCII/inline (?a)
- .NET: Unicode-aware; \d — все Nd, \w — буквы, цифры, соединительная пунктуация и _
- PCRE2: по умолчанию \d ~ [0-9], \w ~ [A-Za-z0-9_]; с модификаторами /u и UCP — по Unicode-свойствам
- Вывод: для строгих ASCII-политик паролей не используйте \d/\w — задавайте диапазоны явно.
- Unicode-нюансы: «двойники» и нормализация
- Визуальные двойники:
- Latin o (U+006F) vs Cyrillic о (U+043E): “pаssword” может содержать кириллическую а (U+0430)
- Греческая Ι (U+0399) vs латинская I (U+0049); кириллическая с (U+0441) vs латинская c (U+0063)
- Комбинируемые знаки: é = U+00E9 vs e + ◌́ (U+0065 U+0301). Без нормализации строки «на глаз» одинаковы, но байтово разные.
- Практика ИБ:
- Для паролей чаще фиксируют точный алфавит (часто ASCII) и не нормализуют.
- Для логинов/имён — применяют нормализацию NFC/NFKC и фильтры на confusables.
- Детект ASCII-только:
- PCRE/JS: ^[\x00-\x7F]+$ или ^[\p{ASCII}]+$/u (JS, PCRE)
- Python/.NET: ^[\x00-\x7F]+$
- Практика: кейсы различий движков
Кейc 1. ^\w+$ и строка “Имя_пользователя”
- JS: ✗ (ASCII-ограниченный \w)
- Python/.NET (Unicode): ✓
- PCRE2: ✗ без UCP; ✓ c /u и UCP
- Рекомендация: для универсальности — явный алфавит или Unicode property escapes: ^[\p{L}\p{Nd}_]+$/u
Кейс 2. ^\d{3}$ и арабско-индийские цифры “١٢٣” (U+0661..U+0663)
- JS: ✗; Python/.NET/PCRE(UCP): ✓
- Для ASCII-цифр — только [0-9]
Кейс 3. Ловля «двойников» в паролях (ASCII-жёстко)
- Разрешены только латиница/цифры: ^[A-Za-z0-9]{8,}$ — кириллическая «о» отсекается
- Разрешить все буквы/цифры (Unicode): ^[\p{L}\p{Nd}]{8,}$/u — но учтите confusables
Кейс 4. Жадность и границы
- Шаблон: ^(?=.*\d).{8,}$ — «хотя бы одна цифра, длина ≥8». Точка жадная, но lookahead фиксирует требование — это корректно.
- Антипаттерн: ^.\d.$ без ^$ вокруг всей политики → проверяет подстроку, не весь пароль.
Кейс 5. Катастрофический бэктрекинг
- Плохо: ^(.*a){10}$ на длинных «почти» подходящих строках
- Лучше: избегать вложенных повторов на «.», использовать атомарные группы/possessive: ^(?:[^a]*+a){10}$ (PCRE/.NET)
- Рецепты-шаблоны
- Строгий ASCII-пароль (буквы/цифры), 8–20: ^[A-Za-z0-9]{8,20}$
- Unicode-буквы/цифры (ECMAScript 2018+): ^[\p{L}\p{Nd}]{8,20}$/u
- «Хотя бы одна заглавная и цифра» (ASCII): ^(?=.[A-Z])(?=.[0-9])[A-Za-z0-9]{8,}$
- Запрет «двойников» O, 0, l, I: ^(?!.*[O0lI])[A-Za-z0-9]{8,}$
- Мини-чек-лист
- Нужен полный матч? Используйте ^…$ или fullmatch.
- Какой алфавит? Если ASCII — никакого \w/\d.
- Unicode нужен? Тогда свойста: \p{L}, \p{Nd} с флагом u (JS/PCRE), или эквиваленты в .NET/Python.
- Есть вложенные квантификаторы? Подумайте о possessive/атомарных группах или переписывании шаблона.
- Тестируйте в разных движках: regex101 (PCRE2, включайте /u и UCP), Python re, .NET, JS с /u.
Фрагмент корпуса для тестов
Примеры строк:
- The quick brown fox jumps over 13 lazy dogs.
- Start_1 middle_text end9
- password PASSword Pa55w0rd GoodPass9
- a____9 A12345 9start1
- name_surname name--surname username_ _username
- abcde9 abcdef ABCDEF1 ABcdefghij1
- HELLOlll IllIlIll safeSAFE2
- abc def abc_def abc-def
- 123456 0000000000 1234567890
