如何在 Linux 上将 CHM 转换为单个 PDF:无垃圾和失效链接

发布: (2026年2月28日 GMT+8 16:04)
6 分钟阅读
原文: 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.htmldefault.htmlstart.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-lef

检查结果

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‑8merge.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 浏览
Back to Blog

相关文章

阅读更多 »

不糟糕的语义失效

缓存问题 如果你在 Web 应用上工作了一段时间,你就会了解缓存的情况。你加入缓存,一切都变快了,然后有人……