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