关于 Jupyter Nbconvert 自定义 LaTeX 模板,中文兼容与格式设置,从 Notebook 构建 LaTeX PDF 文档
为什么会有这篇随笔的内容?
最近因为有写一部数据分析教程的计划,主要的平台是 Jupyter,因此涉及到了从编辑好的 Jupyter Notebook 构建 \(\LaTeX\) 文档的问题。在这个过程中我遇到了一些基本的问题,比如:
- 如何设置自定义的导出样式?
- 如何设置兼容中文格式?
- 如何设置导出 Tex 文件的大标题和小标题、作者、日期和摘要信息?
- 在哪里可以找到开源的 Nbconvert 模板呢?
我翻遍全网找不到几个像样的教程,大部分的模板都是基于 HTML 而不是 \(\LaTeX\) 的。不仅包括国内的中文互联网,就连 GitHub 上,搜索 Jupyter Nbconvert Template 出来的资料也仅仅只有七八页。其中大部分都是 HTML 的模板。同样的,我在 StackOverflow 上也没有找到足够充分的资料。唯一一个可用的解决方案是华东石油大学的同学自己开源的 hakuna-max/course_report,这是一套《数据结构与算法》以及《商务智能分析》课程的课程报告 nbconvert 模板,可以基于该模板将 Jupyter Notebook 转换为符格式要求的课程报告。这个内容是我翻了整整七页的 GitHub repo、几乎已经无望的时候突然找到的。
如果可以的话,我真想写篇博客详细介绍一下这个项目,而我最近又很忙没有时间。所幸原作者很贴心的写了博客文章 从编写到提交:使用Jupyter Notebook完成实验报告,多少可以作为一个参照。
所以诸位现在看到的这篇随笔实际上相当于一篇快报,简单记录一下我在解决这个问题的时候踩的坑,顺便给后来看到本博客文章的人指条明路,指点一下找资料的方向。因为中文互联网上写这个的文档实在太少(反正我是没找到),而我又觉得肯定有很多人跟我有一样的需求。虽然很简陋不全面,但是博客能有我一篇也好。
简述一下我遇到的问题
Nbconvert 转换 .ipynb 文件的基本方法
使用 Nbconvert 转化名为 notebook.ipynb
的方法有两种:你既可以在 Jupyter Notebook 主界面的右上角直接 Download as Tex,也可以在命令行中执行:
jupyter nbconvert ./notebook.ipynb --to latex
当然,毫无疑问你需要将 notebook.ipynb
放在你执行命令的路径下面。
Jupyter Nbconvert 命令还可以指定生成的 \(\LaTeX\) 文件的路径,比如如果我的工作目录下面有一个 latex
文件夹,我就可以执行:
jupyter nbconvert ./notebook.ipynb --to latex --output-dir ./latex/
如果您的 Jupyter Notebook 包含代码运行生成的绘图,那么执行命令之后图画文件会被置于 --output-dir
指定的目录下的 notebook_file
文件夹。需要注意的是,您通过 ![]()
的 Markdown 语句插入的图片并不包含在内,而生成的 \(\LaTeX\) 文件将会保持图片目录的指定原封不动。因此,在编译 .tex
文件之前,你需要确定在 latex
目录下已另存了一份图片文件的副本。
cp ./image ./latex/
cd ./latex/
xelatex ./notebook.tex # 使用 xelatex 编译
Jupyter Nbconvert 构建中文 \(\LaTeX\) 文档的痛点
Jupyter Nbconvert 构建的 \(\LaTeX\) 文档并不会默认包含 CTEX
宏包,同时标题始终为 .ipynb
文件的文件名。如果仅仅只是手工去修改 .tex
文件,添加中文包、缩进设置、修正标题、修改小标题,就会很麻烦。再加上自动构建的 .tex
文档里包含了很多自动生成的文档风格的定义,想要找到一行文字难上加难。
再加上,我们平时在编写 .ipynb
的时候,Markdown 单元格往往都是直接使用 #
作为总标题的,而一级标题则使用 ##
。但是通过这种方式构建文档的时候,#
会被当作一级标题——因为 .ipynb
的文件名成了大标题。
考虑到上述的原因,我就想到要使用 Nbconvert 的模板功能。可以使用如下的命令在 Nbconvert 中指定模板:
jupyter nbconvert ./notebook.ipynb --to latex --output-dir ./latex/ --template <模板名>
这里的模板必须是内建的模板。换句话说,如果你想使用自定义的模板,则需要将你的模板文件放置在 Python 安装目录下的 .\Python<版本号>\share\jupyter\nbconvert\templates
目录下面。如果您还没有自己载入过其他任何的模板文件,那么现在您看到的文件夹应该就是默认的模板。
Jupyter Nbconvert 只接受 JinJa2 模板引擎的输入
Nbconvert 的模板一般不会是一个文件,而是一个文件夹以及其中的若干个文件,而在调用模板的时候,则需要在 --template
后面加上 template
路径下的模板文件夹的名字。文件夹中的模板文件名是 .j2
结尾,这是因为应用到了一种名为 JinJa2 的模板引擎。比如如果是 \(\LaTeX\) 模板,则文件以 .tex.j2
结尾;而如果是 HTML 模板就以 .html.j2
结尾。
需要注意的是:您可能在网上见到一些教程告诉你如何使用 --template-file
参数在 Nbconvert 构建 .tex
的时候指定一个 .tplx
模板。然而经过本人的考证,Jupyter Nbconvert 现在的版本确实已经不再使用 .tplx
格式的模板了。这意味着掌握基本的 \(\LaTeX\) 语法不足以让您学会自行制作模板,您必须学习 JinJa2 模板语言。
临时的解决方案
勉强可用的模板文件
因为实在找不到一套行之有效的解决方案,我只好在 hakuna-max 同学的模板的基础上做了一些删减,删去了如 Bib 引用格式、封面页、DataFrame 转 \(\LaTeX\) 表格、图表 Caption 交叉引用和编号以及页眉中华东石油大学的字样之类的内容。由于不懂 JinJa2 也不是很了解 \(\LaTeX\) 的各种高级写法,只好凭着代码直觉去删改。现在已经能够正常展示导出的中文文档。删减阉割后的简易模板文件我会放在本文的附录里面。
Jupyter Notebook 的元数据功能
这套模板设置大小标题和作者姓名需要编辑 Jupyter Notebook 的元数据来设置。元数据编辑这个功能用的比较少,我在这里写一下:如果您在使用 Jupyter Notebook 则元数据编辑功能可以在这里找到:
而如果您在使用 Jupyter Lab 则可以在这里编辑修改元数据:
具体的编辑方法就是添加如下的内容,具体的操作方法可参考原模板作者的博客文章 从编写到提交:使用Jupyter Notebook完成实验报告。由于我做了删改,因此剩下可用的元数据项只有下面这些。注意这些内容都是 JSON 格式,所以必须按照 JSON 的规范,写在最大一级的括号里面。
}
...
"title": "文件的大标题",
"subtitle": "文件的小标题",
"authors": [
{
"name": "作者1"
},
{
"name": "作者2"
},
{
"name": "作者3"
}
],
...
}
附录:模板文件
模板文件一共是四个,三个 JinJa2 文件和一个 JSON。把这些文件复制到 Python 安装目录下的 .\Python<版本号>\share\jupyter\nbconvert\templates\quickreport
文件夹下,注意要按照给定的文件名称来保存。我给文件夹取名 quickreport
,实际上你也可以给文件夹随便命名,只是在引用模板的时候要和文件夹同名。比如:
jupyter nbconvert ./notebook.ipynb --to latex --output-dir ./latex/ --template quickreport --debug
记得带上 --debug
参数,否则 Nbconvert 就算出错了也不会有输出。
base.tex.j2 文件
((*- extends 'latex/base.tex.j2' -*))
((* block packages *))
\usepackage{xeCJK}
\usepackage{ctex}
\usepackage{natbib}
\usepackage{setspace}
\usepackage{indentfirst}
\usepackage[twoside]{fancyhdr}
((* block definitions *))
((( super() )))
\onehalfspacing % 设置1.5倍行距
\setlength{\parindent}{2em} % 设置段落首行缩进为2个字符大小
((* endblock definitions *))
((* block title -*))
((*- set nb_title = nb.metadata.get('title', '') -*))
((*- set nb_subtitle = nb.metadata.get('subtitle', '') -*))
\title{\textbf{((( nb_title | escape_latex ))) \\[20pt] \Large{((( nb_subtitle | escape_latex )))}} \\[25pt]}
((*- endblock title *))
((*- block input_group -*))
((*- if not cell.metadata.get('hide', False) -*))
((( super() )))
((*- endif -*))
((*- endblock input_group -*))
((*- block markdowncell scoped -*))
((*- if not cell.metadata.get('hide', False) -*))
((*- if cell.source.startswith('![') -*))
\begin{figure}[htbp]
\centering
((*- set image_path = cell.source.split('](')[1].rstrip(')') -*))
\includegraphics[width=0.8\linewidth]{ (((image_path))) }
\vspace{10pt}
((*- if cell.metadata.figcaption -*))
\caption{(((cell.metadata.figcaption)))}
((*- else -*))
% \textbf{\\ \textcolor{magenta}{Warning: No caption specified. Please set a caption in the notebook metadata.}} \\
((*- endif -*))
((*- if cell.metadata.figlabel -*))
\label{(((cell.metadata.figlabel)))}
((*- endif -*))
\end{figure}
((*- else -*))
((( cell.source | citation2latex | strip_files_prefix | convert_pandoc('markdown+tex_math_double_backslash', 'latex'))))
((*- endif -*))
((*- endif -*))
((*- endblock markdowncell -*))
((* block predoc *))
((* block maketitle *))
\maketitle
% \thispagestyle{empty} % No page number on the title page
((* endblock maketitle *))
% Reset page numbering for the main content
\pagenumbering{arabic}
\setcounter{page}{1}
\pagestyle{fancy}
\fancyhead{} % clear all header fields
\fancyhead[LO,RE]{\textbf{((( nb.metadata.subtitle | escape_latex )))}}
\fancyfoot[RE,LO]{\textit{\footnotesize{((( nb.metadata.authors | join(', ', attribute='name') )))}}}
((* endblock predoc *))
((* block postdoc *))
((( make_bibliography() )))
((* endblock postdoc *))
%===============================================================================
% SUPPORT MACROS
%===============================================================================
% Add abstract and keywords
((* macro make_abstract() *))
((*- set nb_abstract = nb.metadata.get('abstract', '') -*))
((*- set nb_keywords = nb.metadata.get('keywords', '') -*))
\begin{center}
((*- if nb_abstract: -*))
\parbox{0.8\columnwidth}{
\textbf{摘要:}(((nb_abstract)))}
\par\vspace{0.5cm}
((*- endif -*))
((*- if nb_keywords: -*))
\parbox{0.8\columnwidth}{\textbf{关键词:}(((nb_keywords)))}
\par\vspace{1cm}
((*- endif -*))
\end{center}
((*- endmacro *))
% Add bibliography
((* macro make_bibliography() *))
((* block bibliography *))
((( add_bibstyle() )))
((( add_bibfile() )))
((* endblock bibliography *))
((* endmacro *))
((* macro add_bibstyle() *))
((*- set nb_bibstyle = nb.metadata.get('bibstyle', '') -*))
((*- if nb_bibstyle: -*))
\bibliographystyle{(((nb_bibstyle)))}
((*- else -*))
% \textbf{\textcolor{magenta}{Warning: No bibstyle specified. Please set a bibliography style in the notebook metadata.}} \\
((*- endif -*))
((* endmacro *))
((* macro add_bibfile() *))
((*- if nb.metadata["bibfile"]: -*))
\bibliography{(((nb.metadata["bibfile"])))}
((*- else -*))
% \noindent \textbf{\textcolor{magenta}{Warning: No bibfile specified. Please set a bibliography file in the notebook metadata.}}
((*- endif -*))
((* endmacro *))
conf.json 文件
{
"base_template": "latex",
"mimetypes": {
"text/latex": true,
"text/tex": true,
"application/pdf": true
}
}
document_contents.tex.j2 文件
((*- extends 'display_priority.j2' -*))
%===============================================================================
% Support blocks
%===============================================================================
% Displaying simple data text
((* block data_text *))
\begin{Verbatim}[commandchars=\\\{\}]
((( output.data['text/plain'] | escape_latex | ansi2latex )))
\end{Verbatim}
((* endblock data_text *))
% Display python error text with colored frame (saves printer ink vs bkgnd)
((* block error *))
\begin{Verbatim}[commandchars=\\\{\}, frame=single, framerule=2mm, rulecolor=\color{outerrorbackground}]
(((- super() )))
\end{Verbatim}
((* endblock error *))
% Display error lines with coloring
((*- block traceback_line *))
((( line | escape_latex | ansi2latex )))
((*- endblock traceback_line *))
% Display stream ouput with coloring
((* block stream *))
\begin{Verbatim}[commandchars=\\\{\}]
((( output.text | escape_latex | ansi2latex )))
\end{Verbatim}
((* endblock stream *))
% Display latex
((* block data_latex -*))
((( output.data['text/latex'] | strip_files_prefix )))
((* endblock data_latex *))
% Display markdown
((* block data_markdown -*))
((( output.data['text/markdown'] | citation2latex | strip_files_prefix | convert_pandoc('markdown+tex_math_double_backslash', 'latex'))))
((* endblock data_markdown *))
% Default mechanism for rendering figures
((*- block data_png -*))((( draw_figure(output.metadata.filenames['image/png']) )))((*- endblock -*))
((*- block data_jpg -*))((( draw_figure(output.metadata.filenames['image/jpeg']) )))((*- endblock -*))
((*- block data_svg -*))((( draw_figure(output.metadata.filenames['image/svg+xml']) )))((*- endblock -*))
((*- block data_pdf -*))((( draw_figure(output.metadata.filenames['application/pdf']) )))((*- endblock -*))
% Draw a figure using the graphicx package.
((* macro draw_figure(filename) -*))
((* set filename = filename | posix_path *))
((*- block figure scoped -*))
\begin{center}
\adjustimage{max size={0.9\linewidth}{0.9\paperheight}}{((( filename )))}
\end{center}
{ \hspace*{\fill} \\}
((*- endblock figure -*))
((*- endmacro *))
% Redirect execute_result to display data priority.
((* block execute_result scoped *))
((* block data_priority scoped *))
((( super() )))
((* endblock *))
((* endblock execute_result *))
% Render markdown
((* block markdowncell scoped *))
((( cell.source | citation2latex | strip_files_prefix | convert_pandoc('markdown+tex_math_double_backslash', 'json',extra_args=[]) | resolve_references | convert_explicitly_relative_paths | convert_pandoc('json','latex'))))
((* endblock markdowncell *))
% Don't display unknown types
((* block unknowncell scoped *))
((* endblock unknowncell *))
index.tex.j2 文件
((*- extends 'latex/index.tex.j2' -*))
%===============================================================================
% Latex Article
%===============================================================================
((*- block docclass -*))
\documentclass[12pt]{article}
((*- endblock docclass -*))