Пишем huntZip — Как я написал библиотеку для архивов на Python

от автора

Hello world!

Как‑то раз мне понадобилось работать с архивами в одном своем проекте, и я знатно прифигел от стандартных инструментов. Возник такой конфуз: для обычного ZIP нужен zipfile, для какого нибудь TAR tarfile, а если понадобится добавить 7z или RAR, то вообще нужны py7zr и rarfile, из за этого код превращается в странный и «не очень красивый» забор из импортов и кусков логики под каждый чих. Меня это ооочень сильно выбесило, и буквально 2–3 дня назад я сел писать свой «комбайн» для архивов, который назвал huntZip.

Какие вообще архивы поддерживаются?

  • ZIP (так же включая Zip64): Поддерживается распаковка и запаковка

  • TAR (а так же его версии GZip, Bzip2, XZ): Полноценная поддержка

  • RAR: В разработке (Попробую добавить запаковку, по поводу распаковки пока не знаю)

Тесты скорости (бенчмарки)

Я решил проверить скорость моей библиотеки по сравнению с zipfile, и пришел к выводу что huntZip быстрее zipfile в 1.26 раз. Все тесты я проводил на Ryzen 5 (Но скорее всего будет так же быстро работать и на других ПК)

  • huntZip: 0.334 сек.

  • zipfile: 0.421 сек.

    Почему так быстро? Дело в том, что я использовал метод f.relative_to(source) и подключил Zip64 через allowZip64=True.

    И вместо старого и уже многим известного метода os.walk, который выдает кучу лишних строк и усложняет код, я использую метод source.rglob("*") из библиотеки pathlib. Он работает как ленивый генератор. Это значит, что Python не загружает весь список файлов папки в память сразу, а перебирает их по одному на лету.

    По поводу f.relative_to(source) — f.relative_to(source) находит относительный путь файла внутри папки. Из‑за этого структура архива получается чистой, без дублирования лишних родительских путей в самом ZIP‑файле.

Как скачать библиотеку?

Скачать вы можете в PyPI по простецкой команде:

pip install huntzip

Либо в разделе Releases на Codeberg

А как вообще работать с библиотекой?

Возьмем в пример ZIP:

from huntzip import pack, unpackpack("my_project", "backup.zip")

(Для других форматов просто меняем расширение.)

А если захотели распаковать:

unpack("backup.7z", "extracted_ready")

(либо вместо 7z поставьте ваш формат)

Как это устроено внутри?

Код получился очень простым и прозрачным. Я собираю расширения через pathlib и вызываю нужный контекстный менеджер под капотом:

import osimport tarfileimport zipfilefrom pathlib import Pathimport py7zrdef pack(source_path: str, archive_name: str) -> str:    source = Path(source_path)    if not source.exists():        raise FileNotFoundError(f"Path not found: {source_path}")    archive = Path(archive_name)    ext = "".join(archive.suffixes).lower()    if ext == ".zip":        with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, allowZip64=True) as zipf:            if source.is_file():                zipf.write(source, source.name)            else:                for f in source.rglob("*"):                    if f.is_file():                        zipf.write(f, f.relative_to(source))    elif ext == ".7z":        with py7zr.SevenZipFile(archive, "w") as sz:            if source.is_file():                sz.write(source, source.name)            else:                sz.writeall(source, source.name)    elif ".tar" in ext or ext.endswith((".gz", ".bz2", ".xz")):        mode = "w"        if ext.endswith(".gz"):            mode = "w:gz"        elif ext.endswith(".bz2"):            mode = "w:bz2"        elif ext.endswith(".xz"):            mode = "w:xz"        with tarfile.open(archive, mode) as tar:            tar.add(source, arcname=source.name)    else:        raise ValueError(f"Unsupported format: {ext}")    return str(archive.resolve())def unpack(archive_path: str, extract_to: str = ".") -> str:    archive = Path(archive_path)    if not archive.exists():        raise FileNotFoundError(f"Archive not found: {archive_path}")    target_dir = Path(extract_to)    target_dir.mkdir(parents=True, exist_ok=True)    ext = "".join(archive.suffixes).lower()    if ext == ".zip":        with zipfile.ZipFile(archive, "r", allowZip64=True) as zipf:            zipf.extractall(target_dir)    elif ext == ".7z":        with py7zr.SevenZipFile(archive, "r") as sz:            sz.extractall(path=target_dir)    elif ".tar" in ext or ext.endswith((".gz", ".bz2", ".xz")):        with tarfile.open(archive, "r:*") as tar:            try:                tar.extractall(target_dir, filter='fully_trusted')            except TypeError:                tar.extractall(target_dir)    else:        raise ValueError(f"Unknown archive format: {ext}")    return str(target_dir.resolve())

Что дальше, и проблема с RAR

Проект развивается, но я есть юридический тупик с RAR.

Изначально хотел сделать и чтение, и запись. Но формат проприетарный, алгоритм сжатия принадлежит WinRAR. Бесплатно и легально можно только распаковать файлы. Напишешь свою запаковку…И прилетит DMCA, а тем самым репозиторий удалят.

Поэтому планы такие:

  1. RAR только на распаковку: Скоро добавлю распаковку через rarfile. При попытке запаковать в .rar библиотека просто выдаст ошибку.

  2. Индикатор прогресса: Сделаю коллбеки, чтобы при сборке больших архивов в консоли бежала полоса загрузки.

Как помочь проекту?

Код открытый, и я буду рад любой помощи. Если хотите докинуть фичу:

  1. Форкайте репозиторий на Codeberg.

  2. Делайте ветку с вашей фичей.

  3. Пишите тесты.

  4. Присылайте Pull Request.

ссылка на оригинал статьи https://habr.com/ru/articles/1072576/