149 lines
7.0 KiB
Python
149 lines
7.0 KiB
Python
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)
|