rarfile
| Language | Python |
|---|---|
| License | ISC |
| Repository | github.com/markokr/rarfile |
| Documentation | rarfile.readthedocs.io |
| Depends on | an external decompressor: unrar (recommended), unar, 7z or bsdtar |
| Read / write | yes / 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#
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#
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#
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#
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:
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#
rarfile.is_rarfile(path)checks the signature; many.cbrfiles are really ZIP, so fall back tozipfilewhen it returnsFalse.- Password-protected archives:
rf.setpassword("secret"), or checkrf.needs_password()first. rf.open(name)returns a file-like object that streams from the tool’s output, useful for large pages.- For untrusted uploads, check names in
infolist()for..and absolute paths before callingextractall.