Как конвертировать CHM в один PDF на Linux: без мусора и битых ссылок
Если у тебя есть bsm_api.chm и нужно получить один нормальный PDF , а не «папку из 900 HTML + слёзы», вот рабочий пайплайн: CHM → HTML → (сборка) → PDF.
В конце будет:
- один manual.pdf
- без битых внутренних ссылок (насколько позволяет исходник)
- с нормальной навигацией/оглавлением (если CHM адекватный)
В чём проблема
CHM — это не “один файл с текстом”, а контейнер: HTML + картинки + оглавление + внутренние ссылки.
Типовые боли:
- конвертеры делают 1000 страниц вместо одного PDF
- ломают ссылки и кодировки
- получаешь PDF, который выглядит как скриншоты из 2007
Рабочее решение
1) Установка инструментов
Ubuntu/Debian:
sudo apt update
sudo apt install -y \
extractchm \
wkhtmltopdf \
python3 \
python3-bs4 \
python3-lxml
Если extractchm нет (редко, но бывает), ставим из chmlib:
sudo apt install -y chmlib-tools
потом будет утилита extract\_chmLib
Проверка:
extractchm -h || true
wkhtmltopdf --version
2) Распаковать CHM в HTML
Создай рабочую папку:
mkdir -p ~/work/chm2pdf/{src,html,out}
cp bsm\_api.chm ~/work/chm2pdf/src/
cd ~/work/chm2pdf
Распаковка:
Вариант A (extractchm):
extractchm -o html src/bsm\_api.chm
Вариант B (extract_chmLib):
extract\_chmLib src/bsm\_api.chm html
Проверка что распаковалось:
ls -la html | head
find html -maxdepth 2 -type f | head
3) Найти “точку входа” (главную страницу)
Обычно это index.html, default.html, start.html или что-то из оглавления.
Быстрый поиск кандидатов:
ls html | grep -iE 'index|default|start|main' || true
Если не очевидно — ищем самый “толстый” HTML:
find html -type f -iname '\*.html' -printf '%s\t%p\n' | sort -n | tail -20
4) Собрать единый HTML (важный шаг)
wkhtmltopdf лучше работает, когда ты отдаёшь ему одну HTML-страницу , а не тысячу.
Сделаем “склейку”: берём список страниц и собираем в out/merged.html.
Вот минимальный Python-скрипт, который:
- читает оглавление (если найдём файл типа .hhc/toc)
- если оглавления нет — склеивает HTML по алфавиту (хуже, но работает)
- вырезает мусорные script/iframe
- приводит относительные ссылки к локальным путям
Создай файл tools/merge.py:
#!/usr/bin/env python3
import os
import re
from pathlib import Path
from bs4 import BeautifulSoup
ROOT = Path("html").resolve()
OUT = Path("out")
OUT.mkdir(parents=True, exist\_ok=True)
def list\_html\_files():
1) Если есть оглавление — можно допилить парсер под конкретный CHM
2) Универсальный fallback — склеим HTML по имени
files = sorted(ROOT.rglob("\*.html"))
выкинем дубли/служебное
files = [p for p in files if p.is\_file() and p.stat().st\_size > 200]
return files
def clean\_body(html\_path: Path) -> str:
data = html\_path.read\_bytes()
грубая попытка с кодировкой
for enc in ("utf-8", "cp1251", "windows-1251", "latin-1"):
try:
text = data.decode(enc)
break
except UnicodeDecodeError:
continue
else:
text = data.decode("utf-8", "ignore")
soup = BeautifulSoup(text, "lxml")
выкидываем потенциальный мусор
for tag in soup(["script", "iframe", "noscript"]):
tag.decompose()
body = soup.body or soup
нормализуем якоря и локальные ресурсы
for a in body.find\_all("a", href=True):
href = a["href"].strip()
убираем внешние и mailto
if href.startswith(("http://", "https://", "mailto:")):
continue
normalize windows slashes
a["href"] = href.replace("\\", "/")
добавим заголовок-разделитель
title = soup.title.get\_text(strip=True) if soup.title else html\_path.name
header = f"
# {title}
\n"
return header + str(body)
def main():
parts = []
for p in list\_html\_files():
try:
parts.append(clean\_body(p))
except Exception as e:
print(f"skip {p}: {e}")
merged = f"""<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>CHM merged</title>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.45; }}
pre, code {{ white-space: pre-wrap; word-wrap: break-word; }}
h1 {{ page-break-before: always; }}
</style>
</head>
<body>
{''.join(parts)}
</body>
</html>
"""
(OUT / "merged.html").write\_text(merged, encoding="utf-8")
print("OK -> out/merged.html")
if \_\_name\_\_ == "\_\_main\_\_":
main()
Запуск:
mkdir -p tools out
python3 tools/merge.py
ls -la out/merged.html
5) Конвертировать merged.html → PDF
wkhtmltopdf \
--enable-local-file-access \
--encoding utf-8 \
--page-size A4 \
--margin-top 12 --margin-bottom 12 --margin-left 10 --margin-right 10 \
out/merged.html out/manual.pdf
Проверка результата:
ls -lah out/manual.pdf
file out/manual.pdf
Проверка результата
Минимум:
- PDF открывается, вес адекватный (не 2 KB и не 2 GB)
- есть текст (не “картинки страниц”)
- код/таблицы читаются
Быстрый sanity-check: извлечь пару строк текста:
python3 - <<'PY'
import subprocess, sys
p = subprocess.run(["pdftotext", "out/manual.pdf", "-"], capture\_output=True, text=True)
print(p.stdout[:800])
PY
Если pdftotext нет:
sudo apt install -y poppler-utils
Типичные ошибки
❌ 1) “Blocked access to file” / не видит картинки
Причина: wkhtmltopdf по умолчанию режет локальные ресурсы.
Решение: добавь --enable-local-file-access (в примере уже есть).
❌ 2) Кракозябры вместо русского
Причина: HTML в CP1251, а ты склеил как UTF-8.
Решение:
- в merge.py мы пробуем cp1251/windows-1251
- если всё равно плохо — найди реальную кодировку исходников:
file -i html/\*.html | head
❌ 3) “Оглавление” нет, порядок глав сломан
Причина: CHM хранит структуру в .hhc/.hhk, а мы склеили “по алфавиту”.
Решение: дописать парсер оглавления под конкретный CHM (реально 20–40 строк, если структура нормальная).
Если хочешь — кидай список файлов из html/ (без содержимого), я подскажу точечно.
Где применять
- Локально: быстро получить PDF, отдать в LLM/коллегам/заказчику.
- CI/CD: автогенерация документации в PDF (если CHM как артефакт).
- VPS: можно крутить без GUI, чисто консолью.
Смотри также
Если будешь ковыряться с файлами и поиском по дереву — пригодится:
- Поиск файлов в Linux: команды для Ubuntu/CentOS — диагностика и find
- find: поиск больших файлов — когда PDF внезапно 900MB
- rsync: безопасное копирование с прогрессом — перенос распакованного CHM или готового PDF

Top comments (0)