Программа принимает папку с документами, извлекает из них текст и метаданные и строит каталог: краткое описание, тезисы и ключевые слова для каждого файла плюс поиск по всему собранному. Ядро: - 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>
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""@file worker.py
|
||
@brief Фоновый поток построения каталога.
|
||
|
||
Разбор библиотеки идёт минутами и часами, поэтому выполняется в отдельном
|
||
потоке QThread: окно остаётся отзывчивым, а ход работы приходит сигналами.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
from PySide6.QtCore import QThread, Signal
|
||
|
||
from .. import analyze as analyze_mod
|
||
from .. import config, db, report, scanner
|
||
|
||
|
||
class BuildWorker(QThread):
|
||
"""@brief Полный цикл: обход папки, анализ, выгрузка отчётов."""
|
||
|
||
#: сделано, всего, поясняющая строка
|
||
progress = Signal(int, int, str)
|
||
#: строка в журнал
|
||
message = Signal(str)
|
||
#: итоговая строка состояния
|
||
finished_ok = Signal(str)
|
||
#: текст ошибки с трассировкой
|
||
failed = Signal(str)
|
||
|
||
def __init__(self, root: Path, jobs: int, max_pages: int,
|
||
force: bool = False, parent=None) -> None:
|
||
super().__init__(parent)
|
||
self._root = Path(root)
|
||
self._jobs = jobs
|
||
self._max_pages = max_pages
|
||
self._force = force
|
||
self._stop = False
|
||
|
||
def request_stop(self) -> None:
|
||
"""@brief Просит остановиться после текущей порции файлов.
|
||
|
||
Разобранное уже записано в базу, поэтому следующий запуск продолжит
|
||
с того же места.
|
||
"""
|
||
self._stop = True
|
||
|
||
def run(self) -> None: # noqa: D102 — контракт QThread
|
||
try:
|
||
config.remember(self._root)
|
||
db_path, out_dir = config.paths_for(self._root)
|
||
con = db.connect(db_path)
|
||
self.message.emit("Папка: %s" % self._root)
|
||
|
||
stats = scanner.scan(
|
||
con, self._root, max_pages=self._max_pages, jobs=self._jobs,
|
||
force=self._force, quiet=True,
|
||
progress=lambda d, t, m: self.progress.emit(d, t, m),
|
||
stop=lambda: self._stop)
|
||
self.message.emit(
|
||
"Разобрано %(processed)d, с ошибками %(errors)d, "
|
||
"пропущено %(skipped)d, за %(seconds)s с" % stats)
|
||
|
||
self.progress.emit(0, 0, "Анализ текстов…")
|
||
analyze_mod.analyze(
|
||
con, quiet=True,
|
||
progress=lambda d, t, m: self.progress.emit(d, t, m))
|
||
|
||
self.progress.emit(0, 0, "Выгрузка отчётов…")
|
||
for kind, path in report.build_all(con, out_dir).items():
|
||
self.message.emit("%-5s %s" % (kind, path))
|
||
total = con.execute("SELECT COUNT(*) c FROM docs").fetchone()["c"]
|
||
con.close()
|
||
self.finished_ok.emit(
|
||
"Каталог готов: %d документов%s"
|
||
% (total, " (остановлено)" if self._stop else ""))
|
||
except Exception: # noqa: BLE001 — окно должно показать причину
|
||
self.failed.emit(traceback.format_exc())
|