Files
catalog_doc/catalogizer/analyze.py
Andrey Kruchinkin 49f91247c7 Каталогизатор документов: разбор папки, тезисы, поиск, GUI
Программа принимает папку с документами, извлекает из них текст и
метаданные и строит каталог: краткое описание, тезисы и ключевые слова
для каждого файла плюс поиск по всему собранному.

Ядро:
- extract: PDF, DJVU, DOC(X), RTF, ODT, CHM, EPUB, FB2, PPT(X), XLS(X), TXT;
  PDF передаётся MuPDF потоком в память (работают длинные пути) и ограничен
  по времени — повреждённый файл иначе чинится минутами;
- summarize: экстрактивное реферирование по TF-IDF, посчитанному на самой
  библиотеке, с лёгким стеммером для русского и английского;
- db: SQLite с полнотекстовым индексом FTS5, морфологический поиск с BM25;
- scanner: инкрементальный многопоточный обход, счётчик попыток защищает
  от файла, обрывающего разбор;
- report: автономный HTML с поиском, Markdown, JSON, CSV.

Интерфейс:
- окно PySide6 в тёмной теме: вкладки разбора и поиска, фоновый поток,
  карточка документа, правка своих ключевых слов;
- командная строка: build, scan, analyze, report, search, show, tag, stats;
- у каждой папки свой каталог, последняя обработанная запоминается.

Сборка: scripts/build_exe.py даёт Catalogizer.exe в Windows и
Catalogizer.app в macOS, иконки .ico и .icns рисуются кодом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:20:29 +03:00

76 lines
3.0 KiB
Python

"""Второй проход: тезисы, ключевые слова, описания (этап «analyze»).
IDF считается по всей библиотеке, поэтому проход выполняется после того,
как тексты всех документов уже лежат в базе.
"""
from __future__ import annotations
import json
import time
from collections import Counter
from . import db
from .summarize import Analyzer, describe
from .textutil import stem, tokens
def build_df(con) -> tuple[dict[str, int], int]:
"""Документная частота каждой основы слова по всей библиотеке."""
df: Counter = Counter()
n = 0
for row in con.execute("SELECT terms FROM docs WHERE terms IS NOT NULL"):
try:
terms = json.loads(row["terms"])
except (TypeError, ValueError):
continue
n += 1
df.update(terms.keys())
return dict(df), n
def analyze(con, *, force: bool = False, quiet: bool = False,
progress=None) -> dict:
df, n_docs = build_df(con)
an = Analyzer(df, n_docs)
where = "" if force else " WHERE analyzed = 0"
ids = [r["id"] for r in con.execute("SELECT id FROM docs" + where)]
total = len(ids)
if not quiet:
print("Документов в базе: %d, к анализу: %d" % (n_docs, total), flush=True)
if progress:
progress(0, total, "Анализ: %d документов" % total)
started = time.time()
for i, doc_id in enumerate(ids, 1):
row = con.execute(
"SELECT title, body, toc, terms, lang, kind, pages, scanned, filename"
" FROM docs WHERE id = ?", (doc_id,)).fetchone()
body = row["body"] or ""
try:
terms = Counter(json.loads(row["terms"] or "{}"))
toc = json.loads(row["toc"] or "[]")
except ValueError:
terms, toc = Counter(), []
keywords = an.keywords(terms, body, row["title"] or "", toc)
theses = an.theses(body, terms) if not row["scanned"] else []
if not theses and toc:
# Для сканов и справок без связного текста тезисы берём из оглавления.
theses = [t for t in toc[:8] if len(t) > 8]
description = describe(row["title"] or row["filename"], row["kind"] or "",
row["lang"] or "?", row["pages"], keywords,
theses, not row["scanned"])
stems = " ".join(stem(t) for t in tokens(body[:20_000]))
db.save_summary(con, doc_id, description, theses, keywords, stems)
if i % 100 == 0:
con.commit()
if not quiet:
print(" %d/%d" % (i, total), flush=True)
if progress:
progress(i, total, "Анализ %d/%d" % (i, total))
con.commit()
return {"analyzed": total, "corpus": n_docs,
"seconds": round(time.time() - started, 1)}