Files
catalog_doc/catalogizer/config.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

120 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Настройки каталогизатора."""
from __future__ import annotations
import os
import sys
from pathlib import Path
def _default_root() -> Path:
"""Папка, предлагаемая при первом запуске.
Переопределяется переменной окружения CATALOG_ROOT или аргументом
команды. Жёсткий путь к библиотеке действует только там, где он есть,
иначе берётся домашняя папка «Документы» — так программа осмысленно
стартует и на macOS, и на Linux.
"""
given = os.environ.get("CATALOG_ROOT")
if given:
return Path(given).expanduser()
library = Path(r"E:\Yandex.Disk\литература")
if library.is_dir():
return library
for name in ("Documents", "Документы"):
candidate = Path.home() / name
if candidate.is_dir():
return candidate
return Path.home()
DEFAULT_ROOT = _default_root()
def _app_dir() -> Path:
"""Папка, где программа держит базы каталогов и отчёты.
Из исходников — корень проекта. В собранной программе — рядом с самим
файлом, кроме macOS: там ``.app`` считается неизменяемым (и может
запускаться из карантина), поэтому данные уходят в
``~/Library/Application Support``.
"""
if not getattr(sys, "frozen", False):
return Path(__file__).resolve().parent.parent
if sys.platform == "darwin":
target = Path.home() / "Library" / "Application Support" / "Catalogizer"
target.mkdir(parents=True, exist_ok=True)
return target
return Path(sys.executable).resolve().parent
APP_DIR = _app_dir()
DB_PATH = APP_DIR / "catalog.db" # база «главной» библиотеки
OUT_DIR = APP_DIR / "out" # отчёты по ней
CATALOGS_DIR = APP_DIR / "catalogs" # базы остальных обработанных папок
CURRENT_FILE = APP_DIR / ".current" # последняя обработанная папка
# Расширения, которые считаем «документами» библиотеки.
DOC_EXTENSIONS = {
".pdf", ".djvu", ".djv",
".doc", ".docx", ".rtf", ".odt",
".chm", ".epub", ".fb2",
".ppt", ".pptx", ".xls", ".xlsx",
".txt", ".md",
}
# Каталоги, которые не имеет смысла разбирать (SDK, репозитории, сборки).
SKIP_DIR_NAMES = {
".git", ".svn", "node_modules", "__pycache__", ".idea", ".vscode",
"Debug", "Release", "obj", "build", "cmake-build-debug",
}
# Мусорные txt/md из состава SDK и репозиториев — не тащим в каталог.
NOISE_FILENAMES = {
"readme.txt", "readme.md", "license.txt", "license.md", "licence.txt",
"changelog.md", "changelog.txt", "contributing.md", "code_of_conduct.md",
"manifest.txt", "makefile.txt", "notice.txt", "authors.md",
}
# Ограничения разбора.
MAX_PDF_PAGES = 20 # сколько страниц читаем из начала документа
MAX_TEXT_CHARS = 30_000 # сколько символов текста храним в базе
MIN_TEXT_CHARS = 200 # меньше — считаем, что текстового слоя нет (скан)
MIN_FILE_SIZE = 2_048 # файлы мельче игнорируем
MAX_PDF_BYTES = 400_000_000 # PDF крупнее не разбираем
PDF_TIME_BUDGET = 25 # секунд на один PDF: повреждённые чинятся вечно
# Сколько тезисов и ключевых слов делать на документ.
N_THESES = 5
N_KEYWORDS = 12
def _slug(root: Path) -> str:
"""Короткое имя каталога, однозначно привязанное к пути папки."""
import hashlib
import re
name = re.sub(r"[^\w\-]+", "_", root.name, flags=re.U).strip("_") or "catalog"
tail = hashlib.sha1(str(root).lower().encode("utf-8")).hexdigest()[:6]
return "%s-%s" % (name[:40], tail)
def paths_for(root: Path) -> tuple[Path, Path]:
"""База и папка отчётов для указанной папки с документами."""
root = Path(root).resolve()
if root == Path(DEFAULT_ROOT).resolve():
return DB_PATH, OUT_DIR
slug = _slug(root)
return CATALOGS_DIR / (slug + ".db"), OUT_DIR / slug
def remember(root: Path) -> None:
"""Запоминает последнюю обработанную папку — её и берут search/report."""
CURRENT_FILE.write_text(str(Path(root).resolve()), encoding="utf-8")
def current_root() -> Path:
"""Папка, с которой работали в последний раз."""
try:
value = CURRENT_FILE.read_text(encoding="utf-8").strip()
except OSError:
return Path(DEFAULT_ROOT)
return Path(value) if value else Path(DEFAULT_ROOT)