#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ build_external_symbols_and_ingest.py ==================================== 1. Парсит 4 словаря из папки dictionaries/: - Tresidder_Slovar-simvolov.3jnU_.fb2 (FB2, CP1251) - словарь символов.chm (CHM / HTML, CP1251) - Кирло Х. - Словарь символов... 2010.pdf (PDF, CP1251 over Latin1) - Чунакова О.М. - Пехлевийский словарь... 2004.pdf (PDF, UTF-8) 2. Для символов, входящих в 1000 знаков Оракула: - Обогащает ТОЛЬКО тестовую базу: assets/data/oracle_passports_staging.json 3. Для ВСЕХ символов, НЕ входящих в 1000: - Сохраняет в отдельную базу внешних символов: assets/data/oracle_external_symbols.json oracle_enricher/data/oracle_external_symbols.json """ import os import sys import json import re import time import subprocess from pathlib import Path from typing import Dict, Any, List, Optional, Set from datetime import datetime PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from oracle_enricher.enricher.passport_schema import ( PROMPTS_78, count_filled_fields, render_passport_markdown, ) DICT_DIR = PROJECT_ROOT / "dictionaries" STAGING_DB_PATH = PROJECT_ROOT / "assets" / "data" / "oracle_passports_staging.json" EXTERNAL_DB_PATH = PROJECT_ROOT / "assets" / "data" / "oracle_external_symbols.json" ENRICHER_EXTERNAL_PATH = PROJECT_ROOT / "oracle_enricher" / "data" / "oracle_external_symbols.json" CHM_EXTRACTED_DIR = PROJECT_ROOT / "oracle_enricher" / "data" / "harvest_cache" / "chm_extracted" try: import fitz # PyMuPDF HAS_FITZ = True except ImportError: HAS_FITZ = False try: from bs4 import BeautifulSoup HAS_BS4 = True except ImportError: HAS_BS4 = False def clean_spaces(text: str) -> str: """Нормализует пробелы и переносы строк.""" return re.sub(r'\s+', ' ', text).strip() def parse_tresidder_all() -> Dict[str, Dict[str, Any]]: """Парсит Джека Тресиддера из FB2 и CHM.""" print("📖 Парсинг Джека Тресиддера («Словарь символов»)...") results: Dict[str, Dict[str, Any]] = {} # 1. Из FB2 fb2_path = DICT_DIR / "Tresidder_Slovar-simvolov.3jnU_.fb2" if fb2_path.exists(): try: with open(fb2_path, "rb") as f: raw = f.read().decode("windows-1251", errors="ignore") paragraphs = re.findall(r'
(.*?)
', raw, re.DOTALL) cur_h, cur_l = None, [] for p in paragraphs: clean = re.sub(r'<[^>]+>', '', p).strip() if clean.isupper() and 2 <= len(clean) <= 40 and not any(skip in clean for skip in ['ПРЕДИСЛОВИЕ', 'ГЛАВА', 'СЛОВАРЬ', 'ЛИТЕРАТУРА', 'УКАЗАТЕЛЬ']): if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "Джек Тресиддер", "work": "«Словарь символов»", "text": body, } cur_h = clean cur_l = [] else: if cur_h and clean: cur_l.append(clean) if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "Джек Тресиддер", "work": "«Словарь символов»", "text": body, } except Exception as e: print(f" [Ошибка FB2: {e}]") # 2. Дополняем из распакованного CHM (если есть) if CHM_EXTRACTED_DIR.exists() and HAS_BS4: for p in CHM_EXTRACTED_DIR.glob("*.php"): try: with open(p, "r", encoding="windows-1251", errors="ignore") as f: text = BeautifulSoup(f.read(), "html.parser").get_text() m = re.search(r'([А-ЯЁ\s\-]{2,40})\s*\n\s*из книги СЛОВАРЬ СИМВОЛОВ', text) if m: head = m.group(1).strip() parts = text.split("Тресиддер") if len(parts) > 1: body = clean_spaces(parts[1].split("<<назад")[0]) term_key = head.lower() if head and body and term_key not in results and len(body) > 20: results[term_key] = { "term": head, "author": "Джек Тресиддер", "work": "«Словарь символов»", "text": body, } except Exception: continue print(f" ✓ Собрано статей Тресиддера: {len(results)}") return results def parse_cirlot_pdf() -> Dict[str, Dict[str, Any]]: """Парсит Хуана Эдуардо Керлота («Словарь символов: 1000 статей...»).""" print("📖 Парсинг Х.Э. Керлота («Словарь символов: 1000 статей...»)...") results: Dict[str, Dict[str, Any]] = {} pdf_path = DICT_DIR / "Кирло Х. - Словарь символов. 1000 статей о важнейших понятиях религии, литературы, архитектуры, истории - 2010.pdf" if not pdf_path.exists(): print(f" [Файл не найден: {pdf_path.name}]") return results try: # Извлекаем текст через pdftotext и декодируем CP1251-over-Latin1 cmd = ["pdftotext", "-f", "52", "-l", "524", str(pdf_path), "-"] out = subprocess.check_output(cmd) txt = out.decode("utf-8", errors="ignore").encode("latin1", errors="ignore").decode("cp1251", errors="ignore") lines = txt.splitlines() cur_h, cur_l = None, [] for line in lines: s = line.strip() if not s: continue if s.isupper() and 2 <= len(s) <= 35 and re.match(r'^[А-ЯЁ\s\-]+$', s) and not re.search(r'\d', s): if s in ['СЛОВАРЬ СИМВОЛОВ', 'СОДЕРЖАНИЕ', 'ВВЕДЕНИЕ', 'АЛФАВИТНЫЙ УКАЗАТЕЛЬ']: continue if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "Х.Э. Керлот", "work": "«Словарь символов»", "text": body, } cur_h = s cur_l = [] else: if cur_h and not s.isdigit(): cur_l.append(s) if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "Х.Э. Керлот", "work": "«Словарь символов»", "text": body, } except Exception as e: print(f" [Ошибка при парсинге Керлота: {e}]") print(f" ✓ Собрано статей Керлота: {len(results)}") return results def parse_chunakova_pdf() -> Dict[str, Dict[str, Any]]: """Парсит словарь О.М. Чунаковой (Пехлевийский словарь зороастрийских терминов).""" print("📖 Парсинг О.М. Чунаковой («Пехлевийский словарь зороастрийских терминов»)...") results: Dict[str, Dict[str, Any]] = {} pdf_path = DICT_DIR / "Чунакова О.М. - Пехлевийский словарь зороастрийских терминов, мифических персонажей и мифологических символов. M. 2004.pdf" if not pdf_path.exists() or not HAS_FITZ: print(f" [Файл или PyMuPDF не найден]") return results try: doc = fitz.open(str(pdf_path)) cur_h, cur_l = None, [] for p in range(12, 285): t = doc[p].get_text() for line in t.splitlines(): s = line.strip() if not s or s.isdigit(): continue m = re.match(r'^([А-ЯЁ][а-яёА-ЯЁ\s\-]{1,35})\s*[—–-]\s*(пехл\.|авест\.|«|[А-ЯЁа-яё])', s) if m and not any(skip in m.group(1).strip() for skip in ['Как', 'В', 'По', 'На', 'При', 'От', 'С', 'Для', 'Из']): head = m.group(1).strip() if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "О.М. Чунакова", "work": "«Пехлевийский словарь зороастрийских терминов»", "text": body, } cur_h = head cur_l = [s] else: if cur_h: cur_l.append(s) if cur_h and cur_l: term_key = cur_h.strip().lower() body = clean_spaces(' '.join(cur_l)) if len(body) > 20: results[term_key] = { "term": cur_h.strip(), "author": "О.М. Чунакова", "work": "«Пехлевийский словарь зороастрийских терминов»", "text": body, } except Exception as e: print(f" [Ошибка при парсинге Чунаковой: {e}]") print(f" ✓ Собрано статей Чунаковой: {len(results)}") return results def main(): print("=" * 75) print("🔮 ОБОГАЩЕНИЕ ТЕСТОВОЙ БАЗЫ И ФОРМИРОВАНИЕ БАЗЫ ВНЕШНИХ СИМВОЛОВ") print("=" * 75) # 1. Загрузка тестовой базы (staging) if not STAGING_DB_PATH.exists(): print(f"[ERROR] Тестовая база не найдена: {STAGING_DB_PATH}") return with open(STAGING_DB_PATH, "r", encoding="utf-8") as f: staging_db = json.load(f) # Индекс 1000 знаков core_index = {} for idx_str, entry in staging_db.items(): w = entry.get("word", "").lower().strip() if w: core_index[w] = idx_str print(f"База канона 1000 знаков загружена: {len(core_index)} слов.") # 2. Загрузка существующей базы внешних символов (если уже есть) external_db: Dict[str, Dict[str, Any]] = {} if EXTERNAL_DB_PATH.exists(): try: with open(EXTERNAL_DB_PATH, "r", encoding="utf-8") as f: external_db = json.load(f) print(f"Существующая база внешних символов загружена: {len(external_db)} записей.") except Exception: external_db = {} # 3. Парсинг словарей t_tresidder = parse_tresidder_all() t_cirlot = parse_cirlot_pdf() t_chunakova = parse_chunakova_pdf() all_books = [ ("cirlot", t_cirlot), ("tresidder", t_tresidder), ("chunakova", t_chunakova), ] matched_staging_count = 0 new_external_count = 0 timestamp_str = datetime.now().isoformat() for book_id, book_entries in all_books: print(f"\nОбработка источника: {book_id} ({len(book_entries)} статей)...") injected_this_book = 0 external_this_book = 0 for norm_term, item in book_entries.items(): author = item["author"] work = item["work"] full_text = item["text"] citation = f"[{author} · {work}]" # Короткая выжимка для цитирования excerpt = (full_text[:380] + "...") if len(full_text) > 380 else full_text quoted_text = f"{citation}: «{excerpt}»" # Проверяем вхождение в 1000 канонических знаков # Прямое совпадение или через форму единственного числа (напр. 'ангелы' -> 'ангел') matched_idx = None if norm_term in core_index: matched_idx = core_index[norm_term] elif norm_term.endswith("ы") and norm_term[:-1] in core_index: matched_idx = core_index[norm_term[:-1]] elif norm_term.endswith("и") and norm_term[:-1] in core_index: matched_idx = core_index[norm_term[:-1]] if matched_idx: # ОБОГАЩАЕМ ТЕСТОВУЮ БАЗУ (STAGING) entry = staging_db[matched_idx] fields = entry.setdefault("fields", {}) field_target = None if book_id == "cirlot": if not fields.get("meta_synthesis"): field_target = "meta_synthesis" elif not fields.get("culture_hermetic_light"): field_target = "culture_hermetic_light" elif book_id == "tresidder": if not fields.get("semantic_core"): field_target = "semantic_core" elif not fields.get("culture_antiquity_light"): field_target = "culture_antiquity_light" elif book_id == "chunakova": if not fields.get("culture_persia_light"): field_target = "culture_persia_light" elif not fields.get("script_persia"): field_target = "script_persia" if field_target and not fields.get(field_target): fields[field_target] = quoted_text injected_this_book += 1 matched_staging_count += 1 filled, _ = count_filled_fields(entry) entry["filled_count"] = filled entry["is_complete"] = (filled >= 78) else: # СИМВОЛ НЕ ВХОДИТ В 1000 -> ПИШЕМ В БАЗУ ВНЕШНИХ СИМВОЛОВ! if norm_term not in external_db: external_db[norm_term] = { "term": item["term"], "normalized": norm_term, "category_guess": "Внешний академический символ", "sources": [], "created_at": timestamp_str, "updated_at": timestamp_str, } new_external_count += 1 external_this_book += 1 # Проверяем, нет ли уже источника от этого автора existing_sources = external_db[norm_term]["sources"] if not any(s.get("author") == author for s in existing_sources): existing_sources.append({ "author": author, "work": work, "excerpt": excerpt, "full_text": full_text, }) external_db[norm_term]["updated_at"] = timestamp_str print(f" -> В тестовую базу обогащено знаков: {injected_this_book}") print(f" -> В базу внешних символов добавлено новых понятий: {external_this_book}") # 4. Сохраняем обновленную тестовую базу print(f"\n💾 Сохранение тестовой базы: {STAGING_DB_PATH}...") with open(STAGING_DB_PATH, "w", encoding="utf-8") as f: json.dump(staging_db, f, ensure_ascii=False, indent=2) # 5. Сохраняем базу внешних символов (в assets и в enricher) print(f"💾 Сохранение базы внешних символов: {EXTERNAL_DB_PATH}...") EXTERNAL_DB_PATH.parent.mkdir(parents=True, exist_ok=True) with open(EXTERNAL_DB_PATH, "w", encoding="utf-8") as f: json.dump(external_db, f, ensure_ascii=False, indent=2) ENRICHER_EXTERNAL_PATH.parent.mkdir(parents=True, exist_ok=True) with open(ENRICHER_EXTERNAL_PATH, "w", encoding="utf-8") as f: json.dump(external_db, f, ensure_ascii=False, indent=2) # Статистика total_fields_staging = sum(e.get("filled_count", 0) for e in staging_db.values()) print("\n" + "=" * 75) print("📊 ИТОГОВЫЙ ОТЧЕТ:") print(f"• Всего внешних символов в реестре: {len(external_db):,} понятий") print(f"• Обогащено ячеек в тестовой базе (staging): +{matched_staging_count}") print(f"• Общая заполненность тестовой базы: {total_fields_staging:,} / 78,000 ячеек") print(f"• Файл внешних символов: {EXTERNAL_DB_PATH} ({EXTERNAL_DB_PATH.stat().st_size / (1024*1024):.2f} MB)") print("=" * 75) if __name__ == "__main__": main()