用pandoc生成大型中文文档的痛点与解决方案

2019-11-25
6 min read

Pandoc是转换文本格式的利器。在用 Pandoc 转换中文文档和生成大型文档时,例如用中文写作毕业论文等时,会遇到一些很麻烦的问题。我在前面的博客里说过我在用 Markdown 写博士毕业论文,这篇博客就我自己的经验讲一下在用 Pandoc 生成大型中文文档的痛点与解决方案。

Pandoc 将 Markdown 生成 pdf 的逻辑

用 Pandoc 将 Markdown 文件生成 pdf,其实是先把 Markdown 格式转换成 LaTeX 格式,再通过 LaTeX 把转换后的 LaTeX 文件生成最终的 pdf 文件。在用 LaTeX 生成 pdf 时,可以指定不同的--pdf-engine,可选的选项有pdflatexxelatexlualatex等。

因为 Pandoc 是通过 LaTeX 来生成 pdf 文件的,所以可以通过修改或添加 LaTeX 源文件来修改最终的 pdf 格式。如果不怕麻烦,可以先用 Pandoc 把 Markdown 文件转换成 LaTeX,再在上面自己改格式。

pandoc --standalone test.md -o test.tex

但是,一般的做法是通过给 Pandoc 传递 LaTeX 命令和 Pandoc 自带的参数来修改 pdf 格式的。当然,也可以直接在 Pandoc 的 pdf template 上进行修改。不过 Pandoc 自带的 template 很复杂,一般用--header-includes给自带的 template 加上需要的 LaTeX 命令就可以了。

另外,Pandoc 自身也提供来一些可以自定义的参数,这些参数一般是在文件开头用 yaml 写的,Pandoc 将这些参数称作“metadata”。所以在用 Pandoc 生成一个自定义格式的大型 pdf 文档时,除了文本文档外,还需要准备preable.texmetadata.yaml两个文件,分别用来自定义 LaTeX 与 Pandoc 自带的参数。

用 Pandoc 生成中文 pdf

要在 Pandoc 生成的 pdf 里显示中文,跟在 LaTeX 里显示中文的逻辑一样,一般用ctex包或xeCJK包就可以了。不过不建议用ctex包,因为它除了显示中文外还定义了太多其他格式。不如直接用xeCJK来显示中文,再直接用 LaTeX 提供的其他包来指定其他格式。

pandoc来生成中文 pdf 时,需要将默认的--pdf-engine改成xelatex

pandoc --pdf-engine=xelatex test.md -o test.pdf

用pandoc生成大型文档

在用 LaTeX 写作大型文档时,可以用\include{}来包含单个文件。在用Markdown写大型文档时,用Pandoc将多个Markdown文件转换成一整个文件时,可以将文件名按顺序放在pandoc命令中进行转换。必须注意的是,要保证生成的文件是按需要的顺序出现的,需要把Markdown文件按照顺序命名,例如第一章的文件名是1.md,第二章为2.md等。

pandoc --pdf-engine=xelatex 1.md 2.md 3.md -o main.pdf

在用这个命令生成大型文档的时候,可能会出现后一个文件变成前一个文件一部分的情况。要避免出现这种情况,必须在每个文档文本后加一个空行。

另外一个问题是,在Markdown中的标题用pandoc生成默认是不编号的。要给标题编号,需要在yaml文件中加上以下这行。

numbersections: true

在用单个Markdown文件写作文章时,可以把yaml参数加在文本上方;但在用多个Markdown文件写作时,最好将yaml参数单独保存成一个文件,在pandoc命令中以--metadata-file来指定该文件就可以了。

pandoc --pdf-engine=xelatex --metadata-file=metadata.yaml 1.md 2.md 3.md -o main.pdf

交叉引用

写作大型文档时,尤其是技术或学术文档时,难免要对图表、公式、章节等进行交叉引用。Pandoc 本身提供了对不同部分进行交叉引用的功能。

