tarfile
| Language | Python |
|---|---|
| License | PSF License (Python standard library) |
| Repository | github.com/python/cpython |
| Documentation | docs.python.org/3/library/tarfile.html |
| Depends on | nothing (standard library) |
| Read / write | yes / yes (ustar, GNU, pax; gzip/bz2/xz/zstd compressed tar) |
tarfile is Python’s built-in TAR module. tarfile.open(path) reads the archive; getmembers() scans all headers once and returns TarInfo objects with names, sizes and data offsets, and extractfile(member) then seeks straight to a member’s data. For an uncompressed CBT that gives random access after one quick scan. Writing with mode "w" produces a plain, uncompressed tar.
Installation#
Nothing to install: import tarfile.
Basic usage: list pages in natural order#
Python
import re
import sys
import tarfile
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 # also skips ._001.jpg
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(tf: tarfile.TarFile) -> list[tarfile.TarInfo]:
pages = [m for m in tf.getmembers() if m.isfile() and is_page(m.name)]
return sorted(pages, key=lambda m: natural_key(m.name))
with tarfile.open(sys.argv[1]) as tf:
for n, m in enumerate(list_pages(tf), 1):
print(f"{n:3} {m.name} ({m.size} bytes)")
tarfile.open without a mode detects compression too; use mode="r:" to accept only plain tar, which is what a CBT should be.
How to extract the cover#
Python
with tarfile.open("comic.cbt") as tf:
pages = list_pages(tf)
if pages:
data = tf.extractfile(pages[0]).read() # seeks to the member's data
with open("cover" + PurePosixPath(pages[0].name).suffix.lower(), "wb") as f:
f.write(data)
How to read ComicInfo.xml#
Python
import xml.etree.ElementTree as ET
with tarfile.open("comic.cbt") as tf:
info = next((m for m in tf.getmembers() if m.isfile() and m.name.lower() == "comicinfo.xml"), None)
if info:
root = ET.fromstring(tf.extractfile(info).read())
print(root.findtext("Series"), "#" + (root.findtext("Number") or ""), root.findtext("Title"))
print("right to left:", root.findtext("Manga") == "YesAndRightToLeft")
for page in root.iterfind("Pages/Page"):
if "FrontCover" in page.get("Type", ""):
print("cover:", list_pages(tf)[int(page.get("Image"))].name) # index in sorted order
How to create a CBT from a folder of images#
Python
import tarfile
from pathlib import Path
def make_cbt(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 tarfile.open(out, "w", format=tarfile.PAX_FORMAT) as tf: # "w" = no compression
for i, p in enumerate(files, 1):
ti = tf.gettarinfo(p, arcname=f"{i:03}{p.suffix.lower()}")
ti.uname = ti.gname = "" # don't store local user names
with open(p, "rb") as f:
tf.addfile(ti, f)
info = Path(folder) / "ComicInfo.xml"
if info.exists():
tf.add(info, arcname="ComicInfo.xml")
make_cbt("Bone 01", "Bone 01.cbt")
Tips#
- When extracting to disk, pass a filter:
tf.extractall(dest, filter="data")rejects absolute paths,..and special files. Python 3.14 makes"data"the default. tarfile.open(fileobj=stream, mode="r|")reads a CBT from a non-seekable stream in one forward pass (nogetmembers()random access).tarfile.is_tarfile(path)checks whether a file is a tar; use it to catch.cbtfiles that are really ZIP.