add: LayoutProcessor - обработка едениц измерения (кажется все)

This commit is contained in:
2025-09-22 01:04:38 +03:00
parent c3e65700b1
commit 67c5bd805a
5 changed files with 118 additions and 82 deletions

View File

@@ -63,6 +63,10 @@ CHAR_ARROW_LR = '\u27f7' # Длинная двунаправленная ст
CHAR_ARROW_L_LONG_DOUBLE = '\u27f8' # Длинная двойная стрелка влево
CHAR_ARROW_R_LONG_DOUBLE = '\u27f9' # Длинная двойная стрелка вправо
CHAR_ARROW_LR_LONG_DOUBLE = '\u27fa' # Длинная двойная двунаправленная стрелка
CHAR_MIDDOT = '\u00b7' # Средняя точка (· иногда используется как знак умножения) / ·
CHAR_UNIT_SEPARATOR = '\u25F0' # Символ временный разделитель для составных единиц (◰), чтобы не уходить
# в "мертвый" цикл при замене на тонкий пробел. Можно взять любой редкий символом.
# === КОНСТАНТЫ ПСЕВДОГРАФИКИ ===
# Для простых замен "строка -> символ" используем список кортежей.
@@ -92,28 +96,6 @@ STR_TO_SYMBOL_REPLACEMENTS = [
('~=', CHAR_AP), # Приблизительно равно (≈)
]
# === КОНСТАНТЫ ДЛЯ ЕДИНИЦ ИЗМЕРЕНИЯ ===
# Пост-позиционные (10 км)
DEFAULT_POST_UNITS = [
# Русские
'гг', 'г.', 'кг', 'мг', 'ц', 'т',
'кв.м', 'куб.м', 'мм', 'см', 'м', 'км', 'л', 'мл', 'сот', 'га',
'сек', 'с.', 'мин', 'ч',
'руб', 'коп',
'тыс', 'млн', 'млрд',
'пп', 'стр', 'рис', 'табл', 'гл', 'п', 'шт',
# Английские
'pp', 'p', 'para', 'sect', 'fig', 'vol', 'ed',
]
# Пред-позиционные (№ 5, $ 10)
DEFAULT_PRE_UNITS = ['', '$', '', '£', '', '#']
# === КОНСТАНТЫ ДЛЯ СЛОЖНЫХ (СОСТАВНЫХ) ЕДИНИЦ ИЗМЕРЕНИЯ ===
# Эти единицы будут автоматически "склеены" неразрывными пробелами внутри LayoutProcessor
DEFAULT_COMPLEX_UNITS = [
'до н. э.',
'н. э.',
]
# === КОНСТАНТЫ ДЛЯ КОДИРОВАНИЯ HTML-МНЕМНОИКОВ ===
# --- ЧЕРНЫЙ СПИСОК: Символы, которые НИКОГДА не нужно кодировать в мнемоники ---
@@ -185,7 +167,7 @@ CUSTOM_ENCODE_MAP = {
'\u0026': '&', # & / & / &
'\u003e': '>', # > / > / >
'\u003c': '&lt;', # < / &lt; / &LT;
'\u00b7': '&middot;', # · / &middot; / &centerdot; / &CenterDot;
CHAR_MIDDOT: '&middot;', # · / &middot; / &centerdot; / &CenterDot;
'\u0060': '&grave;', # ` / &grave; / &DiacriticalGrave;
'\u00a8': '&die;', # ¨ / &die; / &Dot; / &uml; / &DoubleDot;
'\u00b1': '&pm;', # ± / &pm; / &PlusMinus;
@@ -637,4 +619,28 @@ ENCODE_MAP = _build_translation_maps()
# --- Публичный API модуля ---
def get_encode_map():
"""Возвращает готовую карту для кодирования."""
return ENCODE_MAP
return ENCODE_MAP
# === КОНСТАНТЫ ДЛЯ ЕДИНИЦ ИЗМЕРЕНИЯ ===
# ТОЛЬКО АТОМАРНЫЕ единицы измерения: 'г', 'м', 'с', 'км', 'кв', 'куб', 'ч' и так далее.
# Никаких сложных и составных, типа: 'кв.м.', 'км/ч' или "до н.э." ...
# Пост-позиционные (10 км).
DEFAULT_POST_UNITS = [
# Русские
'гг', 'г.', 'в.', 'вв', 'н', 'э',
'кг', 'мг', 'ц', 'т',
'кв', 'куб', 'мм', 'см', 'м', 'км', 'л', 'мл', 'сот', 'га',
'сек', 'с.', 'мин', 'ч',
'руб', 'коп',
'тыс', 'млн', 'млрд', 'трлн', 'трлрд',
'пп', 'стр', 'рис', 'табл', 'гл', 'п', 'шт', 'об'
# Английские
'pp', 'p', 'para', 'sect', 'fig', 'vol', 'ed',
]
# Пред-позиционные (№ 5, $ 10)
DEFAULT_PRE_UNITS = ['', '$', '', '£', '', '#']
# Операторы, которые могут стоять между единицами измерения (км/ч)
# Сложение и вычитание здесь намеренно отсутствуют.
UNIT_MATH_OPERATORS = ['/', '*', '×', CHAR_MIDDOT, '÷']

View File

@@ -3,8 +3,8 @@
import regex
import logging
from etpgrf.config import (LANG_RU, LANG_EN, CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP,
DEFAULT_POST_UNITS, DEFAULT_PRE_UNITS, DEFAULT_COMPLEX_UNITS)
from etpgrf.config import (LANG_RU, LANG_EN, CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP, CHAR_UNIT_SEPARATOR,
DEFAULT_POST_UNITS, DEFAULT_PRE_UNITS, UNIT_MATH_OPERATORS)
from etpgrf.comutil import parse_and_validate_langs
# --
@@ -25,14 +25,12 @@ class LayoutProcessor:
def __init__(self,
langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
process_initials_and_acronyms: bool = True,
process_units: bool | str | list[str] = True,
process_complex_units: bool | list[str] = True):
process_units: bool | str | list[str] = True):
self.langs = parse_and_validate_langs(langs)
self.main_lang = self.langs[0] if self.langs else LANG_RU
self.process_initials_and_acronyms = process_initials_and_acronyms
self.process_units = process_units
self.process_complex_units = process_complex_units
# 1. Паттерн для длинного (—) или среднего () тире, окруженного пробелами.
# (?<=[\p{L}\p{Po}\p{Pf}"\']) - просмотр назад на букву, пунктуацию или кавычку.
self._dash_pattern = regex.compile(rf'(?<=[\p{{L}}\p{{Po}}\p{{Pf}}"\'])\s+([{CHAR_MDASH}{CHAR_NDASH}])\s+(?=\S)')
@@ -62,59 +60,49 @@ class LayoutProcessor:
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,})')
# 5. Паттерны для единиц измерения.
# Паттерн, описывающий "число" - арабское (включая дроби) ИЛИ римское.
# Для римских цифр используется \b, чтобы не спутать 'I' с частью слова.
self._NUMBER_PATTERN = r'(?:\d[\d.,]*|\b[IVXLCDM]+\b)'
# 5. Паттерны для единиц измерения (простые и составные).
self._post_units_pattern = None
self._pre_units_pattern = None
self._complex_unit_pattern = None
self._math_unit_pattern = None
if self.process_units:
post_units = list(DEFAULT_POST_UNITS)
pre_units = list(DEFAULT_PRE_UNITS)
# Проверяем и добавляем пользовательские единицы измерения
custom_units = []
# Обработка составных единиц: "склеиваем" их тонкой шпацией и добавляем в общий список
if self.process_complex_units:
complex_units_to_process = list(DEFAULT_COMPLEX_UNITS)
if isinstance(self.process_complex_units, (list, tuple, set)):
complex_units_to_process.extend(self.process_complex_units)
# "Склеиваем" пробелы внутри составных единиц и добавляем в общий список
post_units.extend([unit.replace(' ', CHAR_THIN_SP) for unit in complex_units_to_process])
all_post_units = list(DEFAULT_POST_UNITS)
if isinstance(self.process_units, str):
# Если кастомные единицы заданы строкой, разбиваем по пробелам
custom_units = self.process_units.split()
all_post_units.extend(self.process_units.split())
elif isinstance(self.process_units, (list, tuple, set)):
# Если кастомные единицы заданы списком/кортежем/множеством, просто конвертируем в список
custom_units = list(self.process_units)
all_post_units.extend(self.process_units)
if custom_units:
post_units.extend(custom_units)
units_pattern_part = ''
if post_units:
# [\d.,]+ - число, возможно, с точкой или запятой
# Используем негативный просмотр вперед (?!), чтобы убедиться, что за единицей
# не следует другая буква. Это надежнее, чем \b, особенно для единиц,
# оканчивающихся на точку (например, "г.").
post_pattern_str = r'(\d[\d.,]*)\s+(' + '|'.join(regex.escape(u) for u in post_units) + r')(?![\p{L}\p{N}])'
self._post_units_pattern = regex.compile(post_pattern_str)
# Общий паттерн для всех остальных единиц
if all_post_units:
sorted_units = sorted(all_post_units, key=len, reverse=True)
units_pattern_part = '|'.join(map(regex.escape, sorted_units))
if pre_units:
# Используем негативный просмотр назад (?<!), чтобы убедиться, что перед единицей
# нет буквы. \b здесь не работает для символов типа "№" или "$".
pre_pattern_str = r'(?<![\p{L}\p{N}])(' + '|'.join(regex.escape(u) for u in pre_units) + r')\s+(\d[\d.,]*)'
self._pre_units_pattern = regex.compile(pre_pattern_str)
if units_pattern_part:
# Простые единицы: число + единица
self._post_units_pattern = regex.compile(rf'({self._NUMBER_PATTERN})\s+({units_pattern_part})(?!\w)')
# Паттерн для составных единиц: ищет пару "единица." + "единица", разделенную пробелами (или без них).
# Обязательное наличие точки `\.` после первой единицы делает цикл обработки безопасным.
self._complex_unit_pattern = regex.compile(r'\b(' + units_pattern_part + r')\.(\s*)(' + units_pattern_part + r')(?!\w)')
# Паттерн для математических операций между единицами
math_ops_pattern = '|'.join(map(regex.escape, UNIT_MATH_OPERATORS))
self._math_unit_pattern = regex.compile(
r'\b(' + units_pattern_part + r')\s*(' + math_ops_pattern + r')\s*(' + units_pattern_part + r')(?!\w)')
# 6. Паттерн для связи единиц-умножителей (тыс., млн.) со следующей единицей.
# Ищет умножитель, за которым может быть точка, а затем пробел.
multiplier_units = ['тыс', 'млн', 'млрд']
self._unit_multiplier_pattern = regex.compile(r'((' + '|'.join(multiplier_units) + r')\.?)\s+')
# Паттерн для пред-позиционных единиц
self._pre_units_pattern = regex.compile(
r'(?<![\p{L}\p{N}])(' + '|'.join(map(regex.escape, DEFAULT_PRE_UNITS)) + rf')\s+({self._NUMBER_PATTERN})')
logger.debug(f"LayoutProcessor `__init__`. "
f"Langs: {self.langs}, "
f"Main lang: {self.main_lang}, "
f"Process initials and acronyms: {self.process_initials_and_acronyms}, "
f"Process units: {bool(self.process_units)}, "
f"Process complex units: {bool(self.process_complex_units)}")
f"Process units: {bool(self.process_units)}")
def _replace_dash_spacing(self, match: regex.Match) -> str:
"""Callback-функция для расстановки пробелов вокруг тире с учетом языка."""
@@ -152,15 +140,24 @@ class LayoutProcessor:
processed_text = self._surname_to_initial_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text)
# 5. Обработка единиц измерения (если включено).
if self.process_units and self._unit_multiplier_pattern:
processed_text = self._unit_multiplier_pattern.sub(r'\1' + CHAR_NBSP, processed_text)
# 6. Обработка единиц измерения (простых и составных).
if self.process_units:
if self._complex_unit_pattern:
# Шаг 1: "Склеиваем" все составные единицы с помощью временного разделителя.
# Цикл безопасен, так как мы заменяем пробелы на непробельный символ, и паттерн не найдет себя снова.
while self._complex_unit_pattern.search(processed_text):
processed_text = self._complex_unit_pattern.sub(
fr'\1.{CHAR_UNIT_SEPARATOR}\3', processed_text, count=1)
if self._math_unit_pattern:
# processed_text = self._math_unit_pattern.sub(r'\1/\2', processed_text)
processed_text = self._math_unit_pattern.sub(r'\1\2\3', processed_text)
# И только потом привязываем простые единицы к числам
if self._post_units_pattern:
processed_text = self._post_units_pattern.sub(f'\\1{CHAR_NBSP}\\2', processed_text)
if self._pre_units_pattern:
processed_text = self._pre_units_pattern.sub(f'\\1{CHAR_NBSP}\\2', processed_text)
# Шаг 2: Заменяем все временные разделители на правильную тонкую шпацию.
processed_text = processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP)
return processed_text

View File

@@ -47,7 +47,7 @@ _RU_OLD_POSTPOSITIVE_PARTICLES = frozenset([
])
_EN_UNBREAKABLE_WORDS = frozenset([
# 1-2 letter words
# 1-2 letter words (I - as pronoun)
'a', 'an', 'as', 'at', 'by', 'in', 'is', 'it', 'of', 'on', 'or', 'so', 'to', 'if',
# 3-4 letter words
'for', 'from', 'into', 'that', 'then', 'they', 'this', 'was', 'were', 'what', 'when', 'with',