add: разделены правила для языков + провеки на языки-алфавиты

This commit is contained in:
Sergei Erjemin 2025-05-12 18:36:05 +03:00
parent 4d9f4a798e
commit f0b9784737
5 changed files with 133 additions and 70 deletions

View File

@ -4,7 +4,7 @@ import os
import regex import regex
def parce_and_validate_mode( def parse_and_validate_mode(
mode_input: str | None = None, mode_input: str | None = None,
) -> str: ) -> str:
""" """

View File

@ -9,9 +9,11 @@ MODE_MIXED = "mixed"
DEFAULT_MODE = MODE_MIXED DEFAULT_MODE = MODE_MIXED
# Языки, поддерживаемые библиотекой # Языки, поддерживаемые библиотекой
SUPPORTED_LANGS = frozenset(['ru', 'en']) LANG_RU = 'ru' # Русский
LANG_EN = 'en' # Английский
SUPPORTED_LANGS = frozenset([LANG_RU, LANG_EN])
# Язык(и) по умолчанию, если не указаны пользователем и не заданы через ETPGRF_DEFAULT_LANGS_MODULE # Язык(и) по умолчанию, если не указаны пользователем и не заданы через ETPGRF_DEFAULT_LANGS_MODULE
DEFAULT_LANGS = 'ru' DEFAULT_LANGS = LANG_RU
# #
# ----------------- соответствия `unicode` и `mnemonic` для типографа # ----------------- соответствия `unicode` и `mnemonic` для типографа

View File

