DEV Community

Андрей Викулов (VProger)
Андрей Викулов (VProger)

Posted on • Originally published at viku-lov.ru on

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

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

Как конвертировать 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

Enter fullscreen mode Exit fullscreen mode

Если extractchm нет (редко, но бывает), ставим из chmlib:


sudo apt install -y chmlib-tools

потом будет утилита extract\_chmLib

Enter fullscreen mode Exit fullscreen mode

Проверка:


extractchm -h || true

wkhtmltopdf --version

Enter fullscreen mode Exit fullscreen mode

2) Распаковать CHM в HTML

Создай рабочую папку:


mkdir -p ~/work/chm2pdf/{src,html,out}

cp bsm\_api.chm ~/work/chm2pdf/src/

cd ~/work/chm2pdf

Enter fullscreen mode Exit fullscreen mode

Распаковка:

Вариант A (extractchm):


extractchm -o html src/bsm\_api.chm

Enter fullscreen mode Exit fullscreen mode

Вариант B (extract_chmLib):


extract\_chmLib src/bsm\_api.chm html

Enter fullscreen mode Exit fullscreen mode

Проверка что распаковалось:


ls -la html | head

find html -maxdepth 2 -type f | head

Enter fullscreen mode Exit fullscreen mode

3) Найти “точку входа” (главную страницу)

Обычно это index.html, default.html, start.html или что-то из оглавления.

Быстрый поиск кандидатов:


ls html | grep -iE 'index|default|start|main' || true

Enter fullscreen mode Exit fullscreen mode

Если не очевидно — ищем самый “толстый” HTML:


find html -type f -iname '\*.html' -printf '%s\t%p\n' | sort -n | tail -20

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Запуск:


mkdir -p tools out

python3 tools/merge.py

ls -la out/merged.html

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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


ls -lah out/manual.pdf

file out/manual.pdf

Enter fullscreen mode Exit fullscreen mode

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

Минимум:

  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

Enter fullscreen mode Exit fullscreen mode

Если pdftotext нет:


sudo apt install -y poppler-utils

Enter fullscreen mode Exit fullscreen mode

Типичные ошибки

❌ 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

Enter fullscreen mode Exit fullscreen mode

❌ 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

Read more on viku-lov.ru

Top comments (0)