pychm
| Language | Python 3.6+ (C extension) |
|---|---|
| License | GPL-2.0-or-later |
| Repository | github.com/dottedmag/pychm |
| Documentation | README and docstrings in chm/chm.py |
| Depends on | CHMLib (C library) |
| Read / write | yes / no |
pychm gives Python access to CHM files through CHMLib. It has 3 layers:
chm.chm.CHMFile: high-level class.LoadCHM()opens a file and reads its title, home page, table of contents (.hhc) and index (.hhk) locations from the internal#SYSTEMand#WINDOWSfiles. It reads documents and searches the built-in full-text index.chm.chmlib: thin wrappers of the C functions (chm_enumerate,chm_resolve_object,chm_retrieve_object).chm.extra: full-text search and encoding detection helpers.
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#
- Text inside a CHM uses the Windows code page of the author’s language (cp1252, cp936 for Simplified Chinese, and so on).
f.GetEncoding()andf.GetLCID()help pick the right codec. ResolveObjectreturns(0, ui)on success and(1, None)when the path doesn’t exist.