Первичная версия: стилометрия реплик, LSA и протокол экспериментов
This commit is contained in:
123
lab1/stylometry/cleaning.py
Normal file
123
lab1/stylometry/cleaning.py
Normal 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)
|
||||
Reference in New Issue
Block a user