"""Нормализация текста реплик. Модуль намеренно не выполняет агрессивную очистку: корпус уже отфильтрован вручную. Здесь только приводятся к единому виду 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: """Заменяет ```` и аналогичные метки на слова без угловых скобок.""" 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)