Jupyter Notebook Server API 筆記(2)

陳小痘
12 min readNov 27, 2019

--

此篇文章是接續上一篇:Jupyter Notebook Server API 筆記(1)
如果有興趣的讀者,想知道如何從頭開始,可以閱覽上一篇文章。
而這些文章主要是筆者為了記錄最近工作所需的技能而筆記,並非
是完整的教學文件 ,因此先讓讀者知道一下。好的,我們接著上次
的進度:Jupyter Notebook 基本簡單操作,繼續介紹 Cell 如何使用,
還有最為重要的:撰寫 Python 程式,透過 WebSocket 向 Jupyter Notebook Server API 傳送程式碼,並把運算完的結果回傳回來。

Jupyter Notebook 的 Cell 如何使用

首先我們可以直接在 Cell(單元格) 撰寫內容,先來簡單的一段程式碼

接著我們按下 "Run"按鈕,即會在下一行把結果顯示出來。眼尖的讀者會
發現除了 "In [1]" 會多出數字 "1" 之外,還會自動生成第二個 Cell。首先
產生數字是因為 Execute Cell 的次數,至於如果你不想要自動生成第二個
Cell,可以按快捷鍵:Ctrl + Enter。原本 “Run”按鈕的快捷鍵:Shift + Enter,詳情快捷鍵請參閱底下連結:
https://www.cheatography.com/weidadeyue/cheat-sheets/jupyter-notebook/

程式碼如何執行講完後,筆者來跟大家稍微介紹一下,另一個 Cell 類型Markdown。首先在 Tool Bar 的下拉式選單,選擇 Markdown。

這時候你會發現第二個 Cell 的 “In [ ]”,消失掉了,那是因為切換成
Markdown 所致。接著我們在第二個 Cell 裡輸入我們要顯示的文本,
比較特別的是 "#" 符號,它會讓此行的文字以標題方式顯示:# 一級
標題(大)、## 二級標題(中)、### 三級標題(小)…最小為 ######
其實還有其他功能,有興趣的讀者可以參閱以下連結:
https://medium.com/ibm-data-science-experience/markdown-for-jupyter-notebooks-cheatsheet-386c05aeebed

按下 “Run”按鈕,即會產生下圖的顯示結果。很明顯可以看出各個標題的
大小,還有非標體的純文字,它們之間的差異。

Cell 簡單的使用就先講到這裡,當然 Code、Markdown、 Raw NBConvert 還有很多值得探索的地方。但目前功能已足夠讓筆者接下來測試使用了,
所以我將繼續下去,開始準備來撰寫 Python 程式吧!

在撰寫 Python 程式之前,我們先準備好 Cell 的內容。目前 Cell 的類型
都為 Code,而每個 Cell 都有各自的程式碼,比較特別的是最後一個 Cell
是一段錯誤的程式碼,到時觀察由 Jupyter Notebook Server API 會傳送
什麼樣的資料回來。

在 Menu:Cell → Run All 按下,即會 Execute All Cell,而在各自 Cell 的
下一行會顯示處理完的結果。

開始撰寫 Python 程式!

現在筆者開始撰寫一支 Python 程式:先透過 Jupyter Notebook API 把每個 Cell 的 Code 抓下來,再來藉由 WebSocket 把這些 Code 丟到 Jupyter Notebook API 去運算,最後把運算的結果 print 出來。這就是筆者這支 Python 程式基本上所處理的內容,接下來細節會搭配程式一一講解:

首先,我們會先 import 將使用到的模組,接著就是宣告三個變數
它們分別是:
notebook_path:notebook 的檔案路徑。
base:jupyter 的網址。
headers:你的身分,這邊需要 token。

至於 jupyter 的網址 和 token,讀者可以打開 cmd(命令提示元),輸入 "jupyter notebook",除了在瀏覽器開啟 jupter 之外,你在 cmd 視窗也
可以看見 jupyter 的網址 和 token 兩則資訊。

import json
import requests
import datetime
import uuid
import traceback
from websocket import create_connection
# base和token會在cmd(命令提示元),啟動jupyter notebook時出現
notebook_path = '/test_01.ipynb'
base = 'http://127.0.0.1:8888'
headers = {'Authorization': 'Token 42b391bcab8ef539d39e896412c2665af4cf2da16d2b87f2'}

第二部分,我們要藉由每次發布的 session 去撈到那唯一的 kernel。
因為我們之後在使用 WebSocket 連接時,需要 kernel 的 ID。
所以需要提前知道 kernel。

url = base + '/api/sessions'
params = '{"path":\"%s\","type":"notebook","name":"","kernel":{"id":null,"name":"python3"}}' % notebook_path
response = requests.post(url, headers=headers, data=params)
session = json.loads(response.text)
kernel = session["kernel"]

第三部分,第一行 url,其實是使用官方 Jyputer Notebook API 所提供的資訊,藉此我們可以 GET 此 notebook 檔案的 JSON 格式,所以我們需要用 json.loads 把 JSON 格式轉換成 dict 格式(字典),供 Python 使用。