Pandoc 本身并没有提供对图表进行交叉引用的功能。要实现这种功能,可以用过滤器--filter来实现。在MacOS上,可以用pandoc-crossref来处理图表、公式的编号与交叉引用;在Windows平台上,对图片、表格、公式的交叉引用可以分别用pandoc-tablenospandoc-fignospandoc-eqnos来实现。这几个过滤器可以用以下表格中的命令来安装1

filter OS installation
pandoc-crossref MacOS brew install pandoc-crossref
pandoc-tablenos Windows pip install pandoc-tablenos
pandoc-fignos Windows pip install pandoc-fignos
pandoc-eqnos Windows pip install pandoc-eqnos

在 Pandoc 中使用过滤器只要用--filter参数指定所要用的过滤器即可。这几个过滤器的使用逻辑都是一致的,可以设置的参数包括图表标题前缀(tableTitlefigureTitle等)、文中引用前缀(tabPrefixfigPrefix)等。其他具体参数可以参考各自的说明文档。

至此,Pandoc 命令就变成了以下这样(以 MacOS 为例):

pandoc --pdf-engine=xelatex --filter pandoc-crossref --metadata-file=metadata.yaml 1.md 2.md 3.md -o main.pdf

章节目录

在 LaTeX 中要对特定的标题不编号,可以用\chapter{}等命令中加*来实现。Pandoc 也提供了这种功能。对于不想编号的标题,标题后面加上{-}或者{.unnumbered}就可以。

要在生成的文件中自动生成目录,只需要将toc这一参数指定为true就可以。文档中包含的表格与图片等也可以自动生成目录,分别将lotlof指定为true就可以了。但是,在 Pandoc 里可以指定目录的标题,例如toc-title: 目录,但是表格目录与图片目录的标题没法指定。这个问题可以用 LaTeX 来解决,只要在 LaTeX head 里指定表格目录与图片目录标题就行了:

\renewcommand\listtablename{表\ 格\ 目\ 录}
\renewcommand\listfigurename{图\ 片\ 目\ 录}

在用 LaTeX 定义章节等标题格式时,经常会用到titlesec这一 LaTeX 包。但是,在head里指定了这个包之后,运行 Pandoc 会出现这样的错误信息:

Error producing PDF.
! Argument of \paragraph has an extra }.
<inserted text> 
                \par 
l.1628 \ttl@extract\paragraph

要解决这个问题,必须在yaml文件里加上subparagraph: yes这一行。

yaml文件参数与含义

前面涉及到了很多yaml文件里的参数。这里将常见的yaml参数条列如下,并简单说明各自的作用。

title: 标题                     # 指定标题
author: 作者                    # 指定作者
numbersections: true           # true 表示给文中的部分编号,默认值为 false
subparagraph: yes             # 要在 LaTeX head 里用 titlesec 这个包,必须加这一行
fontsize: 12pt                # 指定字号大小,默认接受10pt、11pt、12pt
toc: true                     # 生成目录
toc-title: "目录"             # 指定目录标题
lot: true                     # 生成表格目录
lof: true                     # 生成图片目录
tableTitle: "表"      # 表格标题的前缀,pandoc-crossref 与 pandoc-tablenos 中可用
figureTitle: "图"     # 图片标题的前缀,pandoc-crossref 与 pandoc-fignos 中可用
tabPrefix: "表"       # 文中对表格引用的前缀,pandoc-crossref 可用
figPrefix: "表"       # 文中对图片引用的前缀,pandoc-crossref 可用
header-includes:      # 要加进 LaTeX文件的命令,建议放在一个单独的 preamble 文件里

其他 Pandoc 资料

网上有很多其他非常好的 Pandoc 资料,例如:


  1. Windows里的两个过滤器都是用Python的包管理器pip来装的。所以要装这两个过滤器,要先装Python。除了这里给的命令之外,还可以用python -m pip install packageName来装python第三方包。关于pip installpython -m install的差别,参见https://snarky.ca/why-you-should-use-python-m-pip/。 ↩︎