Jupyter Notebook數據格式解析

  • 2019 年 10 月 8 日
  • 筆記

本文2772字,預計閱讀需16分鐘;

比較干,歡迎收藏與實踐

最近遇到一個問題:

如何合併多個Jupyter Notebook的筆記為一個筆記文件?

經常用Jupyter Notebook寫Python程式碼,看到這個需求不是想去找輪子而是想自己做解析和合併。通過深入文件格式去加深對Jupyter Notebook的了解。用Jupyter 寫程式碼有很多優勢:互動式的編程體驗、文檔圖表整合、擴展性強而且非常容易復現結果。

從2017年開始,已有大量的北美頂尖電腦課程,開始完全使用Jupyter Notebook作為工具。如李飛飛的CS231N《電腦視覺與神經網路》課程,在16年時作業還是命令行Python的形式,但是17年的作業就全部在Jupyter Notebook上完成了。雖然現在又推出了Jupyterlab,不能否認的是Notebook仍然是很值得使用和研究的工具,因此除了改主題安插件之外,探索更多的Jupyter Notebook用法和原理是有趣有用的。

用文本編輯器打開一個Jupyter Notebook文件,驚奇地發現不是亂碼,說明不是直接存二進位格式而是文本格式,那就不用按數據塊去解析了。如下圖,熟悉的大括弧和鍵值對讓人想到JSON,仔細看果然是JSON,那讀取就容易了,關鍵就是各個鍵的意義和數據組織。

ipynb文件打開效果

1, 基礎結構

Jupyter Notebook的文件是通過json格式存儲和組織其中的數據的。JSON (JavaScript Object Notation)獨立於程式語言,基礎的結構就是 {鍵1:值1,鍵2:值3}這樣的字典形式,值可以是數字、字元串、數組和字典。Jupyter Notebook的頂層結構是一個鍵值對:{"metadata":{},"nbformat":4,"nbformat_minor":0,"cells":[]}

我們寫程式碼的一個個格子對應的就在cells里,我們交互產生的數據都記錄在cells鍵對應的列表裡,如下圖

cell與我們交互區域的對應

其他的鍵,像metadata是一些描述性的元數據,因此我們重點關注cells的列表。

2, 內容部分

我們在裡面寫程式碼的一個個小塊就是一個個cell,它整體也是一個字典,包含cell_type(內容類型)、source(我們輸入的內容)、metadata(描述性的元數據);這三個鍵就構成了一個cell。如下面的思維導圖,也可以結合上面 程式碼塊區域示例 的圖來理解。

cell的基礎結構

其中execution_count 、output和attachments是不一定每個cell都存在的鍵,因此做解析是要有判斷語句。cell_type有3種選擇:code、markdown、raw,下面對這三種類型分別解析。

程式碼塊通過cell的cell_type標識 "cell_type"="code"

程式碼塊里裝的就是我們寫的一行行程式碼,程式碼裝在source鍵對應的列表裡,source鍵對應的類型是列表list,列表裡是字元串,一行程式碼是一個字元串。executioncount表示執行次數,對應我們前端能看到的In里的次數。

metadata記各種元數據,包括一些插件產生的數據,例如我安裝了一個看執行時間的插件ExecuteTime,每次運行可以看執行耗時和最後一次執行的時間,這個數據也是會記錄在ipynb文件里,對應的就裝在metadata里,如果在一個沒安裝這個插件的環境里運行就不會讀metadata的對應內容,可以說metadata給jupyter提供了很好的擴展性。程式碼輸出的內容在output對應的列表裡。output的列表裡裝的不直接是數值或字元串,而是字典,outputtype有多種可能,包括正常的程式碼輸出的stream、execute_result,還有報錯輸出的error。

各種輸出的效果

Markdown塊是寫報告和文檔常用的cell,在前端會渲染出很好的效果,因為語義和格式就通過markdown本身約定的格式體現,對應記錄的數據比程式碼塊簡單。不涉及輸出所以不需要有output鍵,核心就是source和metadata。

無格式塊的官方說法是叫 Raw NBConvert,對應cell_type的值是raw,因為是純文本效果,在頁面上不做特殊渲染,和markdown有的內容基本一致,核心就在source的字元串列表裡。

以上內容整理為思維導圖如下:

三種內容塊概覽

3, 需求實現

