Files
catalog_doc/scripts/build_exe.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

156 lines
6.8 KiB
Python
Raw Permalink 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.
"""@file build_exe.py
@brief Сборка автономной программы: Python, Qt и PyMuPDF внутри.
Запуск (из корня проекта, интерпретатором с PySide6 и PyInstaller):
python scripts/build_exe.py
Что получается, зависит от системы, на которой идёт сборка:
* Windows — ``dist/Catalogizer.exe``;
* macOS — ``dist/Catalogizer.app``;
* Linux — ``dist/Catalogizer``.
PyInstaller не умеет собирать под чужую систему: программу для macOS
собирают на macOS, для Windows — на Windows. Исходники при этом одни.
Готовая программа не требует ни Python, ни PySide6, ни PyMuPDF.
Ключи разобраны в ``--help``:
* ``--onedir`` — папка вместо одного файла: запускается заметно быстрее
(в macOS так всегда: ``.app`` по устройству и есть папка);
* ``--console`` — оставить консоль, чтобы видеть traceback и сообщения Qt;
* ``--cli`` — дополнительно собрать консольную программу ``catalog``.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
MACOS = sys.platform == "darwin"
WINDOWS = sys.platform == "win32"
#: Идентификатор пакета macOS: по нему система различает приложения.
BUNDLE_ID = "ru.set.catalogizer"
#: Корень проекта: скрипт лежит в scripts/.
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(Path(__file__).resolve().parent)) # для make_icon
DIST = ROOT / "dist"
BUILD = ROOT / "build"
ICON = ROOT / "ico" / ("catalog.icns" if MACOS else "catalog.ico")
#: Модули Qt, которые приложению не нужны. Без этого списка в сборку тянутся
#: QtWebEngine и 3D-стек — сотни мегабайт мимо цели.
UNUSED_QT_MODULES = (
"QtWebEngineCore", "QtWebEngineWidgets", "QtWebEngineQuick",
"QtQuick", "QtQuick3D", "QtQml", "Qt3DCore", "Qt3DRender",
"QtMultimedia", "QtBluetooth", "QtCharts", "QtDataVisualization",
"QtGraphs", "QtPdf", "QtSql", "QtTest", "QtNetwork", "QtSerialPort",
)
#: Что исключается всегда: tkinter в программе не используется, а PySide2
#: рядом с PySide6 ломает сборку.
COMMON_EXCLUDES = ("tkinter", "PySide2", "shiboken2", "matplotlib",
"numpy", "PIL", "pytest")
def check_environment() -> None:
"""@brief Проверяет, что в интерпретаторе есть всё нужное для сборки."""
missing = []
for module, package in (("PyInstaller", "pyinstaller"),
("PySide6", "PySide6"),
("pymupdf", "pymupdf")):
try:
__import__(module)
except ImportError:
missing.append(package)
if missing:
raise SystemExit(
"Не хватает пакетов: %s\nУстановите их:\n %s -m pip install %s"
% (", ".join(missing), sys.executable, " ".join(missing)))
def build_command(name: str, entry: Path, args: argparse.Namespace,
windowed: bool) -> list[str]:
"""@brief Собирает командную строку PyInstaller для одной цели."""
command = [
sys.executable, "-m", "PyInstaller",
"--noconfirm",
"--clean",
"--name", name,
"--distpath", str(DIST),
"--workpath", str(BUILD),
"--specpath", str(BUILD),
# Пакет catalogizer лежит в корне проекта и не обязан быть установлен.
"--paths", str(ROOT),
]
# На macOS .app — это всегда папка, поэтому onefile там смысла не имеет.
command.append("--onedir" if (args.onedir or (MACOS and windowed))
else "--onefile")
# Консоль скрыта: приложение оконное. С --console видны traceback и
# сообщения Qt — так ищут причину, если программа молча не стартует.
command.append("--console" if (args.console or not windowed) else "--windowed")
if ICON.exists():
command += ["--icon", str(ICON)]
if MACOS and windowed:
command += ["--osx-bundle-identifier", BUNDLE_ID]
for module in UNUSED_QT_MODULES:
command += ["--exclude-module", "PySide6.%s" % module]
for module in COMMON_EXCLUDES:
command += ["--exclude-module", module]
command.append(str(entry))
return command
def run(command: list[str]) -> None:
print(" ".join('"%s"' % c if " " in c else c for c in command), flush=True)
result = subprocess.run(command, cwd=str(ROOT))
if result.returncode:
raise SystemExit("PyInstaller завершился с кодом %d" % result.returncode)
def main() -> int:
parser = argparse.ArgumentParser(
description="Сборка автономного Каталогизатора "
"(exe в Windows, .app в macOS)")
parser.add_argument("--onedir", action="store_true",
help="папка вместо одного файла: запуск быстрее")
parser.add_argument("--console", action="store_true",
help="оставить консоль у оконной сборки (для отладки)")
parser.add_argument("--cli", action="store_true",
help="дополнительно собрать консольную программу catalog")
args = parser.parse_args()
check_environment()
if not ICON.exists():
# Иконки не хранятся в репозитории — они целиком рисуются кодом.
import make_icon
print("Рисую иконки…", flush=True)
make_icon.build_ico()
make_icon.build_icns()
if BUILD.exists():
shutil.rmtree(BUILD, ignore_errors=True)
run(build_command("Catalogizer", ROOT / "catalog_gui.py", args, windowed=True))
if args.cli:
run(build_command("catalog", ROOT / "catalog.py", args, windowed=False))
if MACOS:
# Загрузчик .app обязан быть исполняемым, даже если права потерялись.
binary = DIST / "Catalogizer.app" / "Contents" / "MacOS" / "Catalogizer"
if binary.exists():
binary.chmod(0o755)
print("\nГотово. Файлы в %s" % DIST)
return 0
if __name__ == "__main__":
raise SystemExit(main())