🎉 Первый коммит — DokoGen v1.0
Чистый генератор документов 152-ФЗ. Только нужные переменные, без мусора от 187-ФЗ/117-ФСТЭК. Модели, склонение, генератор DOCX, tkinter GUI, импорт из Excel.
This commit is contained in:
+222
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""DokoGen — Склонение (pymorphy3 + правила для ФИО/организаций)"""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Dict
|
||||
import re
|
||||
|
||||
try:
|
||||
import pymorphy3
|
||||
morph = pymorphy3.MorphAnalyzer()
|
||||
MORPH_AVAILABLE = True
|
||||
except ImportError:
|
||||
morph = None
|
||||
MORPH_AVAILABLE = False
|
||||
|
||||
CASES = ["nomn", "gent", "datv", "accs", "ablt", "loct"]
|
||||
CASE_MAP = {
|
||||
"nominative": "nomn", "им": "nomn",
|
||||
"genitive": "gent", "род": "gent",
|
||||
"dative": "datv", "дат": "datv",
|
||||
"accusative": "accs", "вин": "accs",
|
||||
"instrumental": "ablt", "твор": "ablt",
|
||||
"prepositional": "loct", "пр": "loct",
|
||||
}
|
||||
CASE_NAMES = {
|
||||
"nomn": "Именительный (Кто? Что?)",
|
||||
"gent": "Родительный (Кого? Чего?)",
|
||||
"datv": "Дательный (Кому? Чему?)",
|
||||
"accs": "Винительный (Кого? Что?)",
|
||||
"ablt": "Творительный (Кем? Чем?)",
|
||||
"loct": "Предложный (О ком? О чём?)",
|
||||
}
|
||||
CASE_SHORT = {
|
||||
"nomn": "им.п.", "gent": "род.п.", "datv": "дат.п.",
|
||||
"accs": "вин.п.", "ablt": "твор.п.", "loct": "пр.п.",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_case(case: str) -> str:
|
||||
return CASE_MAP.get(case, "nomn" if case not in CASES else case)
|
||||
|
||||
|
||||
def inflect_name(name: str) -> Dict[str, str]:
|
||||
"""Склонение названия организации по всем падежам."""
|
||||
return {gr: company_name_decline(name, gr) for gr in CASES}
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def company_name_decline(name: str, case: str) -> str:
|
||||
"""Склонение названия организации."""
|
||||
if not name:
|
||||
return ""
|
||||
case_gr = _normalize_case(case)
|
||||
if case_gr == "nomn":
|
||||
return name
|
||||
|
||||
m = re.match(r'^(.+?)\s*([\u00ab\"].*[\u00bb\"])$', name.strip())
|
||||
if m:
|
||||
prefix = m.group(1).strip()
|
||||
quoted = m.group(2)
|
||||
if prefix:
|
||||
return decline(prefix, case_gr, decline_all=True) + " " + quoted
|
||||
return name
|
||||
return decline(name, case_gr, decline_all=True)
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def decline(text: str, case: str, decline_all: bool = False) -> str:
|
||||
"""Склонение строки (ФИО, фразы) по падежам."""
|
||||
if not text:
|
||||
return ""
|
||||
case_gr = _normalize_case(case)
|
||||
if case_gr == "nomn":
|
||||
return text
|
||||
|
||||
clean_text, quoted = _extract_quoted(text)
|
||||
tokens = re.split(r"(\s+)", clean_text)
|
||||
content_tokens = [t for t in tokens if not t.isspace() and t.strip()]
|
||||
|
||||
only_first = False
|
||||
if not decline_all and len(content_tokens) >= 2:
|
||||
for ct in content_tokens[1:]:
|
||||
if ct[0].islower():
|
||||
only_first = True
|
||||
break
|
||||
|
||||
result_parts = []
|
||||
content_idx = 0
|
||||
for token in tokens:
|
||||
if token.isspace() or not token.strip():
|
||||
result_parts.append(token)
|
||||
continue
|
||||
stripped = token.strip()
|
||||
if only_first and content_idx > 0:
|
||||
result_parts.append(token)
|
||||
content_idx += 1
|
||||
continue
|
||||
if _is_quoted(stripped):
|
||||
result_parts.append(token)
|
||||
content_idx += 1
|
||||
continue
|
||||
if stripped.isupper() and len(stripped) > 1:
|
||||
result_parts.append(token)
|
||||
content_idx += 1
|
||||
continue
|
||||
inflected = _inflect_word(stripped, case_gr)
|
||||
if stripped[0].isupper():
|
||||
inflected = inflected[0].upper() + inflected[1:]
|
||||
result_parts.append(inflected)
|
||||
content_idx += 1
|
||||
|
||||
return _restore_quoted("".join(result_parts), quoted)
|
||||
|
||||
|
||||
def _inflect_word(word: str, case_gr: str) -> str:
|
||||
"""Склонение одного слова через pymorphy3 с fallback на ручные правила."""
|
||||
if not word or len(word) < 2 or case_gr == "nomn":
|
||||
return word
|
||||
if not MORPH_AVAILABLE or morph is None:
|
||||
return _rules_decline(word, case_gr) or word
|
||||
try:
|
||||
parsed = morph.parse(word)
|
||||
if not parsed:
|
||||
return _rules_decline(word, case_gr) or word
|
||||
best = parsed[0]
|
||||
inflected = best.inflect({case_gr})
|
||||
if inflected:
|
||||
return inflected.word
|
||||
return _rules_decline(word, case_gr) or word
|
||||
except Exception:
|
||||
return _rules_decline(word, case_gr) or word
|
||||
|
||||
|
||||
def _rules_decline(word: str, case_gr: str) -> str:
|
||||
"""Ручное склонение фамилий."""
|
||||
w = word.lower()
|
||||
# Мужские фамилии на -ов/-ев/-ёв
|
||||
if w.endswith(('ов', 'ев', 'ёв')) and not w.endswith(('ова', 'ева', 'ёва')):
|
||||
return {
|
||||
'gent': word + 'а', 'datv': word + 'у', 'accs': word + 'а',
|
||||
'ablt': word + 'ым', 'loct': word + 'е'
|
||||
}.get(case_gr, word)
|
||||
# Мужские фамилии на -ин
|
||||
if w.endswith('ин') and not w.endswith('ина'):
|
||||
return {
|
||||
'gent': word + 'а', 'datv': word + 'у', 'accs': word + 'а',
|
||||
'ablt': word + 'ым', 'loct': word + 'е'
|
||||
}.get(case_gr, word)
|
||||
# Женские фамилии на -ова/-ева/-ёва
|
||||
if w.endswith(('ова', 'ева', 'ёва')):
|
||||
stem = word[:-1]
|
||||
return {
|
||||
'gent': stem + 'ой', 'datv': stem + 'ой', 'accs': stem + 'у',
|
||||
'ablt': stem + 'ой', 'loct': stem + 'ой'
|
||||
}.get(case_gr, word)
|
||||
# Женские фамилии на -ина
|
||||
if w.endswith('ина'):
|
||||
stem = word[:-1]
|
||||
return {
|
||||
'gent': stem + 'ой', 'datv': stem + 'ой', 'accs': stem + 'у',
|
||||
'ablt': stem + 'ой', 'loct': stem + 'ой'
|
||||
}.get(case_gr, word)
|
||||
return word
|
||||
|
||||
|
||||
def _extract_quoted(text: str):
|
||||
"""Извлекает подстроки в кавычках, заменяет плейсхолдерами."""
|
||||
if not text:
|
||||
return text, {}
|
||||
pattern = re.compile(r'(\u00ab[^\u00bb]*\u00bb|"[^"]*")')
|
||||
placeholders = {}
|
||||
counter = [0]
|
||||
|
||||
def _replace(m):
|
||||
ph = f'__Q_{counter[0]}__'
|
||||
placeholders[ph] = m.group(0)
|
||||
counter[0] += 1
|
||||
return ph
|
||||
|
||||
return pattern.sub(_replace, text), placeholders
|
||||
|
||||
|
||||
def _restore_quoted(text: str, placeholders: dict) -> str:
|
||||
for ph, original in placeholders.items():
|
||||
text = text.replace(ph, original)
|
||||
return text
|
||||
|
||||
|
||||
def _is_quoted(word: str) -> bool:
|
||||
w = word.strip()
|
||||
if not w:
|
||||
return False
|
||||
return (w.startswith("\u00ab") and w.endswith("\u00bb")) or \
|
||||
(w.startswith('"') and w.endswith('"'))
|
||||
|
||||
|
||||
# ==================== ФОРМАТИРОВАНИЕ ФИО ====================
|
||||
|
||||
def get_short_fio(fio: str) -> str:
|
||||
"""Фамилия И.О."""
|
||||
if not fio:
|
||||
return ""
|
||||
parts = fio.split()
|
||||
if len(parts) >= 2:
|
||||
initials = parts[1][0] + "."
|
||||
if len(parts) >= 3:
|
||||
initials += parts[2][0] + "."
|
||||
return parts[0] + " " + initials
|
||||
return fio
|
||||
|
||||
|
||||
def get_initials(fio: str) -> str:
|
||||
"""И.О. Фамилия"""
|
||||
if not fio:
|
||||
return ""
|
||||
parts = fio.split()
|
||||
if len(parts) >= 2:
|
||||
initials = parts[1][0] + "."
|
||||
if len(parts) >= 3:
|
||||
initials += parts[2][0] + "."
|
||||
return initials + " " + parts[0]
|
||||
return fio
|
||||
Reference in New Issue
Block a user