Files
NLP/lab1/stylometry/cleaning.py

124 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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