add: LayoutProcessor - обработка тонких пробелов в инициалах и акронимах

This commit is contained in:
2025-08-31 15:41:44 +03:00
parent 4918645496
commit a26c9107f2
3 changed files with 50 additions and 27 deletions

View File

@@ -37,6 +37,7 @@ EN_ALPHABET_FULL = EN_ALPHABET_UPPER | EN_ALPHABET_LOWER
# --- Специальные символы ---
CHAR_NBSP = '\u00a0' # Неразрывный пробел ( )
CHAR_SHY = '\u00ad' # Мягкий перенос (­)
CHAR_THIN_SP = '\u2009' # Тонкий пробел (шпация,  )
CHAR_NDASH = '\u2013' # Cреднее тире ( / –)
CHAR_MDASH = '\u2014' # Длинное тире (— / —)
CHAR_HELLIP = '\u2026' # Многоточие (… / …)
@@ -108,7 +109,7 @@ SAFE_MODE_CHARS_TO_MNEMONIC = frozenset([
'\u2003', # Широкий пробел (Em Space) --  
'\u2007', # Цифровой пробел --  
'\u2008', # Пунктуационный пробел --  
'\u2009', # Межсимвольный пробел --  '
CHAR_THIN_SP, # Межсимвольный пробел, тонкий пробел, шпация --  '
'\u200A', # Толщина волоса (Hair Space) --  
'\u200B', # Негативный пробел (Negative Space) -- ​
'\u200C', # Нулевая ширина (без объединения) (Zero Width Non-Joiner) -- ‍
@@ -546,7 +547,7 @@ CUSTOM_ENCODE_MAP = {
'\u231d': '⌝', # ⌝ / ⌝ / ⌝
'\u2016': '‖', # ‖ / ‖ / ‖
'\u2228': '∨', # / ∨ / ∨
'\u2009': ' ', # /   /  
CHAR_THIN_SP: ' ', # /   /  
'\u2240': '≀', # ≀ / ≀ / ≀ / ≀
'\u2128': 'ℨ', # / ℨ / ℨ
'\u2118': '℘', # ℘ / ℘ / ℘

View File

@@ -3,7 +3,7 @@
import regex
import logging
from etpgrf.config import LANG_RU, LANG_EN, CHAR_NBSP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP
from etpgrf.config import LANG_RU, LANG_EN, CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP
from etpgrf.comutil import parse_and_validate_langs
# --
@@ -23,10 +23,10 @@ class LayoutProcessor:
def __init__(self,
langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
process_initials: bool = True):
process_initials_and_acronyms: bool = True):
self.langs = parse_and_validate_langs(langs)
self.main_lang = self.langs[0] if self.langs else LANG_RU
self.process_initials = process_initials
self.process_initials_and_acronyms = process_initials_and_acronyms
# 1. Паттерн для длинного (—) или среднего () тире, окруженного пробелами.
# (?<=\S) и (?=\S) гарантируют, что тире находится между словами, а не в начале/конце строки.
@@ -46,18 +46,23 @@ class LayoutProcessor:
# в выражениях типа "10 - 5".
self._negative_number_pattern = regex.compile(r'(?<!\d)\s+-(\d+)')
# 4. Паттерны для обработки инициалов.
# 4. Паттерны для обработки инициалов и акронимов.
# \p{Lu} - любая заглавная буква в Unicode.
# Этот паттерн находит пробел между фамилией и следующим за ней инициалом.
self._surname_initial_pattern = regex.compile(r'(\p{Lu}\p{L}{1,})\s+(?=\p{Lu}\.)')
# Этот паттерн находит пробел между инициалом и следующим за ним инициалом или фамилией.
# (?=\p{Lu}[\p{L}.]) - просмотр вперед на заглавную букву, за которой идет или буква (фамилия), или точка (инициал).
self._initial_pattern = regex.compile(r'(\p{Lu}\.)\s+(?=\p{Lu}[\p{L}.])')
# Правила для случаев, когда пробел УЖЕ ЕСТЬ (заменяем на неразрывный)
# Используем ` +` (пробел) вместо `\s+`, чтобы не заменять уже вставленные тонкие пробелы.
self._initial_to_initial_ws_pattern = regex.compile(r'(\p{Lu}\.) +(?=\p{Lu}\.)')
self._initial_to_surname_ws_pattern = regex.compile(r'(\p{Lu}\.) +(?=\p{Lu}\p{L}{1,})')
self._surname_to_initial_ws_pattern = regex.compile(r'(\p{Lu}\p{L}{2,}) +(?=\p{Lu}\.)')
# Правила для случаев, когда пробела НЕТ (вставляем тонкий пробел)
self._initial_to_initial_ns_pattern = regex.compile(r'(\p{Lu}\.)(?=\p{Lu}\.)')
self._initial_to_surname_ns_pattern = regex.compile(r'(\p{Lu}\.)(?=\p{Lu}\p{L}{1,})')
logger.debug(f"LayoutProcessor `__init__`. "
f"Langs: {self.langs}, "
f"Main lang: {self.main_lang}, "
f"Process initials: {self.process_initials}")
f"Process initials and acronyms: {self.process_initials_and_acronyms}")
def _replace_dash_spacing(self, match: regex.Match) -> str:
@@ -85,10 +90,14 @@ class LayoutProcessor:
processed_text = self._negative_number_pattern.sub(f'{CHAR_NBSP}-\\1', processed_text)
# 4. Обработка инициалов (если включено).
if self.process_initials:
# Сначала связываем фамилию с первым инициалом (Пушкин А. -> Пушкин{NBSP}А.)
processed_text = self._surname_initial_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
# Затем связываем инициалы между собой и с фамилией (А. С. Пушкин -> А.{NBSP}С.{NBSP}Пушкин)
processed_text = self._initial_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
if self.process_initials_and_acronyms:
# Сначала вставляем тонкие пробелы там, где пробелов не было.
processed_text = self._initial_to_initial_ns_pattern.sub(f'\\1{CHAR_THIN_SP}', processed_text)
processed_text = self._initial_to_surname_ns_pattern.sub(f'\\1{CHAR_THIN_SP}', processed_text)
# Затем заменяем существующие пробелы на неразрывные.
processed_text = self._initial_to_initial_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
processed_text = self._initial_to_surname_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
processed_text = self._surname_to_initial_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
return processed_text

View File

@@ -2,8 +2,8 @@
# Тестирует модуль LayoutProcessor. Проверяет обработку тире и специальных символов в тексте.
import pytest
from etpgrf.layout import LayoutProcessor
from etpgrf.config import CHAR_NBSP, CHAR_HELLIP
from etpgrf.layout import LayoutProcessor, CHAR_THIN_SP
from etpgrf.config import CHAR_NBSP, CHAR_HELLIP, CHAR_THIN_SP
LAYOUT_TEST_CASES = [
# --- Длинное тире (—) для русского языка ---
@@ -71,16 +71,29 @@ LAYOUT_TEST_CASES = [
('ru', "1-2-3-4-5, я иду тебя искать", "1-2-3-4-5, я иду тебя искать"), # Дефис, а не минус
# --- Инициалы (должны обрабатываться по умолчанию) ---
# Разные комбинации пробелов
('ru', "А. С. Пушкин", f"А.{CHAR_NBSP}С.{CHAR_NBSP}Пушкин"),
('ru', "А.С. Пушкин", f"А.С.{CHAR_NBSP}Пушкин"),
('ru', "Пушкин А. С.", f"Пушкин{CHAR_NBSP}А.{CHAR_NBSP}С."),
('ru', "Пушкин А.С.", f"Пушкин{CHAR_NBSP}А.С."),
('ru', "А.С. Пушкин", f"А.{CHAR_THIN_SP}С.{CHAR_NBSP}Пушкин"),
('ru', "А.С.Пушкин", f"А.{CHAR_THIN_SP}С.{CHAR_THIN_SP}Пушкин"),
('en', "J. R. R. Tolkien", f"J.{CHAR_NBSP}R.{CHAR_NBSP}R.{CHAR_NBSP}Tolkien"),
('en', "J.R.R. Tolkien", f"J.R.R.{CHAR_NBSP}Tolkien"),
('en', "J.R.R. Tolkien", f"J.{CHAR_THIN_SP}R.{CHAR_THIN_SP}R.{CHAR_NBSP}Tolkien"),
('ru', "Пушкин А. С.", f"Пушкин{CHAR_NBSP}А.{CHAR_NBSP}С."),
('ru', "Пушкин А.С.", f"Пушкин{CHAR_NBSP}А.{CHAR_THIN_SP}С."),
('en', "Tolkien J. R. R.", f"Tolkien{CHAR_NBSP}J.{CHAR_NBSP}R.{CHAR_NBSP}R."),
('en', "Tolkien J.R.R.", f"Tolkien{CHAR_NBSP}J.R.R."),
('en', "Tolkien J.R.R.", f"Tolkien{CHAR_NBSP}J.{CHAR_THIN_SP}R.{CHAR_THIN_SP}R."),
# Один инициал
('ru', "Это был В. Высоцкий.", f"Это был В.{CHAR_NBSP}Высоцкий."),
('ru', "Высоцкий В. С. был гением.", f"Высоцкий{CHAR_NBSP}В.{CHAR_NBSP}С. был гением."),
('ru', "Высоцкий В. был гением.", f"Высоцкий{CHAR_NBSP}В. был гением."),
# Акронимы (бонус)
('ru', "Сделано в С.Ш.А.", f"Сделано в С.{CHAR_THIN_SP}Ш.{CHAR_THIN_SP}А."),
('ru', "Сделано в С. Ш. А.", f"Сделано в С.{CHAR_NBSP}Ш.{CHAR_NBSP}А."),
('en', "На замке стояло клеймо «Made in U.S.A.»", f"На замке стояло клеймо «Made in U.{CHAR_THIN_SP}S.{CHAR_THIN_SP}A.»"),
('en', "На замке стояло клеймо «Made in U. S. A.»", f"На замке стояло клеймо «Made in U.{CHAR_NBSP}S.{CHAR_NBSP}A.»"),
# Никаких изменений, если пробелы другого типа
('ru', "А.\u200DС.\u200AПушкин", "А.\u200DС.\u200AПушкин"),
('ru', "Пушкин А.\u200AС.", f"Пушкин{CHAR_NBSP}А.\u200AС."),
('en', "J.\u200DR.\u200DR.\u200ATolkien", "J.\u200DR.\u200DR.\u200ATolkien"),
('en', "Tolkien J.\u200AR.\u200AR.", f"Tolkien{CHAR_NBSP}J.\u200AR.\u200AR."),
# --- Инициалы (проверка отключения опции) ---
# ('ru', "А. С. Пушкин", "А. С. Пушкин", False),
@@ -89,7 +102,7 @@ LAYOUT_TEST_CASES = [
# --- Комбинированные случаи ---
('ru', f"Да — это так{CHAR_HELLIP} а может и нет. Счёт -10.",
f"Да{CHAR_NBSP}— это так{CHAR_HELLIP}{CHAR_NBSP}а может и нет. Счёт{CHAR_NBSP}-10."),
('ru', f"По мнению А. С. Пушкина — это...", f"По мнению А.{CHAR_NBSP}С.{CHAR_NBSP}Пушкина{CHAR_NBSP}— это..."),
('ru', f"По мнению А.С.Пушкина — это...", f"По мнению А.{CHAR_THIN_SP}С.{CHAR_THIN_SP}Пушкина{CHAR_NBSP}— это..."),
]