使用 pandoc 将 HTML 和 markdown 转化为 pdf 和 epub

最近因为拉勾的会员要到期了,正好极客时间也送了免费会员,但是他们的客户端在电子书上的效果都比较差,而且是一个比一个差,所以就萌生了转成 epub 的想法(没错,pdf 只是顺便的)。

当然,pdf 对于平板来说更友好一点,而 epub 对于水墨屏电子书来说更友好,两者都比客户端更适合阅读和做笔记。

数据源

在数据源的获取中,简单介绍,我们会分为几段去获取数据,之后根据数据去制作目录:

i-love-study-design.drawio.png

  • meta 信息:包括课程名、作者、封面等信息,保存在一个 meta 文件中,后续读取
  • 分节:作为目录结构的一部分,一本电子书会由多级标题组成,这里我们主要是分节 - 内容标题 - 单文章内部 heading 的多级结构组成。将这个作为目录结构。
  • 内容详情:单篇文章的数据,原始数据是 html,同时为了便于后续操作,会同时转换并存储一份单文章 markdown,如果本身只带正文内容,可以人工拼上文章标题。

数据收集侧就介绍这么多,具体的爬虫实现出于版权的考虑不做介绍了,是比较典型的爬虫处理模式,只需要注意:

  • 限制并发量
  • 限制爬取频率
  • 伪造的更像一个真实请求

这几点就可以了,需要注意的是,我们本身在反爬中,就会通过 IP、特征值和用户 id 几个维度进行封禁处理,所以换 IP 对于一个健全的 waf 来说大概率并不能作为提高频率、并发量的设计。

转换并制作电子书

有了数据源之后,接下来基本上就不需要对平台的网络请求,因为我们已经在数据源阶段把基本处理步骤都做完了。

在转换阶段,最重要其实是两步:

  1. 整合:将每一篇文章整合成长篇,补上「分节」这个层级结构
  2. 转换:填入 meta 信息,把整合后的文章转换成电子书

这里需要注意标签的层级结构,那么分节就是 h1,文章从 h2 开始,如果文章本身并不是从 h2开始的,则需要平移 headings,否则做出来的目录结构会有问题——当然,部分文章如果根本不遵守语义化标签,那也没办法,除非精修,但对于批处理来说精修成本太大,先以「遵守」处理。

另外,等号在转换时会变成 heading,如果有换行等号应该手动 replace:replace('\n=\n', ' = ')

整合环节不多介绍,核心部分其实就是按分节遍历,在分节的每篇文章里补上分节标题。

转换环节,我们使用了标题中提到的 pandoc:https://pandoc.org

它支持多种格式的互相转换,太多了就不在这里列出来了,可以看着一趴:https://pandoc.org/MANUAL.html#general-options

至少我们知道,对于 html, markdown 都支持转到 epub 和 pdf。而 markdown 和 html 是支持互相转换的;在 Python 中,我们使用 pandoc 这个库进行转换(但看了一下本质上其实就是运行 pandoc 命令,而且会把 JSON 作为中转,所以会有性能折损):https://boisgera.github.io/pandoc。因此其实我们也可以自己封装一个 pandoc 包。

相比其他 epub / pdf 的转换中,对于代码高亮、扩展支持、图片嵌入来说更加方便,解析的更加全面,也可以自动下载网络图片,免于离线处理;对于 epub 来说,塞封面和其他 meta 信息也很方便,支持的很全面。

当然,如果本文是 pandoc 的介绍,那这篇文章就不用写了,这里主要想说的是在转换时踩得一些坑以及调的一些参数,这些参数调的我甚至觉得我在炼丹。

使用 Latex 转换为 pdf 不支持 utf8

latex 本身也有编码和字体,这种时候要针对 latex 去定制头,如果你本身不熟悉 Latex,电脑里没有 Latex 的情况,建议使用 wkhtmltopdf 从 html 去转到 pdf。

自定义样式

