Каталогизатор документов: разбор папки, тезисы, поиск, 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>
This commit is contained in:
75
catalogizer/analyze.py
Normal file
75
catalogizer/analyze.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Второй проход: тезисы, ключевые слова, описания (этап «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)}
|
||||
Reference in New Issue
Block a user