# etpgrf/sanitizer.py # Модуль для очистки и нормализации HTML-кода перед типографикой. import logging from bs4 import BeautifulSoup from .config import (SANITIZE_ALL_HTML, SANITIZE_ETPGRF, SANITIZE_NONE, PROTECTED_HTML_TAGS, HANGING_PUNCTUATION_SYMBOLS_CLASSES, HANGING_PUNCTUATION_SPACE_CLASSES_FLAT, CHARS_SYMBOLS_TO_BAN) logger = logging.getLogger(__name__) class SanitizerProcessor: """ Выполняет очистку HTML-кода в соответствии с заданным режимом. """ def __init__(self, mode: str | bool | None = SANITIZE_NONE): """ :param mode: Режим очистки: - 'etp' (SANITIZE_ETPGRF): удаляет только разметку висячей пунктуации. - 'html' (SANITIZE_ALL_HTML): удаляет все HTML-теги. - None или False: ничего не делает. """ if mode is False: mode = SANITIZE_NONE self.mode = mode # Оптимизация: заранее готовим CSS-селектор для поиска висячей пунктуации if self.mode == SANITIZE_ETPGRF: # Собираем уникальные классы из отдельных коллекций (чтобы избежать пустого селектора) symbol_classes = set(HANGING_PUNCTUATION_SYMBOLS_CLASSES.values()) space_classes = set(HANGING_PUNCTUATION_SPACE_CLASSES_FLAT.values()) unique_classes = sorted(symbol_classes | space_classes) # Формируем селектор вида: span.class1, span.class2, ... # Это позволяет использовать нативный парсер (lxml) для поиска, что намного быстрее python-лямбд. self._etp_selector = ", ".join(f"span.{cls}" for cls in unique_classes) else: self._etp_selector = None logger.debug(f"SanitizerProcessor `__init__`. Mode: {self.mode}") def process(self, soup: BeautifulSoup) -> BeautifulSoup | str: """ Применяет правила очистки к `soup`-объекту. :param soup: Объект BeautifulSoup для обработки. :return: Обработанный объект BeautifulSoup или строка (в режиме 'html'). """ if self.mode == SANITIZE_ETPGRF: if not self._etp_selector: self._strip_banned_chars_from_soup(soup) return soup # Используем CSS-селектор для быстрого поиска всех нужных элементов spans_to_clean = soup.select(self._etp_selector) # "Агрессивная" очистка: просто "разворачиваем" все найденные теги, # заменяя их своим содержимым. for span in spans_to_clean: span.unwrap() self._strip_banned_chars_from_soup(soup) return soup elif self.mode == SANITIZE_ALL_HTML: # Оптимизированный подход: # 1. Удаляем защищенные теги (script, style и т.д.) вместе с содержимым. # Используем select для поиска, так как это обычно быстрее. if PROTECTED_HTML_TAGS: # Формируем селектор: script, style, pre, ... protected_selector = ", ".join(PROTECTED_HTML_TAGS) for tag in soup.select(protected_selector): tag.decompose() # Полное удаление тега из дерева # 2. Извлекаем чистый текст из оставшегося дерева. # get_text() работает на уровне C (в lxml) и намного быстрее ручного обхода. text = soup.get_text() return self._strip_banned_chars_from_string(text) # Если режим не задан, ничего не делаем return soup def _strip_banned_chars_from_soup(self, soup: BeautifulSoup) -> None: """ Удаляет запрещенные символы из всего содержимого soup-объекта. :param soup: Объект BeautifulSoup для обработки. """ for element in soup.find_all(string=True): if isinstance(element, str): new_string = self._strip_banned_chars_from_string(element) element.replace_with(new_string) def _strip_banned_chars_from_string(self, text: str) -> str: """ Удаляет запрещенные символы из строки. :param text: Исходная строка. :return: Строка без запрещенных символов. """ # Удаляем все символы, которые есть в CHARS_SYMBOLS_TO_BAN for char in CHARS_SYMBOLS_TO_BAN: text = text.replace(char, "") return text