基於以上我們對Jupyter Notebook文件結構的了解,就可以開工寫合併多個ipynb文件為一個的程式碼了。假設我們需要合併一個文件夾下的所有ipynb文件為一個,根據文件名的順序組織。我們首先讀取得到需要合併的文件名的列表,然後通過json庫讀取ipynb文件的內容,因為我們寫的程式碼、文字、程式碼輸出結果這些都在cells里,而且順序是cells列表裡元素的順序,所以我們合併cells里的內容就實現了這一需求。程式碼如下:

import osimport jsonwpt='d:/readingForDS/'#文件所在路徑for root, dirs, files in os.walk(wpt):    flst=filesflst=[wpt+f for f in flst if f.endswith('.ipynb')]jmain=json.load(open(flst[0],'r',encoding='utf-8'))for f in flst[1:]:    jn=json.load(open(f,'r',encoding='utf-8'))    jmain['cells'].extend(jn['cells'])  with open('ipynb-combine.ipynb','w',encoding='utf-8') as wf:    json.dump(jmain,wf)#寫入文件

因為nbformat等鍵是通用的,所以程式碼中直接用了第一個ipynb文件的nbformat值。一個合併的效果如下圖

合併多個ipynb文件示例

關於合併多個ipynb文件這個需求有一個挺好的輪子是https://github.com/jbn/nbmerge 。

同樣的思路我們可以根據一些條件對一個大的ipynb文件拆分為多個文件,例如按章拆分一個讀書筆記(每個章節的特徵是用了markdown語法,如 ## 第3章 用Python讀寫Excel文件)。

4, 應用舉例

了解了Jupyter Notebook的文件組織結構之後,除了合併ipynb文件還可以做哪些事情呢?其實我們可以造很多輪子。例如自己實現:

  • 導出ipynb文件為py腳本文件:
  • 導出ipynb文件為markdown文件;
  • 導出為HTML文件;

導出ipynb文件為py腳本文件的程式碼示例如下:

#ipynb 2 pyjn_py=[]jn_py.extend(['#!/usr/bin/env python','# coding: utf-8'])ja=json.load(open('ipynb2pdf.ipynb','r',encoding='utf-8'))for c in ja['cells']:    if c['cell_type']=='markdown':        jn_py.append('n{0}n'.format('# '.join(c['source'])))    elif c['cell_type']=='code':        if c['execution_count']==None:            jn_py.append('# In[ ]:')        else:            jn_py.append('# In[{0}]:'.format(c['execution_count']))        jn_py.append(''.join(c['source'])+'n')    elif c['cell_type']=='raw':        jn_py.append('n{0}n'.format('#'.join(c['source'])))  with open('ipynb2pdf-c2py.py','w',encoding='utf-8') as wf:    for k in jn_py:        wf.write(k+'n')  # ipynb 2 mdmd_str='' #兩種模式:直接裝到一個字元串里或裝到列表裡,一行是一個字元串for c in ja['cells']:    if c['cell_type']=='markdown':        md_str=md_str+'n'+''.join(c['source'])+'nn'    elif c['cell_type']=='code':        md_str=md_str+'n```python n'+''.join(c['source'])+'n```nn'        if len(c['outputs'])>0: # !=[]            for o in c['outputs']:                if 'text/html' in o['data']: #keys                    md_str=md_str+'n'+''.join(o['data']['text/html'])+'n'                elif 'text/plain' in o['data']:                    md_str=md_str+'n'+''.join(o['data']['text/plain'])+'n'    elif c['cell_type']=='raw':        md_str=md_str+'n'+''.join(c['source'])+'n'  with open('ipynb2pdf-c2md.md','w',encoding='utf-8') as wf:    wf.write(md_str)

直接導出py與程式碼導出對比

因為有時候我們在Github上看ipynb格式的資料時,可能會載入不出來渲染的效果,這時候懂得了上面的Jupyter Notebook的文件組織結構後,我們可以從原始數據大致確定看的ipynb里有那些程式碼,輸出的結果。

5, 總結

總結這篇文章的內容:

  • Jupyter Notebook有良好的文檔圖表整合能力和擴展性,已有大量的北美CS課程使用Jupyter Notebook作為編程環境;
  • .ipynb文件是以json格式組織數據的;我們編寫的程式碼、文本和輸出存在cell列表裡;
  • 程式碼的順序就是cell列表中元素順序;
  • 基於以上特點我們可以寫程式碼合併和拆分Notebook文件,還可實現ipynb文件轉換為py、html格式文件。

以上內容自己整理了一個xmind腦圖,獲取思維導圖文件和文中示例程式碼ipynb文件可在公眾號後台回復 jupyter 獲取。

格式解析導圖概覽