Первичная версия: стилометрия реплик, LSA и протокол экспериментов

This commit is contained in:
2026-10-24 01:17:36 +03:00
commit c390224500
52 changed files with 10099 additions and 0 deletions

123
lab1/stylometry/cleaning.py Normal file
View File

@@ -0,0 +1,123 @@
"""Нормализация текста реплик.
Модуль намеренно не выполняет агрессивную очистку: корпус уже отфильтрован
вручную. Здесь только приводятся к единому виду Unicode, плейсхолдеры и
пробельные символы. Пунктуация, регистр и разговорные элементы сохраняются,
поскольку они являются стилометрическими признаками.
"""
from __future__ import annotations
import re
import unicodedata
from typing import Iterable
# Плейсхолдеры игрового движка. Они не являются авторским текстом, поэтому
# заменяются на нейтральные слова, а их количество учитывается отдельным
# признаком.
PLACEHOLDER_PATTERN = re.compile(r"<([A-Z][A-Z0-9_]*)>")
PLACEHOLDER_MAP: dict[str, str] = {
"PLAYER": "PLAYER",
"FARM": "FARM",
"PET": "PET",
"SPOUSE": "SPOUSE",
"TIME": "TIME",
"NOUN": "NOUN",
"FIRST_LETTER": "FIRST_LETTER",
"NAME": "NAME",
}
# Символы, которые в корпусе могут встречаться в разных типографских
# вариантах. Приводим их к ASCII-эквивалентам, чтобы признаки были
# сопоставимы между репликами.
_UNICODE_REPLACEMENTS: dict[str, str] = {
"\u2026": "...", # многоточие
"\u2018": "'",
"\u2019": "'",
"\u201a": "'",
"\u201b": "'",
"\u201c": '"',
"\u201d": '"',
"\u201e": '"',
"\u2013": "-", # короткое тире
"\u2014": "-", # длинное тире
"\u2212": "-", # математический минус
"\u00a0": " ", # неразрывный пробел
"\u2009": " ", # тонкий пробел
"\u200a": " ",
"\u200b": "", # нулевой пробел
"\u200c": "",
"\u200d": "",
"\ufeff": "",
}
def normalize_unicode(text: str) -> str:
"""Приводит Unicode к предсказуемому виду, не меняя смысл текста."""
text = unicodedata.normalize("NFKC", str(text))
for source, target in _UNICODE_REPLACEMENTS.items():
text = text.replace(source, target)
return text
def normalize_placeholders(text: str) -> str:
"""Заменяет ``<PLAYER>`` и аналогичные метки на слова без угловых скобок."""
def _replace(match: re.Match[str]) -> str:
name = match.group(1)
return PLACEHOLDER_MAP.get(name, "ENTITY")
return PLACEHOLDER_PATTERN.sub(_replace, text)
def normalize_whitespace(text: str) -> str:
"""Схлопывает повторные пробелы и переводы строк."""
text = re.sub(r"[ \t\f\v]+", " ", text)
text = re.sub(r"\s*\n\s*", " ", text)
return text.strip()
def clean_text(text: str) -> str:
"""Основная функция нормализации одной реплики."""
text = normalize_unicode(text)
text = normalize_placeholders(text)
text = normalize_whitespace(text)
return text
def normalized_key(text: str) -> str:
"""Ключ для поиска точных дублей.
Регистр, пунктуация и плейсхолдеры игнорируются. Такой ключ использовался
при аудите корпуса; для обучения он не применяется.
"""
text = text.lower()
text = PLACEHOLDER_PATTERN.sub("", text)
text = re.sub(r"[^a-z0-9' ]+", " ", text)
return normalize_whitespace(text)
def count_placeholders(text: str) -> int:
"""Возвращает число плейсхолдеров в исходной реплике."""
return len(PLACEHOLDER_PATTERN.findall(text))
def placeholder_types(text: str) -> list[str]:
"""Возвращает типы плейсхолдеров в порядке появления."""
return [match.group(1) for match in PLACEHOLDER_PATTERN.finditer(text)]
def is_valid_line(text: str) -> bool:
"""Отбрасывает пустые строки и строки без латинских букв."""
return bool(re.search(r"[A-Za-z]", text))
def tokenize_for_audit(text: str) -> list[str]:
"""Простая токенизация для аудита; не заменяет NLTK-токенизатор."""
return re.findall(r"[A-Za-z]+(?:'[A-Za-z]+)?|\d+|[^\w\s]", text)
def join_tokens(tokens: Iterable[str]) -> str:
"""Склеивает токены через пробел, сохраняя их как отдельные единицы."""
return " ".join(str(token) for token in tokens)