All formats

pychm

LanguagePython 3.6+ (C extension)
LicenseGPL-2.0-or-later
Repositorygithub.com/dottedmag/pychm
DocumentationREADME and docstrings in chm/chm.py
Depends onCHMLib (C library)
Read / writeyes / no

pychm gives Python access to CHM files through CHMLib. It has 3 layers:

Paths and contents are bytes. The project is in maintenance mode: it accepts only bug and security fixes. It’s used by archmage and other CHM tools.

Installation#

Install CHMLib with headers, then pychm:

Shell
# Debian / Ubuntu
sudo apt install libchm-dev
# macOS
brew install chmlib

pip install pychm

Basic usage: open a CHM and read the home page#

Python
from chm.chm import CHMFile

f = CHMFile()
if not f.LoadCHM("help.chm"):
    raise SystemExit("cannot open help.chm")

print("title:", f.title.decode("latin-1"))   # bytes in the CHM's code page
print("home: ", f.home)                       # e.g. b'/index.html'

result, ui = f.ResolveObject(f.home)
if result == 0:                               # 0 means found
    size, content = f.RetrieveObject(ui)
    print(content[:200])

f.CloseCHM()

How to list all files in a CHM#

Python
import chm.chmlib as chmlib
from chm.chm import CHMFile

f = CHMFile()
f.LoadCHM("help.chm")

paths = []
def collect(chm_file, ui, context):
    paths.append((ui.path, ui.length))
    return chmlib.CHM_ENUMERATOR_CONTINUE

chmlib.chm_enumerate(f.file, chmlib.CHM_ENUMERATE_NORMAL | chmlib.CHM_ENUMERATE_FILES, collect, None)
for path, length in paths:
    print(length, path.decode("utf-8", "replace"))
f.CloseCHM()

chm_enumerate_dir(f.file, b"/images/", ...) lists one folder.

How to extract all files from a CHM#

Python
import os
import chm.chmlib as chmlib
from chm.chm import CHMFile

f = CHMFile()
f.LoadCHM("help.chm")

def extract(chm_file, ui, context):
    if ui.length == 0 or not ui.path.startswith(b"/"):
        return chmlib.CHM_ENUMERATOR_CONTINUE
    rel = ui.path.decode("utf-8", "replace").lstrip("/")
    dest = os.path.normpath(os.path.join("out", rel))
    if not dest.startswith("out" + os.sep):       # skip paths that escape the folder
        return chmlib.CHM_ENUMERATOR_CONTINUE
    os.makedirs(os.path.dirname(dest), exist_ok=True)
    size, data = f.RetrieveObject(ui)
    with open(dest, "wb") as out:
        out.write(data)
    return chmlib.CHM_ENUMERATOR_CONTINUE

chmlib.chm_enumerate(f.file, chmlib.CHM_ENUMERATE_NORMAL | chmlib.CHM_ENUMERATE_FILES, extract, None)
f.CloseCHM()

How to get the table of contents#

GetTopicsTree() returns the raw .hhc file: HTML with <object type="text/sitemap"> entries. Parse it with any HTML parser:

Python
from html.parser import HTMLParser
from chm.chm import CHMFile

class Sitemap(HTMLParser):
    def __init__(self):
        super().__init__()
        self.depth, self.item, self.items = 0, None, []
    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "ul":
            self.depth += 1
        elif tag == "object" and a.get("type", "").lower() == "text/sitemap":
            self.item = {"depth": self.depth}
        elif tag == "param" and self.item is not None:
            self.item[a.get("name", "").lower()] = a.get("value", "")
    def handle_endtag(self, tag):
        if tag == "ul":
            self.depth -= 1
        elif tag == "object" and self.item is not None:
            self.items.append(self.item)
            self.item = None

f = CHMFile()
f.LoadCHM("help.chm")
hhc = f.GetTopicsTree()                  # bytes or None
if hhc:
    p = Sitemap()
    p.feed(hhc.decode("latin-1"))        # use the CHM's encoding (f.GetEncoding())
    for it in p.items:
        print("  " * it["depth"], it.get("name"), "->", it.get("local"))
f.CloseCHM()

GetIndex() returns the .hhk keyword index in the same format.

How to search a CHM#

CHM files compiled with full-text search contain a $FIftiMain index:

Python
f = CHMFile()
f.LoadCHM("help.chm")
if f.IsSearchable():
    partial, results = f.Search(b"install")   # {title: path}
    for title, path in results.items():
        print(title.decode("latin-1"), path.decode("latin-1"))
f.CloseCHM()

Tips#