在版本控制下使用IPython笔记本

时间:2013-09-11 07:05:35

标签: version-control ipython jupyter-notebook

IPython笔记本保留在版本控制之下的好策略是什么?

笔记本格式非常适合版本控制:如果想要版本控制笔记本和输出,那么这很有效。当人们只想对输入进行版本控制时,就会产生烦恼,除了可以是大型二进制blob的单元输出(也就是#34;构建产品"),特别是对于电影和情节。特别是,我正在努力寻找一个良好的工作流程:

  • 允许我选择包括或排除输出,
  • 如果我不想要,
  • 会阻止我意外提交输出,
  • 允许我保留本地版本的输出
  • 允许我看看我何时使用我的版本控制系统对输入进行了更改(即,如果我只是版本控制输入但我的本地文件有输出,那么我希望能够看到输入是否已更改(需要提交)。使用版本控制状态命令将始终注册差异,因为本地文件有输出。)
  • 允许我从更新的干净笔记本更新我的工作笔记本(包含输出)。的(更新)

如上所述,如果我选择包含输出(例如,在使用nbviewer时这是可取的),那么一切都很好。问题是当我想要版本控制输出时。有一些工具和脚本用于剥离笔记本的输出,但我经常遇到以下问题:

  1. 我不小心提交了一个带有输出的版本,从而污染了我的存储库。
  2. 我清除输出以使用版本控制,但我宁愿将输出保留在我的本地副本中(例如,有时需要一段时间才能重现)。
  3. Cell/All Output/Clear菜单选项相比,剥离输出的某些脚本会稍微改变格式,从而在差异中产生不必要的噪音。这可以通过一些答案来解决。
  4. 当将更改提取到文件的干净版本时,我需要找到一些方法将这些更改合并到我的工作笔记本中而无需重新运行所有内容。 的(更新)
  5. 我已经考虑过几个选项,我将在下面讨论,但尚未找到一个很好的综合解决方案。完整的解决方案可能需要对IPython进行一些更改,或者可能依赖于一些简单的外部脚本。我目前使用mercurial,但想要一个也适用于git的解决方案:理想的解决方案是与版本控制无关。

    此问题已多次讨论,但从用户的角度来看,没有明确或明确的解决方案。这个问题的答案应该提供明确的策略。如果它需要最近(甚至开发)版本的IPython或一个易于安装的扩展名,那就没问题了。

    更新:我一直在使用my modified notebook版本,可选择使用Gregory Crosswhite's suggestions保存每次保存的.clean版本。这满足了我的大多数约束条件,但仍未解决以下问题:

    1. 这还不是一个标准的解决方案(需要修改ipython源代码。有没有办法通过简单的扩展来实现这种行为?需要一些on-save hook。
    2. 我对当前工作流程的一个问题是拉动更改。这些将进入.clean文件,然后需要以某种方式集成到我的工作版本中。 (当然,我总是可以重新执行笔记本,但这可能很痛苦,特别是如果某些结果取决于长时间的计算,并行计算等)。我还不知道如何解决这个问题。也许涉及像ipycache这样的扩展的工作流可能会起作用,但这似乎有点过于复杂。
    3. 注释

      删除(剥离)输出

      • 当笔记本电脑运行时,可以使用Cell/All Output/Clear菜单选项删除输出。
      • 有一些用于删除输出的脚本,例如删除输出的脚本nbstripout.py,但不会产生与使用笔记本界面相同的输出。这最终包含在ipython/nbconvert仓库中,但已关闭,说明这些更改现已包含在ipython/ipython中,但相应的功能似乎尚未包含在内。 (更新)话虽如此,Gregory Crosswhite's solution表明这很容易做到,即使没有调用ipython/nbconvert,所以如果它可以正确挂钩,这种方法可能是可行的。(将它附加到每个版本控制系统,似乎不是一个好主意 - 这应该以某种方式挂钩到笔记本机制。)

      新闻组

      问题

      拉请求

22 个答案:

答案 0 :(得分:115)

这是我用git的解决方案。它允许你像往常一样添加和提交(和diff):这些操作不会改变你的工作树,同时(重新)运行一个笔记本不会改变你的git历史。

虽然这可能适用于其他VCS,但我知道它不能满足您的要求(至少VSC不可知)。尽管如此,它对我来说仍然是完美的,虽然没有什么特别精彩,很多人可能已经使用过它,但我没有找到关于如何通过Google搜索来实现它的明确指示。所以它可能对其他人有用。

  1. 在某处保存this content的文件(对于以下内容,我们假设为~/bin/ipynb_output_filter.py
  2. 使其可执行(chmod +x ~/bin/ipynb_output_filter.py
  3. 使用以下内容

    创建文件~/.gitattributes
    *.ipynb    filter=dropoutput_ipynb
    
  4. 运行以下命令:

    git config --global core.attributesfile ~/.gitattributes
    git config --global filter.dropoutput_ipynb.clean ~/bin/ipynb_output_filter.py
    git config --global filter.dropoutput_ipynb.smudge cat
    
  5. 完成!

    <强>限制:

    • 仅适用于git
    • 在git中,如果你在分支somebranch而你做git checkout otherbranch; git checkout somebranch,你通常希望工作树不变。相反,您将失去两个分支之间源不同的笔记本电脑的输出和单元格编号。
    • 更一般地说,输出完全没有版本化,就像Gregory的解决方案一样。为了不是每次你做任何涉及结账的事情都扔掉它,可以通过将它存储在单独的文件中来改变方法(但请注意,在运行上面的代码时,提交id是未知的!),并且可能对它们进行版本化(但请注意,这需要的不仅仅是git commit notebook_file.ipynb,尽管它至少可以使git diff notebook_file.ipynb免于base64垃圾。)
    • 说,顺便说一句,如果你确实拉代码(即由不使用这种方法的其他人提交)包含一些输出,则输出会正常检出。只丢失本地产生的输出。

    我的解决方案反映了这样一个事实:我个人不喜欢将生成的内容保留为版本 - 请注意,执行涉及输出的合并几乎可以保证输出无效或< / em>两者。

    修改

    • 如果您按照我的建议采用了解决方案 - 也就是说,全局 - 您将遇到麻烦,以防某些git repo 想要到版本输出。因此,如果您想禁用特定git存储库的输出过滤,只需在其中创建一个文件 .git / info / attributes ,并使用

      **。ipynb filter =

    作为内容。显然,以相同的方式可以执行相反的操作:为特定存储库启用仅过滤

    • 代码现在保留在自己的git repo

    • 如果上述说明导致ImportErrors,请尝试在脚本路径前添加“ipython”:

      git config --global filter.dropoutput_ipynb.clean ipython ~/bin/ipynb_output_filter.py
      

    编辑:2016年5月(2017年2月更新):我的脚本有多种替代方案 - 为了完整性,以下是我所知道的列表:nbstripout({{3} } other),variantsnbstrip

答案 1 :(得分:56)

我们有一个合作项目,其产品是Jupyter笔记本电脑,我们在过去六个月中使用了一种方法很有效:我们激活自动保存.py文件并跟踪.ipynb文件1}}文件和.py文件。

这样,如果有人想查看/下载最新的笔记本,他们可以通过github或nbviewer这样做,如果有人想看看笔记本代码是如何变化的,他们可以看看{{1文件。

对于.py笔记本服务器,可以通过添加行来实现

Jupyter

import os from subprocess import check_call def post_save(model, os_path, contents_manager): """post-save hook for converting notebooks to .py scripts""" if model['type'] != 'notebook': return # only do this for notebooks d, fname = os.path.split(os_path) check_call(['jupyter', 'nbconvert', '--to', 'script', fname], cwd=d) c.FileContentsManager.post_save_hook = post_save 文件并重新启动笔记本服务器。

如果您不确定在哪个目录中找到jupyter_notebook_config.py文件,可以输入jupyter_notebook_config.py,如果您在那里找不到该文件,则可以创建该文件输入jupyter --config-dir

对于jupyter notebook --generate-config笔记本服务器,可以通过添加行来实现

Ipython 3

import os from subprocess import check_call def post_save(model, os_path, contents_manager): """post-save hook for converting notebooks to .py scripts""" if model['type'] != 'notebook': return # only do this for notebooks d, fname = os.path.split(os_path) check_call(['ipython', 'nbconvert', '--to', 'script', fname], cwd=d) c.FileContentsManager.post_save_hook = post_save 文件并重新启动笔记本服务器。这些行来自github问题答案@minrk provided,而@dror也包含在他的SO答案中。

对于ipython_notebook_config.py笔记本服务器,可以通过以下方式启动服务器来完成:

Ipython 2

或添加行

ipython notebook --script

c.FileNotebookManager.save_script = True 文件并重新启动笔记本服务器。

如果您不确定在哪个目录中找到ipython_notebook_config.py文件,可以输入ipython_notebook_config.py,如果您在那里找不到该文件,则可以创建该文件输入ipython locate profile default

此处our project on github that is using this approach:此处是github example of exploring recent changes to a notebook

我们对此非常满意。

答案 2 :(得分:36)

我创建了nbstripout,基于MinRKs gist,支持Git和Mercurial(感谢mforbes)。它既可以在命令行上单独使用,也可以作为过滤器使用,可以通过nbstripout install / nbstripout uninstall轻松(非)安装在当前存储库中。

PyPI或只是

获取
pip install nbstripout

答案 3 :(得分:13)

以下是Cyrille Rossant针对IPython 3.0的新解决方案,该解决方案坚持使用markdown文件而不是基于json的ipymd文件:

https://github.com/rossant/ipymd

答案 4 :(得分:11)

(2017-02)

<强>策略

  • on_commit():
    • 剥离输出&gt; name.ipynb(nbstripout,)
    • 剥离输出&gt; name.clean.ipynb(nbstripout,)
    • 总是nbconvert到python:name.ipynb.py(nbconvert
    • 始终转换为markdown:name.ipynb.md(nbconvertipymd
  • vcs.configure():
    • git difftool,mergetool:来自nbdime的nbdiff和nbmerge

工具

答案 5 :(得分:8)

正如所指出的,--script已弃用3.x。可以通过应用post-save-hook来使用此方法。特别是,将以下内容添加到ipython_notebook_config.py

import os
from subprocess import check_call

def post_save(model, os_path, contents_manager):
    """post-save hook for converting notebooks to .py scripts"""
    if model['type'] != 'notebook':
        return # only do this for notebooks
    d, fname = os.path.split(os_path)
    check_call(['ipython', 'nbconvert', '--to', 'script', fname], cwd=d)

c.FileContentsManager.post_save_hook = post_save

代码取自#8009

答案 6 :(得分:8)

我终于找到了一种使Jupyter和Git完美配合的有效且简单的方法。我仍处于起步阶段,但我认为它比所有其他复杂解决方案都要好。

Visual Studio Code是Microsoft的一款很酷的开源代码编辑器。它具有出色的Python扩展,现在允许您import a Jupyter Notebook作为python代码。

将笔记本导入到python文件后,所有代码和markdown都将放在一个普通的python文件中,并在注释中带有特殊标记。您可以在下图中看到:

VSCode editor with a notebook converted to python

您的python文件仅包含笔记本输入单元格的内容。输出将在拆分窗口中生成。您的笔记本中有纯代码,仅在执行时它不会更改。没有与您的代码混合输出。没有奇怪的Json难以理解的格式来分析差异。

仅是纯python代码,您可以在其中轻松识别每个差异。

我什至无需再对.ipynb文件进行版本控制。我可以在*.ipynb中放入.gitignore行。

需要生成一个笔记本来与某人发布或共享吗?没问题,只需在交互式python窗口中click the export button

Exporting a python file to Notebook format

我已经使用了一天,但最终我可以很高兴地将Jupyter与Git一起使用。

P.S.:VSCode代码完成比Jupyter好很多。

答案 7 :(得分:6)

我采用非常务实的方法;这对于几个笔记本电脑来说很有效。它甚至可以让我转移&#39;笔记本周围。它适用于Windows作为Unix / MacOS Al认为这很简单,就是解决上面的问题......

概念

基本上,跟踪.ipnyb - 文件,只跟踪相应的.py - 文件。
通过使用--script选项启动 notebook-server ,可以在保存笔记本时自动创建/保存该文件。

那些.py - 文件确实包含所有输入;非代码保存到注释中,单元格边框也是如此。可以将这些文件读取/导入(并拖动)到笔记本服务器中以(重新)创建笔记本。只有输出消失了;直到重新运行。

我个人使用 mercurial 来跟踪.py个文件;并使用普通(命令行)命令添加,签入(ect)。大多数其他(D)VCS将允许这样做。

现在跟踪历史很简单; .py是小的,文本的和简单的差异。有一段时间,我们需要一个克隆(只是分支;在那里启动第二个笔记本 - 服务器),或旧版本(检出并导入到笔记本服务器中)等。

提示&amp;技巧

  • * .ipynb 添加到&#39; .hgignore &#39;,以便Mercurial知道它可以忽略这些文件
  • 创建一个(bash)脚本以启动服务器(使用--script选项)并对其进行版本跟踪
  • 保存笔记本会保存.py - 文件,但会将其签入。
    • 这是缺点:可以忘记
    • 它还是一个功能:可以保存笔记本(并在以后继续),而无需集群存储库历史记录。

愿望

  • 在笔记本电脑Dashboard中有一个用于登记/添加/等的按钮会很不错。
  • 结帐(例如)file@date+rev.py)应该会有所帮助 添加它会有很多工作要做;也许我会这样做一次。到现在为止,我只是手工完成。

答案 8 :(得分:6)

不幸的是,我对Mercurial了解不多,但我可以给你一个可以与Git一起使用的解决方案,希望你能将我的Git命令翻译成他们的Mercurial等价物。

对于后台,在Git中,add命令将对文件所做的更改存储到暂存区域。完成此操作后,Git将忽略对该文件的任何后续更改,除非您告诉它也将其暂存。因此,以下脚本为每个给定文件删除所有outputsprompt_number sections,对已剥离文件进行分阶段,然后恢复原始文件:

注意:如果运行此操作会收到ImportError: No module named IPython.nbformat之类的错误消息,请使用ipython运行脚本而不是python

from IPython.nbformat import current
import io
from os import remove, rename
from shutil import copyfile
from subprocess import Popen
from sys import argv

for filename in argv[1:]:
    # Backup the current file
    backup_filename = filename + ".backup"
    copyfile(filename,backup_filename)

    try:
        # Read in the notebook
        with io.open(filename,'r',encoding='utf-8') as f:
            notebook = current.reads(f.read(),format="ipynb")

        # Strip out all of the output and prompt_number sections
        for worksheet in notebook["worksheets"]:
            for cell in worksheet["cells"]:
               cell.outputs = []
               if "prompt_number" in cell:
                    del cell["prompt_number"]

        # Write the stripped file
        with io.open(filename, 'w', encoding='utf-8') as f:
            current.write(notebook,f,format='ipynb')

        # Run git add to stage the non-output changes
        print("git add",filename)
        Popen(["git","add",filename]).wait()

    finally:
        # Restore the original file;  remove is needed in case
        # we are running in windows.
        remove(filename)
        rename(backup_filename,filename)

在您要提交更改的文件上运行脚本后,只需运行git commit

答案 9 :(得分:4)

在删除笔记本电脑的输出几年后,我试图提出一个更好的解决方案。现在,我使用Jupytext,这是我设计的Jupyter Notebook和Jupyter Lab的扩展。

Jupytext可以将Jupyter笔记本转换为各种文本格式(脚本,Markdown和R Markdown)。相反。它还提供了将笔记本与这些格式之一进行配对,并自动同步笔记本的两种表示形式(.ipynb.md/.py/.R文件)的选项。 / p>

让我解释一下Jupytext如何回答上述问题:

  

允许我在包含或排除输出之间进行选择,

.md/.py/.R文件仅包含输入单元格。您应该始终跟踪该文件。仅在要跟踪输出时才对.ipynb文件进行版本控制。

  

防止我不想要的输出,

*.ipynb添加到.gitignore

  

允许我将输出保留在本地版本中,

输出保留在(本地).ipynb文件中

  

允许我使用版本控制系统查看何时更改了输入(即,如果仅对版本进行控制,但是本地文件具有输出,那么我希望能够查看输入是否已更改(需要提交)。由于本地文件具有输出,因此使用版本控制状态命令将始终记录差异。)

您正在寻找.py/.R.md文件上的差异

  

允许我从更新的清洁笔记本中更新工作的笔记本(包含输出)。 (更新)

拉出.py/.R.md文件的最新版本,然后在Jupyter(Ctrl + R)中刷新笔记本。您将从文本文件中获得最新的输入单元格,并从.ipynb文件中获得匹配的输出。内核不受影响,这意味着将保留您的局部变量-您可以在离开内核的地方继续工作。

我对Jupytext的爱是可以在您喜欢的IDE中编辑笔记本(以.py/.R.md文件的形式)。使用这种方法,重构笔记本变得容易。完成后,您只需要在Jupyter中刷新笔记本即可。

如果想尝试一下:用pip install jupytext安装Jupytext,然后重新启动Jupyter Notebook或Lab编辑器。使用Jupyter笔记本中的Jupytext Menu(或Jupyter Lab中的Jupytext commands),打开您要进行版本控制的笔记本,并配对与Markdown文件(或脚本)。 )。保存您的笔记本,您将获得两个文件:原始.ipynb,再加上笔记本的承诺文本表示形式,非常适合版本控制!

对于那些可能感兴趣的人:command line上也提供Jupytext。

答案 10 :(得分:4)

只需遇到“ jupytext”,它看起来就是一个完美的解决方案。它从笔记本生成一个.py文件,然后使两者保持同步。您可以通过.py文件对输入进行版本控制,区分和合并,而不会丢失输出。打开笔记本时,它使用.py作为输入单元格,并使用.ipynb作为输出单元。而且,如果要将输出包含在git中,则只需添加ipynb。

https://github.com/mwouts/jupytext

答案 11 :(得分:3)

我已经构建了解决这个问题的python包

https://github.com/brookisme/gitnb

它提供了一个带有git启发语法的CLI,用于在git仓库中跟踪/更新/区分笔记本。

继承人的一个例子

# add a notebook to be tracked
gitnb add SomeNotebook.ipynb

# check the changes before commiting
gitnb diff SomeNotebook.ipynb

# commit your changes (to your git repo)
gitnb commit -am "I fixed a bug"

请注意,我使用“gitnb commit”的最后一步是提交到你的git repo。它本质上是

的包装
# get the latest changes from your python notebooks
gitnb update

# commit your changes ** this time with the native git commit **
git commit -am "I fixed a bug"

还有其他几种方法,可以进行配置,以便在每个阶段都需要更多或更少的用户输入,但这就是一般的想法。

答案 12 :(得分:3)

与2016年更好的方法相比,上述2016年非常受欢迎的答案是不一致的黑客行为。

存在几种选择,回答该问题的最佳选择是Jupytext。

Jupytext

赶上Towards Data Science article on Jupytext

它与版本控制一起工作的方式是将.py和.ipynb文件都放入版本控制中。如果需要输入差异,请查看.py,如果需要最新的渲染输出,请查看.ipynb。

值得注意的是:VS studio,nbconvert,nbdime,氢

我认为,通过更多的工作,VS Studio和/或氢气(或类似产品)将成为此工作流程解决方案中的主导者。

答案 13 :(得分:3)

在挖掘之后,我终于找到了this relatively simple pre-save hook on the Jupyter docs。它剥离单元输出数据。您必须将其粘贴到 INCLUDE=TABLE:"IN (select table_name from TAB_LIST)" 文件中(有关说明,请参阅下文)。

jupyter_notebook_config.py

来自Rich Signell's answer

  

如果您不确定在哪个目录中找到def scrub_output_pre_save(model, **kwargs): """scrub output before saving notebooks""" # only run on notebooks if model['type'] != 'notebook': return # only run on nbformat v4 if model['content']['nbformat'] != 4: return for cell in model['content']['cells']: if cell['cell_type'] != 'code': continue cell['outputs'] = [] cell['execution_count'] = None # Added by binaryfunt: if 'collapsed' in cell['metadata']: cell['metadata'].pop('collapsed', 0) c.FileContentsManager.pre_save_hook = scrub_output_pre_save 文件,可以在命令提示符/终端中键入jupyter_notebook_config.py,如果在那里找不到该文件,则为可以通过键入jupyter --config-dir来创建它。

答案 14 :(得分:3)

如果您收到像这样的Unicode解析错误,请跟进Pietro Battiston的优秀脚本:

Traceback (most recent call last):
  File "/Users/kwisatz/bin/ipynb_output_filter.py", line 33, in <module>
write(json_in, sys.stdout, NO_CONVERT)
  File "/Users/kwisatz/anaconda/lib/python2.7/site-packages/IPython/nbformat/__init__.py", line 161, in write
fp.write(s)
UnicodeEncodeError: 'ascii' codec can't encode character u'\u2014' in position 11549: ordinal not in range(128)

您可以在脚本的开头添加:

reload(sys)
sys.setdefaultencoding('utf8')

答案 15 :(得分:2)

好的,按照讨论here,看起来当前最好的解决方案是使git过滤器在提交时自动从ipynb文件中删除输出。

这是我为使其工作所做的工作(从该讨论中复制):

我稍微修改了cfriedline的nbstripout文件,以便在您无法导入最新的IPython时提供信息错误: https://github.com/petered/plato/blob/fb2f4e252f50c79768920d0e47b870a8d799e92b/notebooks/config/strip_notebook_output 并将其添加到我的仓库中,让我们在./relative/path/to/strip_notebook_output

中说

还将文件.gitattributes文件添加到repo的根目录,其中包含:

*.ipynb filter=stripoutput

并创建了一个包含<{p}的setup_git_filters.sh

git config filter.stripoutput.clean "$(git rev-parse --show-toplevel)/relative/path/to/strip_notebook_output" 
git config filter.stripoutput.smudge cat
git config filter.stripoutput.required true

然后跑source setup_git_filters.sh。花哨的$(git rev-parse ...)就是在任何(Unix)机器上找到你的仓库的本地路径。

答案 16 :(得分:2)

由于存在许多处理笔记本版本控制的策略和工具,我尝试创建流程图以选择合适的策略(于2019年4月创建)

Decision flow to pick version control strategy

答案 17 :(得分:2)

我做了艾伯特&amp; amp; Rich做了 - 不要版本.ipynb文件(因为这些文件可能包含混乱的图像)。相反,要么始终运行ipython notebook --script,要么将c.FileNotebookManager.save_script = True放入配置文件中,以便在保存笔记本时始终创建(可版本化的).py文件。

要重新生成笔记本(在签出仓库或切换分支后),我将脚本py_file_to_notebooks.py放在我存储笔记本的目录中。

现在,在签出回购后,只需运行python py_file_to_notebooks.py即可生成ipynb文件。切换分支后,您可能必须运行python py_file_to_notebooks.py -ov来覆盖现有的ipynb文件。

为了安全起见,还可以添加 *.ipynb.gitignore文件。

编辑:我不再这样做了,因为(A)你必须在每次结账时都从py文件中重新生成你的笔记本,而且(B)还有其他东西,例如你丢失的笔记本中的降价。我改为使用git过滤器从笔记本中删除输出。关于如何执行此操作的讨论是here

答案 18 :(得分:1)

这个jupyter扩展允许用户将jupyter笔记本直接推送到github。

请看这里

https://github.com/sat28/githubcommit

答案 19 :(得分:1)

这是2020年4月,有许多用于Jupyter笔记本版本控制的策略和工具。这是您可以使用的所有工具的快速概述,

  • nbdime-非常适合笔记本的本地差异和合并

  • nbstripout-一个git过滤器,可在每次提交之前自动删除笔记本的输出

  • jupytext-保持.py随播文件同步到每个笔记本。您只提交.py文件

  • nbconvert-将笔记本转换为python脚本或HTML(或两者)并提交这些备用文件类型

  • ReviewNB-显示GitHub上任何提交或请求请求的笔记本差异(以及输出)。人们还可以在笔记本电脑的单元格上写评论以讨论更改(以下屏幕截图)。

enter image description here

免责声明:我建立了ReviewNB。

答案 20 :(得分:1)

我还将向其他人添加https://nbdev.fast.ai/的支持,这是最先进的“唐纳德·克努斯(Donald Knuth)在1983年所设想的文学编程环境!”。

它也有一些git钩子,可以帮助一点https://nbdev.fast.ai/#Avoiding-and-handling-git-conflicts和其他命令,例如:

  • nbdev_read_nbs
  • nbdev_clean_nbs
  • nbdev_diff_nbs
  • nbdev_test_nbs

因此,您也可以在编写库时随时随地创建文档,例如其中一些:

除了第一个链接之外,您还可以在nbdev tutorial这里观看视频。

答案 21 :(得分:0)

在下面的帖子中讨论的想法如何,应该保留笔记本的输出,并且可能需要很长时间来生成它,并且它很方便,因为GitHub现在可以渲染笔记本。为导出.py文件添加了自动保存挂钩,用于差异和.html与不使用笔记本或git的团队成员共享。

https://towardsdatascience.com/version-control-for-jupyter-notebook-3e6cef13392d