All formats

zipfile

LanguagePython
LicensePSF License (Python standard library)
Repositorygithub.com/python/cpython
Documentationdocs.python.org/3/library/zipfile.html
Depends onnothing (standard library; zlib, bz2, lzma modules for compression)
Read / writeyes / yes (ZIP, ZIP64; reads legacy ZipCrypto, no AES)

zipfile is Python’s built-in ZIP module. ZipFile.namelist() returns every entry name from the central directory, and zf.read(name) decompresses one entry, so you can show page 200 without touching the others. Writing with ZIP_STORED creates a CBZ without recompressing images.

Everything comic-specific (natural sort, cover, ComicInfo.xml) takes a few lines, shown below. For metadata in all comic formats, or CBR/CB7/CBT support, use comicbox.

Installation#

Nothing to install: import zipfile.

Basic usage: list pages in natural order#

Python
import re
import sys
import zipfile
from pathlib import PurePosixPath

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):
    # "Page 10.jpg" -> ["page ", 10, ".jpg"]: digit runs compare as numbers
    return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", name)]


def list_pages(zf: zipfile.ZipFile) -> list[str]:
    names = [i.filename for i in zf.infolist() if not i.is_dir() and is_page(i.filename)]
    return sorted(names, key=natural_key)


with zipfile.ZipFile(sys.argv[1]) as zf:
    for n, name in enumerate(list_pages(zf), 1):
        print(f"{n:3} {name} ({zf.getinfo(name).file_size} bytes)")

The natural_key trick (split on digit runs, convert them to int) is the standard way to sort pages in Python. The natsort package does the same with more edge cases handled.

How to extract the cover#

The cover is the first page in natural order:

Python
with zipfile.ZipFile("comic.cbz") as zf:
    pages = list_pages(zf)
    if pages:
        cover = pages[0]
        data = zf.read(cover)
        with open("cover" + PurePosixPath(cover).suffix.lower(), "wb") as f:
            f.write(data)

To make a thumbnail, open the bytes with Pillow: Image.open(io.BytesIO(data)).thumbnail((300, 450)).

How to read ComicInfo.xml#

Python
import xml.etree.ElementTree as ET


def read_comic_info(zf: zipfile.ZipFile) -> dict | None:
    name = next((n for n in zf.namelist() if n.lower() == "comicinfo.xml"), None)
    if name is None:
        return None
    root = ET.fromstring(zf.read(name))
    info = {child.tag: (child.text or "").strip() for child in root if child.tag != "Pages"}
    info["Pages"] = [dict(p.attrib) for p in root.iterfind("Pages/Page")]
    return info


with zipfile.ZipFile("comic.cbz") as zf:
    info = read_comic_info(zf)
    if info:
        print(f"{info.get('Series')} #{info.get('Number')}: {info.get('Title')}")
        print("right to left:", info.get("Manga") == "YesAndRightToLeft")
        # Image is the 0-based index into the sorted page list
        covers = [int(p["Image"]) for p in info["Pages"] if "FrontCover" in p.get("Type", "")]
        if covers:
            print("cover:", list_pages(zf)[covers[0]])

ElementTree doesn’t resolve external entities, so it’s safe for untrusted ComicInfo.xml. For maximum safety with hostile input use the defusedxml package.

How to create a CBZ from a folder of images#

Python
import zipfile
from pathlib import Path


def make_cbz(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 zipfile.ZipFile(out, "w", compression=zipfile.ZIP_STORED) as zf:
        for i, p in enumerate(files, 1):
            zf.write(p, arcname=f"{i:03}{p.suffix.lower()}")  # 001.jpg, 002.jpg, ...
        info = Path(folder) / "ComicInfo.xml"
        if info.exists():
            zf.write(info, arcname="ComicInfo.xml", compress_type=zipfile.ZIP_DEFLATED)


make_cbz("Batman 001", "Batman 001.cbz")

How to add or replace ComicInfo.xml#

zipfile can’t delete or replace an entry in place. Copy the archive to a new file, skipping the old metadata:

Python
import os
import zipfile


def set_comic_info(cbz: str, xml: str) -> None:
    tmp = cbz + ".tmp"
    with zipfile.ZipFile(cbz) as src, zipfile.ZipFile(tmp, "w") as dst:
        for item in src.infolist():
            if item.filename.lower() != "comicinfo.xml":
                dst.writestr(item, src.read(item))  # keeps the entry's compression
        dst.writestr("ComicInfo.xml", xml, compress_type=zipfile.ZIP_DEFLATED)
    os.replace(tmp, cbz)

Tips#