@ -1,13 +1,13 @@
import regex import regex
from etpgrf.config import DEFAULT_MODE, DEFAULT_LANGS, SHY_ENTITIES, MODE_UNICODE from etpgrf.config import LANG_RU, LANG_EN, DEFAULT_MODE, DEFAULT_LANGS, SHY_ENTITIES, MODE_UNICODE
from etpgrf.comutil import parce_and_validate_mode, parse_and_validate_langs from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я']) _RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ']) _RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ'])
_RU_J_SOUND_UPPER = frozenset(['Й']) _RU_J_SOUND_UPPER = frozenset(['Й'])
_RU_SIGNS_UPPER = frozenset(['Ь', 'Ъ']) _RU_SIGNS_UPPER = frozenset(['Ь', 'Ъ'])
_EN_VOWELS_UPPER = frozenset(['A', 'E', 'I', 'O', 'U']) _EN_VOWELS_UPPER = frozenset(['A', 'E', 'I', 'O', 'U', 'Æ', 'Œ'])
_EN_CONSONANTS_UPPER = frozenset(['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z']) _EN_CONSONANTS_UPPER = frozenset(['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])
@ -15,12 +15,12 @@ class Hyphenator:
"""Правила расстановки переносов для разных языков. """Правила расстановки переносов для разных языков.
""" """
def __init__(self, def __init__(self,
langs: frozenset[str] = None, # Языки, которые обрабатываем в переносе слов langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
mode: str = None, # Режим обработки текста mode: str = None, # Режим обработки текста
max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы
min_chars_per_part: int = 3): # Минимальная длина после переноса (хвост, который разрешено переносить) min_chars_per_part: int = 3): # Минимальная длина после переноса (хвост, который разрешено переносить)
self.langs: frozenset[str] = parse_and_validate_langs(langs) self.langs: frozenset[str] = parse_and_validate_langs(langs)
self.mode: str = parce_and_validate_mode(mode) self.mode: str = parse_and_validate_mode(mode)
self.max_unhyphenated_len = max_unhyphenated_len self.max_unhyphenated_len = max_unhyphenated_len
self.min_chars_per_part = min_chars_per_part self.min_chars_per_part = min_chars_per_part
@ -29,6 +29,8 @@ class Hyphenator:
self._consonants: frozenset = frozenset() self._consonants: frozenset = frozenset()
self._j_sound_upper: frozenset = frozenset() self._j_sound_upper: frozenset = frozenset()
self._signs_upper: frozenset = frozenset() self._signs_upper: frozenset = frozenset()
self._ru_alphabet_upper: frozenset = frozenset()
self._en_alphabet_upper: frozenset = frozenset()
# Загружает наборы символов на основе self.langs # Загружает наборы символов на основе self.langs
self._load_language_resources_for_hyphenation() self._load_language_resources_for_hyphenation()
# Определяем символ переноса в зависимости от режима # Определяем символ переноса в зависимости от режима
@ -37,14 +39,16 @@ class Hyphenator:
def _load_language_resources_for_hyphenation(self): def _load_language_resources_for_hyphenation(self):
# Определяем наборы гласных, согласных и т.д. в зависимости языков. # Определяем наборы гласных, согласных и т.д. в зависимости языков.
if "ru" in self.langs: if LANG_RU in self.langs:
self._vowels |= _RU_VOWELS_UPPER self._vowels |= _RU_VOWELS_UPPER
self._consonants |= _RU_CONSONANTS_UPPER self._consonants |= _RU_CONSONANTS_UPPER
self._j_sound_upper |= _RU_J_SOUND_UPPER self._j_sound_upper |= _RU_J_SOUND_UPPER
self._signs_upper |= _RU_SIGNS_UPPER self._signs_upper |= _RU_SIGNS_UPPER
if "en" in self.langs: self._ru_alphabet_upper |= _RU_VOWELS_UPPER | _RU_CONSONANTS_UPPER | _RU_SIGNS_UPPER | _RU_J_SOUND_UPPER
if LANG_EN in self.langs:
self._vowels |= _EN_VOWELS_UPPER self._vowels |= _EN_VOWELS_UPPER
self._consonants |= _EN_CONSONANTS_UPPER self._consonants |= _EN_CONSONANTS_UPPER
self._en_alphabet_upper |= _EN_VOWELS_UPPER | _EN_CONSONANTS_UPPER
# ... и для других языков, если они поддерживаются переносами # ... и для других языков, если они поддерживаются переносами
# --- Сюда переносятся все методы, связанные с переносами --- # --- Сюда переносятся все методы, связанные с переносами ---
@ -79,12 +83,25 @@ class Hyphenator:
:param word: Слово, в котором надо расставить переносы :param word: Слово, в котором надо расставить переносы
:return: Слово с расставленными переносами :return: Слово с расставленными переносами
""" """
# 1. ОБЩИЕ ПРОВЕРКИ
# TODO: возможно, для скорости, надо сделать проверку на пробелы и другие разделители, которых не должно быть
if not word:
# Добавим явную проверку на пустую строку
return ""
if len(word) <= self.max_unhyphenated_len or not any(self._is_vow(c) for c in word):
# Если слово короткое или не содержит гласных, перенос не нужен
return word
# 2. ОБНАРУЖЕНИЕ ЯЗЫКА И ПОДКЛЮЧЕНИЕ ЯЗЫКОВОЙ ЛОГИКИ
# Поиск вхождения букв строки (слова) через `frozenset` -- O(1). Это быстрее регулярного выражения -- O(n)
# 2.1. Проверяем RU
if LANG_RU in self.langs and frozenset(word.upper()) <= self._ru_alphabet_upper:
# Пользователь подключил русскую логику, и слово содержит только русские буквы
# Поиск допустимой позиции для переноса около заданного индекса # Поиск допустимой позиции для переноса около заданного индекса
def find_hyphen_point(word_segment: str, start_idx: int) -> int: def find_hyphen_point_ru(word_segment: str, start_idx: int) -> int:
vow_indices = [i for i, char_w in enumerate(word_segment) if self._is_vow(char_w)] vow_indices = [i for i, char_w in enumerate(word_segment) if self._is_vow(char_w)]
if not vow_indices:
# Если в слове нет гласных, то перенос невозможен # Если в слове нет гласных, то перенос невозможен
if not vow_indices:
return -1 return -1
# Ищем ближайшую гласную до или после start_idx # Ищем ближайшую гласную до или после start_idx
for i in vow_indices: for i in vow_indices:
@ -98,48 +115,87 @@ class Hyphenator:
if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part: if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
# Не отделяем 3 символ с начала или конца (это некрасиво) # Не отделяем 3 символ с начала или конца (это некрасиво)
continue continue
if self._is_sign(word_segment[ind]) or self._is_sign(word_segment[-1]): if self._is_sign(word_segment[ind]) or (ind > 0 and self._is_sign(word_segment[ind-1])):
# Пропускаем мягкий/твердый знак. Согласно правилам русской типографики (например, ГОСТ 7.62-2008 # Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается на них (ГОСТ 7.62-2008)
# или рекомендации по набору текста), перенос не должен разрывать слово так, чтобы мягкий или
# твердый знак оказывался в начале или конце строки
continue continue
return ind return ind
return -1 # Не нашли подходящую позицию return -1 # Не нашли подходящую позицию
# Рекурсивное деление слова # Рекурсивное деление слова
def split_word(word_to_split: str) -> str: def split_word_ru(word_to_split: str) -> str:
if len(word_to_split) <= self.max_unhyphenated_len: # Если длина укладывается в лимит, перенос не нужен # Если длина укладывается в лимит, перенос не нужен
if len(word_to_split) <= self.max_unhyphenated_len:
return word_to_split return word_to_split
# Ищем точку переноса около середины
hyphen_idx = find_hyphen_point(word_to_split, len(word_to_split) // 2) # Ищем точку переноса около середины hyphen_idx = find_hyphen_point_ru(word_to_split, len(word_to_split) // 2)
# Если не нашли точку переноса
if hyphen_idx == -1: # Если не нашли точку переноса if hyphen_idx == -1:
return word_to_split return word_to_split
# Разделяем слово на две части (до и после точки переноса)
left_part = word_to_split[:hyphen_idx] left_part = word_to_split[:hyphen_idx]
right_part = word_to_split[hyphen_idx:] right_part = word_to_split[hyphen_idx:]
# Рекурсивно делим левую и правую части и соединяем их через символ переноса # Рекурсивно делим левую и правую части и соединяем их через символ переноса
return split_word(left_part) + self._split_code + split_word(right_part) return split_word_ru(left_part) + self._split_code + split_word_ru(right_part)
# Основная логика # Основная логика
if len(word) <= self.max_unhyphenated_len or not any(self._is_vow(c) for c in word): return split_word_ru(word) # Рекурсивно делим слово на части с переносами
# Короткое слово или без гласных "делению не подлежит", выходим из рекурсии
# 2.2. Проверяем EN
elif LANG_EN in self.langs and frozenset(word.upper()) <= self._en_alphabet_upper:
# Пользователь подключил английскую логику, и слово содержит только английские буквы
print(f"#### Applying English rules to: {word}") # Для отладки
# --- Начало логики для английского языка (заглушка) ---
# ПРИМЕЧАНИЕ: Это очень упрощенная заглушка.
def find_hyphen_point_en(word_segment: str) -> int:
for i in range(self.min_chars_per_part, len(word_segment) - self.min_chars_per_part):
if self._is_vow(word_segment[i - 1]) and self._is_cons(word_segment[i]):
if len(word_segment[:i]) >= self.min_chars_per_part and \
len(word_segment[i:]) >= self.min_chars_per_part:
return i
return -1
def split_word_en(word_to_split: str) -> str:
if len(word_to_split) <= self.max_unhyphenated_len:
return word_to_split
hyphen_idx = find_hyphen_point_en(word_to_split)
if hyphen_idx != -1:
return word_to_split[:hyphen_idx] + self._split_code + word_to_split[hyphen_idx:]
return word_to_split
# --- Конец логики для английского языка ---
return split_word_en(word)
else:
# кстати "слова" в которых есть пробелы или другие разделители, тоже попадают сюда
print("!!!!ФИГНЯ")
return word return word
return split_word(word) # Рекурсивно делим слово на части с переносами
def hyp_in_text(self, text: str) -> str: def hyp_in_text(self, text: str) -> str:
""" Расстановка переносов в тексте """ Расстановка переносов в тексте
:param text: Строка, которую надо обработать (главный аргумент). :param text: Строка, которую надо обработать (главный аргумент).
:return: str: :return: str: Строка с расставленными переносами.
""" """
rus_worlds = regex.findall(r'\b[а-яА-Я]+\b', text) # ищем все русскоязычные слова в тексте
for word in rus_worlds: # 1. Определяем функцию, которая будет вызываться для каждого найденного слова
if len(word) > self.max_unhyphenated_len: def replace_word_with_hyphenated(match_obj):
hyphenated_word = self.hyp_in_word(word) # Модуль regex автоматически передает сюда match_obj для каждого совпадения.
print(f'{word} -> {hyphenated_word}') # Чтобы получить `слово` из 'совпадения' делаем .group() или .group(0).
text = text.replace(word, hyphenated_word) word_to_process = match_obj.group(0)
return text # И оправляем это слово на расстановку переносов (внутри hyp_in_word уже есть все проверки).
hyphenated_word = self.hyp_in_word(word_to_process)
# ============= Для отладки (слова в которых появились переносы) ==================
if word_to_process != hyphenated_word:
print(f"hyp_in_text: '{word_to_process}' -> '{hyphenated_word}'")
return hyphenated_word
# 2. regex.sub() -- поиск с заменой. Ищем по паттерну `r'\b\p{L}+\b'` (`\b` - граница слова;
# `\p{L}` - любая буква Unicode; `+` - одно или более вхождений).
# Второй аргумент - это наша функция replace_word_with_hyphenated.
# regex.sub вызовет ее для каждого найденного слова, передав match_obj.
processed_text = regex.sub(pattern=r'\b\p{L}+\b', repl=replace_word_with_hyphenated, string=text)
return processed_text

View File

@ -1,4 +1,4 @@
from etpgrf.comutil import parce_and_validate_mode, parse_and_validate_langs from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
from etpgrf.hyphenation import Hyphenator from etpgrf.hyphenation import Hyphenator
import copy import copy
@ -17,7 +17,7 @@ class Typographer:
self.langs: frozenset[str] = parse_and_validate_langs(langs) self.langs: frozenset[str] = parse_and_validate_langs(langs)
# --- Обработка и валидация параметра mode --- # --- Обработка и валидация параметра mode ---
self.mode: str = parce_and_validate_mode(mode) self.mode: str = parse_and_validate_mode(mode)
# Сохраняем переданные модули правил # Сохраняем переданные модули правил
if hyphenation_rule is not None: if hyphenation_rule is not None:

13
main.py
View File

@ -3,20 +3,25 @@ import etpgrf
if __name__ == '__main__': if __name__ == '__main__':
# --- Пример использования --- # --- Пример использования ---
ETPGRF_DEFAULT_LANGS = "ru"
print("\n--- Пример использования класса---\n") print("\n--- Пример использования класса---\n")
# Определяем пользовательские правила переносов # Определяем пользовательские правила переносов
hyphen_settings = etpgrf.Hyphenator(langs=frozenset(['ru']), max_unhyphenated_len=8) hyphen_settings = etpgrf.Hyphenator(langs='ru', max_unhyphenated_len=8)
# Определяем пользовательские правила типографа # Определяем пользовательские правила типографа
typo = etpgrf.Typographer(langs='ru', mode='mnemonic', hyphenation_rule=hyphen_settings)
result = hyphen_settings.hyp_in_text("Бармалейщина") result = hyphen_settings.hyp_in_text("Бармалейщина")
print(result, "\n\n") print(result, "\n\n")
result = hyphen_settings.hyp_in_word("Длинношеевый жираф") result = hyphen_settings.hyp_in_word("Длинношеевый жираф")
print(result, "\n\n") print(result, "\n\n")
hyphen_settings2 = etpgrf.Hyphenator(langs='en', max_unhyphenated_len=8)
result = hyphen_settings2.hyp_in_text("frozenseter")
print(result, "\n\n")
typo = etpgrf.Typographer(langs='ru', mode='mnemonic', hyphenation_rule=hyphen_settings)
result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!") result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!")
print(result, "\n\n") print(result, "\n\n")
result = typo.process(text="Привет, World! Это <i>тестовый текст для проверки расстановки</i> переносов в словах. Миллион 100-метровошеих жирножирафов.") result = typo.process(text="Привет, frozenseter! Это <i>тестовый текст для проверки расстановки</i> переносов"
" в словах. Миллион 100-метровошеих жирножирафов.")
print(result, "\n\n") print(result, "\n\n")
txt = ("Каждое пальто, которое мы создаём&nbsp;— это не&nbsp;просто одежда. Это" txt = ("Каждое пальто, которое мы создаём&nbsp;— это не&nbsp;просто одежда. Это"