☰
CHM转PDF完整指南:从解包到书签生成与中文乱码解决
2026/10/14 20:12:18 网站建设 项目流程

简介:CHM2PDF是一款面向技术文档阅读者与资料整理者的格式转换工具,专门解决CHM帮助文件在非Windows平台阅读不便、分享受限的问题。CHM格式虽体积小、便于分发,但阅读体验和跨平台兼容性欠佳,而PDF作为开放标准能保留原始布局、字体与图像,并支持书签、注释与加密,因此将CHM转为PDF成为不少用户的实际需求。本资源包共11个文件,以exe可执行程序、dll动态库、txt说明文档为主,另含htm与chm帮助文件及url快捷方式,压缩包约1.86MB,涵盖主程序、转换组件与使用说明,结构紧凑。已有715人学习下载。借助该工具,用户可将CHM中的文本、图像、表格与超链接导出为PDF,保留章节结构和内链,便于在电脑、平板或手机上一致阅读与分享;说明文档还提示了源文件完整性、图片质量、超链接维护与排版一致性等注意事项,适合需要批量处理技术手册与电子书的读者参考使用。

1. CHM 转 PDF 这件事,卡住你的从来不是“转”这个动作

手里有一份 CHM 帮助文档,想把它变成能随手翻、能全文检索、能丢进平板看的 PDF——听起来是个五分钟的小需求,真动手才发现处处是坑:目录树点进去是空的、中文全是方块、代码块被截断、图片丢了一半。CHM 转 PDF 这个方向,本质上是把一个微软早期的压缩 HTML 帮助格式,还原成一套可打印的静态文档,再交给排版引擎重新分页。它解决的不是“格式转换”这么简单,而是“把一份带层级目录、带索引、带内嵌资源的离线文档,完整搬到另一个载体上”。适合谁?做技术文档归档的、要给客户交付离线手册的、手里攒了一堆老帮助文件想统一管理的从业者。下面这套路径,是我自己反复跑过、能复现的方案,不依赖任何在线服务。

2. 先搞懂 CHM 里到底装了什么:拆包比转换更重要

很多人一上来就找“一键 CHM 转 PDF 工具”,结果转出来目录全乱。问题出在跳过了拆包这一步。CHM 不是单一文件,它是一个压缩容器,内部结构决定了后面能不能还原出正确的层级和顺序。

2.1 CHM 的内部结构:三个关键文件决定成败

一个标准 CHM 解包后,你会看到一堆 .htm 文件、图片、样式表,外加几个不起眼但至关重要的元数据文件:

文件作用缺失后果
.hhc目录树定义,决定章节层级和顺序转出的 PDF 没有书签,章节乱序
.hhk索引关键字,用于全文检索锚点检索功能丢失
.hhp工程文件,记录编译时的默认页和窗口定义首页可能定位错误

常见做法是先用命令行工具把 CHM 完整解包,而不是直接喂给转换器。我一般用7z或extract_chmLib,前者跨平台,后者对中文路径更稳。

# 用 7z 解包 CHM,-o 指定输出目录 7z x manual.chm -o./chm_extracted/ # 查看解包结果,确认 .hhc 和 .hhk 是否存在 ls -la ./chm_extracted/ | grep -E "\.(hhc|hhk|hhp)$"

逻辑说明:7z x是解压并保留目录结构,-o后面跟输出路径,注意不要有空格。解包后第一件事不是急着转,而是确认.hhc文件在不在——它决定了后面 PDF 书签能不能自动生成。如果.hhc缺失,说明这个 CHM 编译时就没做目录,后面只能靠 HTML 里的标题层级手动重建。

参数说明:7z对 CHM 的支持依赖p7zip-full包,部分精简系统需要先装。如果解包报错“Can not open file as archive”,换extract_chmLib再试,它对非标准 CHM 的兼容性更好。

2.2 为什么不能直接“打印成 PDF”:分页与资源路径的坑

