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
+148
View File
@@ -0,0 +1,148 @@
import re
import json
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'
CATALOG_PATH = BASE_DIR / 'assets/data/oracle_illustrations_catalog.json'
COOPER_PDF = Path('/home/alexives/Desktop/newsources/Kuper_Dzh_Entsiklopedia_simvolov.pdf')
ILLUST_DIR = BASE_DIR / 'assets/illustrations'
def dehyphenate(text: str) -> str:
# Fix soft hyphens, hyphen-minus, en-dash, em-dash followed by newline
text = re.sub(r'([А-Яа-яA-Za-zЁё]+)[-\xad\u2010\u2011\u2012\u2013\u2014]\s*\n\s*([А-Яа-яA-Za-zЁё]+)', r'\1\2', text)
# Collapse irregular whitespace inside line
return text
def parse_cooper():
print("📖 Парсинг Дж. Купер («Энциклопедия символов»)...")
doc = fitz.open(str(COOPER_PDF))
# 1. Сначала извлечем иллюстрации с их номерами и подписями
# В Купере подписи имеют вид: "Рис. 12. Текст..." или "Рис.12.\nТекст..."
page_illustrations = {} # page_no -> list of {file, caption, num}
for pno in range(6, len(doc)-1):
txt = doc[pno].get_text()
# Поиск подписей к рисункам
fig_matches = re.finditer(r'Рис\.\s*(\d+)[\.:]?\s*\n?([^\n]+(?:\n[^\n]+)?)', txt)
imgs = doc[pno].get_images()
extracted_on_page = []
for f_idx, fm in enumerate(fig_matches):
f_num = fm.group(1)
raw_caption = fm.group(2).replace('\n', ' ').strip()
# Убираем обрезки
clean_caption = re.sub(r'\s+', ' ', raw_caption)[:120].strip()
# Если на странице есть изображения
if f_idx < len(imgs):
xref = imgs[f_idx][0]
try:
base = doc.extract_image(xref)
w, h = base["width"], base["height"]
ext = base["ext"]
if w >= 150 and h >= 150:
fname = f"cooper_fig_{f_num}_p{pno+1}.{ext}"
out_path = ILLUST_DIR / fname
if not out_path.exists():
with open(out_path, "wb") as f_out:
f_out.write(base["image"])
extracted_on_page.append({
"id": f"cooper_fig_{f_num}",
"file": f"assets/illustrations/{fname}",
"caption": f"Рис. {f_num}. {clean_caption}",
"page": pno + 1,
"width": w,
"height": h,
"source": "Дж. Купер · «Энциклопедия символов» (1995)"
})
except Exception:
pass
if extracted_on_page:
page_illustrations[pno] = extracted_on_page
print(f" ✓ Извлечено {sum(len(v) for v in page_illustrations.values())} канонических иллюстраций Купера")
# 2. Теперь парсим словарные статьи
# Каждая статья начинается с названия в начале строки с точкой или тире
entries = {} # term -> {text, illustrations, page}
cur_term = None
cur_lines = []
cur_page = 0
stop_words = {
'энциклопедия символов', 'иллюстрации', 'содержание', 'указатель',
'латинские термины', 'библиография', 'введение', 'предисловие',
'рис', 'табл', 'приложение', 'часть'
}
for pno in range(6, len(doc)-1):
raw_txt = doc[pno].get_text()
clean_txt = dehyphenate(raw_txt)
lines = [l.strip() for l in clean_txt.splitlines() if l.strip()]
for l in lines:
# Игнорируем номера страниц и одиночные колонтитулы
if l.isdigit() or len(l) <= 1:
continue
# Проверяем, не заголовок ли статьи:
# Например: "Абрикос. Текст...", "Агнец. Текст...", "Белый орел. Текст..."
# Заголовок начинается с заглавной буквы, до 35 символов, заканчивается точкой или тире
m = re.match(r'^([А-ЯЁ][А-Яа-яЁё\s\-«»\(\)]{1,32}?)(?:\.|\s+—|\s+-)\s*(.*)', l)
is_valid_header = False
if m:
cand_term = m.group(1).strip()
rest = m.group(2).strip()
norm = cand_term.lower()
# Фильтрация мусора: не колонтитул с номером, не стоп-слово, не целое предложение (<=3 слов)
words = norm.split()
if (1 <= len(words) <= 3 and
norm not in stop_words and
not any(c.isdigit() for c in norm) and
not norm.startswith('см.') and
not norm.startswith('рис.') and
not norm.startswith('в ') and
not norm.startswith('у ') and
not norm.startswith('от ')):
is_valid_header = True
if is_valid_header:
if cur_term and cur_lines:
full_text = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip()
if len(full_text) >= 30:
entries[cur_term] = {
'text': full_text,
'page': cur_page,
'illustrations': page_illustrations.get(cur_page, [])
}
cur_term = cand_term.capitalize()
cur_page = pno
cur_lines = [rest] if rest else []
else:
if cur_term:
# Игнорируем строки-подписи рисунков в теле статьи
if not l.startswith('Рис.'):
cur_lines.append(l)
if cur_term and cur_lines:
full_text = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip()
if len(full_text) >= 30:
entries[cur_term] = {
'text': full_text,
'page': cur_page,
'illustrations': page_illustrations.get(cur_page, [])
}
print(f" ✓ Распознано {len(entries)} чистых словарных статей Купера!")
return entries
if __name__ == '__main__':
entries = parse_cooper()
sample = list(entries.keys())[:25]
print("Первые 25 чистых символов Купера:", sample)