Семестр 3 · Модуль 3 · Неделя 9
Regex advanced и основы NLP
Группы и named groups · lookahead/lookbehind · флаги · жадность ·
токенизация · стемминг vs лемматизация · Bag-of-Words · TF-IDF
2 ч теории. Данные — access.log (80 строк),
contact_text.txt, news_headlines.csv. После лекции —
live-coding и задачи семинара.
Прежде чем начать: откройте access.log и посмотрите на
одну строку — сколько полей «зашито» в ней?
Логи сервера — где живёт regex
- DevOps-инженер живёт на таких логах: nginx, Apache, приложения —
это строки, из которых нужно доставать IP, даты, URL, статусы.
- Продвинутые регулярные выражения — база обработки логов
(Компьютерные сети, DevOps).
- NLP-курс начнётся с токенизации — это будет здесь: понять, как текст
превращается в числа для моделей.
192.168.1.10 - - [01/Sep/2026:08:00:58 +0000] "POST /images/logo.png HTTP/1.1" 200 22093 "-" "python-requests/2.31.0"
Одна строка = уже данные: IP, дата, метод, URL, статус, байты.
Регулярные выражения — способ их достать за один проход.
Какие поля вы видите в этой строке лога? Как бы вы
извлекли IP и статус, если бы не знали regex?
Повторение базы: метасимволы и классы
| Конструкция | Смысл | Пример |
. | любой символ | a.c → «abc» |
^ $ | начало / конец строки | ^GET |
* + ? | 0+, 1+, 0..1 повторов | \d+ |
[...] | класс символов | [0-9]+ |
\d \w \s | цифра / буква / пробел | \d{4}-\d{2}-\d{2} |
{n,m} | от n до m повторов | \d{1,3} |
Это вы знаете из С1–С2; сегодня добавим группы, lookaround и
флаги — то, что превращает regex из «поиска» в «парсер».
Какой паттерн найдёт все даты вида 2026-09-01?
Группы и named groups: извлекаем поля из строки
import re
pattern = re.compile(
r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) - - \[(?P<date>[^\]]+)\] '
r'"(?P<method>[A-Z]+) (?P<url>\S+) HTTP/\d\.\d" '
r'(?P<status>\d{3}) (?P<bytes>\d+)')
m = pattern.search(line)
m.group('ip'); m.group('status')
'192.168.1.10' '200'
(...) — захватывающая группа; (?:...) — не
захватывает.
- named groups
(?P<name>...) — обращение
match.group('name'), а не по номеру.
- IP:
\d{1,3}(?:\.\d{1,3}){3} — 1–3 цифры, трижды «точка + 1–3 цифры».
Почему для IP используют (?:...), а не
(...)? Что изменится в groupdict()?
finditer → DataFrame: лог становится таблицей
import pandas as pd
rows = [m.groupdict()
for m in pattern.finditer(log_text)]
df = pd.DataFrame(rows)
df.head()
ip date method url status bytes
0 192.168.1.10 01/Sep/2026:08:00:58 +0000 POST /images/logo.png 200 22093
1 198.51.100.2 01/Sep/2026:08:06:36 +0000 PUT /static/style.css 500 264
2 192.168.1.10 01/Sep/2026:08:06:20 +0000 DELETE /api/users 200 65134
3 192.168.1.45 01/Sep/2026:08:08:49 +0000 PUT /download/report.pdf 200 80511
4 192.168.1.22 01/Sep/2026:08:09:33 +0000 DELETE /api/orders/42 200 402421
finditer перебирает все совпадения; каждая
группа — колонка DataFrame. Дальше — обычный pandas: value_counts, groupby, графики.
11 ответов 5xx (14%) — сервер «отвечает ошибкой» на заметной доле запросов.
Как из этого DataFrame посчитать распределение статусов
(2xx/4xx/5xx)? Что вернёт df['ip'].value_counts()?
Lookahead и lookbehind: заглядываем вперёд и назад
re.findall(r'\d+(?=%)', 'ВВП вырос на 4%, инфляция 3%')
['4', '3'] # (?=...) — число ПЕРЕД знаком %
re.findall(r'(?<=£)\d+', '£12 и £45')
['12', '45'] # (?<=...) — число ПОСЛЕ £
(?=X) — позиция, после которой идёт X (lookahead).
(?!X) — отрицание: позиция, после которой НЕТ X.
(?<=X) — lookbehind: перед совпадением X.
- Ограничение Python: lookbehind — только фиксированной
длины.
(?<=\d+) → re.error.
Чем \d+(?=%) отличается от \d+%?
Что из них попадёт в результат?
Жадность и ленивость: «съедает» лишнее
text = '<p>a</p><p>b</p>'
re.findall(r'<p>.*</p>', text)
['<p>a</p><p>b</p>'] # жадный * «съел» два тега
re.findall(r'<p>.*?</p>', text)
['<p>a</p>', '<p>b</p>'] # ленивый *?
* и + жадные: захватывают
максимум до последнего возможного совпадения.
*? и +? ленивые: минимум до
первого возможного совпадения.
- Для парных тегов/кавычек почти всегда нужен ленивый вариант.
Почему жадный .* дал один «тег» на всю
строку? Что искал движок — первый или последний </p>?
Флаги и re.compile: читаемый и быстрый паттерн
re.compile(
r'''(?P<ip>\d{1,3}(?:\.\d{1,3}){3})''',
re.IGNORECASE | re.VERBOSE) # флаги передаются в compile
re.IGNORECASE — без учёта регистра (get = GET).
re.MULTILINE — ^/$ на каждой строке.
re.DOTALL — точка совпадает и с переносом строки.
re.VERBOSE — паттерн можно писать с пробелами и комментариями.
re.compile — предкомпиляция: паттерн собирается один раз,
методы findall/finditer/search/match/fullmatch/sub вызываются у объекта.
Чем re.search отличается от
re.match? На какой строке лога они дадут разный результат?
re.escape: пользовательский ввод — не «регулярное выражение»
user_input = 'search.me?'
re.findall(user_input, text) # '.' и '?' — метасимволы!
re.findall(re.escape(user_input), text) # 'search\.me\?'
- Если вы подставляете в паттерн строку от пользователя/из файла —
метасимволы интерпретируются.
re.escape экранирует все спецсимволы — ищется буквально.
- Правило: данные ≠ код; чужая строка — это данные.
Что найдёт re.findall('a.b', 'a.b') без
escape? А с re.escape('a.b')?
NLP: токенизация и стоп-слова
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
tokens = word_tokenize(sentence.lower())
['the', 'researchers', 'were', 'running', 'machine', 'learning',
'experiments', 'and', 'analyzed', 'the', 'better', 'results', '.']
words = [w for w in tokens
if w.isalpha() and w not in stopwords.words('english')]
['researchers', 'running', 'machine', 'learning', 'experiments',
'analyzed', 'better', 'results']
- Токенизация — текст на слова/токены
(
word_tokenize).
- Стоп-слова — «the, and, was...»: частые, но не несущие
смысла; удаляются перед анализом.
Почему перед векторизацией удаляют стоп-слова? Что будет
в топ-терминах TF-IDF, если их оставить?
Стемминг vs лемматизация
from nltk.stem import PorterStemmer, WordNetLemmatizer
ps = PorterStemmer(); lm = WordNetLemmatizer()
ps.stem('running'); lm.lemmatize('running', pos='v')
'run' 'run'
ps.stem('better'); lm.lemmatize('better', pos='a')
'better' 'good'
- Стеммер — грубое обрезание суффиксов по правилам:
быстрый, но «running»→«run», «better»→«better» (не меняет).
- Лемматизатор — приводит к словарной форме по словарю:
«better»→«good», «was»→«be». Точнее, медленнее, требует словарь.
- Для русского языка —
SnowballStemmer('russian').
Почему лемматизатор дал «good» из «better», а стеммер —
не смог? Что «знает» лемматизатор?
Bag-of-Words: текст в числа
from sklearn.feature_extraction.text import CountVectorizer
docs = ['the cat sat on the mat', 'the dog sat on the log']
cv = CountVectorizer(); X = cv.fit_transform(docs)
X.toarray(); cv.get_feature_names_out()
[[0 1 0 0 1 1 1 2]
[0 0 1 1 0 1 1 2]]
['and' 'cat' 'dog' 'log' 'mat' 'on' 'sat' 'the']
- Bag-of-Words — матрица «документ × термин», ячейка =
частота термина в документе.
- Порядок слов теряется («мешок слов»), но для многих задач достаточно.
- Проблема: частые термины («the» — 2 в каждом) доминируют.
В какой ячейке матрицы стоит «2» и почему? Что это за
термин?
TF-IDF: поднять редкие, опустить частые
from sklearn.feature_extraction.text import TfidfVectorizer
tv = TfidfVectorizer(stop_words='english')
Xt = tv.fit_transform(headlines)
# топ-термин каждого заголовка из news_headlines.csv:
doc1: chess 0.403 · learns 0.403 · better 0.403
doc3: data 0.408 · programming 0.408 · popular 0.408
doc6: analyze 0.393 · scientific 0.393 · papers 0.393
- TF — частота термина в документе; IDF —
обратная частота документов: чем в большем числе документов термин
встречается, тем меньше его вес.
- Итог: редкие «специфичные» слова («chess», «xray») поднимаются,
общие («model», «data») — опускаются.
- Стоп-слова удаляются до расчёта — иначе топ забит союзами.
Почему «the» почти не влияет на TF-IDF? Что вернёт
вектор документа, если он весь состоит из одних стоп-слов?
Типичные ошибки недели 9
- re.match вместо re.search — match ищет только с начала
строки; на логе «ничего не нашлось».
- Жадный квантификатор «съедает» лишнее — нужен ленивый
*?/+?.
- Lookbehind переменной длины —
re.error в
Python; это ограничение движка, не ваша ошибка.
- Забыт
re.escape для пользовательского ввода.
- Стемминг и лемматизация перепутаны («better» → «good» делает только
лемматизатор).
- TF-IDF без удаления стоп-слов — топ-термины забиты союзами.
- Паттерн без
(?:...) — в groupdict() лезут
лишние группы.
Студент пишет re.match(pattern, line) на
логе и получает None. Что он перепутал и как чинить?
Вопросы для проверки понимания
- Как извлечь все email-адреса из текста (паттерн + named groups)?
- Чем
(?<=...) отличается от (?=...)?
- Какой квантификатор ленивый и зачем он нужен?
- Чем TF-IDF отличается от Bag-of-Words и зачем IDF?
- Чем стемминг отличается от лемматизации (пример «better»)?
Ответьте письменно за 3 минуты — это мини-самооценка перед
семинаром.
Переход: на семинаре вы распарсите access.log в
DataFrame, извлечёте сущности и построите TF-IDF на новостных заголовках.
Что дальше
- Семинар недели 9: «Парсинг логов access.log» (базовый),
«Агрегация по логам + извлечение сущностей» (стандартный), «NLP-минимум +
TF-IDF» (стандартный/продвинутый), «parse_access_log + pytest» (challenge).
- ДЗ недели 9 = проект модуля: численный анализ (SciPy) +
парсер логов (regex) + визуализация в одном ноутбуке.
- К концу недели вы умеете извлекать структурированные
данные из текста и логов и понимаете базовые NLP-концепции.
Готовы? Откройте семинар-9.md и распарсите первую строку
access.log с named groups.