add: разделены правила для языков + провеки на языки-алфавиты
This commit is contained in:
parent
4d9f4a798e
commit
f0b9784737
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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` для типографа
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
13
main.py
@ -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 = ("Каждое пальто, которое мы создаём — это не просто одежда. Это"
|
txt = ("Каждое пальто, которое мы создаём — это не просто одежда. Это"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user