fix: нормализация КАПСа по словам (город + должности)

- раньше: если в тексте есть аббревиатура (ООО, И.О.) или цифры
  (индекс в адресе) — весь текст оставался КАПСОМ
- теперь: каждое слово нормализуется отдельно, аббревиатуры
  (АРМ, СКУД, ООО, ИНН и т.п.) и инициалы сохраняются
- город: '350000, Г. КРАСНОДАР...' → 'Г. Краснодар' → город
  снова определяется при импорте
- должность: 'ГЛАВА' → 'Глава', ФИО: 'ИВАНОВ ИВАН ИВАНОВИЧ' → 'Иванов Иван Иванович'
This commit is contained in:
2026-08-03 12:45:10 +04:00
parent 4fca29e7d6
commit 3a4cd2fde4
+60 -13
View File
@@ -15,6 +15,62 @@ except ImportError:
from .import_settings import load_settings, parse_cell_ref, cell_value
# Известные аббревиатуры, которые сохраняются КАПСОМ при нормализации
_CAPS_ABBR = {
'АРМ', 'СКУД', 'СЗИ', 'СКЗИ', 'СОВ', 'ЛВС', 'ИНН', 'КПП', 'ОГРН', 'ЦОД',
'МБУ', 'МКУ', 'ООО', 'СНИЛС', 'СБИС', 'ЕМСЭД', 'ФИО', 'ИСПДН', 'ККТ',
}
def _normalize_caps(text: str) -> str:
"""Приводит КАПС-текст к нормальному регистру по словам.
Сохраняет:
- аббревиатуры из белого списка (АРМ, СКУД, ООО, ИНН и т.п.)
- инициалы и короткие служебные слова (И., Г., УЛ., №1)
- числа и номера (350000, №1)
- слова, уже содержащие строчные буквы
Длинные слова целиком заглавными (ДИРЕКТОР, КРАСНОДАР, ПЕТРОВ) → «Директор», «Краснодар».
"""
words = text.split()
result = []
for w in words:
# отделяем пунктуацию по краям слова
pre = ''
post = ''
core = w
while core and not core[0].isalnum():
pre += core[0]
core = core[1:]
while core and not core[-1].isalnum():
post = core[-1] + post
core = core[:-1]
if not core:
result.append(w)
continue
# в слове уже есть строчные буквы — не трогаем
if any('а' <= c <= 'я' or c == 'ё' for c in core):
result.append(w)
continue
# нет заглавных кириллических букв (числа, латиница и т.п.) — не трогаем
if not any('А' <= c <= 'Я' or c == 'Ё' for c in core):
result.append(w)
continue
letters = [c for c in core if c.isalpha()]
# инициалы и служебные слова (1-2 буквы: И., Г., УЛ., ООО — но ООО в белом списке)
if len(letters) <= 2:
result.append(w)
continue
# известная аббревиатура — сохраняем как есть
if core.upper() in _CAPS_ABBR:
result.append(w)
continue
# обычное длинное слово: первая заглавная, остальные строчные
normalized = core[0].upper() + core[1:].lower()
result.append(pre + normalized + post)
return ' '.join(result)
def clean_value(val):
"""Очистка и нормализация значения ячейки Excel."""
if val is None:
@@ -33,20 +89,11 @@ def clean_value(val):
text = str(val).strip()
text = text.replace('\n', ' ').replace('\r', ' ')
text = ' '.join(text.split())
# Приведение регистра: если текст содержит только заглавные буквы РУ
# НО не трогаем аббревиатуры/номера (АРМ №1, ИС, ЕМСЭД, СКУД и т.п.)
if len(text) > 4 and any('А' <= c <= 'Я' or c == 'Ё' for c in text):
# Если весь текст КАПСОМ (нет строчных букв) — нормализуем по словам
if len(text) > 2 and any('А' <= c <= 'Я' or c == 'Ё' for c in text):
has_lower = any('а' <= c <= 'я' or c == 'ё' for c in text)
has_digit_or_num = bool(re.search(r'[0-9№«»"\']', text))
if not has_lower and not has_digit_or_num:
words = text.split()
# если есть короткая аббревиатура целиком заглавными — не трогаем
has_abbr = any(len(w) <= 4 and w.isupper() and any('А' <= c <= 'Я' for c in w) for w in words)
if not has_abbr:
if len(words) >= 3:
text = text.title()
else:
text = text.capitalize()
if not has_lower:
text = _normalize_caps(text)
return text