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

@@ -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