當我們收集完 notebook 檔案的內容後,我們要萃取出每個 Cell 裡面的 Code,所以 file[‘content’][‘cells’] 裡面的 c[‘source’] 就是每個 Cell 的
Code,只要裡面有 Code (len(c[‘source’])>0),我們就把它存進 "code"
這個 list 變數裡面。

# 讀取notebook檔案,並獲取每個Cell裡的Code
url = base + '/api/contents' + notebook_path
response = requests.get(url,headers=headers)
file = json.loads(response.text)
code = [ c['source'] for c in file['content']['cells'] if len(c['source'])>0 ]

第四部分,我們寫個函式,它是負責把 WebSocket 送出請求給 Jupyter Notebook API 的內容整理好。而第三部分的 "code" 就會在此時餵進來(def send_execute_request(code)),簡單來說就是 Jupyter Notebook API 會依照你給的 Code,去處理並把結果 Response 回來。

def send_execute_request(code):
msg_type = 'execute_request';
content = { 'code' : code, 'silent':False }
hdr = { 'msg_id' : uuid.uuid1().hex,
'username': 'test',
'session': uuid.uuid1().hex,
'data': datetime.datetime.now().isoformat(),
'msg_type': msg_type,
'version' : '5.0' }
msg = { 'header': hdr, 'parent_header': hdr,
'metadata': {},
'content': content }
return msg

第五部分,我們使用 websocket 模組裡的 create_connection 函式,去啟動 WebSocket 連接我們 Jupyter Notebook API。連接好後,就把 Code 一一的丟過去處理,你會發現 json.dumps 這段特別的程式碼,其實它是把 Python 的 dict 格式轉換成 JSON 格式,轉好後才送給 Jupyter Notebook API 運算。

# 開始啟動 WebSocket channels (request/reply)
ws = create_connection("ws://127.0.0.1:8888/api/kernels/"+kernel["id"]+"/channels?session_id"+session["id"], header=headers)
for c in code:
ws.send(json.dumps(send_execute_request(c)))

最後部分,也是看似最難的部分,其實只是程式碼長,一點也不難,筆者開始來說明:一開始的迴圈是依照 Code 有幾組,我們就跑幾次。而進來後 ws.recv() 是 WebSocket 接受 Jyputer Notebook API 的 Response 訊息,但因為訊息格式為 JSON 格式,所以我們再使用 json.loads 把它轉成 dict,Python 所看得懂的格式。接著就是 "msg_type",它是 Message
的類型。如果是:
“stream”:程式碼列印一些訊息。
“execute_result”:程式碼可能顯示圖片,或者把執行結果做輸出。
“display_data”:程式碼進行運算,並把結果以圖表來顯示。
“error”:程式碼遇到錯誤的資訊。

比較特別的是,最後 msg_type == “status” 當它的 “execution_state” 為 “idle” 而非 "busy",就代表這組 "ws.recv()" 已經再也接收不到 Jyputer Notebook API 的 Response 訊息,這時候我們就可以 break 離開這個 while 迴圈,執行下一組 "ws.recv()" 所接收到的訊息。

重點!當一切都跑完後,記得 "ws.close()",WebSocket 一定要關掉,不然它不會消失掉,會一直占用空間,滿了就會讓你的 Jyputer Notebook 爆掉!最慘就是要因此而重新再生成一個全新的 Jyputer Notebook 平台來使用。

# 我們只拿Code執行完的訊息結果,其他訊息將被忽略
for i in range(0, len(code)):
try:
msg_type = ''
while True:
rsp = json.loads(ws.recv())
msg_type = rsp["msg_type"]
# 顯示列印內容
if msg_type == "stream":
print(rsp["content"]["text"])
elif msg_type == "execute_result":
# 顯示圖片編碼
if "image/png" in (rsp["content"]["data"].keys()):
print(rsp["content"]["data"]["image/png"])
# 顯示輸出結果
else:
print(rsp["content"]["data"]["text/plain"])
# 顯示計算表格
elif msg_type == "display_data":
print(rsp["content"]["data"]["image/png"])
# 顯示錯誤訊息
elif msg_type == "error":
print(rsp["content"]["traceback"])
# 當狀態為idle,代表ws.recv()已經沒有任何訊息
elif msg_type == "status" and rsp["content"]["execution_state"] == "idle":
break
except:
traceback.print_exc()
ws.close()

ws.close()

執行 Python 程式!

跑完的結果就是以下的輸出。

(1) print 字串

(2) output 算數結果

(3) output 現在時間

(4) output 圖片(編碼字元)

(5) error 資訊

以上是筆者從 Jupyter Notebook 的 Cell 如何操控,到撰寫 Python 程式去使用 Jupyter Notebook API 的講解。未來有空的話,會再分享 Jupyter Notebook API 還有哪些可以使用的功能,如有興趣的讀者,請記得持續關注哦~~

--

--