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

Using IPython notebooks under version control

什么是保持ipython笔记本在版本控制下的好策略?

笔记本的格式对于版本控制来说是相当合适的:如果你想对笔记本和输出进行版本控制,那么这就非常有效。当人们只想对输入进行版本控制时,麻烦就来了,不包括单元输出(aka)。构建产品"),可以是大的二进制斑点,尤其是电影和情节。特别是,我正试图找到一个好的工作流:

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

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

  • 我不小心用输出提交了一个版本,从而污染了我的存储库。
  • 我清除输出以使用版本控制,但实际上更愿意将输出保存在本地副本中(例如,有时复制需要一段时间)。
  • Cell/All Output/Clear菜单选项相比,一些去掉输出的脚本稍微改变了格式,从而在diff中产生不需要的噪声。这可以通过一些答案来解决。
  • 当将更改拉到文件的干净版本时,我需要找到一些方法将这些更改合并到我的工作笔记本中,而不必重新运行所有内容。(更新)
  • 我已经考虑了下面要讨论的几个选项,但还没有找到一个好的全面的解决方案。完整的解决方案可能需要对IPython进行一些更改,或者可能依赖于一些简单的外部脚本。我目前使用的是Mercurial,但我希望有一个同样适用于Git的解决方案:一个理想的解决方案是版本控制不可知论。

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

    更新:我一直在玩我的修改过的笔记本版本,它可以选择保存一个.clean版本,每次保存都使用格雷戈里·克罗希特的建议。这满足了我的大部分约束条件,但仍然没有解决以下问题:

  • 这还不是一个标准的解决方案(需要修改ipython源代码)。有没有一种简单的扩展来实现这种行为的方法?需要某种保存钩子。
  • 我在当前工作流中遇到的一个问题是拉取更改。这些将进入.clean文件,然后需要以某种方式集成到我的工作版本中。(当然,我总是可以重新执行笔记本,但这可能是一个痛苦,特别是如果一些结果依赖于长时间计算、并行计算等),我还不知道如何解决这个问题。也许一个包含扩展的工作流(如ipycache)可以工作,但这似乎有点太复杂了。
  • 笔记移除(剥离)输出

    • 当笔记本电脑运行时,可以使用Cell/All Output/Clear菜单选项删除输出。
    • 有一些用于删除输出的脚本,例如用于删除输出的脚本nbstripout.py,但不会生成与使用笔记本界面相同的输出。这最终包括在ipython/nbconvert repo中,但这已关闭,说明更改现在包含在ipython/ipython中,但相应的功能似乎尚未包含。(更新)也就是说,Gregory Crosshite的解决方案表明,这是非常容易做到的,即使不调用ipython/nbconvert,所以如果能够正确地连接,这种方法可能是可行的。(然而,将它附加到每个版本控制系统上似乎不是一个好主意——这应该以某种方式连接到笔记本机制中。)

    新闻组

    • 对版本控制笔记本格式的思考。

    问题

    • 977:笔记本功能请求(打开)。
    • 1280:清除所有保存选项(打开)。(以下是本讨论的内容。)
    • 3295:自动导出n


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

      虽然这可能适用于其他VCS,但我知道它不满足您的要求(至少VSC不可知性)。不过,它对我来说还是完美的,虽然它没有什么特别出色的地方,而且很多人可能已经使用过它,但我没有找到关于如何通过谷歌搜索来实现它的明确说明。所以它可能对其他人有用。

    • 将包含此内容的文件保存在某个位置(对于以下内容,我们假设~/bin/ipynb_output_filter.py)
    • 使其可执行(chmod +x ~/bin/ipynb_output_filter.py)
    • 创建文件~/.gitattributes,内容如下

      1
      *.ipynb    filter=dropoutput_ipynb
    • 运行以下命令:

      1
      2
      3
      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
    • 完成!

      局限性:

      • 它只适用于Git
      • 在git中,如果您在somebranch分支中,而您在git checkout otherbranch; git checkout somebranch分支中,通常希望工作树不变。在这里,您将丢失笔记本的输出和单元格编号,它们的源代码在两个分支之间有所不同。
      • 更一般地说,输出根本没有版本控制,就像格雷戈里的解决方案一样。为了不只是在每次执行任何涉及签出的操作时都将其丢弃,可以通过将其存储在单独的文件中来更改方法(但请注意,在运行上述代码时,提交ID是未知的!),也可能对它们进行版本控制(但请注意,这需要的不仅仅是git commit notebook_file.ipynb,尽管它至少可以使git diff notebook_file.ipynb不受base64垃圾的影响)。
      • 也就是说,顺便说一下,如果您确实提取了包含一些输出的代码(即由不使用此方法的其他人提交的代码),那么输出将正常签出。只有本地产生的输出丢失。

      我的解决方案反映了这样一个事实,即我个人不喜欢对生成的东西进行版本控制——请注意,涉及输出的合并几乎可以保证使输出或生产率无效,或者两者都无效。

      编辑:

      • 如果你按照我的建议采用这个解决方案——也就是说,在全球范围内——你会遇到一些麻烦,以防你想要版本输出的Git回购。因此,如果要禁用特定Git存储库的输出筛选,只需在其中创建一个文件.git/info/attributes,其中

        **.ipynb滤波器=

      作为内容。显然,以同样的方式,可以做相反的事情:只为特定的存储库启用过滤。

      • 代码现在在自己的git repo中维护。

      • 如果上述说明导致导入错误,请尝试在脚本路径之前添加"ipython":

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

      编辑:2016年5月(2017年2月更新):我的脚本有几个备选方案-为了完整性,这里有一个我知道的列表:nbstripout(其他变体),nbstrip,jq。


      我们有一个协作项目,产品是Jupyter笔记本,在过去的六个月里我们使用了一种非常有效的方法:我们自动保存.py文件,并跟踪.ipynb文件和.py文件。

      这样,如果有人想查看/下载最新的笔记本,他们可以通过Github或NBviewer进行查看,如果有人想查看笔记本代码是如何更改的,他们只需查看.py文件的更改即可。

      对于Jupyter笔记本服务器,这可以通过添加行来完成。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      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笔记本服务器,这可以通过添加行来实现。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      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

      ipython_notebook_config.py文件并重新启动笔记本服务器。这些行来自于Github提供的答案@minrk,而@dror也将它们包含在他的so答案中。

      对于Ipython 2笔记本服务器,可以使用以下方法启动服务器:

      1
      ipython notebook --script

      或者添加行

      1
      c.FileNotebookManager.save_script = True

      ipython_notebook_config.py文件并重新启动笔记本服务器。

      如果您不确定要在哪个目录中找到您的ipython_notebook_config.py文件,您可以键入ipython locate profile default,如果在那里找不到该文件,您可以通过键入ipython profile create来创建该文件。

      下面是我们在GitHub上使用这种方法的项目:下面是一个GitHub示例,介绍了最近对笔记本电脑所做的更改。

      我们对此非常满意。


      我已经创建了基于minrks-gist的nbstripout,它支持git和mercurial(多亏了mforbes)。它可以在命令行上单独使用,也可以用作过滤器,通过nbstripout install/nbstripout uninstall很容易(un)安装在当前存储库中。

      从Pypi那里得到或者简单地

      1
      pip install nbstripout


      CyrilleRossant为ipython 3.0提供了一个新的解决方案,它坚持标记文件而不是基于json的ipymd文件:

      https://github.com/rossant/ipymd


      (2017—02)

      策略

      • OnOpType():
        • 去掉输出>name.ipynb(nbstripout)
        • 去掉输出>name.clean.ipynb(nbstripout,)
        • 总是从nbconvert到python:name.ipynb.py(nbconvert)
        • 始终转换为降价:name.ipynb.md(nbconvertipymd)
      • vcs.configure():
        • git difftool,mergetool:nbdiff和nbmerge来自nbdime

      工具

      • nbstripout:从笔记本中去掉输出。
        • 网址:https://gist.github.com/minrk/6176788
        • 来源:https://github.com/kynan/nbstripout
          • pip install nbstripout; nbstripout install
      • ipynb_output_filter:从笔记本中去掉输出。
        • 来源:https://github.com/toobaz/ipynb_output_filter/blob/master/ipynb_output_filter.py
      • ipymd:转换为jupyter,markdown,o'reilly atlas markdown,opendocument,.py
        • 网址:https://github.com/rossant/ipymd
      • nbdime"Jupyter笔记本的区分和合并工具"(2015)
        • 来源:https://github.com/jupyter/nbdime
        • 文档:http://nbdime.readthedocs.io/
          • nbdiff:以终端友好的方式比较笔记本电脑
            • nbdime nbdiff作为git diff工具工作:https://nbdime.readthedocs.io/en/latest/git集成快速启动
          • nbmerge:具有自动冲突解决功能的笔记本电脑的三方合并
            • nbdime nbmerge用作git合并工具
          • nbdiff-web:显示笔记本电脑的丰富差异。
          • nbmerge-web:为笔记本提供了一个基于Web的三向合并工具
          • nbshow:以终端友好的方式展示一个笔记本

      如前所述,--script3.x中被否决。这种方法可以通过应用post-save钩子来使用。特别是在ipython_notebook_config.py中增加以下内容:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      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。


      我终于找到了一个有效而简单的方法,让朱彼特和吉特一起打得很好。我仍然处在第一步,但我已经认为这比所有其他复杂的解决方案都要好得多。

      Visual Studio代码是一个很酷的开源代码编辑器,来自Microsoft。它有一个优秀的python扩展,现在允许您将jupyter笔记本作为python代码导入。

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

      VSCode editor with a notebook converted to python

      python文件只包含笔记本输入单元格的内容。输出将在拆分窗口中生成。你在笔记本里有纯粹的代码,只要你执行它,它就不会改变。没有与代码混合的输出。没有奇怪的JSON不可理解的格式来分析您的差异。

      只是纯Python代码,您可以轻松地识别每个差异。

      我甚至不再需要修改我的.ipynb文件了。我可以在.gitignore里放一条*.ipynb线。

      需要生成一个笔记本来发布或与某人共享?没问题,只需单击交互式Python窗口中的导出按钮。

      Exporting a python file to Notebook format

      我刚用了一天,但最后我可以很高兴地使用jupyter和git。

      P.S.:vscode代码完成比jupyter好很多。


      不幸的是,我对mercurial不太了解,但是我可以为您提供一个可以与git一起使用的解决方案,希望您能够将我的git命令转换为它们的mercurial等价物。

      对于后台,在git中,add命令将对文件所做的更改存储到临时区域中。完成此操作后,Git将忽略对该文件的任何后续更改,除非您告诉它也要对其进行阶段化。因此,对于每个给定的文件,下面的脚本将除去所有的outputsprompt_number sections,对除去的文件进行分段,然后还原原始文件:

      注意:如果运行该命令会收到一条错误消息,如ImportError: No module named IPython.nbformat,那么使用ipython来运行脚本,而不是python

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      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


      我使用一种非常务实的方法;它在多个方面对多个笔记本都很有效。它甚至可以让我"转移"笔记本电脑。它既适用于Windows,也适用于Unix/MacOS。
      艾尔认为这很简单,就是解决上面的问题…

      概念

      基本上,不要跟踪.ipnyb文件,只跟踪对应的.py文件。
      通过使用--script选项启动笔记本服务器,该文件将在保存笔记本时自动创建/保存。

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

      我个人使用mercurial对.py文件进行版本跟踪,并使用普通(命令行)命令添加、签入(etc)文件。大多数其他(d)VCS都允许这样做。

      现在跟踪历史是很简单的;.py是小的、文本的,而且很容易区分。偶尔,我们需要一个克隆(只是分支;在那里启动第二个笔记本服务器),或者一个旧版本(签出并导入到笔记本服务器)等。

      提示和技巧

      • 将*.ipynb添加到".hgignore",这样Mercurial就知道它可以忽略这些文件。
      • 创建(bash)脚本以启动服务器(使用--script选项),并进行版本跟踪
      • 保存笔记本会保存.py文件,但不会将其签入。
        • 这是一个缺点:人们可以忘记
        • 这也是一个特性:可以保存一个笔记本(稍后继续),而不需要集群存储库历史记录。

      祝愿

      • 最好在笔记本仪表板上有一个签到/添加/等按钮
      • 结帐(例如)file@date+rev.py应该是有帮助的。加上这一点需要做很多工作,也许我会做一次。直到现在,我都是用手做的。


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

      https://github.com/mwouts/jupytext


      在深入研究之后,我终于在Jupyter Docs上找到了这个相对简单的预存钩子。它除去单元输出数据。您必须将其粘贴到jupyter_notebook_config.py文件中(有关说明,请参阅下面的)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      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

      来自Rich Signell的答案:

      If you aren't sure in which directory to find your jupyter_notebook_config.py file, you can type jupyter --config-dir [into command prompt/terminal], and if you don't find the file there, you can create it by typing jupyter notebook --generate-config.


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

      网址:https://github.com/brookisme/gitnb

      它为CLI提供了一种受Git启发的语法,用于在Git报告中跟踪/更新/diff笔记本。

      这是一个例子

      1
      2
      3
      4
      5
      6
      7
      8
      # 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。它本质上是

      1
      2
      3
      4
      5
      # 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"

      还有更多的方法,可以配置为在每个阶段都需要或多或少的用户输入,但这是一般的想法。


      与2019年更好的方法相比,2016年最流行的答案是不一致的黑客攻击。

      有几个选项,最好的答案是jupytext。

      木星文字

      抓住JupyText上的"走向数据科学"文章

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

      值得注意的是:vs studio,nbconvert,nbdime,hydrogen

      我认为随着更多的工作,vs studio和/或hydrogen(或类似产品)将成为解决此工作流的主要参与者。


      要跟进Pietro Battiston编写的优秀脚本,如果出现这样的Unicode解析错误:

      1
      2
      3
      4
      5
      6
      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)

      您可以在脚本开头添加:

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


      由于有如此多的策略和工具来处理笔记本的版本控制,我试图创建一个流程图来选择合适的策略(创建于2019年4月)

      Decision flow to pick version control strategy


      好的,因此,根据这里的讨论,当前最好的解决方案是制作一个Git过滤器,在提交时自动从IPynB文件中除去输出。

      以下是我所做的工作(从讨论中复制的):

      我稍微修改了cfriedline的nbstripout文件,以便在无法导入最新的ipython时提供信息性错误:https://github.com/petered/plato/blob/fb2f4e252f50c79768920d0e47b870a8d799e92b/notebook/config/strip_notebook_输出把它加到我的回购协议中,比如说在./relative/path/to/strip_notebook_output中。

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

      1
      *.ipynb filter=stripoutput

      创建了一个包含

      1
      2
      3
      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)机器上找到您的repo的本地路径。


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

      要重新生成笔记本(在签出回购协议或切换分支之后),我将脚本py_file_放在存储笔记本的目录中。

      现在,在签出repo之后,只需运行python py_file_to_notebooks.py来生成ipynb文件。切换分支后,可能需要运行python py_file_to_notebooks.py -ov来覆盖现有的ipynb文件。

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

      编辑:我不再这样做了,因为(a)每次签出分支时,都必须从py文件中重新生成笔记本;(b)笔记本中还有其他东西丢失了,比如记下。我改为使用git过滤器从笔记本中删除输出。关于如何做到这一点的讨论就在这里。


      下面的文章中讨论的这个想法怎么样,笔记本的输出应该保存在哪里,理由是生成它可能需要很长时间,而且它很方便,因为Github现在可以渲染笔记本。为导出.py文件添加了自动保存挂钩,用于diff和.html,用于与不使用笔记本或git的团队成员共享。

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


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

      请看这里

      https://github.com/sat28/githubcommit