Jupyter Notebookの内部構造


[Jupyter Advent Calendar 2017](https://qiita.com/advent-calendar/2017/jupyter) 12日目です。
Jupyter Notebookの内部構造を解説します。

# Jupyter Notebookを輸出する

Jupyter は、科学技術文書を作成する環境として大変便利ですね。`Download as` のメニューから、Notebook以外の形式でも保存できます。現時点で保存可能な形式は、HTML、markdown、PDF、LaTeX、reST (restructured text)、使用中言語ソース (下では Julia) です。

<img width="349" alt="スクリーンショット 2017-12-10 11.43.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/103217/2cdc050d-4e88-1115-8a5b-3a6eacbd87ec.png">

Jupyterから直接保存できない形式で使う場合には、Notebookファイルから直接変換するのがよいかもしれません。Notebook形式 (.ipynb) は　JSONファイルですから、読み書きするのも簡単です。　

# Notebookの内部構造

Notebook形式(.ipynb)の内部構造は、[The Notebook file format](https://nbformat.readthedocs.io/en/latest/format_description.html#notebook-file-format) に説明がありますが、構文規則のみです。実務家としては、実際の Notebook の中身をみるのが分かりやすいでしょう。

以下のような、Juliaプログラムの Notebookの中身を見てみます。gist に貼っておきます → https://gist.github.com/anonymous/8d51c367d5cee2828f3da30390033010

<a href="https://gyazo.com/6b88dbbadac14fe8d9d9eb61ce2b43e4"><img src="https://i.gyazo.com/6b88dbbadac14fe8d9d9eb61ce2b43e4.png" alt="https://gyazo.com/6b88dbbadac14fe8d9d9eb61ce2b43e4" width="1021"/></a>


```Julia
using PyPlot
ts=linspace(0,2pi)
plot( ts .* cos.(ts), ts .* sin.(ts))
```

NotebookのJSONの中身は、[gist のraw形式](https://gist.githubusercontent.com/anonymous/8d51c367d5cee2828f3da30390033010/raw/72429b9bc6fe08e8c7dbc2f9c3dbdfebdfdeeefa/Untitled10.ipynb)で表示できます。そのまま貼り付けるのも芸がないので、Rubyを使ってYAMLで表示してみます。参考 → [YAML <-> JSON 変換 (Rubyで実装)](https://qiita.com/lnznt/items/d4798cb42d463cf1acf1)

```Ruby
require "JSON"
json=JSON.parse( File.read("Untitled10.ipynb"));
require "YAML"
YAML.dump(json)
---
cells:
- cell_type: markdown
  metadata: {}
  source:
  - "# アルキメデスの渦"
- cell_type: markdown
  metadata: {}
  source:
  - "$$r = \\theta\\quad\\text{極座標}$$"
- cell_type: code
  execution_count: 1
  metadata: {}
  outputs: []
  source:
  - using PyPlot
- cell_type: code
  execution_count: 2
  metadata: {}
  outputs:
  - data:
      text/plain:
      - 0.0:0.1282282715750936:6.283185307179586
    execution_count: 2
    metadata: {}
    output_type: execute_result
  source:
  - ts=linspace(0,2pi)
- cell_type: code
  execution_count: 3
  metadata: {}
  outputs:
  - data:
      image/png:
      iVBORw0KGgoAAAANSUhE ..(途中省略).. 64AAAAASUVORK5CYII=
      text/plain:
      - PyPlot.Figure(PyObject <matplotlib.figure.Figure object at 0x11e764eb8>)
    metadata: {}
    output_type: display_data
  - data:
      text/plain:
      - '1-element Array{PyCall.PyObject,1}:

'
      - " PyObject <matplotlib.lines.Line2D object at 0x1248e2eb8>"
    execution_count: 3
    metadata: {}
    output_type: execute_result
  source:
  - plot( ts .* cos.(ts), ts .* sin.(ts))
- cell_type: code
  execution_count:
  metadata: {}
  outputs: []
  source: []
metadata:
  kernelspec:
    display_name: Julia 0.6.1
    language: julia
    name: julia-0.6
  language_info:
    file_extension: ".jl"
    mimetype: application/julia
    name: julia
    version: 0.6.1
nbformat: 4
nbformat_minor: 2
```

以下のような内部構造が読み取れます。

1. 最上位の要素は `metadata`, `cells`, `nbformat`, `nbformat_minor` です。前２つは配列で、最後の二つは整数です。
2. `metadata`には、Jupyterのカーネルと、言語の情報が入ります。
3. `cells`には、Notebookの各セルが入ります。
    1. 各セルで重要な要素は、`cell_type`, `metadata`, `sources` です。
    2. `cell_type`は、セルの型です。 markdownセルでは `markdown`、codeセルでは`code`という値が入ります。 `Raw NBConvert` セル(=表示の際に変換されたくないデータを入れる)では `raw`という値が入ります。
        1. `source`には、入力文字列の各行が配列で入ります
        2. codeセルには、`execution_count`と`output`の要素があります。
            1. `execution_count` は、入力セルの通し番号です。 `In[n]` の n に相当する整数または`null`が入ります。
            2. `output`には、出力結果が配列で入ります。
                1. 出力結果の重要な要素は `output_type`と`data`です。
                2. `outout_type`は、出力の形式です。
                3. `data` には、データ本体が入ります。
以上です。

本例では、画像を一つ含んでいます。 画像に対応する出力セルは　`output_type=display_data` という値となり、`data`には`image/png`の行に引き続き、BASE64形式で　png画像が格納されていました。
ちなみに昨年のAdvent Calendarでは、Mayavi　という3DオブジェクトをNotebookに埋め込む試みを紹介しました。　→ [[Python, Julia] Jupyter で 3D 表示 - Mayavi ライブラリ](https://qiita.com/tenfu2tea/items/088b750f0204debe4f7f)
Mayaviオブジェクトに対応する出力セルは、`output_type　= execute_result` となり、`data`には`text/html` に続き、xml形式のデータが含まれていました。


# Scrapboxに輸出したい

Nota Inc. が提供する[Scrapbox](https://scrapbox.io/product)は、複数人でリアルタイムに編集できるWikiです。各ページ中のタグやリンクを使うと、関連する複数のページが整理されて表示されます、Markdownと比べて記法が簡単です。 → [Scrapboxヘルプ: 記法](https://scrapbox.io/help-jp/%E8%A8%98%E6%B3%95)

<a href="https://gyazo.com/092e74b612f8d212b24dcfc4b80a6f8b"><img src="https://i.gyazo.com/092e74b612f8d212b24dcfc4b80a6f8b.png" alt="https://gyazo.com/092e74b612f8d212b24dcfc4b80a6f8b" width="1206"/></a>

私は、Jupyter NotebookをScrapboxに輸出したくなりました。始めは手作業で、以下のようにすれば、scrapboxのページと親和性が高いことが分かりました。
- markdownセルは、Scrapboxの地のテキストに入れる。
- codeセルは、`source`(= プログラム）と出力テキストを、別々の[コードブロック](https://scrapbox.io/help-jp/%E8%A8%98%E6%B3%95#57c7d750000000000000d3026d)に格納する。

# JupyterToScrapboxの紹介

Notebook形式からScrapbox形式への変換を支援するツール`JupyterToScrapbox`を紹介します。 Rubyで書かれていて gemでインストールできます。 bundler を使っていますが、その辺の環境設定は割愛します。

```shell-session
gem install jupyter_to_scrapbox
```

簡単な使い方は、以下の通りです。 Scrapboxで輸入できる形式のJSONファイルが　`scrapbox.json` に格納されます。

```shell-session
bundle exec jupyter_to_scrapbox convert notebook1.ipynb notebook2.ipynb > scrapbox.json
```

さらに、Notebook内の画像を取り込む機能もつけました。[Scrapboxの画像](https://scrapbox.io/help-jp/%E8%A8%98%E6%B3%95#57ff2b7a8ee92a00006bb597)は、Nota Inc. の画像キャプチャサービス [Gyazo](https://support.gyazo.com/hc/ja)への各リンクになっています。[Gyazo　API](https://gyazo.com/api/docs)へのuser access tokenを環境変数`GYAZO_TOKEN`に指定しておき、`--image`オプションをつけて　jupyter_to_scrapboxを起動します。

```shell-session 
export GYAZO_TOKEN="(gyazo user access token)"
bundle exec jupyter_to_scrapbox convert Untitled10.ipynb --image > scrapbox.json
```

冒頭の Notebookから生成したScrapbox形式JSONを、YAMLで表示しましょう。

```ruby
- title: '2017-12-10 01:15:04 +0900'
  lines:
  - '2017-12-10 01:15:04 +0900'
  - "Untitled10.ipynb"
  - ''
  - "[*** アルキメデスの渦]"
  - "[$ r = \\theta\\quad\\text{極座標} ]"
  - code:source.jl
  - "\t# In[1]"
  - "\tusing PyPlot"
  - ''
  - code:source.jl
  - "\t# In[2]"
  - "\tts=linspace(0,2pi)"
  - ''
  - code:output.txt
  - "\t0.0:0.1282282715750936:6.283185307179586"
  - ''
  - code:source.jl
  - "\t# In[3]"
  - "\tplot( ts .* cos.(ts), ts .* sin.(ts))"
  - ''
  - code:output.txt
  - "\timage/png"
  - "[https://i.gyazo.com/f720e453a77bd42bfe666fb5f8a2d914.png]"
  - ''
  - code:output.txt
  - "\tPyPlot.Figure(PyObject <matplotlib.figure.Figure object at 0x11e764eb8>)"
  - ''
  - code:output.txt
  - "\t1-element Array{PyCall.PyObject,1}:"
  - "\t PyObject <matplotlib.lines.Line2D object at 0x1248e2eb8>"
  - ''
  - code:source.jl
  - "\t# In[]"
  - ''
```

codeブロックは、`code:(ファイル名)` で始まり、空行`''`で終わる範囲です。
プログラム列は`source.jl`ブロックに、出力テキストは`output.txt`ブロックに格納することにしました。 

これをScrapboxに取り込むと、以下のようになります。画像も含めて取り込まれています。

https://scrapbox.io/JuliaExamples/2017-12-10_01:15:04_+0900

<a href="https://gyazo.com/f6336483e04cc3d97097b731c4599c59"><img src="https://i.gyazo.com/f6336483e04cc3d97097b731c4599c59.png" alt="https://gyazo.com/f6336483e04cc3d97097b731c4599c59" width="1105"/></a>

なお、上のURLに source.jlを付与すると、プログラム列のみが取り出せます。
https://scrapbox.io/api/code/JuliaExamples/2017-12-10_01:15:04_+0900/source.jl

```source.jl
# In[1]
using PyPlot
# In[2]
ts=linspace(0,2pi)
# In[3]
plot( ts .* cos.(ts), ts .* sin.(ts))
# In[]
```

# 終わりに

Jupyter Notebookの内部構造と、Scrapboxへの変換ツールを紹介しました。 たくさんのNotebookをScrapboxに輸出して実例集を作ってみたいと考えています。詳細は　Julia Advent Calendarとして書きます。