有人图省事,用浏览器打开 CHM 解包后的首页,然后 Ctrl+P 打印成 PDF。这个做法在纯文字文档上勉强能用,但一旦有代码块、表格、图片,就会翻车:浏览器打印不会自动处理跨页表格的断行,代码块会被拦腰截断,图片路径如果是相对路径且没解包完整,直接显示裂图。

更隐蔽的问题是分页。CHM 的每个 .htm 是独立页面,浏览器打印时会把它们当成连续流,但页眉页脚、页码、章节起始页全都不受控。我见过最离谱的情况是:一份 300 页的 CHM,打印出来 280 页,中间 20 页因为图片加载失败被浏览器自动跳过了。

所以正确的路径是:解包 → 合并 HTML → 交给排版引擎重新分页。排版引擎我推荐 WeasyPrint 或 wkhtmltopdf,前者对 CSS 分页支持更好,后者对老式 HTML 兼容性更强。

2.3 用 Python 把解包后的 HTML 合并成单一文档

解包后的 HTML 是碎片化的,每个文件对应 CHM 里的一个页面。要转成 PDF,先得把它们按.hhc里的顺序拼成一个完整的 HTML。下面这段脚本做三件事:读.hhc解析目录顺序、按顺序读取每个 .htm、把内容塞进一个带层级标题的模板。

import os import re from bs4 import BeautifulSoup # 解析 .hhc 文件,提取目录顺序 def parse_hhc(hhc_path): with open(hhc_path, 'r', encoding='gbk', errors='ignore') as f: content = f.read() # .hhc 本质是 HTML,用 BeautifulSoup 解析 soup = BeautifulSoup(content, 'html.parser') # 提取所有 <param name="Local" value="xxx.htm"> 的顺序 order = [] for obj in soup.find_all('object'): param = obj.find('param', {'name': 'Local'}) if param: order.append(param.get('value')) return order # 按顺序合并 HTML def merge_html(base_dir, order, output_file): merged = ['<html><head><meta charset="utf-8"></head><body>'] for htm_file in order: path = os.path.join(base_dir, htm_file) if not os.path.exists(path): continue with open(path, 'r', encoding='gbk', errors='ignore') as f: soup = BeautifulSoup(f.read(), 'html.parser') # 只取 body 内容,去掉原有的 head 和样式冲突 body = soup.find('body') if body: merged.append(str(body)) merged.append('</body></html>') with open(output_file, 'w', encoding='utf-8') as f: f.write('\n'.join(merged)) # 执行 order = parse_hhc('./chm_extracted/manual.hhc') merge_html('./chm_extracted', order, './merged.html')

逻辑说明:.hhc文件本身是 HTML 格式,里面用<object>标签定义每个目录项,<param name="Local">指向对应的 .htm 文件。按这个顺序读取,就能还原出 CHM 的原始章节顺序。合并时只取<body>内容,避免多个<head>冲突。

参数说明:encoding='gbk'是因为很多老 CHM 用 GBK 编码,如果报 UnicodeDecodeError,换成gb18030兼容性更好。errors='ignore'是防止个别乱码字节导致整个文件读失败。输出统一用 UTF-8,方便后续排版引擎处理。

3. 用 WeasyPrint 把合并后的 HTML 转成带书签的 PDF

合并完 HTML,下一步是排版。WeasyPrint 的优势在于它支持 CSS Paged Media 规范,能自动根据<h1>到<h6>生成 PDF 书签,还能控制分页、页眉页脚。wkhtmltopdf 虽然也能转,但对 CSS3 分页属性支持不全,书签生成需要额外参数。

3.1 安装 WeasyPrint 与依赖处理

WeasyPrint 依赖 GTK 库,在 Linux 上需要先装系统包,Windows 上建议用 MSYS2 或 WSL。我一般直接在 Linux 环境跑,省去依赖折腾。

