← Назад в блог

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

Пошагово: распаковать CHM в HTML, почистить ссылки и собрать один PDF через wkhtmltopdf. Диагностика, типовые ошибки, проверка результата.

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

Требования

  • Linux (Ubuntu/Debian предпочтительно)
  • Доступ к консоли (ssh/локально)
  • Утилиты: extract_chmLib + wkhtmltopdf

Как конвертировать 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"<h1>{title}</h1>\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

Проверка результата

Минимум:

  1. PDF открывается, вес адекватный (не 2 KB и не 2 GB)
  2. есть текст (не “картинки страниц”)
  3. код/таблицы читаются

Быстрый 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, чисто консолью.

Смотри также

Если будешь ковыряться с файлами и поиском по дереву — пригодится:

0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!