All formats

rarfile

LanguagePython
LicenseISC
Repositorygithub.com/markokr/rarfile
Documentationrarfile.readthedocs.io
Depends onan external decompressor: unrar (recommended), unar, 7z or bsdtar
Read / writeyes / no (RAR4 and RAR5, solid, multi-volume, encrypted)

rarfile reads RAR archives with the same interface as the standard zipfile module: RarFile(path), namelist(), infolist(), read(name), open(name). It parses the archive headers in pure Python, so listing pages works without any tool; reading file data runs an external decompressor in a subprocess.

unrar from RARLAB supports everything. The alternatives have gaps: bsdtar (libarchive) can’t extract every RAR, and 7-Zip builds without the RAR codec can’t extract at all. If no tool is found, read() raises rarfile.RarCannotExec.

Installation#

Shell
pip install rarfile
# plus a decompressor
sudo apt install unrar        # Debian/Ubuntu (non-free)
brew install rar              # macOS (includes unrar)

On Windows, put UnRAR.exe from RARLAB on PATH, or set rarfile.UNRAR_TOOL = r"C:\Program Files\WinRAR\UnRAR.exe".

Basic usage: list pages in natural order#

Python
import re
import sys
from pathlib import PurePosixPath

import rarfile

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(rf: rarfile.RarFile) -> list[str]:
    names = [i.filename for i in rf.infolist() if not i.is_dir() and is_page(i.filename)]
    return sorted(names, key=natural_key)


with rarfile.RarFile(sys.argv[1]) as rf:   # listing needs no external tool
    print("solid:", rf.is_solid())
    for n, name in enumerate(list_pages(rf), 1):
        print(f"{n:3} {name} ({rf.getinfo(name).file_size} bytes)")

How to extract the cover#

Python
with rarfile.RarFile("comic.cbr") as rf:
    pages = list_pages(rf)
    if pages:
        data = rf.read(pages[0])  # runs unrar
        with open("cover" + PurePosixPath(pages[0]).suffix.lower(), "wb") as f:
            f.write(data)

How to read ComicInfo.xml#

Python
import xml.etree.ElementTree as ET

with rarfile.RarFile("comic.cbr") as rf:
    name = next((n for n in rf.namelist() if n.lower() == "comicinfo.xml"), None)
    if name:
        root = ET.fromstring(rf.read(name))
        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(rf)[int(page.get("Image"))])  # index in sorted order

How to convert CBR to CBZ#

Each read() call starts a new unrar process, and in a solid archive each one decompresses everything before the requested file. For conversion, extract once to a temporary folder:

Python
import tempfile
import zipfile
from pathlib import Path

import rarfile


def cbr_to_cbz(cbr: str, cbz: str) -> None:
    with tempfile.TemporaryDirectory() as tmp, rarfile.RarFile(cbr) as rf:
        rf.extractall(tmp)  # one unrar run
        with zipfile.ZipFile(cbz, "w", compression=zipfile.ZIP_STORED) as zf:
            for info in rf.infolist():
                if not info.is_dir():
                    zf.write(Path(tmp) / info.filename, arcname=info.filename.replace("\\", "/"))


cbr_to_cbz("comic.cbr", "comic.cbz")

Tips#