将IPython笔记本电脑置于版本控制之下的好策略是什么?
笔记本格式非常适合版本控制:如果想要版本控制笔记本和输出,那么这非常有效。当人们只想对输入进行版本控制时,就会产生烦恼,不包括可能是大型二进制blob的单元格输出(也就是“构建产品”),特别是对于电影和情节。特别是,我试图找到一个良好的工作流程:
如上所述,如果我选择包含输出(例如,当使用nbviewer时这是可取的),那么一切都很好。问题是当我不想版本控制输出时。有一些工具和脚本可以剥离笔记本的输出,但我经常会遇到以下问题:
Cell/All Output/Clear
菜单选项相比,剥离输出的一些脚本会稍微改变格式,从而在差异中产生不必要的噪声。这可以通过一些答案来解决。我已经考虑过几个选项,我将在下面讨论,但还没有找到一个很好的综合解决方案。完整的解决方案可能需要对IPython进行一些更改,或者可能依赖于一些简单的外部脚本。我目前使用mercurial,但想要一个也适用于git的解决方案:理想的解决方案是版本控制不可知。
这个问题已经多次讨论过,但从用户的角度来看,没有明确或明确的解决方案。这个问题的答案应该提供明确的策略。如果它需要最近(甚至开发)版本的IPython或一个易于安装的扩展,这很好。
更新:我一直在玩my modified notebook版本,可选择使用.clean
保存Gregory Crosswhite's suggestions版本。这满足了我的大多数约束,但是仍然没有解决以下问题:
.clean
文件,然后需要以某种方式集成到我的工作版本中。 (当然,我总是可以重新执行笔记本,但这可能会很痛苦,特别是如果某些结果取决于长时间的计算,并行计算等)。我还不知道如何解决这个问题。也许涉及像ipycache这样的扩展的工作流程可能会起作用,但这似乎有点过于复杂。Cell/All Output/Clear
菜单选项删除输出。这是我用git的解决方案。它允许您像往常一样添加和提交(和差异):这些操作不会改变您的工作树,同时(重新)运行笔记本不会改变您的git历史记录。
虽然这可能适用于其他VCS,但我知道它不能满足您的要求(至少VSC不可知)。尽管如此,它对我来说仍然是完美的,虽然没有什么特别精彩,很多人可能已经使用过它,但我没有找到关于如何通过Google搜索来实现它的明确指示。所以它可能对其他人有用。
~/bin/ipynb_output_filter.py
)chmod +x ~/bin/ipynb_output_filter.py
)~/.gitattributes
*.ipynb filter=dropoutput_ipynb
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
完成!
限制:
somebranch
并且你做git checkout otherbranch; git checkout somebranch
,你通常希望工作树不变。相反,您将丢失两个分支之间源不同的笔记本的输出和单元格编号。git commit notebook_file.ipynb
,尽管它至少可以使git diff notebook_file.ipynb
免于base64垃圾)。我的解决方案反映了这样一个事实,即我个人不喜欢将生成的内容保留为版本 - 请注意,涉及输出的合并几乎可以保证输出或生产力无效或两者兼而有之。
编辑:
作为内容。显然,以相同的方式可以执行相反的操作:仅针对特定存储库启用过滤。
git config --global filter.dropoutput_ipynb.clean ipython ~/bin/ipynb_output_filter.py
编辑:2016年5月(2017年2月更新):我的脚本有几种替代方案 - 为了完整性,这里列出了我所知道的:nbstripout(other variants),nbstrip,jq。
刚刚遇到“jupytext”,看起来像一个完美的解决方案。它从笔记本生成一个.py文件,然后保持同步。您可以通过.py文件对版本控制,差异和合并输入进行版本控制,而不会丢失输出。当您打开笔记本时,它使用.py作为输入单元格,使用.ipynb作为输出。如果你想在git中包含输出,那么你可以添加ipynb。
要跟进Pietro Battiston的优秀脚本,如果你得到这样的Unicode解析错误:
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')
我已经构建了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"
还有几种方法,可以进行配置,以便在每个阶段需要更多或更少的用户输入,但这是一般的想法。
在挖掘之后,我终于找到了this relatively simple pre-save hook on the Jupyter docs。它剥离单元输出数据。您必须将其粘贴到jupyter_notebook_config.py
文件中(有关说明,请参阅下文)。
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
[进入命令提示符/终端],如果在那里找不到该文件,可以通过输入jupyter notebook --generate-config
来创建它。
与2019年更好的方法相比,上面非常受欢迎的2016年答案是不一致的黑客。
存在几种选择,回答问题的最好的选择是Jupytext。
抓住Towards Data Science article on Jupytext
它与版本控制一起使用的方式是将.py和.ipynb文件放在版本控制中。如果你想要输入差异,请查看.py,如果你想要最新的渲染输出,请查看.ipynb。
值得注意的提到:VS工作室,nbconvert,nbdime,氢气
我认为通过更多的工作,VS工作室和/或氢气(或类似的)将成为该工作流程解决方案的主要参与者。
我做了Albert&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。
好的,所以看起来当前最好的解决方案,根据讨论here,是使git过滤器在提交时自动剥离ipynb文件的输出。
以下是我为使其工作所做的工作(从该讨论中复制):
当你无法导入最新的IPython时,我稍微修改了cfriedline的nbstripout文件以提供信息错误:https://github.com/petered/plato/blob/fb2f4e252f50c79768920d0e47b870a8d799e92b/notebooks/config/strip_notebook_output并将其添加到我的repo中,让我们说在./relative/path/to/strip_notebook_output
还将文件.gitattributes文件添加到repo的根目录,其中包含:
*.ipynb filter=stripoutput
并创建了一个包含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)机器上找到你的repo的本地路径。
在下面的帖子中讨论的想法如何,应该保留笔记本的输出,并且可能需要很长时间来生成它,并且它很方便,因为GitHub现在可以渲染笔记本。为导出.py文件添加了自动保存挂钩,用于差异和.html与不使用笔记本或git的团队成员共享。
https://towardsdatascience.com/version-control-for-jupyter-notebook-3e6cef13392d
我们有一个合作项目,产品是Jupyter笔记本,我们在过去的六个月中使用了一种方法很好:我们激活自动保存.py
文件并跟踪.ipynb
文件和.py
文件。
这样,如果有人想查看/下载最新的笔记本,他们可以通过github或nbviewer这样做,如果有人想看看笔记本代码是如何变化的,他们可以看看.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
到ipython_notebook_config.py
文件并重新启动笔记本服务器。这些行来自github问题答案@minrk provided和@dror也包括他们的SO答案。
对于Ipython 2
笔记本服务器,可以通过使用以下命令启动服务器来完成:
ipython notebook --script
或者添加该行
c.FileNotebookManager.save_script = True
到ipython_notebook_config.py
文件并重新启动笔记本服务器。
如果您不确定在哪个目录中找到ipython_notebook_config.py
文件,可以输入ipython locate profile default
,如果在那里找不到该文件,可以通过输入ipython profile create
来创建它。
这是our project on github that is using this approach:这是一个github example of exploring recent changes to a notebook。
我们对此非常满意。
我创建了基于nbstripout
的MinRKs gist,它支持Git和Mercurial(感谢mforbes)。它既可以在命令行中单独使用,也可以作为过滤器使用,可以通过nbstripout install
/ nbstripout uninstall
轻松(非)安装在当前存储库中。
从PyPI或简单地获取它
pip install nbstripout
这是Cyrille Rossant针对IPython 3.0的新解决方案,它坚持使用markdown文件而不是基于json的ipymd文件:
(2017-02)
策略
nbstripout
,)
剥离输出> name.clean.ipynb(nbstripout
,)
总是nbconvert
到python:name.ipynb.py(nbconvert
)
总是转换为markdown:name.ipynb.md(nbconvert
,ipymd
)工具
nbstripout
:剥离笔记本电脑的输出
src:https://gist.github.com/minrk/6176788
src:https://github.com/kynan/nbstripout
pip install nbstripout; nbstripout install
ipynb_output_filter
:剥离笔记本电脑的输出
src:https://github.com/toobaz/ipynb_output_filter/blob/master/ipynb_output_filter.pyipymd
:转换{Jupyter,Markdown,O'Reilly Atlas Markdown,OpenDocument,.py}
src:https://github.com/rossant/ipymdnbdime
:“用于区分和合并Jupyter笔记本的工具。” (2015)
src:https://github.com/jupyter/nbdime
docs:http://nbdime.readthedocs.io/
nbdiff
:以对终端友好的方式比较笔记本电脑
nbdime nbdiff作为git diff工具:https://nbdime.readthedocs.io/en/latest/#git-integration-quickstart
nbmerge
:笔记本电脑与自动冲突解决方案的三方合并
nbdime nbmerge是一个git合并工具
nbdiff-web
:向您展示丰富的笔记本渲染差异
nbmerge-web
:为笔记本电脑提供基于Web的三向合并工具
nbshow
:以终端友好的方式呈现单个笔记本正如所指出的那样,--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。
我终于找到了一种高效而简单的方法让Jupyter和Git很好地融合在一起。我还在迈出第一步,但我已经认为它比其他所有复杂的解决方案都要好得多。
Visual Studio Code是一款来自微软的酷炫开源代码编辑器。它有一个很好的Python扩展,现在允许你将import a Jupyter Notebook作为python代码。
将笔记本导入python文件后,所有代码和markdown将一起放在普通的python文件中,注释中带有特殊标记。您可以在下图中看到:
你的python文件只包含笔记本输入单元格的内容。输出将在拆分窗口中生成。你在笔记本中有纯粹的代码,当你执行它时它不会改变。没有与您的代码混合输出。没有奇怪的Json难以理解的格式来分析你的差异。
只需纯Python代码,您可以轻松识别每个差异。
我甚至不需要再版我的.ipynb
文件了。我可以在*.ipynb
放一条.gitignore
线。
需要生成笔记本才能发布或与他人分享?没问题,只需在交互式python窗口中使用click the export button
我一直在使用它只有一天,但最后我可以愉快地使用Jupyter与Git。
P.S。:VSCode代码完成比Jupyter好很多。
我用一种非常务实的方法;这对于几个笔记本电脑来说很有效。它甚至可以让我“转移”笔记本电脑。它适用于Windows作为Unix / MacOS。 Al认为很简单,就是解决上面的问题......
基本上,不要跟踪.ipnyb
文件,只跟踪相应的.py
文件。
通过使用--script
选项启动笔记本电脑 - 服务器,保存笔记本时会自动创建/保存该文件。
那些.py
文件确实包含所有输入;非代码保存到注释中,单元格边框也是如此。可以将这些文件读取/导入(并拖动)到笔记本服务器中以(重新)创建笔记本。只有输出消失了;直到重新运行。
我个人使用mercurial版本跟踪.py
文件;并使用普通(命令行)命令添加,签入(ect)。大多数其他(D)VCS将允许这样做。
现在很容易跟踪历史; .py
很小,文字和简单的差异。有一段时间,我们需要一个克隆(只是分支;在那里启动第二个笔记本 - 服务器),或者旧版本(检出并导入到笔记本服务器中)等。
--script
选项)并对其进行版本跟踪.py
文件,但不会将其签入。
这是一个缺点:人们可以忘记这一点
它也是一个特性:可以保存笔记本(并在以后继续),而无需集群存储库历史记录。file@date+rev.py
)应该是有用的。添加它会有很多工作;也许我会这样做一次。到现在为止,我只是手工完成。不幸的是,我对Mercurial了解不多,但我可以给你一个与Git一起使用的可能解决方案,希望你能将我的Git命令转换成他们的Mercurial等价物。
对于后台,在Git中,add
命令将对文件所做的更改存储到暂存区域。完成此操作后,Git将忽略对该文件的任何后续更改,除非您告诉它也将其暂存。因此,下面的脚本,对于每个给定的文件,删除所有outputs
和prompt_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
。