feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
import fitz
|
||||
|
||||
BASE_DIR = Path('/home/alexives/Desktop/StudioProjects/999')
|
||||
EXT_SYMBOLS_PATH = BASE_DIR / 'assets/data/oracle_external_symbols.json'
|
||||
PASSPORTS_PATH = BASE_DIR / 'assets/data/oracle_passports.json'
|
||||
STAGING_PATH = BASE_DIR / 'assets/data/oracle_passports_staging.json'
|
||||
CATALOG_PATH = BASE_DIR / 'assets/data/oracle_illustrations_catalog.json'
|
||||
CHUNAKOVA_PDF = BASE_DIR / 'dictionaries/Чунакова О.М. - Пехлевийский словарь зороастрийских терминов, мифических персонажей и мифологических символов. M. 2004.pdf'
|
||||
|
||||
t0 = time.time()
|
||||
print("=" * 80)
|
||||
print("🧹 DEEP CORPUS CLEANING & UNIVERSAL DE-HYPHENATION")
|
||||
print("=" * 80)
|
||||
|
||||
# Load existing databases
|
||||
with open(EXT_SYMBOLS_PATH, 'r', encoding='utf-8') as f:
|
||||
ext = json.load(f)
|
||||
|
||||
with open(PASSPORTS_PATH, 'r', encoding='utf-8') as f:
|
||||
passports = json.load(f)
|
||||
|
||||
with open(CATALOG_PATH, 'r', encoding='utf-8') as f:
|
||||
catalog = json.load(f)
|
||||
|
||||
print(f"Initial: {len(ext)} ext symbols, {len(passports)} passports, {len(catalog)} catalog entries")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 1. PARSE CLEAN CHUNAKOVA ARTICLES & PURGE BLOATED/CORRUPTED ONES
|
||||
# ------------------------------------------------------------------------------
|
||||
print(">>> Step 1: Parsing clean Chunakova articles...")
|
||||
|
||||
doc_ch = fitz.open(str(CHUNAKOVA_PDF))
|
||||
|
||||
def dehyphen_simple(text: str) -> str:
|
||||
text = re.sub(r'([а-яёА-ЯЁA-Za-z]+)\xad\s*\n\s*([а-яёА-ЯЁA-Za-z]+)', r'\1\2', text)
|
||||
text = re.sub(r'([а-яёА-ЯЁA-Za-z]+)\xad\s+([а-яёА-ЯЁA-Za-z]+)', r'\1\2', text)
|
||||
text = text.replace('\xad', '')
|
||||
return text
|
||||
|
||||
bad_prefixes = ('его ', 'ее ', 'их ', 'этот ', 'эта ', 'эти ', 'определение ', 'эпитеты ', 'символ ', 'как ', 'в ', 'по ', 'на ', 'при ', 'от ', 'с ', 'для ', 'из ', 'три ', 'четыре ', 'пять ', 'шесть ', 'семь ', 'восемь ', 'девять ', 'десять ')
|
||||
|
||||
clean_chunakova = {}
|
||||
cur_h = None
|
||||
cur_lines = []
|
||||
|
||||
for p in range(12, 285):
|
||||
raw = doc_ch[p].get_text()
|
||||
cleaned = dehyphen_simple(raw)
|
||||
for line in cleaned.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.isdigit() or len(line) <= 2:
|
||||
continue
|
||||
m = re.match(r'^([А-ЯЁ][а-яёА-ЯЁ\s\-]{1,35}?)\s*[—–-]\s*(.*)', line)
|
||||
if m:
|
||||
head = m.group(1).strip()
|
||||
rest = m.group(2).strip()
|
||||
norm = head.lower()
|
||||
if not any(norm.startswith(bp) for bp in bad_prefixes) and not any(c.isdigit() for c in head):
|
||||
if cur_h and cur_lines:
|
||||
txt = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip()
|
||||
if len(txt) >= 25:
|
||||
clean_chunakova[cur_h.lower()] = (cur_h, txt)
|
||||
cur_h = head
|
||||
cur_lines = [rest] if rest else []
|
||||
continue
|
||||
if cur_h:
|
||||
cur_lines.append(line)
|
||||
|
||||
if cur_h and cur_lines:
|
||||
txt = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip()
|
||||
if len(txt) >= 25:
|
||||
clean_chunakova[cur_h.lower()] = (cur_h, txt)
|
||||
|
||||
print(f"Extracted {len(clean_chunakova)} clean Chunakova articles in {time.time()-t0:.2f}s")
|
||||
|
||||
# Purge corrupted Chunakova entries from ext
|
||||
chunakova_bloat = ['его символ', 'определение дахака', 'три сферы-стоянки', 'эпитеты ахримана']
|
||||
for b_k in chunakova_bloat:
|
||||
if b_k in ext:
|
||||
del ext[b_k]
|
||||
|
||||
# Inject clean Chunakova articles
|
||||
for norm_k, (term_c, text_c) in clean_chunakova.items():
|
||||
if norm_k in ext:
|
||||
# Update Chunakova source
|
||||
sources = [s for s in ext[norm_k].get('sources', []) if 'Чунакова' not in s.get('author', '')]
|
||||
sources.append({
|
||||
'author': 'О.М. Чунакова',
|
||||
'work': '«Пехлевийский словарь зороастрийских терминов»',
|
||||
'excerpt': (text_c[:250] + '…') if len(text_c) > 250 else text_c,
|
||||
'full_text': text_c
|
||||
})
|
||||
ext[norm_k]['sources'] = sources
|
||||
else:
|
||||
# Create concise entry if valid length
|
||||
if len(text_c) >= 30:
|
||||
ext[norm_k] = {
|
||||
'term': term_c,
|
||||
'normalized': norm_k,
|
||||
'category_guess': 'Зороастрийская сакральная традиция',
|
||||
'sources': [{
|
||||
'author': 'О.М. Чунакова',
|
||||
'work': '«Пехлевийский словарь зороастрийских терминов»',
|
||||
'excerpt': (text_c[:250] + '…') if len(text_c) > 250 else text_c,
|
||||
'full_text': text_c
|
||||
}],
|
||||
'illustrations': []
|
||||
}
|
||||
|
||||
print(">>> Step 1 complete.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 2. PURGE GHOST EMPTY KEYS (97 KEYS) & MOVE VALUABLE ILLUSTRATIONS
|
||||
# ------------------------------------------------------------------------------
|
||||
print(">>> Step 2: Purging empty ghost keys and saving illustrations...")
|
||||
|
||||
# Move valuable illustrations to legitimate targets
|
||||
ill_relocations = {
|
||||
'огонь элемент': 'огонь',
|
||||
'сердце символизируется лотосом': 'сердце',
|
||||
'корзина с фруктами': 'фрукты',
|
||||
'большие рыбы': 'рыба',
|
||||
'фламандские пословицы': 'пословицы',
|
||||
'меняла с женой': 'меняла',
|
||||
'аллегории пяти чувств': 'пять чувств'
|
||||
}
|
||||
|
||||
for ghost_k, target_k in ill_relocations.items():
|
||||
if ghost_k in ext and target_k in ext:
|
||||
for ill in ext[ghost_k].get('illustrations', []):
|
||||
if not any(x.get('file') == ill.get('file') for x in ext[target_k].get('illustrations', [])):
|
||||
ext[target_k].setdefault('illustrations', []).append(ill)
|
||||
|
||||
# Find and delete all items with 0 sources
|
||||
ghosts_deleted = 0
|
||||
for k in list(ext.keys()):
|
||||
if not ext[k].get('sources'):
|
||||
del ext[k]
|
||||
ghosts_deleted += 1
|
||||
|
||||
print(f">>> Step 2 complete: Purged {ghosts_deleted} ghost keys with 0 sources.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 3. UNIVERSAL DE-HYPHENATION ENGINE & ARTIFACT REPAIR
|
||||
# ------------------------------------------------------------------------------
|
||||
print(">>> Step 3: Running universal de-hyphenation engine across all texts...")
|
||||
|
||||
compound_prefixes = {
|
||||
'северо', 'юго', 'западно', 'восточно', 'темно', 'светло', 'ярко', 'бледно',
|
||||
'военно', 'научно', 'социально', 'политико', 'физико', 'химико', 'историко',
|
||||
'литературно', 'торгово', 'русско', 'греко', 'англо', 'франко', 'индо', 'китае',
|
||||
'евро', 'афро', 'арабо', 'мусульмано', 'христиано', 'иудео', 'буддо', 'инь', 'ян'
|
||||
}
|
||||
|
||||
particles = {'то', 'либо', 'нибудь', 'таки'}
|
||||
|
||||
def universal_clean_text(text: str) -> str:
|
||||
if not text or not isinstance(text, str):
|
||||
return text
|
||||
|
||||
# 1. Embedded footnotes / OCR captions inside words
|
||||
text = re.sub(r'грече1\s+П\s+о\s+р\s+о\s+к[^\.\n]*?ского', 'греческого', text)
|
||||
text = re.sub(r'по1\s+Г\s+о\s+р\s+г\s+у\s+л\s+ь\s+я[^\.\n]*?сланников', 'посланников', text)
|
||||
text = re.sub(r'улы1\s+Вспомним[^\.\n]*?бающаяся', 'улыбающаяся', text)
|
||||
text = re.sub(r'вер393\s+Сирена[^\.\n]*?сии', 'версии', text)
|
||||
text = re.sub(r'отдыха141\s+Дерево[^\.\n]*?древо', 'отдыха', text)
|
||||
text = re.sub(r'Восемь благоприятных символов\s+23\s+Будды', 'Восемь благоприятных символов Будды', text)
|
||||
text = re.sub(r'1\s+С\s+у\s+з\s+н\s+и\s+ц\s+а\s*\.[^\.\n]*?э\s+л\s+е\s+м\s*', '', text)
|
||||
|
||||
# 2. Cut off trailing index catalog in 'ястреб-перепелятник'
|
||||
if 'Птицы; Хаос; Ребенок; Колонны' in text:
|
||||
text = text.split('Птицы; Хаос; Ребенок')[0].strip()
|
||||
|
||||
# 3. Soft hyphens (\xad)
|
||||
text = re.sub(r'([а-яёА-ЯЁA-Za-z]{2,})\xad\s+([а-яёА-ЯЁA-Za-z]{2,})', r'\1\2', text)
|
||||
text = text.replace('\xad', '')
|
||||
|
||||
# 4. Newline broken hyphens
|
||||
def repl_nl(m):
|
||||
w1, w2 = m.group(1), m.group(2)
|
||||
if w1.lower() in compound_prefixes:
|
||||
return f'{w1}-{w2}'
|
||||
if w1.lower().endswith(('о', 'е')) and w2.lower() in ('и', 'или'):
|
||||
return f'{w1}- {w2}'
|
||||
return f'{w1}{w2}'
|
||||
text = re.sub(r'([а-яёА-ЯЁ]{2,})[-\u2010\u2011]\s*\n\s*([а-яё]{2,})', repl_nl, text)
|
||||
|
||||
# 5. In-line broken hyphenations: 'сло- во'
|
||||
def repl_hyphen_space(m):
|
||||
w1, w2 = m.group(1), m.group(2)
|
||||
w1_low = w1.lower()
|
||||
w2_low = w2.lower()
|
||||
# Hanging hyphen: 'научно- и'
|
||||
if w1_low.endswith(('о', 'е')) and w2_low in ('и', 'или'):
|
||||
return f'{w1}- {w2}'
|
||||
# Compound prefix: 'северо- восточный' -> 'северо-восточный'
|
||||
if w1_low in compound_prefixes:
|
||||
return f'{w1}-{w2}'
|
||||
# Particles: 'какой- то' -> 'какой-то'
|
||||
if w2_low in particles:
|
||||
return f'{w1}-{w2}'
|
||||
if w1_low in ('из', 'по') and w2_low in ('за', 'под', 'прежнему', 'новому', 'видимому'):
|
||||
return f'{w1}-{w2}'
|
||||
# Default: fuse word split
|
||||
return f'{w1}{w2}'
|
||||
|
||||
# Double pass for chained hyphenations
|
||||
text = re.sub(r'([а-яёА-ЯЁ]{2,})[-\u2010\u2011]\s+([а-яё]{2,})', repl_hyphen_space, text)
|
||||
text = re.sub(r'([а-яёА-ЯЁ]{2,})[-\u2010\u2011]\s+([а-яё]{2,})', repl_hyphen_space, text)
|
||||
|
||||
# 6. Spacing before punctuation
|
||||
text = re.sub(r'\s+([,.:;?!])', r'\1', text)
|
||||
text = re.sub(r'\(\s+', '(', text)
|
||||
text = re.sub(r'\s+\)', ')', text)
|
||||
|
||||
# 7. Normalize whitespace
|
||||
text = re.sub(r'[ \t]{2,}', ' ', text)
|
||||
return text.strip()
|
||||
|
||||
def clean_data_structure(data):
|
||||
"""Recursively clean all strings in dicts/lists."""
|
||||
if isinstance(data, dict):
|
||||
return {k: clean_data_structure(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [clean_data_structure(item) for item in data]
|
||||
elif isinstance(data, str):
|
||||
return universal_clean_text(data)
|
||||
return data
|
||||
|
||||
ext = clean_data_structure(ext)
|
||||
passports = clean_data_structure(passports)
|
||||
catalog = clean_data_structure(catalog)
|
||||
|
||||
print(">>> Step 3 complete: All strings de-hyphenated and cleaned.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 4. FINAL SOURCE DEDUPLICATION
|
||||
# ------------------------------------------------------------------------------
|
||||
print(">>> Step 4: Final source deduplication...")
|
||||
|
||||
dedup_count = 0
|
||||
for k, item in ext.items():
|
||||
sources = item.get('sources', [])
|
||||
if not sources:
|
||||
continue
|
||||
unique = []
|
||||
seen = set()
|
||||
for s in sources:
|
||||
author = s.get('author', '').strip().lower()
|
||||
work = s.get('work', '').strip().lower()
|
||||
snippet = s.get('full_text', s.get('excerpt', ''))[:50].strip().lower()
|
||||
sig = (author, work, snippet)
|
||||
if sig not in seen:
|
||||
seen.add(sig)
|
||||
unique.append(s)
|
||||
else:
|
||||
dedup_count += 1
|
||||
item['sources'] = unique
|
||||
|
||||
print(f">>> Step 4 complete: Deduplicated {dedup_count} sources.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 5. SAVE ALL DATABASES
|
||||
# ------------------------------------------------------------------------------
|
||||
print(">>> Step 5: Saving databases...")
|
||||
|
||||
with open(EXT_SYMBOLS_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(ext, f, ensure_ascii=False, indent=2)
|
||||
|
||||
with open(PASSPORTS_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(passports, f, ensure_ascii=False, indent=2)
|
||||
|
||||
with open(STAGING_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(passports, f, ensure_ascii=False, indent=2)
|
||||
|
||||
with open(CATALOG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(catalog, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("=" * 80)
|
||||
print(f"✅ DEEP CLEANING COMPLETE IN {time.time()-t0:.2f}s!")
|
||||
print(f"Ext symbols: {len(ext)}")
|
||||
print(f"Passports: {len(passports)}")
|
||||
print(f"Catalog entries: {len(catalog)}")
|
||||
print("=" * 80)
|
||||
Reference in New Issue
Block a user