Files
999/scripts/sanitize_battistini_corpus.py

480 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()