Семестр 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

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'
Почему для 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
топ-10 IP по числу запросов (агрегация из распарсенного лога)
статусы: 2xx=55, 5xx=11, 4xx=9, 3xx=5

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'] # (?<=...) — число ПОСЛЕ £
Чем \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.search отличается от re.match? На какой строке лога они дадут разный результат?

re.escape: пользовательский ввод — не «регулярное выражение»

user_input = 'search.me?' re.findall(user_input, text) # '.' и '?' — метасимволы! re.findall(re.escape(user_input), text) # 'search\.me\?'
Что найдёт 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']
Почему перед векторизацией удаляют стоп-слова? Что будет в топ-терминах 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'
Почему лемматизатор дал «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']
В какой ячейке матрицы стоит «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
Почему «the» почти не влияет на TF-IDF? Что вернёт вектор документа, если он весь состоит из одних стоп-слов?

Типичные ошибки недели 9

  1. re.match вместо re.search — match ищет только с начала строки; на логе «ничего не нашлось».
  2. Жадный квантификатор «съедает» лишнее — нужен ленивый *?/+?.
  3. Lookbehind переменной длиныre.error в Python; это ограничение движка, не ваша ошибка.
  4. Забыт re.escape для пользовательского ввода.
  5. Стемминг и лемматизация перепутаны («better» → «good» делает только лемматизатор).
  6. TF-IDF без удаления стоп-слов — топ-термины забиты союзами.
  7. Паттерн без (?:...) — в groupdict() лезут лишние группы.
Студент пишет re.match(pattern, line) на логе и получает None. Что он перепутал и как чинить?

Вопросы для проверки понимания

  1. Как извлечь все email-адреса из текста (паттерн + named groups)?
  2. Чем (?<=...) отличается от (?=...)?
  3. Какой квантификатор ленивый и зачем он нужен?
  4. Чем TF-IDF отличается от Bag-of-Words и зачем IDF?
  5. Чем стемминг отличается от лемматизации (пример «better»)?

Ответьте письменно за 3 минуты — это мини-самооценка перед семинаром.

Переход: на семинаре вы распарсите access.log в DataFrame, извлечёте сущности и построите TF-IDF на новостных заголовках.

Что дальше

Готовы? Откройте семинар-9.md и распарсите первую строку access.log с named groups.