关于 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 则元数据编辑功能可以在这里找到:

image

而如果您在使用 Jupyter Lab 则可以在这里编辑修改元数据:

image

具体的编辑方法就是添加如下的内容,具体的操作方法可参考原模板作者的博客文章 从编写到提交:使用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 -*))
posted @ 2024-06-19 18:33  多玩我的世界盒子  阅读(181)  评论(0编辑  收藏  举报