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

Как‑то раз мне понадобилось работать с архивами в одном своем проекте, и я знатно прифигел от стандартных инструментов. Возник такой конфуз: для обычного 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, unpack
pack("my_project", "backup.zip")(Для других форматов просто меняем расширение.)
А если захотели распаковать:
unpack("backup.7z", "extracted_ready")(либо вместо 7z поставьте ваш формат)
Как это устроено внутри?
Код получился очень простым и прозрачным. Я собираю расширения через pathlib и вызываю нужный контекстный менеджер под капотом:
import os
import tarfile
import zipfile
from pathlib import Path
import py7zr
def 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, а тем самым репозиторий удалят.
Поэтому планы такие:
RAR только на распаковку: Скоро добавлю распаковку через
rarfile. При попытке запаковать в.rarбиблиотека просто выдаст ошибку.Индикатор прогресса: Сделаю коллбеки, чтобы при сборке больших архивов в консоли бежала полоса загрузки.
Как помочь проекту?
Код открытый, и я буду рад любой помощи. Если хотите докинуть фичу:
Форкайте репозиторий на Codeberg.
Делайте ветку с вашей фичей.
Пишите тесты.
Присылайте Pull Request.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.