默认的样式肯定不够好看,这里可以使用 --css 去读取本地的 css 文件,建议尤其处理一下图片和代码换行的情况。避免图片过大以及代码不换行,在 pdf 和 epub 的展示问题。

转换到 pdf 榨干了所有内存

在转换过程中,遇到了某个课程死活都转换不出来的情况,观察内存,已经上升到了使用几十 G 内存,且没有结束的势头。

这个课程的特征主要是里面的图片本身都是动图,而转换时应该是先转换为 base64 的,加上 markdown 本身使用的是 pandoc 自己带大量扩展的 markdown 预发,在本身解析和转换阶段花了极大的开销。而大部分场合,我们可能并永不倒这么多插件,普普通通的 markdown 到普普通通的 html 就可以,这里可以使用 commonmark 去替代 markdown,一个比较 basic 的 markdown 格式,扩展本身几乎没有,经过实际测试,从 commonmark 转起的性能大概是 markdown 本身的十倍(当然这和数据大小和复杂度本身也有关系(比如上面说的图片资源))。

代码高亮

代码高亮首先需要在 markdown 源中定义 lang,语法一致,但是有趣的是从 html 转换为 markdown 的时候标签会变成 lang-[lang] ,无法被正常识别,需要人工处理一下。

处理完了之后,使用 --highlight-style=[style] 就可以进行高亮了,style 的值在文档里好像并没有详细说明,可以参考这两篇(但是实测并不是每个值都能用):

pdf 无法定义页边距

在文档中说有 --variable margin-[top|bottom|left|right]=1mm 似乎是可以定义的,甚至还说 CSS 可以定义,而在 wkhtmltopdf 中其实也有关于页边距的设置,理论上使用 --pdf-engine-opt 也可以设置,但是实测每一种方法都无效,翻了半天,发现一个 MR:https://github.com/jgm/pandoc/issues/7676;实际上需要使用 --metadata 去定义而不是 --variable,这里文档也没有写清楚。

math 公式支持

使用 commonmark 转换之后发现公式似乎没有被解析,如果解析的话有两个方案,一个是使用文档中的:Math Rendering in HTML 的几种解析器,一般肯定是选择 Mathjax。

但是实测转不出 pdf,无限 Pending,这里没有找到对应的 issue 或者 MR,但是如果你需要定义 URL,可以参考 https://github.com/jgm/pandoc/pull/6600,它使用的是:

defaultMathJaxURL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js"

因此替换时也需要用这个文件的 CDN。

另一种方法是使用 extension,pandoc --list-extensions=FORMAT 可以列出某个格式支持的所有插件,即使是 commonmark 也有基础的 tex_math_dollars,如果需要启用,可以把 format 改为 commonmark+tex_math_dollars,或者直接使用 markdown_mmd 或者 markdown;但是启用后转换效率会下降,所以如果不是强依赖公式,建议实际遇到了再单独转换。

Python 代码参考

最终转换函数配置可以参考以下代码:

# epub
doc = pandoc.write(pandoc.read(content, format=format), format='epub',
    options=[f'--title={title}', f'--css={css}', f'--epub-cover-image={cover_image}',
    '--metadata', f'author={author}', '--highlight-style=kate'
])

# pdf
doc = pandoc.write(pandoc.read(content, format=format), format='pdf',
    options=[
        '--metadata', 'margin-bottom=15mm',
        '--metadata', 'margin-right=15mm',
        '--metadata', 'margin-top=15mm',
        '--metadata', 'margin-left=15mm',
        '--pdf-engine=wkhtmltopdf',
        '--metadata', f'title={title}',
        f'--css={css}', '--metadata', f'author={author}',
        '--highlight-style=kate'
    ])

后记

根据这个思路,实际上我们甚至可以把一些文档、专题教程也变成 epub 或者 pdf 方便查看和检索,甚至是做成一个平台工具推送到你的电子书中,本文也只是其中一个场景。

植入部分

如果您觉得文章不错,可以通过赞助支持我。

如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。

标签: 知识, 语法

添加新评论