(...) — захватывающая группа: то, что совпало внутри, можно достать
по номеру через m.groups(). Скобки делают из «найти» — «извлечь»:
три поля одной маской.
Почему в IP используется (?:...), а не
(...)?
Named groups (?P<name>...): читаемый доступ к полям
Named groups (?P<name>...) — обращение по имени
(m.group('ip')), а не по номеру; m.groupdict() —
словарь всех полей. Это «интерфейс» парсера: поля именованы и не зависят от
порядка в паттерне.
Чем group('ip') лучше group(1)?
Построчный разбор паттерна access.log
(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # IP: 1–3 цифры, трижды «точка + 1–3 цифры»
- - \[(?P<date>[^\]]+)\] # литералы " - - ["; дата = всё, кроме ']' (в ней пробел!)
"(?P<method>[A-Z]+) # метод — только заглавные буквы (POST/GET/...)
(?P<url>\S+) HTTP/\d\.\d" # URL — непробельные; литерал "HTTP/1.1"
(?P<status>\d{3}) # статус — 3 цифры
(?P<bytes>\d+) # байты — 1+ цифр
Ключевые решения: [^\]]+ для даты — внутри неё пробел, поэтому
\S+ не подойдёт, а .* жадный; \S+ для URL —
путь без пробелов; HTTP/\d\.\d — литерал с экранированной точкой.
Почему для даты нужен [^\]]+, а не \S+?
Predict: что вернёт groupdict() на строке лога
line = '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"'
m = pattern.search(line)
print(m.group('url')); print(m.group('date'))
Не запуская: что выведется? Почему url не захватил
HTTP/1.1, а date содержит пробел и +0000?
re.match vs re.search
re.match(pattern, line) # ищет СОВПАДЕНИЕ С НАЧАЛА строки
re.search(pattern, line) # ищет ПЕРВОЕ совпадение в любом месте
re.match(pattern, ' ' + line) # ведущие пробелы в началеNone # match привязан к началу — «ничего не нашлось»
re.search(pattern, ' ' + line)
Match # search находит и не с начала
Правило: для поиска по тексту используем search; match —
только когда нужна привязка к началу.
На какой строке лога match и search
дадут разный результат?
finditer → DataFrame: лог становится таблицей
import pandas as pd
log_text = open('access.log', encoding='utf-8').read()
rows = [m.groupdict() for m in pattern.finditer(log_text)]
df = pd.DataFrame(rows)
df.head()
ip
date
method
url
status
bytes
192.168.1.10
01/Sep/2026:08:00:58 +0000
POST
/images/logo.png
200
22093
198.51.100.2
01/Sep/2026:08:06:36 +0000
PUT
/static/style.css
500
264
192.168.1.10
01/Sep/2026:08:06:20 +0000
DELETE
/api/users
200
65134
192.168.1.45
01/Sep/2026:08:08:49 +0000
PUT
/download/report.pdf
200
80511
192.168.1.22
01/Sep/2026:08:09:33 +0000
DELETE
/api/orders/42
200
402421
finditer перебирает все совпадения; каждая группа — колонка DataFrame.
Дальше — обычный pandas. Всё 80 строк файла распарсены
(len(df) == 80).
Как из этого DataFrame посчитать распределение статусов по
классам 2xx/4xx/5xx?
Агрегации: топ-10 IP, топ-10 URL, статусы 2xx/5xx
df['status'] = df['status'].astype(int)
top_ip = df['ip'].value_counts().head(10)
top_url = df['url'].value_counts().head(10)
defbucket(s):
if s < 300: return'2xx'if s < 400: return'3xx'if s < 500: return'4xx'return'5xx'
statuses = df['status'].apply(bucket).value_counts()
«Распределение статусов (2xx/3xx/4xx/5xx)»: ось Y с нуля, классы по порядку.
Источник: access.log, 80 строк.
Числа: топ-1 IP — 192.168.1.10 (17 запросов; в логе 9 уникальных IP);
топ-1 URL — /images/logo.png (10, в паре с /api/items — 10);
статусы: 2xx = 55, 3xx = 5, 4xx = 9, 5xx = 11. 11 ответов 5xx ≈ 14% — сервер
«отвечает внутренней ошибкой» на заметной доле запросов.
Почему value_counts() по сырому статусу
(200, 301, 404, 500) — не то, что нужно?
Зачем приводить типы status/bytes к int до агрегации? Почему
finditer, а не match?
Lookahead (?=...): число перед %
re.findall(r'\d+(?=%)', 'ВВП вырос на 4%, инфляция 3%')
['4', '3'] # (?=...) — позиция, после которой идёт %
(?=X) — lookahead: совпадение происходит, только если после него идёт X;
X в результат не включается. Контраст: \d+% вернул бы
['4%', '3%'] — с символом.
Чем \d+(?=%) отличается от \d+%? Что
из них попадёт в результат?
Lookbehind (?<=...): фиксированная длина, иначе re.error
re.findall(r'(?<=£)\d+', '£12 и £45')
['12', '45'] # (?<=X) — перед совпадением X
(?<=X) — lookbehind: перед совпадением должно стоять X. В Python
lookbehind — только фиксированной длины: (?<=\d+)
даёт re.error. Это ограничение движка, а не ваша ошибка.
Почему (?<=\d+) даёт ошибку, а
(?<=£) — нет?
Отрицательные lookaround (?!...), (?<!...)
re.findall(r'\d+(?!%)', 'ВВП 4%, а курс 75 руб')
['75'] # '4' отброшено: после него идёт %; '75' целиком: после '5' нет %
(?!X) — отрицательный lookahead: совпадение возможно, только если
после него нет X. Неочевидность: '4' исключено (после него %),
а '75' совпало целиком — жадный \d+ берёт всё число, и проверка
смотрит за его конец, а не за каждую цифру.
Почему '4' не попало в результат, а '75' попало целиком?
Жадный .* «съедает» два тега — ленивый .*? спасает
re.findall(r'<p>.*</p>', text)
['<p>a</p><p>b</p>']# жадный * «съел» два тега
* и + жадные: захватывают максимум до последнего
возможного совпадения; *? и +? ленивые: минимум до
первого. Для парных тегов/кавычек почти всегда нужен ленивый вариант.
Что искал жадный .* — первый или последний
</p>? Почему получился один «тег» на всю строку?
Флаги: IGNORECASE / MULTILINE / DOTALL / VERBOSE
Флаг
Эффект
re.IGNORECASE
без учёта регистра (get = GET)
re.MULTILINE
^/$ — на каждой строке, не только в начале/конце текста
Флаги передаются в re.compile (или как 4-й аргумент функций) и
комбинируются через |. re.VERBOSE полезен для длинных
паттернов — их можно писать с комментариями.
Зачем нужен re.VERBOSE для паттерна access.log?
re.compile: методы findall/finditer/search/match/fullmatch/sub
pat = re.compile(r'(\d{4})-(\d{2})-(\d{2})')
pat.findall('2026-11-15 и 2026-11-16')
[('2026', '11', '15'), ('2026', '11', '16')]
pat.sub(r'\3.\2.\1', 'конференция 2026-11-15')
'конференция 15.11.2026'
Метод
Что делает
findall
все совпадения
finditer
итератор с позициями (match.start(), match.end())
search
первое совпадение в любом месте
match
с начала строки
fullmatch
вся строка целиком
sub
замена с обратными ссылками \1
Чем search отличается от match? А
fullmatch от search?
re.escape: пользовательский ввод — не «регулярное выражение»
Если подставляете в паттерн строку от пользователя/из файла — метасимволы
интерпретируются. re.escape экранирует все спецсимволы — ищется
буквально. Данные ≠ код; чужая строка — это данные.
Что найдёт re.findall('a.b', 'a.b') без escape?
А с re.escape('a.b')?
Токенизация word_tokenize и стоп-слова
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
sentence = ("The researchers were running machine learning experiments ""and analyzed the better results.")
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']
Токенизация — разбиение текста на токены (слова, знаки); стоп-слова (the, and,
was…) — частые, но не несущие смысла; удаляются перед анализом. Важны
.lower() (регистр) и .isalpha() (убрать пунктуацию).
Стеммер — грубое обрезание суффиксов по правилам: быстро, но
running→run, а better не меняет. Лемматизатор —
приведение к словарной форме по словарю (WordNet): better→good,
was→be. Точнее, медленнее, требует словарь. Для русского —
SnowballStemmer('russian').
Что вернёт lm.lemmatize('better') без
pos='a'?
Predict: что сделает стеммер/лемматизатор с 5 словами
for w in ['running', 'researchers', 'analyzed', 'better', 'experiments']:
print(w, '→', ps.stem(w), '|', lm.lemmatize(w, pos='v'))
Не запуская: что напечатается? Помните: стеммер режет грубо,
лемматизатор зависит от pos.
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()
[[1 0 0 1 1 1 2]
[0 1 1 0 1 1 2]]
cv.get_feature_names_out()
['cat' 'dog' 'log' 'mat' 'on' 'sat' 'the']
Bag-of-Words — матрица «документ × термин», ячейка = частота термина в документе;
порядок слов теряется («мешок слов»). Проблема: частые термины («the» — 2 в
каждом документе) доминируют.
В какой ячейке матрицы стоит «2» и почему?
TF-IDF на новостных заголовках: топ-термины
from sklearn.feature_extraction.text import TfidfVectorizer
headlines = pd.read_csv('news_headlines.csv')['headline'].tolist()
tv = TfidfVectorizer(stop_words='english')
Xt = tv.fit_transform(headlines)
# топ-3 термина каждого заголовка (news_headlines.csv, 6 док., 38 терм.):doc1: chess 0.403 · better 0.403 · learns 0.403
doc2: weather 0.397 · train 0.397 · predict 0.397
doc3: language 0.408 · data 0.408 · python 0.408
doc6: use 0.393 · analyze 0.393 · papers 0.393
TF-IDF выделил «chess», «python» — редкие термины, которые отличают документ от
остальных. Обычные слова вроде «model», «neural» весят меньше. Внутри топ-3 веса
равны — привязка к версии sklearn.
Почему «chess» поднялся в топ, хотя встречается один раз?
Зачем IDF: редкие поднимаются, частые опускаются
TF — частота термина в документе: насколько термин представлен в документе.
IDF — обратная документная частота: чем в большем числе документов встречается термин, тем меньше его вес.
idf(t) = ln(N / df(t) + 1)
TF-IDF = TF × IDF
Редкие «специфичные» слова («chess», «xray») поднимаются, общие («model», «data»)
— опускаются. Стоп-слова удаляются до расчёта — иначе топ забит союзами.
TF-IDF находит не «главные слова текста», а слова, отличающие документ от остальных.
Lookbehind переменной длины — re.error в Python; это ограничение движка, не ваша ошибка.
Забыт re.escape для пользовательского ввода.
Стемминг и лемматизация перепутаны («better» → «good» делает только лемматизатор с pos='a').
TF-IDF без удаления стоп-слов — топ-термины забиты союзами.
Паттерн без (?:...) — в groupdict() лезут лишние группы.
Студент пишет re.match(pattern, line) на логе и
получает None. Что он перепутал и как чинить?
Проект модуля: парсер логов — блок 3 и сборка
Проект модуля 3 = численный анализ (SciPy) + парсер логов (regex) +
визуализация в одном ноутбуке module3_project.ipynb.
Блок 3 (неделя 9): parse_access_log(path) -> pd.DataFrame с named groups,
топ-10 IP/URL, статусы, графики.
Сборка: блок 1 (дашборд/heatmap, нед. 7) + блок 2 (fit_model, нед. 8) +
блок 3 (парсер, нед. 9) → выводы по обоим блокам.
Autograder: тесты на parse_access_log и fit_model;
regex — с named groups; агрегации — pandas; графики подписаны.
Что входит в каждый из трёх блоков проекта модуля?
Итоги: 6 навыков; семинар; проект; one-minute paper
Сегодня вы научились: (1) паттерны с named groups; (2) lookahead/lookbehind;
(3) флаги и методы re.compile; (4) ленивые квантификаторы; (5) парсить логи
в DataFrame и строить агрегации; (6) различать BoW и TF-IDF, стемминг и лемматизацию.