Как конвертировать CHM в один PDF на Linux: без мусора и битых ссылок

Published: (February 28, 2026 at 03:04 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Если у тебя есть bsm_api.chm и нужно получить один нормальный PDF, а не «папку из 900 HTML + слёзы», вот рабочий пайплайн:

CHM → HTML → (сборка) → PDF

В итоге будет:

  • один manual.pdf;
  • без битых внутренних ссылок (насколько позволяет исходник);
  • с нормальной навигацией/оглавлением (если CHM адекватный).

В чём проблема

CHM — это не «один файл с текстом», а контейнер: HTML + картинки + оглавление + внутренние ссылки.

Типовые боли:

ПроблемаПочему происходит
Конвертеры делают 1000 страниц вместо одного PDFОбрабатывают каждый HTML‑файл отдельно
Ломают ссылки и кодировкиНе учитывают относительные пути и разные charset
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‑страницу, а не тысячу.
Ниже – минимальный 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():
    """
    Универсальный fallback — склеим HTML по имени.
    При желании можно добавить парсер .hhc/.hhk.
    """
    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
        a["href"] = href.replace("\\", "/")   # windows‑style слеши

    # добавляем заголовок‑разделитель
    title = soup.title.get_text(strip=True) if soup.title else html_path.name
    header = f"\n## {title}\n\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 = """

CHM merged

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; }

""" + "".join(parts) + """

"""
    (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

Быстрый sanity‑check: извлечь пару строк текста из PDF.

python3 - <<'PY'
import subprocess
p = subprocess.run(
    ["pdftotext", "out/manual.pdf", "-"],
    capture_output=True, text=True
)
print(p.stdout[:800])
PY

Если pdftotext отсутствует:

sudo apt install -y poppler-utils

Типичные ошибки и их решения

ОшибкаПричинаРешение
Blocked access to file / картинки не видныwkhtmltopdf по умолчанию блокирует локальные ресурсыДобавь --enable-local-file-access (см. выше)
❌ Кракозябры вместо русскогоHTML в CP1251, а склеили как UTF‑8В merge.py уже пробуем cp1251/windows-1251. Если всё равно плохо – проверь реальную кодировку: `file -i html/*.html
❌ Оглавление отсутствует, порядок глав сломанCHM хранит структуру в .hhc/.hhk, а мы склеили «по алфавиту»Напиши парсер оглавления под конкретный CHM (обычно 20‑40 строк). Если нужна помощь – пришли список файлов из html/, подскажу точечно.

Где применять

  • Локально – быстро получить PDF, отдать в LLM/коллегам/заказчику.
  • CI/CD – автогенерация документации в PDF (если CHM выступает артефактом).
  • VPS – можно крутить без GUI, чисто консолью.

Смотрите также

  • Поиск файлов в Linux: команды для Ubuntu/CentOS — диагностика и find.
  • find: поиск больших файлов — когда PDF внезапно «взросл».

Если понадобится доработать скрипт под конкретный CHM – дайте знать, помогу адаптировать парсер оглавления.


rsync: безопасное копирование с прогрессом — перенос распакованного CHM или готового PDF

Read more on viku‑lov.ru

0 views
Back to Blog

Related posts

Read more »