feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
+405
View File
@@ -0,0 +1,405 @@
#!/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'<p>(.*?)</p>', 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()