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給我們使用帶來便利,其基於.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

首先要介紹的是nbdime,它源於nbdiff項目(已經停止),提供對Jupyter Notebook的內容做差異比較和合並的工具。nbdime很有潛力成為對初學者友好的通用型Notebook處理工具,但截止當前,它還只是測試版本,而僅僅可用來查看Notebook內容的差異,無法用來清除輸出信息導致差異噪聲的問題。

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:

然後就可以使用

nbstrip_jq xx-parsing.ipynb> stripped.ipynb

就可以清理煩人的元數據,而且還非常快,大概不到nbstripout的十分之一。

自動化git集成

上面我們用jq腳本的方式解決了問題,但是如何在git操作時自動運行,仍值得研究。為了實現自動化需要用gitattributes,特別是過濾器部分。通過gitattributes濾器是在將數據檢入或檢出git存儲庫時做轉換數據的操作,以便Notebook輸出單元格在將JSON數據添加到git信息庫之前,進行清理操作。gitattributes濾器原理見下圖:

一般情況下,還可以定義一個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

然後通過~/.gitattributes_global設置:

*.ipynb filter= nbstrip_full

我們也可以在版本庫的.gitattributes中配置,只在項目級啟用。

總結

本文我們通過對Jupyter Notebbook項目和Git集成中存在的運行時緩存差異噪聲問題進行探索和解決,最後基於jq腳本和gitattributes過濾器完美解決了問題,實現了Jupyter Notebbook項目和Git的無縫集成。這對Jupyter Notebbook的版本管理和共享發佈等非常有意義。

最後本文作為一個典型的問題發現,探索,解決和自動化案例,本案例也非常值得學習。