文档格式转换实战指南:Markdown、HTML、PDF 互转全解析
每个文档都有它最适合的格式。Markdown 适合写作,HTML 适合网页展示,PDF 适合打印分发,Word 适合办公协作。麻烦在于,现实工作中你经常需要在这些格式之间穿梭——把博客文章转成 PDF 发给客户,把 Word 文档迁移到知识库,把网页内容提取成 Markdown 归档。
格式转换看似简单,真正动手时却问题频出:格式乱了、表格消失了、中文变乱码了。这篇指南梳理各种转换路径的最佳方案,帮你少走弯路。
四大格式特点对比
在选择转换路径之前,先理解各格式的本质特点:
| 格式 | 可读性 | 可编辑性 | 渲染效果 | 文件大小 | 适用场景 |
|---|---|---|---|---|---|
| Markdown (.md) | 极高(纯文本) | 极高 | 依赖渲染器 | 极小 | 技术文档、博客写作、README |
| HTML (.html) | 中(含标签) | 高 | 浏览器渲染 | 小~中 | 网页、邮件模板、文档展示 |
| PDF (.pdf) | 高(视觉) | 极低 | 固定排版 | 中~大 | 打印、正式文件、跨平台分发 |
| Word (.docx) | 高 | 高 | Office 渲染 | 中 | 办公协作、需要批注修订 |
核心规律:可编辑性越高的格式,排版固定性越低;排版越固定的格式,转换损耗越大。 PDF 是单向格式,从 PDF 往外转时质量损耗最大。
Markdown → PDF
这是最常见的转换需求,有三种方案,效果各有差异:
方案一:浏览器打印(推荐入门用)
- 在支持 Markdown 预览的工具中打开文档(VS Code 预览、MagicTools、Typora)
- 按
Ctrl+P(Mac:Cmd+P)打开打印对话框 - 目标打印机选择「另存为 PDF」
- 调整页面设置:去掉页眉页脚勾选,设置合适的边距
优点:操作简单,零学习成本,所见即所得。 缺点:代码高亮、字体、分页位置依赖浏览器渲染,不同机器可能有细微差异。
方案二:Pandoc 命令行(推荐专业用)
Pandoc 是格式转换领域的瑞士军刀,支持 40+ 种格式互转。
# 安装(Mac)
brew install pandoc
# 同时需要安装 LaTeX(用于 PDF 输出)
brew install --cask mactex-no-gui
# 基本转换
pandoc input.md -o output.pdf
# 带中文支持(必须指定字体,否则中文显示为方块)
pandoc input.md -o output.pdf \
--pdf-engine=xelatex \
-V mainfont="PingFang SC" \
-V geometry:margin=2cm
# 自定义 CSS 样式(通过 HTML 中间步骤)
pandoc input.md -o output.pdf \
--pdf-engine=wkhtmltopdf \
--css=style.css
优点:输出质量最高,格式控制精细,支持批量处理,适合生产环境。 缺点:需要安装 LaTeX 环境(约 4GB),中文配置有一定学习成本。
方案三:在线工具(临时需求)
MagicTools 内置导出功能,写好 Markdown 后一键导出 PDF,无需安装任何软件。适合偶发性需求。
Markdown → HTML
Markdown 转 HTML 是最"无损"的转换,因为 Markdown 本来就是 HTML 的简化写法。
静态博客生成
主流方案:Hugo、Jekyll、Gatsby、Astro。以 Hugo 为例:
# 一条命令把整个 articles/ 目录转成网站
hugo --source . --destination ./public
# 生成的 public/ 包含完整的 HTML 站点,可直接部署
这类工具会自动处理代码高亮、目录生成、相关文章推荐等功能。
单文件转换
# Pandoc 转换,嵌入完整样式(standalone 模式)
pandoc input.md -o output.html --standalone
# 引用外部样式文件
pandoc input.md -o output.html --standalone --css=github-markdown.css
转换结果可以直接在浏览器打开,或嵌入到现有网页中。
邮件模板
把 Markdown 内容转成 HTML 后,注意邮件客户端对 CSS 支持极差,需要把所有样式内联化:
# 使用 juice 工具内联 CSS
npm install -g juice
pandoc input.md -o temp.html --standalone --css=email.css
juice temp.html output-email.html
HTML → Markdown
这条路径最常用于:博客平台迁移(WordPress → Hexo/Hugo)、从网页提取内容归档、清洗爬虫抓取的内容。
在线工具(推荐)
MagicTools HTML 转 Markdown 支持三种输入方式:
- 直接粘贴 HTML 代码
- 输入 URL 自动抓取网页
- 粘贴富文本(从网页复制后粘贴)
背后使用 Turndown 引擎,能正确处理表格、代码块、列表嵌套等复杂结构。
命令行批量转换
迁移整个 WordPress 博客时,命令行方案更高效:
# 安装 html2text(Python)
pip install html2text
# 单文件转换
html2text article.html > article.md
# 批量转换整个目录
for f in html-pages/*.html; do
html2text "$f" > "markdown-output/${f%.html}.md"
done
转换质量注意事项:
- 导航栏、侧边栏、广告等无关内容需要手动清理
- 图片 URL 会保留原始地址,需要另行下载并替换为本地路径
- 复杂的嵌套表格可能丢失格式
PDF → Markdown
这是所有转换路径中质量最差的一条,原因是 PDF 本质上是"打印指令集",文字的逻辑顺序(段落、标题层级)并不直接存储在文件里。
OCR 方案(适合扫描版 PDF)
扫描版 PDF(纸质文档扫描而来)必须用 OCR:
- Adobe Acrobat Pro:识别精度最高,支持中文,价格较贵
- Microsoft Office Lens:免费移动端 APP,扫描 + OCR,输出 Word 后再转 Markdown
- 在线 OCR:ilovepdf.com、smallpdf.com 提供免费的 OCR 转换
文字层 PDF 转换
如果 PDF 包含可选中的文字(不是扫描版),可以用:
# pdftotext(poppler 工具包)
pdftotext -layout input.pdf output.txt
# 注意:只能提取纯文本,标题层级和格式信息会丢失
# pdf2md 工具(更好保留结构)
npm install -g pdf2md-cli
pdf2md input.pdf > output.md
现实预期:PDF → Markdown 几乎必然需要手动修正,只适合"有比没有好"的场景。重要文档建议保留源文件(Word、Markdown 原稿)。
Word/DOCX → Markdown
企业环境中大量文档存在于 Word 格式,迁移到 Markdown 知识库时这条路径很常用。
Pandoc(推荐,质量最高)
# 基本转换
pandoc input.docx -o output.md
# 提取 Word 中的图片到 media/ 目录
pandoc input.docx -o output.md --extract-media=./media
# 批量转换
for f in *.docx; do
pandoc "$f" -o "${f%.docx}.md" --extract-media="./media/${f%.docx}"
done
Pandoc 能正确识别 Word 的标题样式(Heading 1 → #,Heading 2 → ##),保留粗体、斜体、表格和图片。
Word 另存为(简单但质量差)
在 Word 中 File → Save As → Plain Text,只能保留纯文本,丢失所有格式。不推荐作为迁移方案。
各转换路径质量评级
| 转换路径 | 格式保真度 | 操作难度 | 推荐工具 |
|---|---|---|---|
| Markdown → HTML | ★★★★★ | 低 | Pandoc / 在线工具 |
| Markdown → PDF | ★★★★☆ | 中 | Pandoc + XeLaTeX |
| HTML → Markdown | ★★★★☆ | 低 | MagicTools / Turndown |
| Word → Markdown | ★★★★☆ | 低 | Pandoc |
| Markdown → Word | ★★★☆☆ | 低 | Pandoc |
| PDF → Markdown | ★★☆☆☆ | 高 | OCR + 手动修正 |
| PDF → Word | ★★★☆☆ | 低 | Adobe / 在线工具 |
本机实测:8 个转换方向各跑一次真实文件
问题背景
上面的评级表是经验打分,不是测出来的。本站自己就有一套基于 Gotenberg(LibreOffice + Chromium)的文件格式转换工具,8 个方向上线以来,我没有拿一份「带中文、带表格、带图片」的真实文件从头到尾走一遍,也没有量过一次转换到底要几秒、表格和图片能不能活着到 PDF。这一节把这件事补上。
问题分析
要把「能转」和「转得对」分开量:
- 耗时只有一个可信口径:从本机发出请求到拿到完整 PDF 的端到端时间(curl 的
time_total),它包含上传、排队、引擎渲染和下载。Gotenberg 内部的纯渲染时间对外没有暴露,我不拆、也不估。 - 文本保真用 poppler 的
pdftotext把 PDF 里的文字抽出来,去掉全部空白后和源文逐字比对(Pythondifflib字符级相似度),再单独数 6 个关键标记(ZX7Q-4K、磁悬浮、3.1415926这类不会撞车的 token)和 24 个表格单元格各召回了几个。中文乱码、丢字、表格整块消失都会直接掉分。 - 结构与视觉用
pdfinfo数页数,用pdftoppm渲染出 PNG,数非白像素比例判断是不是白页,再数示意图专用的两种颜色(蓝柱、绿圆)的像素数判断图片有没有进 PDF。 - 浏览器端方向(pdf-to-jpg / pdf-to-png)还要额外证明一件事:文件确实没离开浏览器。办法是拦下页面发出的所有非 GET 请求,为零才算数。
技术方案与选型
- 直接打生产端点,不在本机起转换栈。 本机没有 Docker,Homebrew 也没有 Gotenberg 的 formula,本地起不了 LibreOffice + Chromium 那套容器;而
npm run dev连的是生产数据库,每次转换都会写一条计数记录进去。生产端点本来就是用户真实走的路径,测它才对题。代价是数字里带网络,后面会明说。 - 源文件用
uv装 python-docx / python-pptx / openpyxl / Pillow 现造,不用网上下载的样例。三份 Office 文件内容一致:四段中文正文、一行关键标记、一张 6 行 4 列表格、一张 Pillow 画的 640×320 示意图;HTML 和 Markdown 手写,同样的段落和表格,Markdown 多一个代码块。排除 Word 或 PowerPoint 另存:本机没装,而且手工做的文件不可复现。 - 服务端 5 个方向用 curl 打
POST /api/convert?direction=<id>,字段名file,带浏览器 UA(裸 curl 的 UA 会被拦成 403),-L跟随重定向。匿名限额每 IP 每天 10 次,成功和失败都计数,响应头X-Quota-Used会告诉你用了几次。预算:5 个方向各 1 次,最慢的 markdown-to-pdf 再补 2 次、word-to-pdf 补 1 次看抖动,留 1 次容错,一次也没浪费在试请求格式上。 - pdf-to-jpg / pdf-to-png 用 Playwright 打开线上工具页真实点一遍:
setInputFiles喂文件、点「开始转换」、等结果图出现,再从页面里把 blob 图片取回本机。输入就是本次 markdown-to-pdf 吐出来的那份 PDF,不另找。 - merge-pdf 走
/api/pdf/process(pdf-lib),匿名可用,不占转换配额,把 docx、pptx、xlsx 三份产出合成一份。
实测过程
先造文件:
uv venv tmp/2026-09-12-doc-convert/.venv
uv pip install python-docx python-pptx openpyxl pillow
.venv/bin/python make-sources.py
# figure.png 2024 B · sample.docx 39149 B · sample.pptx 31732 B
# sample.xlsx 8321 B · sample.html 1618 B · sample.md 1152 B
服务端方向的请求长这样(每个方向换 direction 和文件):
curl -sL -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/150.0.0.0 Safari/537.36' \
-D out/markdown-to-pdf-1.hdr -F 'file=@src/sample.md' -o out/markdown-to-pdf-1.pdf \
-w '{"http":%{http_code},"time_total":%{time_total},"time_starttransfer":%{time_starttransfer}}' \
'https://tools.cooconsbit.com/api/convert?direction=markdown-to-pdf'
6 个服务端方向加 3 次重复样本一次跑完,全部 HTTP 200,配额从 2/10 用到 9/10:

浏览器端两个方向由 Playwright 驱动,页面里「100% 本地转换」那行绿色提示不是摆设,脚本拦截到的非 GET 请求是 0:

这是 Playwright 点完「开始转换」后的真实页面:

实践效果
耗时(本机 → 生产端点端到端,含网络;DNS 把请求解析到了阿里云北京那台机器):
| 方向 | 引擎 | 样本数 | 耗时 | 产出 |
|---|---|---|---|---|
| markdown-to-pdf | Chromium | 3 | 10.97 / 11.84 / 14.56 s(min / 中位 / max) | 1 页,261200 B |
| html-to-pdf | Chromium | 1 | 8.02 s | 1 页,195736 B |
| word-to-pdf | LibreOffice | 2 | 2.93 / 8.01 s | 1 页,67686 B |
| excel-to-pdf | LibreOffice | 1 | 3.67 s | 2 页,59157 B |
| ppt-to-pdf | LibreOffice | 1 | 1.22 s | 2 页,62871 B |
| merge-pdf | pdf-lib | 1 | 0.30 s | 5 页,164713 B |
| pdf-to-jpg | 浏览器 pdfjs | 1 | 点击到出图 2.56 s(页面加载另计 1.60 s) | 1224×1584 JPEG,192570 B |
| pdf-to-png | 浏览器 pdfjs | 1 | 点击到出图 2.12 s(页面加载另计 0.95 s) | 1224×1584 PNG,255420 B |
三点读法:
- Chromium 引擎比 LibreOffice 慢一个量级。同样是一页纸,markdown-to-pdf 三次都在 11 秒以上,html-to-pdf 8 秒;而 LibreOffice 三个 Office 方向最慢 8 秒、最快 1.2 秒。
time_starttransfer和time_total只差几十毫秒,说明时间几乎全花在服务端等第一个字节,不是下载。 - LibreOffice 有明显的冷启动:word-to-pdf 第一次 8.01 秒,几分钟后第二次 2.93 秒;紧跟在 docx、xlsx 之后跑的 ppt-to-pdf 只要 1.22 秒。Chromium 侧没看到这种冷热差,三次 markdown 都在 11 到 15 秒之间。
- 浏览器端的 2 秒多里大头是首次加载 pdfjs 库。文件确实没有离开浏览器,两次转换各拦截到 0 次上传请求。
保真度矩阵(pdftotext 文本相似度取 -layout 与 -raw 两种抽取的较高值;像素统计取示意图所在页):

| 方向 | 页数 | 文本相似度 | 关键标记 | 表格单元格 | 图片 |
|---|---|---|---|---|---|
| word-to-pdf | 1 | 100.00% | 6/6 | 24/24 | 在(蓝 17375 / 绿 5671 像素) |
| html-to-pdf | 1 | 100.00% | 6/6 | 24/24 | 源文件无图 |
| ppt-to-pdf | 2 | 99.65% | 6/6 | 24/24 | 在(第 2 页,蓝 10514 / 绿 3407) |
| markdown-to-pdf | 1 | 96.04% | 6/6 | 24/24 | 源文件无图 |
| excel-to-pdf | 2 | 85.93% | 6/6 | 24/24 | 在(蓝 9402 / 绿 3069) |
| merge-pdf | 5 = 1 + 2 + 2 | 三份标题 3/3 | — | — | 在 |
| pdf-to-jpg | 1 张 | 与 poppler 渲染同页均值绝对差 4.77 / 255 | — | — | — |
| pdf-to-png | 1 张 | 4.72 / 255 | — | — | — |
Word 那条是满分:标题、四段、标记行、6×4 表格带边框、示意图、结尾一句,全部在,位置也对:

没拿满分的三条各有具体原因,不是「转换质量一般」四个字能概括的:
- excel-to-pdf 85.93%:表被横向劈成两页。 我把 A 列设成 60 个字符宽、后面 4 列各 16,超过了 A4 竖版可打印宽度,LibreOffice 按默认打印设置把「样本大小 / 备注 / 数值」三列连同合计推到了第 2 页,A5 那句长文本也在页边被裁掉半句。24 个单元格和 6 个标记一个没丢,数字列的
SUM公式算出了 1665,只是读者要翻页拼。相似度掉分掉在这里。 - ppt-to-pdf 99.65%:内容全在,但第 2 页版式重叠。 标题「表格与图片 ZX7Q-4K / 磁悬浮 / 2026-09-12」字号太大折成两行压到了表格上;表格单元格里「word-to-pdf」折行后行高撑大,整张表向下长,和我放在 4.5 英寸处的文本框叠在一起。这是源文件排得太密,LibreOffice 照实渲染;本机没有 PowerPoint,无法对照它自己会怎么排。
- markdown-to-pdf 96.04%:代码块右侧被裁。 那行
curl ... -o out.pdf命令超过了 800px 的正文宽度,包装模板给pre的是overflow-x: auto,屏幕上能横向滚,打印成 PDF 没有滚动条,pdftotext抽出来的那行到/api/c就断了。正文四段、表格、标记全部无损。
字体是另一个只有实测才看得见的事。用 pdffonts 看三份 PDF 内嵌了什么:
| 方向 | 内嵌字体 |
|---|---|
| markdown-to-pdf | NotoSansCJKjp-Regular |
| html-to-pdf | NotoSansCJKsc-Bold / Regular |
| word-to-pdf | NotoSansCJKhk-Regular + Carlito(Calibri 替身)+ NotoSerif |
Markdown 的包装模板写的是 font-family: -apple-system, "Noto Sans SC", ...,Gotenberg 镜像里没有叫这个名字的字体,fontconfig 回退到了 Noto Sans CJK 的日文变体;我手写的 HTML 声明了 lang="zh" 和 "Noto Sans CJK SC",拿到的就是简体变体。这份样本里我肉眼没找出写法不对的字,但日文变体对一部分汉字的默认字形和简体不同,换一批字就可能露出来。DOCX 那条则是 LibreOffice 自己的回退:Calibri 换成度量兼容的 Carlito,中文落到了港版变体。
踩坑
- macOS 自带的 bash 是 3.2,
declare -A关联数组不存在。 我用[word-to-pdf]=src/sample.docx建方向到文件的映射,报的错是word: unbound variable:bash 3.2 把方括号里的word-to-pdf当算术表达式求值了。换成case函数即可,或者显式用/opt/homebrew/bin/bash。好在报错发生在 curl 之前,没烧配额。 pdftotext -layout会把多列表格交错着输出,单元格召回会误报丢字。 PPT 那页表格里「word-to-pdf」折成了两行,-layout模式下这两半中间夹着同一行其他列的内容,去空白后拼不回原词,第一版脚本报了 19/24。改成-layout和-raw两种抽取取并集后 24/24。判「丢没丢」要用内容流顺序,判「版式对不对」再用-layout。- 「疑似丢图」是我自己数错了页。 第一版脚本只渲染第 1 页数颜色像素,PPT 的图在第 2 页,于是报了丢图。像素统计必须落在图所在的那一页,否则测的是白纸。
- Excel 转 PDF 前先设「调整为一页宽」。 列宽加起来超过纸宽,LibreOffice 和 Excel 都会横向分页,文字被页边裁掉是设计如此,不是转换坏了。
- Markdown 里超长的单行代码在 PDF 里会被裁掉,不会折行。 这是本站包装模板
pre { overflow-x: auto }在打印介质下的必然结果,正确做法是给pre加white-space: pre-wrap。这一条和上面的字体名回退都是本次实测挖出来的自家问题,当天已改:模板换成<html lang="zh-CN">、字体名改为镜像里真实存在的"Noto Sans CJK SC"、pre改white-space: pre-wrap; word-break: break-all。验证方式是把改后的模板套在同一份 Markdown 渲染出的 HTML 上,走生产的 html-to-pdf(同一个 Chromium)再跑一次:pdffonts里只剩 NotoSansCJKsc,那行 curl 命令折成两行、-o out.pdf完整出现在pdftotext结果里。 - 裸 curl 打接口会被拦成 403,必须带浏览器 UA。响应头没有 Cloudflare 的痕迹(
server: nginx/1.26.2),拦截发生在站点自己这一层。
原始输出、源文件、四份脚本(make-sources.py / run-server.sh / run-pdfjs.mjs / fidelity.py)和 results.jsonl 都留在仓库的 tmp/2026-09-12-doc-convert/,表里每个数字都能回到那里对上。
FAQ
Q:转换后格式乱了怎么办?
A:格式乱的原因通常有三种:第一,源文件格式不规范(如 Word 中用空行代替段落样式),解决方法是规范源文件再转;第二,转换工具不支持某些特性(如特殊字体、复杂表格),换一个工具试试;第三,编码问题(中文乱码),Pandoc 命令行加 --from=utf-8 或确保文件以 UTF-8 保存。如果是 PDF → 其他格式,格式乱基本是正常现象,需要手动修正。
Q:表格在转换中容易丢失怎么处理?
A:表格是格式转换最容易出问题的元素。处理建议:HTML → Markdown 转换时,优先使用 MagicTools 或 Turndown,它对表格支持最好;PDF 中的表格丢失基本无法自动恢复,只能手动重新写 Markdown 表格;Word 表格用 Pandoc 转换质量较好,但合并单元格会丢失合并信息;万不得已时,可以把表格截图,用图片替代——虽然不优雅,但总比格式乱好。
Q:免费工具能处理批量转换吗?
A:完全可以。Pandoc 是免费开源工具,可以写 Shell 脚本批量处理整个目录。Python 的 html2text、markdownify 库也支持批量调用。对于 100 个以内文件的批量转换,命令行脚本配合 Pandoc 是最高效的方案,完全免费,且可以精确控制输出格式。超过 1000 个文件时可以考虑并行处理(xargs -P 4 或 Python 的 multiprocessing)以提升速度。
总结
文档格式转换没有银弹,但有规律可循:
- 高频需求:Markdown ↔ HTML、Markdown → PDF,这两条路径工具成熟,质量可靠
- 迁移需求:Word → Markdown、HTML → Markdown,Pandoc + 在线工具能处理绝大多数情况
- 最难转:PDF → 任何格式,做好手动修正的心理准备
最重要的建议:保留原始格式文件。转换是临时操作,原稿才是资产。用 Markdown 写作,用 Git 版本管理,随时可以转换成任何你需要的格式,这才是最有弹性的文档工作流。