480 lines
22 KiB
Python
480 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
scripts/sanitize_battistini_corpus.py
|
||
|
||
Complete re-parsing and sanitization of M. Battistini's «Символы и аллегории» (2008),
|
||
and deep sanitization of all databases (passports, staging, external symbols, illustration catalog).
|
||
Fixes:
|
||
- OCR artifacts (drop caps, Latin homoglyphs, column/caption bleeding, mangled names).
|
||
- Broken hyphenation (soft wraps) across the entire corpus.
|
||
- Purges fake/fragmented symbols and installs 71 canonical symbols + 12 calendar months.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import glob
|
||
from pathlib import Path
|
||
import fitz
|
||
|
||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||
DATA_DIR = BASE_DIR / "assets" / "data"
|
||
PDF_PATH = Path("/home/alexives/Desktop/newsources/Battistini - SIMVOLY I ALLEGORII (Enziklopedija iskusstva) - 2008, pdf.pdf")
|
||
|
||
# 71 Canonical symbols in Battistini's index (PDF page 314)
|
||
# (Name, Start PDF Page, End PDF Page)
|
||
CANONICAL_SYMBOLS = [
|
||
("Уроборус", 7, 9),
|
||
("Океан", 10, 11),
|
||
("Случай", 12, 12),
|
||
("Эон", 13, 13),
|
||
("Время", 14, 18),
|
||
("Христос-Хронократор", 19, 23),
|
||
("Зодиак", 24, 25),
|
||
("Времена года", 26, 27),
|
||
("Весна", 28, 29),
|
||
("Лето", 30, 32),
|
||
("Осень", 33, 35),
|
||
("Зима", 36, 39),
|
||
("Месяцы", 40, 50),
|
||
("Аврора", 51, 53),
|
||
("Полдень", 54, 55),
|
||
("Сумерки", 56, 57),
|
||
("Ночь", 58, 62),
|
||
("Оры", 63, 65),
|
||
("Жизнь", 66, 69),
|
||
("Смерть", 70, 76),
|
||
("Возрасты человечества", 77, 79),
|
||
("Возрасты человека", 80, 83),
|
||
("Андрогин", 84, 85),
|
||
("Микрокосм", 86, 91),
|
||
("Христос", 92, 97),
|
||
("Дева Мария", 98, 101),
|
||
("Троица", 102, 104),
|
||
("Ева", 105, 107),
|
||
("Мать", 108, 112),
|
||
("Яйцо", 113, 117),
|
||
("Зеркало", 118, 123),
|
||
("Крест", 124, 126),
|
||
("Ореол", 127, 128),
|
||
("Ангел", 129, 134),
|
||
("Сатана", 135, 138),
|
||
("Чудовище", 139, 143),
|
||
("Полицефал", 144, 145),
|
||
("Тетраморф", 146, 147),
|
||
("Зооморф", 148, 152),
|
||
("Хаос", 153, 155),
|
||
("Космос", 156, 160),
|
||
("Небо", 161, 165),
|
||
("Солнце", 166, 167),
|
||
("Луна", 168, 169),
|
||
("Земля", 170, 174),
|
||
("Стихии", 175, 180),
|
||
("Потусторонний мир", 181, 183),
|
||
("Рай", 184, 185),
|
||
("Чистилище", 186, 188),
|
||
("Лимб", 189, 190),
|
||
("Ад", 191, 192),
|
||
("Странствия", 193, 199),
|
||
("Сон", 200, 205),
|
||
("Лестница", 206, 208),
|
||
("Гора", 209, 210),
|
||
("Лес", 211, 213),
|
||
("Дерево", 214, 217),
|
||
("Сад", 218, 222),
|
||
("Источник", 223, 225),
|
||
("Лабиринт", 226, 228),
|
||
("Город", 229, 233),
|
||
("Башня", 234, 237),
|
||
("Пороки", 238, 251),
|
||
("Добродетели", 252, 264),
|
||
("Фортуна", 265, 270),
|
||
("Пять чувств", 271, 276),
|
||
("Темпераменты", 277, 286),
|
||
("Любовь", 287, 298),
|
||
("Искусства", 299, 303),
|
||
("Науки", 304, 308),
|
||
("Суета сует (Vanitas)", 309, 314),
|
||
]
|
||
|
||
# 12 Individual months
|
||
MONTHS_SPEC = [
|
||
("Январь", 41, 41),
|
||
("Февраль", 42, 42),
|
||
("Март", 43, 43),
|
||
("Апрель", 44, 44),
|
||
("Май", 45, 45),
|
||
("Июнь", 46, 46),
|
||
("Июль", 47, 47),
|
||
("Август", 47, 47),
|
||
("Сентябрь", 48, 48),
|
||
("Октябрь", 48, 48),
|
||
("Ноябрь", 49, 49),
|
||
("Декабрь", 50, 50),
|
||
]
|
||
|
||
def clean_russian_ocr(text: str) -> str:
|
||
"""Comprehensive text sanitizer for OCR and hyphenation artifacts."""
|
||
if not text:
|
||
return ""
|
||
|
||
# Unhyphenate words broken across line wraps: 'сло- \n во' -> 'слово'
|
||
text = re.sub(r'([а-яёА-ЯЁa-zA-Z]+)-\s*\n\s*([а-яёА-ЯЁa-zA-Z]+)', r'\1\2', text)
|
||
# Unhyphenate words broken within line: 'сло- во' -> 'слово'
|
||
text = re.sub(r'([а-яёА-ЯЁa-zA-Z]+)-\s+([а-яёА-ЯЁa-zA-Z]+)', r'\1\2', text)
|
||
|
||
# Specific OCR homoglyphs and corruptions
|
||
text = re.sub(r'\b11\s*редставляют', 'Представляют', text)
|
||
text = re.sub(r'\b11редставляют', 'Представляют', text)
|
||
text = re.sub(r'\bcivUate\b', 'civitate', text)
|
||
text = re.sub(r'\bvirtite\b', 'virtute', text)
|
||
text = re.sub(r'\bdei-v\b', 'девы', text, flags=re.I)
|
||
text = re.sub(r'\bдеывы\b', 'девы', text, flags=re.I)
|
||
text = re.sub(r'\bдеыва\b', 'дева', text, flags=re.I)
|
||
text = re.sub(r'\bдеыву\b', 'деву', text, flags=re.I)
|
||
text = re.sub(r'\bобродетели\b', 'добродетели', text, flags=re.I)
|
||
text = re.sub(r'\bобродетель\b', 'добродетель', text, flags=re.I)
|
||
text = re.sub(r'\bаимб\b', 'лимб', text, flags=re.I)
|
||
text = re.sub(r'\bАимб\b', 'Лимб', text)
|
||
text = re.sub(r'\bТор од\b', 'Город', text)
|
||
text = re.sub(r'\bгор\s+од\b', 'город', text, flags=re.I)
|
||
text = re.sub(r'\bГор\s+од\b', 'Город', text)
|
||
text = re.sub(r'\bекбрь\b', 'декабрь', text, flags=re.I)
|
||
text = re.sub(r'Месяцы\s*•?\s*екбрь', 'Месяцы • Декабрь', text, flags=re.I)
|
||
text = re.sub(r'Месяцы\s*•?\s*нь', 'Месяцы • Июнь', text, flags=re.I)
|
||
text = re.sub(r'христо\s*с-?\s*хронократор', 'Христос-Хронократор', text, flags=re.I)
|
||
text = re.sub(r'христохронократор', 'Христос-Хронократор', text, flags=re.I)
|
||
text = re.sub(r'ХристосХронократор', 'Христос-Хронократор', text)
|
||
text = re.sub(r'христос-хронократор', 'Христос-Хронократор', text, flags=re.I)
|
||
text = re.sub(r'\bcon\b', 'сон', text, flags=re.I)
|
||
text = re.sub(r'\bCon\b', 'Сон', text)
|
||
text = re.sub(r'пьер\s*д[\'’`]?аль?и\s*образ(?:\s*мира)?', "Пьер д'Айи («Образ мира»)", text, flags=re.I)
|
||
text = re.sub(r'пьердали', "Пьер д'Айи", text, flags=re.I)
|
||
text = re.sub(r'\bпердаль\b', "Пьер д'Айи", text, flags=re.I)
|
||
|
||
# Double spaces cleanup
|
||
text = re.sub(r'[ \t]+', ' ', text)
|
||
return text.strip()
|
||
|
||
def extract_clean_battistini_symbol(doc, name: str, start_p: int, end_p: int):
|
||
"""Extracts pristine, structured entry for a Battistini symbol across its page range."""
|
||
page_texts = []
|
||
artworks = []
|
||
|
||
for p in range(start_p, end_p + 1):
|
||
raw = doc[p].get_text()
|
||
cleaned = clean_russian_ocr(raw)
|
||
page_texts.append(cleaned)
|
||
|
||
# Check for artwork attributions on illustration pages
|
||
# Format usually: "• Имя Фамилия. Название картины. Год. Музей"
|
||
m_art = re.findall(r'([А-ЯЁ][а-яёA-Za-z\s]+(?:\([^\)]+\))?\.\s+[А-ЯЁ«"][^.\n]+(?:\.[^.\n]+)?\.\s*(?:Около\s*)?\d{3,4}(?:[—–-]\d{3,4})?\.\s*[^.\n]+)', cleaned)
|
||
for art in m_art:
|
||
art_str = ' '.join(art.split()).strip()
|
||
if len(art_str) > 20 and not any(skip in art_str for skip in ['Происхождение', 'Характеристики', 'Изображается']):
|
||
if art_str not in artworks:
|
||
artworks.append(art_str)
|
||
|
||
first_page_text = page_texts[0]
|
||
|
||
# Extract metadata fields if present
|
||
etymology = ""
|
||
m_etym = re.search(r'Происхождение слова\s+([^.\n]+(?:\.[^.\n]+)*)', first_page_text)
|
||
if m_etym:
|
||
etymology = ' '.join(m_etym.group(1).split())
|
||
|
||
characteristics = ""
|
||
m_char = re.search(r'Характеристики\s+([^.\n]+(?:\.[^.\n]+){1,3})', first_page_text)
|
||
if m_char:
|
||
characteristics = ' '.join(m_char.group(1).split())
|
||
|
||
iconography = ""
|
||
m_icon = re.search(r'(Изображается[^\n]+(?:\.[^\n]+)?)', first_page_text)
|
||
if m_icon:
|
||
iconography = ' '.join(m_icon.group(1).split())
|
||
|
||
# Extract deep synthesis essay
|
||
# Synthesis is usually the main continuous text block on the first page
|
||
synthesis_lines = []
|
||
for para in first_page_text.split('\n\n'):
|
||
para = ' '.join(para.split()).strip()
|
||
if len(para) > 120:
|
||
if not any(header in para for header in ['Происхождение слова', 'Родственные божества', 'Религиозно-философские']):
|
||
synthesis_lines.append(para)
|
||
|
||
synthesis = ' '.join(synthesis_lines) if synthesis_lines else first_page_text[:400]
|
||
synthesis = ' '.join(synthesis.split())
|
||
|
||
# Build rich, elegant excerpt (ideal for passport quotes & summaries)
|
||
excerpt_parts = []
|
||
if characteristics and len(characteristics) > 20:
|
||
excerpt_parts.append(characteristics)
|
||
if iconography and len(iconography) > 20:
|
||
excerpt_parts.append(iconography)
|
||
if synthesis and len(synthesis) > 50:
|
||
# Add first 1-2 sentences of synthesis
|
||
s_sentences = [s.strip() for s in synthesis.split('.') if s.strip()]
|
||
if s_sentences:
|
||
excerpt_parts.append('. '.join(s_sentences[:2]) + '.')
|
||
|
||
full_excerpt = ' '.join(excerpt_parts) if excerpt_parts else synthesis[:350]
|
||
# Clean up excerpt formatting
|
||
full_excerpt = ' '.join(full_excerpt.split())
|
||
if len(full_excerpt) > 450:
|
||
# trim at sentence boundary
|
||
cut = full_excerpt[:450].rfind('.')
|
||
if cut > 200:
|
||
full_excerpt = full_excerpt[:cut+1]
|
||
|
||
# Full formatted text
|
||
full_text = f"СИМВОЛ И АЛЛЕГОРИЯ: {name.upper()}\n(М. Баттистини · «Символы и аллегории в искусстве», 2008)\n\n"
|
||
if etymology:
|
||
full_text += f"• Происхождение слова: {etymology}\n"
|
||
if characteristics:
|
||
full_text += f"• Характеристики: {characteristics}\n"
|
||
if iconography:
|
||
full_text += f"• Иконография: {iconography}\n"
|
||
full_text += f"\nСИНТЕЗ И ЗНАЧЕНИЕ В ИСКУССТВЕ:\n{synthesis}\n\n"
|
||
if artworks:
|
||
full_text += "КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ:\n"
|
||
for art in artworks[:6]:
|
||
full_text += f" — {art}\n"
|
||
|
||
return {
|
||
"term": name,
|
||
"normalized": name.lower(),
|
||
"pages": (start_p, end_p),
|
||
"excerpt": full_excerpt,
|
||
"full_text": full_text.strip(),
|
||
"artworks": artworks
|
||
}
|
||
|
||
def main():
|
||
print("=" * 70)
|
||
print("🧹 САНИТАРИЗАЦИЯ И РЕПАРСИНГ КОРПУСА М. БАТТИСТИНИ")
|
||
print("=" * 70)
|
||
|
||
if not PDF_PATH.exists():
|
||
print(f"❌ Файл PDF не найден: {PDF_PATH}")
|
||
return
|
||
|
||
doc = fitz.open(str(PDF_PATH))
|
||
print(f"📖 PDF открыт: {len(doc)} страниц.")
|
||
|
||
# 1. Парсим все 71 канонических символов
|
||
print("\n1. Извлечение 71 канонических символов Баттистини...")
|
||
clean_canon = {}
|
||
for name, sp, ep in CANONICAL_SYMBOLS:
|
||
clean_canon[name.lower()] = extract_clean_battistini_symbol(doc, name, sp, ep)
|
||
|
||
# 2. Парсим 12 календарных месяцев
|
||
print("2. Извлечение 12 календарных месяцев из Баттистини...")
|
||
for name, sp, ep in MONTHS_SPEC:
|
||
raw_m = clean_russian_ocr(doc[sp].get_text())
|
||
m_lines = [l.strip() for l in raw_m.splitlines() if l.strip()]
|
||
m_txt = ' '.join(m_lines[:8])
|
||
clean_canon[name.lower()] = {
|
||
"term": name,
|
||
"normalized": name.lower(),
|
||
"pages": (sp, ep),
|
||
"excerpt": f"Месяц {name} в традиции европейской живописи и аллегории: {m_txt}",
|
||
"full_text": f"КАЛЕНДАРНЫЙ СИМВОЛ: {name.upper()}\n(М. Баттистини · «Символы и аллегории», раздел «Месяцы»)\n\n{raw_m}",
|
||
"artworks": []
|
||
}
|
||
|
||
print(f"✓ Подготовлено {len(clean_canon)} чистых статей Баттистини.")
|
||
|
||
# 3. Привязка иллюстраций к каноническим символам
|
||
print("\n3. Сопоставление иллюстраций с каноническими символами...")
|
||
ill_files = glob.glob(str(BASE_DIR / "assets/illustrations/bat_*.jpeg")) + glob.glob(str(BASE_DIR / "assets/illustrations/bat_*.png"))
|
||
print(f" Всего файлов иллюстраций Баттистини: {len(ill_files)}")
|
||
|
||
symbol_illustrations = {}
|
||
for img_path_str in ill_files:
|
||
rel_path = f"assets/illustrations/{Path(img_path_str).name}"
|
||
m_p = re.search(r'bat_(\d+)_', Path(img_path_str).name)
|
||
if m_p:
|
||
pdf_p = int(m_p.group(1))
|
||
# Находим канонический символ, покрывающий эту страницу
|
||
target_sym = None
|
||
# Сначала проверяем индивидуальные месяцы
|
||
for m_name, m_sp, m_ep in MONTHS_SPEC:
|
||
if m_sp == pdf_p:
|
||
target_sym = m_name.lower()
|
||
break
|
||
if not target_sym:
|
||
for c_name, c_sp, c_ep in CANONICAL_SYMBOLS:
|
||
if c_sp <= pdf_p <= c_ep:
|
||
target_sym = c_name.lower()
|
||
break
|
||
|
||
if target_sym and target_sym in clean_canon:
|
||
symbol_illustrations.setdefault(target_sym, []).append({
|
||
"id": Path(img_path_str).stem,
|
||
"file": rel_path,
|
||
"symbol": clean_canon[target_sym]["term"],
|
||
"caption": f"Аллегория «{clean_canon[target_sym]['term']}» (М. Баттистини, стр. {pdf_p})",
|
||
"page": pdf_p,
|
||
"source": "М. Баттистини · «Символы и аллегории»",
|
||
"normalized": target_sym,
|
||
"is_core_1000": False,
|
||
"core_id": None
|
||
})
|
||
|
||
print(f" Привязаны иллюстрации к {len(symbol_illustrations)} символам.")
|
||
|
||
# 4. Обновление assets/data/oracle_external_symbols.json
|
||
print("\n4. Очистка и обновление oracle_external_symbols.json...")
|
||
ext_path = DATA_DIR / "oracle_external_symbols.json"
|
||
with open(ext_path, 'r', encoding='utf-8') as f:
|
||
ext_db = json.load(f)
|
||
|
||
# Удаляем ВСЕ 66 старых записей Баттистини (включая все глюки)
|
||
old_bat_keys = []
|
||
for k, v in list(ext_db.items()):
|
||
has_bat = any("баттистини" in s.get("author", "").lower() for s in v.get("sources", []))
|
||
if has_bat:
|
||
old_bat_keys.append(k)
|
||
del ext_db[k]
|
||
|
||
print(f" Удалено старых записей Баттистини (включая фантомные 'con', 'аимб', 'гор од' и др.): {len(old_bat_keys)}")
|
||
|
||
# Вставляем чистый канон
|
||
now_ts = "2026-10-08T19:50:00.000000"
|
||
for norm_k, item in clean_canon.items():
|
||
ext_db[norm_k] = {
|
||
"term": item["term"],
|
||
"normalized": norm_k,
|
||
"category_guess": "Аллегории и символы искусства",
|
||
"sources": [
|
||
{
|
||
"author": "М. Баттистини",
|
||
"work": "«Символы и аллегории» (2008)",
|
||
"excerpt": item["excerpt"],
|
||
"full_text": item["full_text"],
|
||
}
|
||
],
|
||
"illustrations": symbol_illustrations.get(norm_k, []),
|
||
"created_at": now_ts,
|
||
"updated_at": now_ts,
|
||
}
|
||
|
||
# Санитаризуем все оставшиеся внешние символы от дефисных разрывов
|
||
cleaned_ext_count = 0
|
||
for k, v in ext_db.items():
|
||
v["term"] = clean_russian_ocr(v.get("term", ""))
|
||
for s in v.get("sources", []):
|
||
if "excerpt" in s:
|
||
s["excerpt"] = clean_russian_ocr(s["excerpt"])
|
||
if "full_text" in s:
|
||
s["full_text"] = clean_russian_ocr(s["full_text"])
|
||
cleaned_ext_count += 1
|
||
|
||
with open(ext_path, 'w', encoding='utf-8') as f:
|
||
json.dump(ext_db, f, ensure_ascii=False, indent=2)
|
||
print(f" ✓ oracle_external_symbols.json сохранен ({len(ext_db)} символов).")
|
||
|
||
# 5. Обновление assets/data/oracle_passports.json и oracle_passports_staging.json
|
||
print("\n5. Обновление oracle_passports.json и oracle_passports_staging.json...")
|
||
for db_filename in ["oracle_passports.json", "oracle_passports_staging.json"]:
|
||
db_path = DATA_DIR / db_filename
|
||
with open(db_path, 'r', encoding='utf-8') as f:
|
||
passports = json.load(f)
|
||
|
||
updated_bat_in_passports = 0
|
||
total_fields_sanitized = 0
|
||
|
||
for p_id, entry in passports.items():
|
||
w = entry.get("word", "").lower()
|
||
fields = entry.get("fields", {})
|
||
|
||
# 1. Если для этого слова есть каноническая статья Баттистини — заменяем цитаты Баттистини на чистые
|
||
matched_canon = clean_canon.get(w)
|
||
# Дополнительные сопоставления (например, 'стихи' -> 'стихии')
|
||
if not matched_canon:
|
||
if w == "стихи": matched_canon = clean_canon.get("стихии")
|
||
elif w == "месяц": matched_canon = clean_canon.get("месяцы")
|
||
|
||
for fk, fv in list(fields.items()):
|
||
if isinstance(fv, str):
|
||
if "баттистини" in fv.lower():
|
||
if matched_canon:
|
||
# Ставим чистый, глубокий академический синтез
|
||
fields[fk] = f"[М. Баттистини · «Символы и аллегории»]: «{matched_canon['excerpt']}»"
|
||
updated_bat_in_passports += 1
|
||
else:
|
||
# Санитаризуем существующую цитату
|
||
fields[fk] = clean_russian_ocr(fv)
|
||
else:
|
||
# Санитаризуем любые другие поля от опечаток и переносов слов
|
||
fields[fk] = clean_russian_ocr(fv)
|
||
total_fields_sanitized += 1
|
||
|
||
# Санитаризуем метаданные карточки
|
||
if "description" in entry and isinstance(entry["description"], str):
|
||
entry["description"] = clean_russian_ocr(entry["description"])
|
||
if "definition" in entry and isinstance(entry["definition"], str):
|
||
entry["definition"] = clean_russian_ocr(entry["definition"])
|
||
|
||
# Привязываем чистые иллюстрации Баттистини к карточке ядра
|
||
if matched_canon and matched_canon["normalized"] in symbol_illustrations:
|
||
target_ills = entry.setdefault("illustrations", [])
|
||
for ill in symbol_illustrations[matched_canon["normalized"]]:
|
||
ill_copy = dict(ill)
|
||
ill_copy["is_core_1000"] = True
|
||
ill_copy["core_id"] = p_id
|
||
if not any(x.get("file") == ill_copy["file"] for x in target_ills):
|
||
target_ills.append(ill_copy)
|
||
|
||
with open(db_path, 'w', encoding='utf-8') as f:
|
||
json.dump(passports, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f" ✓ {db_filename}: обновлено {updated_bat_in_passports} цитат Баттистини, санитаризовано {total_fields_sanitized} полей.")
|
||
|
||
# 6. Обновление каталога иллюстраций oracle_illustrations_catalog.json
|
||
print("\n6. Обновление каталога иллюстраций oracle_illustrations_catalog.json...")
|
||
cat_path = DATA_DIR / "oracle_illustrations_catalog.json"
|
||
if cat_path.exists():
|
||
with open(cat_path, 'r', encoding='utf-8') as f:
|
||
catalog = json.load(f)
|
||
|
||
# Очищаем все привязки к фантомным символам Баттистини
|
||
cleaned_cat = []
|
||
for ill in catalog:
|
||
if "bat_" in ill.get("id", "") or "bat_" in ill.get("file", ""):
|
||
m_p = re.search(r'bat_(\d+)_', ill.get("file", ""))
|
||
if m_p:
|
||
pdf_p = int(m_p.group(1))
|
||
target_sym = None
|
||
for m_name, m_sp, m_ep in MONTHS_SPEC:
|
||
if m_sp == pdf_p:
|
||
target_sym = m_name.lower()
|
||
break
|
||
if not target_sym:
|
||
for c_name, c_sp, c_ep in CANONICAL_SYMBOLS:
|
||
if c_sp <= pdf_p <= c_ep:
|
||
target_sym = c_name.lower()
|
||
break
|
||
if target_sym and target_sym in clean_canon:
|
||
ill["symbol"] = clean_canon[target_sym]["term"]
|
||
ill["normalized"] = target_sym
|
||
ill["caption"] = f"Аллегория «{clean_canon[target_sym]['term']}» (М. Баттистини)"
|
||
cleaned_cat.append(ill)
|
||
else:
|
||
# Другие иллюстрации просто санитаризуем
|
||
ill["symbol"] = clean_russian_ocr(ill.get("symbol", ""))
|
||
ill["caption"] = clean_russian_ocr(ill.get("caption", ""))
|
||
cleaned_cat.append(ill)
|
||
|
||
with open(cat_path, 'w', encoding='utf-8') as f:
|
||
json.dump(cleaned_cat, f, ensure_ascii=False, indent=2)
|
||
print(f" ✓ Каталог иллюстраций обновлен: {len(cleaned_cat)} элементов.")
|
||
|
||
print("\n" + "=" * 70)
|
||
print("✅ ВСЕ ДАННЫЕ УСПЕШНО ОЧИЩЕНЫ И ВОССТАНОВЛЕНЫ ПО БАТТИСТИНИ!")
|
||
print(" (НИКАКИЕ СБОРКИ НЕ ЗАПУСКАЛИСЬ)")
|
||
print("=" * 70)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|