# Ubuntu/Debian 安装依赖 sudo apt install -y python3-pip libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev # 安装 WeasyPrint pip install weasyprint # 验证安装 python3 -c "import weasyprint; print(weasyprint.__version__)"

逻辑说明:libpango和libpangocairo是文字渲染的核心依赖,缺了会报OSError: cannot load library 'pango-1.0'。libgdk-pixbuf负责图片加载。装完用一行 Python 验证版本,能打印出版本号就说明环境通了。

参数说明:如果公司网络限制 pip 源,加-i https://pypi.tuna.tsinghua.edu.cn/simple换国内源。WeasyPrint 版本建议 60 以上,对 CSS 分页支持更完整。

3.2 写一份控制分页与书签的 CSS

WeasyPrint 的书签生成规则很简单:HTML 里的<h1>到<h6>自动成为 PDF 书签,层级按标签级别。所以合并 HTML 时,要确保每个章节的标题用正确的<h>标签。下面这份 CSS 控制分页和页眉页脚。

/* print.css */ @page { size: A4; margin: 2cm 1.5cm; @bottom-center { content: counter(page); font-size: 9pt; color: #666; } @top-right { content: string(doctitle); font-size: 9pt; color: #999; } } /* 每个 h1 新起一页 */ h1 { page-break-before: always; string-set: doctitle content(); } /* 代码块不跨页断开 */ pre, code { page-break-inside: avoid; white-space: pre-wrap; word-wrap: break-word; } /* 表格跨页时重复表头 */ table { page-break-inside: auto; } thead { display: table-header-group; }

逻辑说明:@page里的@bottom-center和@top-right是 CSS Paged Media 的页边距框,counter(page)自动生成页码,string(doctitle)把当前页所属的 h1 标题提取到页眉。page-break-before: always让每个一级标题从新页开始,这是技术手册的常见排版要求。

参数说明:white-space: pre-wrap让代码块保留换行但允许自动折行,避免长行溢出页面。page-break-inside: avoid防止代码块被分页截断,但如果代码块本身超过一页,这个属性会失效,需要手动拆分。

3.3 执行转换并验证书签层级

CSS 准备好后,用一行 Python 调用 WeasyPrint 完成转换。

from weasyprint import HTML, CSS # 读取合并后的 HTML 和自定义 CSS HTML('./merged.html').write_pdf( './output.pdf', stylesheets=[CSS('./print.css')] ) print("转换完成,检查 output.pdf 的书签层级")

逻辑说明:write_pdf的stylesheets参数接受一个 CSS 对象列表,会覆盖 HTML 内联样式。转换完成后,用 PDF 阅读器打开,检查左侧书签面板是否按 h1/h2/h3 正确嵌套。如果书签全是平的,说明合并 HTML 时标题标签用错了,比如全用了<h1>或者用了<div>加粗代替。

参数说明:如果 HTML 里有外部图片链接,WeasyPrint 默认会尝试下载,离线环境会卡住。加base_url='./chm_extracted/'让相对路径图片能本地找到。如果图片还是丢,检查解包时图片目录是否完整。

4. 中文乱码与字体嵌入:最容易被忽略的翻车点

转出来的 PDF 打开一看,中文全是方块或者问号——这是 CHM 转 PDF 最高频的翻车现场。原因通常有两个:HTML 编码没统一,或者 PDF 生成时没嵌入中文字体。

4.1 编码统一:从 GBK 到 UTF-8 的批量转换

老 CHM 里的 HTML 大多是 GBK 编码,合并时如果没转成 UTF-8,WeasyPrint 会按默认编码解析,中文直接乱码。上面的合并脚本已经用encoding='gbk'读取,但输出时写的是 UTF-8,这一步是关键。如果解包后的 HTML 本身编码混杂,用chardet检测后再转。

import chardet def detect_encoding(file_path): with open(file_path, 'rb') as f: raw = f.read() result = chardet.detect(raw) return result['encoding'] # 对每个 htm 文件检测编码后再读取 encoding = detect_encoding('./chm_extracted/page1.htm') print(f"检测到编码: {encoding}")

逻辑说明:chardet.detect返回一个字典,encoding字段是推测的编码格式。对于中文文档,常见结果是GB2312、GBK或UTF-8。拿到编码后,用对应编码读取再以 UTF-8 写入,就能统一。

参数说明:chardet对短文件检测可能不准,建议对多个文件采样后取多数结果。如果检测结果是None,说明文件可能是二进制或空文件,跳过即可。

4.2 字体嵌入:让 PDF 在任何设备上都不乱码

即使 HTML 编码正确,如果生成 PDF 时没嵌入中文字体,在没装对应字体的设备上打开依然会乱码。WeasyPrint 默认会嵌入字体,但前提是系统里有中文字体。Linux 服务器通常没装中文字体,需要手动指定。

/* 在 print.css 中指定中文字体 */ body { font-family: "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; }

逻辑说明:font-family按顺序查找,第一个找不到就用第二个。Noto Sans CJK SC是 Google 的开源中文字体,WenQuanYi Micro Hei是文泉驿微米黑,两者在 Linux 上都能通过包管理器安装。WeasyPrint 会把实际使用的字体子集嵌入 PDF,确保跨设备显示一致。

参数说明:如果系统里没有这些字体,先装:sudo apt install fonts-noto-cjk fonts-wqy-microhei。装完用fc-list :lang=zh确认中文字体已注册。如果 PDF 里中文还是方块,检查 CSS 里font-family是否被其他样式覆盖。

5. 避坑与排查:CHM 转 PDF 的五个血泪教训

这一章记录我实际踩过的坑,每条按“现象 → 原因 → 解决”写,方便你对照排查。

5.1 目录书签全是平的,没有层级

现象:PDF 左侧书签面板里所有条目都在同一级,点进去没有嵌套。

原因:合并 HTML 时,所有章节标题都用了<h1>,或者原始 CHM 的.hhc里层级信息在解包时丢了。

解决:检查.hhc文件里的<object>嵌套关系,<ul>的嵌套层数对应标题级别。如果.hhc本身是平的,只能靠 HTML 里的<h>标签手动重建层级。我一般写个脚本,根据.hhc的缩进深度给标题分配 h1/h2/h3。

5.2 代码块被拦腰截断,跨页后内容丢失

现象:PDF 里代码块在页面底部被切断,下一页直接跳到新章节,中间几行代码不见了。

原因:CSS 里没设page-break-inside: avoid,或者代码块本身超过一页高度。

解决:给pre和code加page-break-inside: avoid。如果代码块确实超过一页,手动在合适位置插入分页符,或者把长代码块拆成多个短块。WeasyPrint 对超长元素的处理是直接截断,不会自动续页,这点要特别注意。

5.3 图片全部裂图,路径找不到

现象:PDF 里所有图片位置显示裂图图标或空白。

原因:合并 HTML 时,图片的相对路径基于原始 .htm 文件位置,合并后基准路径变了。

解决:合并时把<img src="...">的相对路径改成基于合并后 HTML 的路径,或者用base_url参数指定图片根目录。更稳妥的做法是把所有图片转成 base64 内嵌到 HTML 里,彻底消除路径依赖。

import base64 def img_to_base64(img_path): with open(img_path, 'rb') as f: data = base64.b64encode(f.read()).decode() ext = img_path.split('.')[-1] return f"data:image/{ext};base64,{data}"

逻辑说明:base64.b64encode把图片二进制转成字符串,拼成 data URI 格式直接塞进src属性。这样 HTML 自包含,不依赖外部文件。

参数说明:base64 会让 HTML 体积增大约 33%,如果图片很多很大,合并后的 HTML 可能上百 MB,WeasyPrint 处理会变慢。建议只对小图标用 base64,大图还是用本地路径加base_url。

5.4 页码从第一页就开始,封面也被编号

现象:PDF 封面页底部显示“1”,目录页显示“2”,正文从“3”开始。

原因:CSS 里没排除封面和目录页的页码。

解决:用@page :first排除第一页,或者给封面和目录单独定义@page规则,设@bottom-center { content: none; }。更精细的做法是用命名页@page cover { ... },在 HTML 里给封面元素加page: cover。

5.5 转换速度极慢,几百页文档跑十几分钟

现象:WeasyPrint 处理 300 页以上的 CHM 时,CPU 跑满,进度条几乎不动。

原因:WeasyPrint 是纯 Python 实现,对复杂 CSS 和大量图片的渲染效率不高。如果 HTML 里嵌了大量 base64 图片,解析开销更大。

解决:分章节转,每章生成独立 PDF 再用pypdf合并。或者换 wkhtmltopdf,它基于 WebKit,渲染速度快很多,但书签生成需要额外配置。我一般对 200 页以内的用 WeasyPrint,超过 200 页换 wkhtmltopdf 加--outline参数。

6. 进阶技巧:批量转换与自动化校验

单份 CHM 转 PDF 跑通后,下一步是批量处理。我手里经常有几十份 CHM 要归档,手动一个个转不现实。下面这套脚本做三件事:遍历目录下所有 CHM、自动解包合并转换、校验输出 PDF 的页数和书签数。

import os import subprocess from weasyprint import HTML, CSS def batch_convert(chm_dir, output_dir): os.makedirs(output_dir, exist_ok=True) for file in os.listdir(chm_dir): if not file.endswith('.chm'): continue base_name = os.path.splitext(file)[0] work_dir = os.path.join(output_dir, base_name) os.makedirs(work_dir, exist_ok=True) # 解包 subprocess.run(['7z', 'x', os.path.join(chm_dir, file), f'-o{work_dir}', '-y'], check=True) # 查找 .hhc 文件 hhc_files = [f for f in os.listdir(work_dir) if f.endswith('.hhc')] if not hhc_files: print(f"[跳过] {file} 没有 .hhc,无法生成书签") continue # 合并与转换(复用前面的函数) order = parse_hhc(os.path.join(work_dir, hhc_files[0])) merged_path = os.path.join(work_dir, 'merged.html') merge_html(work_dir, order, merged_path) pdf_path = os.path.join(output_dir, f"{base_name}.pdf") HTML(merged_path).write_pdf(pdf_path, stylesheets=[CSS('./print.css')]) # 校验:用 pypdf 读取页数和书签数 from pypdf import PdfReader reader = PdfReader(pdf_path) outline = reader.outline print(f"[完成] {file} -> {len(reader.pages)} 页, {len(outline)} 个书签") batch_convert('./chm_files', './pdf_output')

逻辑说明:subprocess.run调用 7z 解包,check=True确保解包失败时抛异常而不是静默跳过。pypdf读取生成的 PDF,reader.pages是页数,reader.outline是书签列表。如果书签数为 0,说明 CSS 里的标题标签没生效,需要回头检查合并 HTML 的标题层级。

参数说明:-y是 7z 的覆盖确认参数,批量处理时避免卡在交互提示。pypdf需要单独安装:pip install pypdf。如果 CHM 文件名有中文,subprocess在 Windows 上可能乱码,建议用subprocess.run(..., encoding='utf-8')显式指定。

校验环节我一般会加一条规则:如果 PDF 页数少于 CHM 解包后 .htm 文件数的 80%,说明有页面丢失,需要人工检查。这个阈值不是绝对的,但能筛出大部分异常。

最后说个习惯:我每次转完一批 CHM,会随机抽三份用不同阅读器打开——系统自带的、浏览器内置的、第三方 PDF 工具——确认书签、中文、图片都正常。因为不同阅读器对 PDF 书签编码的处理有差异,曾经遇到过在某个阅读器里书签正常、换一个就全乱码的情况,根因是书签标题的编码没统一成 UTF-16。这个坑没有捷径,只能多开几个阅读器验证。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询