py7zr
| Language | Python (3.10+) |
|---|---|
| License | LGPL-2.1-or-later |
| Repository | github.com/miurahr/py7zr |
| Documentation | py7zr.readthedocs.io |
| Depends on | Python packages (pycryptodomex, pyppmd, pybcj, brotli, inflate64, multivolumefile; backports.zstd before Python 3.14) |
| Read / write | yes / yes (LZMA, LZMA2, BZip2, Deflate, Zstd, PPMd, Copy; AES encryption) |
py7zr reads and writes 7z archives without the 7-Zip binary. SevenZipFile lists entries with getnames() or list(), extracts to disk with extract/extractall, and writes with write, writeall and writestr. It’s the library comicbox uses for CB7.
Version 1.0 removed the old read() and readall() methods. To get file contents in memory, pass a factory such as py7zr.io.BytesIOFactory to extract: py7zr writes each extracted file into an in-memory buffer you can read afterwards. After any extract call the archive object is at its end; call reset() before extracting again.
Installation#
pip install py7zr
Basic usage: list pages in natural order#
import re
import sys
from pathlib import PurePosixPath
import py7zr
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif", ".jxl"}
def is_page(name: str) -> bool:
name = name.replace("\\", "/")
if "__MACOSX/" in name:
return False
p = PurePosixPath(name)
return not p.name.startswith(".") and p.suffix.lower() in IMAGE_EXTS
def natural_key(name: str):
return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", name)]
def list_pages(z: py7zr.SevenZipFile) -> list[str]:
names = [f.filename for f in z.list() if not f.is_directory and is_page(f.filename)]
return sorted(names, key=natural_key)
with py7zr.SevenZipFile(sys.argv[1]) as z:
print("solid:", z.archiveinfo().solid)
for n, name in enumerate(list_pages(z), 1):
print(f"{n:3} {name}")
How to read pages into memory#
BytesIOFactory(limit) collects each extracted file in a buffer of at most limit bytes. Seek to the start before reading:
from py7zr.io import BytesIOFactory
MAX_PAGE = 100 * 1024 * 1024 # bytes; larger files are truncated
def read_files(path: str, names: list[str]) -> dict[str, bytes]:
factory = BytesIOFactory(MAX_PAGE)
with py7zr.SevenZipFile(path) as z:
z.extract(targets=names, factory=factory)
out = {}
for name in names:
buf = factory.get(name)
buf.seek(0)
out[name] = buf.read()
return out
Extract several pages in one call: in a solid archive every extract decodes the block from the start, so one call with all the pages you need is much faster than one call per page.
How to extract the cover#
with py7zr.SevenZipFile("comic.cb7") as z:
pages = list_pages(z)
if pages:
data = read_files("comic.cb7", [pages[0]])[pages[0]]
with open("cover" + PurePosixPath(pages[0]).suffix.lower(), "wb") as f:
f.write(data)
How to read ComicInfo.xml#
import xml.etree.ElementTree as ET
with py7zr.SevenZipFile("comic.cb7") as z:
names = z.getnames()
pages = list_pages(z)
info_name = next((n for n in names if n.lower() == "comicinfo.xml"), None)
if info_name:
root = ET.fromstring(read_files("comic.cb7", [info_name])[info_name])
print(root.findtext("Series"), "#" + (root.findtext("Number") or ""), root.findtext("Title"))
for page in root.iterfind("Pages/Page"):
if "FrontCover" in page.get("Type", ""):
print("cover:", pages[int(page.get("Image"))]) # index in sorted page order
How to create a CB7 from a folder of images#
Use the Copy filter (no compression) for JPEG/PNG pages. py7zr writes all files into one solid block, so with real compression every page read would decode the whole archive; with Copy that costs almost nothing.
from pathlib import Path
import py7zr
def make_cb7(folder: str, out: str) -> None:
files = sorted((p for p in Path(folder).iterdir() if p.is_file() and is_page(p.name)),
key=lambda p: natural_key(p.name))
with py7zr.SevenZipFile(out, "w", filters=[{"id": py7zr.FILTER_COPY}]) as z:
for i, p in enumerate(files, 1):
z.write(p, arcname=f"{i:03}{p.suffix.lower()}")
info = Path(folder) / "ComicInfo.xml"
if info.exists():
z.write(info, arcname="ComicInfo.xml")
make_cb7("Akira v01", "Akira v01.cb7")
For uncompressed source images (BMP, TIFF) use filters=[{"id": py7zr.FILTER_LZMA2, "preset": 9}] instead. z.writestr(data, "ComicInfo.xml") adds a file from a string or bytes.
Tips#
py7zr.is_7zfile(path)checks the signature; use it to catch.cb7files that are really ZIP or RAR.- Encrypted archives:
py7zr.SevenZipFile(path, password="secret");z.needs_password()tells you whether one is needed. - py7zr is pure Python and slower than the native 7-Zip. For large libraries, a native library (libarchive via a binding) or converting to CBZ once is faster.