12.04 Jupyter Notebook和Git版本管理无缝集成

Jupyter Notebook是一个强大的在线交互式编程平台,还是一种强大的文档处理平台。使用Jupyter Notebook制作教程和其他文档的方式非常方便,尤其是在文档编辑、代码处理和执行结果的交叉展现;对图形以及LaTeX编码的格式和公式处理也提供了很好的支持。甚至可以用来生成文档,PPT展现等。

但是Jupyter Notebook存在不必要的运行时缓存数据在于Git集成时会造成差异干扰。本文我们介绍一种通过jq单行脚本快速处理,并制作成强大的git clean fileter,实现从Jupyter Notebook文件中删除不需要的缓存数据来实现Jupyter Notebook和Git的无缝集成。

Jupyter Notebook和Git版本管理无缝集成

缘起

强大的Jupyter Notebook给我们使用带来便利,其基于.ipynb的项目保存和导入是个非常好的功能,可以让我们重现,以及和别人分享操作和界面。但是其基于所见即所得的编辑器存在一个一直以来共性的问题:其文档文件往往包含的内容不止是文本,因此很难用基于纯文本管理的版本控制工具处理。Jupyter 本身的.ipynb格式与纯文本格式的差异也不太大,ipynb格式只一种自定义的JSON数据结构,偶尔还有嵌入的对图像和其他二进制数据base-64编码的blob数据。按理来说,Git之类的VCS处理他们是绰绰有余的。但是其中base-64编码的blob数据较长时对其git diff比较变化差异时候就是个问题。尤其是需要多人协作的一个变化频繁的项目就是个很棘手的问题。

当然可以使用手动方法:在提交Notebook之前,可以通过手动单击"单元格"->"所有输出"->"清除"。然后保存工作。这样存档中就不会有单元格的输出(绘图,打印等)的非文本记录。

这样做的一个问题就是,下次导入再运行时候需要重新运行所有计算过程,比较耗时费力,我们需要费心地打开最近重新运行的各个Notebook,提交之前清除,然后保存。

这样做,也不能完全解决"文件差异噪声"的问题,因为每个Notebook 还包含一个"元数据"部分,比如下面就是个元数据的示例:

{ "metadata": {

"kernelspec": {

"display_name": "Python 2",

"language": "python",

"name": "python2"

},

"language_info": {

"codemirror_mode": {

"name": "ipython",

"version": 2

},

"file_extension": ".py",

"mimetype": "text/x-python",

"name": "python",

"nbconvert_exporter": "python",

"pygments_lexer": "ipython2",

"version": "2.7.12"

}

如上元数据部分实际上是一块空白模版。元数据具有多种可能的用途,但对于大多数用户而言,它仅包含上述内容。它对于检查以前运行的Notebook环境很有用,但在将项目运行在Python版本有差异的多用户环境时,该数据是不必要的信息,而且会对项目文件差异产生干扰。

解决方案

为了解决差异的问题,需要额外引入工具。实际上有一些Python项目专门处理Jupyter Notebook的内容差异的问题。

nbdime

Jupyter Notebook和Git版本管理无缝集成

首先要介绍的是nbdime,它源于nbdiff项目(已经停止),提供对Jupyter Notebook的内容做差异比较和合并的工具。nbdime很有潜力成为对初学者友好的通用型Notebook处理工具,但截止当前,它还只是测试版本,而仅仅可用来查看Notebook内容的差异,无法用来清除输出信息导致差异噪声的问题。

Jupyter Notebook和Git版本管理无缝集成

nbstripout

另一个需要提及的工具是nbstripout,它是一个包含nbformat处理功能的单模块Python脚本,并添加了一些用于设置git config的自动选项。可以解决上述手工"清除所有输出"的过程,使其自动化。但是它也无法解决 "元数据"差异噪声干扰的问题。手动运行脚本并希望有短暂的延迟是可以接受的,但是需要将它集成到git设置中,这是nbstripout性能问题就很突出,导致在运行git diff时会有无法忍受的延迟。

JQ

幸运的是,还有另外的选择。由于nbformat只是JSON,一个可以使用"轻量级且灵活的命令行JSON处理器"jq,(相当于JSON数据的sed)。由于jq有其自己的查询/过滤器语言,设置方便,文档丰富。一个jq处理nbformat的示例:

jq --indent 1 \\

'(.cells[] | select(has("outputs")) | .outputs) = []

| (.cells[] | select(has("execution_count")) | .execution_count) = null

| .metadata = {"language_info": {"name":"python", "pygments_lexer": "ipython3"}}

| .cells[].metadata = {}

' XXX.ipynb

单引号内的每一行都定义了一个jq过滤器。

第一行从"单元格"列表中选择所有条目,然后将所有输出清空。

第二行重置所有执行计数。

第三行清除了Notebook的元数据,将其替换为最少的必需信息,以使Notebook仍可正常运行而不会出现问题,并在使用nbsphinx格式化时可以正常工作。

第四行".cells[].metadata = {}",用来过滤行单元格属性。在Jupyter的最新版本中,每个单元格设置属性(hidden / collapsed / write-protected等)。一般来说我们对这些元数据不感兴趣,但是你可以按照需要针对性保留。

这样,我们就有了一个精简化的Notebook,其中包含执行任何本地Python安装时执行所需的公共信息。

注意上述单行脚本请需要jq 1.5或更高版本,因为--indent选项是最新添加的;而且必须符合nbformat。

为了方便我把上面单行保存为别名,保存在.bashrc:

Jupyter Notebook和Git版本管理无缝集成

然后就可以使用

nbstrip_jq xx-parsing.ipynb> stripped.ipynb

就可以清理烦人的元数据,而且还非常快,大概不到nbstripout的十分之一。

自动化git集成

上面我们用jq脚本的方式解决了问题,但是如何在git操作时自动运行,仍值得研究。为了实现自动化需要用gitattributes,特别是过滤器部分。通过gitattributes滤器是在将数据检入或检出git存储库时做转换数据的操作,以便Notebook输出单元格在将JSON数据添加到git信息库之前,进行清理操作。gitattributes滤器原理见下图:

Jupyter Notebook和Git版本管理无缝集成

一般情况下,还可以定义一个smudge-filter来获取存储库内容,并对其进行一些处理以使其本地化,在这并不需要,可用cat命令用作占位符。我们希望所有git存储库中的Notebook默认进行数据清理,通过在用户配置文件~/.gitconfig文件设置:

[core]

attributesfile = ~/.gitattributes_global

[filter "nbstrip_full"]

clean = "jq --indent 1 \\

'(.cells[] | select(has(\\"outputs\\")) | .outputs) = [] \\

| (.cells[] | select(has(\\"execution_count\\")) | .execution_count) = null \\

| .metadata = {\\"language_info\\": {\\"name\\": \\"python\\", \\"pygments_lexer\\": \\"ipython3\\"}} \\

| .cells[].metadata = {} \\

'"

smudge = cat

required = true

Jupyter Notebook和Git版本管理无缝集成

然后通过~/.gitattributes_global设置:

*.ipynb filter= nbstrip_full

我们也可以在版本库的.gitattributes中配置,只在项目级启用。

总结

本文我们通过对Jupyter Notebbook项目和Git集成中存在的运行时缓存差异噪声问题进行探索和解决,最后基于jq脚本和gitattributes过滤器完美解决了问题,实现了Jupyter Notebbook项目和Git的无缝集成。这对Jupyter Notebbook的版本管理和共享发布等非常有意义。

最后本文作为一个典型的问题发现,探索,解决和自动化案例,本案例也非常值得学习。


分享到:


相關文章: