【PHP8】Jupyter と PHP8.0 カーネルのノートブックを Mac やラズパイや Windows の Docker 上で動かす

> PHP8.0 でも Jupyter の Notebook を使いたい。macOS や RaspberryPi や Windows で。
>
> でも、いろいろインストールしてローカルを汚したくない。

そんな遊び心満載なユーザー向けなものが <ruby>Docker<rt>どこか</rt></ruby> にないものか。

## TL; DR

> **Jupyter-PHP の PHP8 用修正パッチ**と macOS/Windows10/Raspbian で動く JupyterLab の Dockerfile を作ってみました。
>
> <sup>作り方や経緯、「Jupyter？何それ。typoってない？」と言う方は TS; DR をご覧下さい。</sup>

### PHP8 カーネル入り Dockerfile の URL

- JupyterLab/Jupyter Notebook の PHP8 カーネル入り Dockerfile（[JIT](https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9) 有効済み）
    - https://github.com/KEINOS/Jupyter-PHP8 @ GitHub
- Docker 使いたくない人向け
    - [Jupyter-PHP-installer](https://github.com/Litipk/Jupyter-PHP-Installer) の修正パッチ
        - [diff.jupyter-php.patch](https://github.com/KEINOS/Jupyter-PHP8/blob/docker/diff.jupyter-php.patch) @ GitHub
        - [パッチを当てる例](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L81-L88) @ Dockerfile


### 使い方

```shell-session:ダウンロードとビルド
$ # Dockerfile 一式のクローン（ダウンロードでも可）
$ git clone https://github.com/KEINOS/Jupyter-PHP8.git
...

$ # Docker イメージのビルド
$ cd Jupyter-PHP8
$ docker build -t jupyter:local .
...（いささか時間がかかります）...
```
```shell-session:コンテナ（Jupyterサーバ）の起動
$ # Docker イメージからコンテナ（Jupyter サーバ）の起動。
$ # ローカルのポート 8001 をコンテナの 8000 ポート（Jupyter サーバのポート）につなげて起動する
$ docker run --rm -p 8001:8000 -v $(pwd)/data:/workspace jupyter:local
...（途中でアクセストークンが表示されるのでコピーしておく）
[I 12:43:22.853 LabApp] Jupyter Notebook 6.1.5 is running at:
[I 12:43:22.854 LabApp] http://44a5fb18069a:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.854 LabApp]  or http://127.0.0.1:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.855 LabApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
...（アクセス・ログが表示されていく。サーバを終了するには Ctrl+C）
```

- 上記で Jupyter サーバのコンテナが起動したら、ブラウザからアクセスします。
    - ポートは 8001 です。8001 で都合が悪い場合は、上記の `docker run` で 8001 を他のポート番号にしてください。
    - ローカル（同一マシン）からアクセスする場合:
        - `http://localhost:8001/?token=<アクセストークン>`
    - 同じネットワークの別のマシンからアクセスする場合
        - `http://<ホストのIPアドレス>:8001/?token=<アクセストークン>`

<details><summary>イメージ・インフォ</summary><div><br>

<h3>イメージの諸情報</h3>

- Intel/AMD および ARM v7l の CPU で動作確認しています。
    - macOS Catalina (OSX 10.15.7, MacBook Pro Early 2015, Docker version 19.03.13)
    - Raspbian Stretch (Debian 9, Raspberry Pi3+, Docker version 19.03.13) 
    - Windows 10 Pro 64bit (20H2 ビルド 19042.630, Intel Pentium Silver N5000, Docker v20.10.0-rc1)
- Docker のベースイメージ: [keinos/php8-jit:latest](https://github.com/KEINOS/Dockerfile_of_PHP8-JIT) Alpine ベース
- PHP のバージョン: PHP8.0.0（2020/11/28 時点の `master`）+ [JIT](https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9) 有効
- デフォルトで JupyterLab が起動しますが、以下のサービスもインストールされています。
    - jupyter core
    - jupyter-notebook
    - qtconsole
    - ipython
    - ipykernel
    - jupyter client
    - jupyter lab
    - nbconvert
    - pywidgets
    - nbformat
    - traitlets

</div></details>

## TS; DR

Jupyter や Jupyter Notebook をご存知でしょうか。私は知りませんでした。

いや、名前や存在自体は知っていたのです。ペチペチ PHPer の私は「Python かぁ... Python かぁ...」とか言いながら、完全に理解すらしていない機械学習の記事や本を読んでいると、Jupyter やらノートブックと言う単語にチョイチョイ出くわしていたのです。それが何であるかの説明もなく、当然のごとく話しが進むのです。

しかし、どうもテキスト・エディタや IDE のように見えます。なんてっちゃって [IDLE](https://ja.wikipedia.org/wiki/IDLE_(Python)) みたいなものかな？、と。Win の頃は秀丸エディタ、サクラ・エディタ、Mac に移ってからは [Coda2](https://panic.com/jp/coda/)、Atom、XCode を経て、やっと [VSCode](https://ja.wikipedia.org/wiki/Visual_Studio_Code) に落ち着いた自分としては新たなエディタとして使うには汎用性に欠けると思い込んでおり、避けていました。

すると、先日（2020/11/18）とあるニュースが目に飛び込んできました。

> 『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』
> 「Python」言語拡張と切り離して、それ以外のプログラミング言語でも利用可能に
>
> <sup>（[『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』](https://forest.watch.impress.co.jp/docs/news/1289446.html)@ インプレス Watch より）</sup>

「ん？ VSCode で Jupyter？どう言うこと？」となりました。「VSCode で秀丸エディタが使える！」と言うような意味不明なニュースに見えたからです。

そこで、偏見や食わず嫌いなのかもしれない、と思い。改めて調べてみました。

### Jupyter/Jupyter Notebook とは

プログラムのソースコードに、注釈や補助情報としてコメントを入れることがあると思います。

恐れずに言うなら、Jupyter は「その逆」です。つまり、**Jupyter は Markdown のドキュメントにプログラムをコメントとして埋め込んでステップ実行できるもの**と言えそうです。

そのファイルが「ノートブック」（正しくは「Jupyter ノートブック」）と呼ばれ、JSON 形式の「Jupyter ノートブック・フォーマット」で書かれたものです。拡張子は `.ipynb`で `iPython NoteBook` の略です。

例えば、以下のような「なんちゃって論文」を Markdown ドキュメントで書きたいとします。

> \# All about Hello, world
>
> \`\`\`python3
> print("HelloWorld")
> \`\`\`

しかし、コードブロック内の `print()` をユーザー（読者）が実行して試せるようにしたいので、Jupyter ノートブック形式で作りたいとします。

最終的に以下のような JSON 形式になるのですが、ざっと構成だけみて下さい。「へぇ」と思う反面、手打ちで作成するには結構面倒だと思います。

```json:sample.ipynb
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# All about Hello, world"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "print(\"HelloWorld\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
````

上記をみると、Markdown 行のブロックと、コード行のブロックで分かれており、Jupyter のフォーマットでは、この各ブロックを「セル」（`cell`）と呼んでいます。そして最後に実行環境のメタデータが埋め込まれているのがわかると思います。シンプルですが、柔軟性があるフォーマットだと思います。

つまり、エディタを使ってドキュメントを書く時は以下を繰り返して、積み上げるイメージで作成して行きます。

1. セルを挿入する
2. 各々のセルに「`markdown`」や「`code`」といった属性を付ける
3. セルに内容（テキスト）を記入

そして、これらのファイルのエディタやビューアとなるのが `Jupyter Notebook` や `JupyterLab` と呼ばれる Python で書かれた Web アプリです。

`Jupyter Notebook` は従来からあるシンプルなエディタ＆ビューアーですが、ファイルとしての`Jupyter Notebook` なのか、アプリとしての `Jupyter Notebook` なのかがわかりづらいことから、「クラシック」（`Classic Jupyter Notebook`）とも呼ばれます。

`JupyterLab` は `Classic Jupyter Notebook` の後続の上位バージョンです。より多機能になっており、IDE に近いアプリになっています。

[公式のオンラインのデモ](https://jupyter.org/try)が触れるので試してみて下さい。

- [オンラインで JupyterLab を触ってみる](https://mybinder.org/v2/gh/jupyterlab/jupyterlab-demo/master?urlpath=lab/tree/demo) | try @ Jupyter.org
- [オンラインで Classic Jupyter Notebook を触ってみる](https://mybinder.org/v2/gh/ipython/ipython-in-depth/master?filepath=binder/Index.ipynb) | try @ Jupyter.org

どちらも画面上部に「▶︎」の再生ボタンが表示されるので、押していって動きをみてください。ステップ実行的に実行されていくのがわかると思います。本記事は、これに PHP8 を動かすための記事ということになります。

### Jupyter Notebook で書くと何が良いのか

何回目か分からない人工知能（AI）ブームの昨今、毎日のように機械学習の新しい論文やアイデアが発表されているそうです。そして、どの分野の論文もそうですが[査読](https://ja.wikipedia.org/wiki/%E6%9F%BB%E8%AA%AD)、つまり「内容の確認と検証」などを経て認知されます。

しかし、論文と同じ環境を用意したり、論文中に書かれたコードが本当に同じ結果を出すのか、写経のごとく別途コードを入力して検証するのも大変です。

Jupyter のノートブック形式であれば、「▶︎」の再生ボタンを押して行けば Markdown 中に埋め込まれた順にコードが実行されていきます。コードエディタや IDE で言うステップ実行と同じ感覚です。

つまり、**論文と共に検証実証もセットで配布できる仕組み**が研究者に好まれているのだと思います。

さすが Python パイセン。後輩の PHP が、止まることより動き続けることに重きを置くフラジャイル開発に始まり（嘘）、型々言うな、小さな気合と根性の積み重ねが大事なんだ、とスタミナ勝負の[アジャ](https://www.google.com/search?q=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%B3%E3%83%B3%E3%82%B0&tbm=isch)ガイル開発を続け（嘘）、問題を後任へ後任へとパスしながら前へ進むスクラム開発を経て（違）PHP が一世を風靡したのは今や昔（本当）。

体育会系の PHP と違い、教育を念頭に力を入れてきた理系の Python が、ここに来て真価が現れてきたのだと思います。

これは PHPer の私も見習わないと！、と思い Jupyter で PHP は使えないのか調べ始めました。

### Jupyter のカーネルとは

木星の `Jupiter` でなく `Jupyter` なのは、元々は「[Julia](https://ja.wikipedia.org/wiki/Julia_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E))、Pythonと [R 言語](https://ja.wikipedia.org/wiki/R%E8%A8%80%E8%AA%9E)向けだったから」だそうで、それらを組み合わせて `Ju-Pyt-Er` つまり `Jupyter` なんだそうです。お洒落ですね。PHP が Programmers Hate PHP なのとは大違い（違）。

その後、「この仕組みが勉強やアイデアの共有には便利」と言うことで他の言語でも使えるようになりました。その際の仲介役、つまり Jupyter から受け取ったスクリプトを該当するプログラム言語で実行するプログラムを「カーネル」と呼ぶようになります。

つまり、PHP を Jupyter で動かすためには PHP カーネルが必要になります。

しかし、残念なことに 2020/11/19 現在、PHP カーネルは <code>[IPHP](https://github.com/dawehner/ipython-php)</code> と <code>[Jupyter-PHP](https://github.com/Litipk/Jupyter-PHP)</code> の2つしか存在していません。

- [Jupyter がサポートするカーネル一覧](https://github.com/jupyter/jupyter/wiki/Jupyter-kernels) | Wiki | Jupyter @ GitHub

しかも `IPHP` は開発は中止、`Jupyter-PHP` を見ろと言いつつも、肝心の `Jupyter-PHP` は事実上放置されています。

> 🐒 【カーネルとは】
>
> <sup>OS では Linux や Darwin のカーネルがあったり、機械学習ではカーネル関数があったりと、「カーネル」と言う言葉をあちこちで聞きます。ケンタの[白髭のおっさん](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%83%BB%E3%82%B5%E3%83%B3%E3%83%80%E3%83%BC%E3%82%B9)のカーネル（`Colonel`）ではなく、`Kernel` です。</sup>
> 
> <sup>**`Kernel` とは「何かの中核や本質をなすもので、何かに覆われており見つけにくいもの」**を言います。語源は[中期オランダ語](https://ja.wikipedia.org/wiki/%E4%B8%AD%E6%9C%9F%E3%82%AA%E3%83%A9%E3%83%B3%E3%83%80%E8%AA%9E)の「`cornel`」（粗食）から[中高ドイツ語](https://ja.wikipedia.org/wiki/%E4%B8%AD%E9%AB%98%E3%83%89%E3%82%A4%E3%83%84%E8%AA%9E)の「`kornel`」（穀物）を経て、`corn` の語源でもある[古英語](https://ja.wikipedia.org/wiki/%E5%8F%A4%E8%8B%B1%E8%AA%9E)の「<code>[cyrnel](https://en.wiktionary.org/wiki/cyrnel)</code>」（種や、木を見た時の根っこなど。`corn`+`-nel` と同義の `cyrnel`）から[来ています](https://www.etymonline.com/word/kernel)。日本語で「種」「核」「（牡蠣などの）身」的なニュアンスです。</sup>
> 
> <sup>例えば、OS の場合は、シェルなどを通してしかユーザーがアクセスできない OS の中核をなすプログラム群を「カーネル」と言います。機械学習では、一見すると A と B に直線で分けづらい（線引きしづらい） X,Y の２次元データに、もう 1 データ加えて 3 次元にすると面で線引きできる（現れてくる）データを「カーネル」と言います。</sup>
> ![](https://miro.medium.com/max/1400/0*5AtpuJ7X08V4zc9f.png)
> <sup>（[ディープラーニングで動画の顔画像の入れ替え実施編＆そこから学ぶ機械学習の基礎の基礎 ](https://qiita.com/KEINOS/items/f9a630af1755207f61b5#%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%81%95%E3%82%93%E5%87%BA%E7%95%AA%E3%81%A0%E3%81%99) @ Qiita より）</sup>

### Jupyter-PHP を PHP8 で動かす

公式の Docker の PHP イメージ（`php:8.0-rc-alpine`）は RaspberryPi Zero で動かなかったり、JIT がデフォルトで有効になっていなかったり、リージョンが日本になっていなかったり、`mbstring` が有効になっていなかったり、と、すぐに使うには色々と面倒臭いので設定済みのイメージを使います。

- <code>[keinos/php8-jit:latest](https://github.com/KEINOS/Dockerfile_of_PHP8-JIT)</code> @ GitHub

これをベースに Dockerfile を構築することにしました。最終的な成果物は[こちら](https://github.com/KEINOS/Jupyter-PHP8)です。

- https://github.com/KEINOS/Jupyter-PHP8 @ GitHub

さて、問題の PHP カーネルですがゼロから作るよりは既存のカーネルを修正して動くなら、それを使うのがベストと言うもの。`IPHP` と `Jupyter-PHP` で違いを調べてみました。

`IPHP` は <code>[IPython](https://ja.wikipedia.org/wiki/IPython)</code> が使えれば composer だけで使えていたらしいのですが不具合や限界も多く、`Jupyter-PHP` は、さらに [PHP エクステンションの zmq](https://www.php.net/manual/ja/book.zmq.php) を必要としています。

しかし、理論上 `Jupyter` フォーマットが変わらない限り、受け取った PHP コードをパースして PHP ランタイムに渡せば良いだけです。そこで、なんとか `Jupyter-PHP` を使えないものか奮闘してみました。

[Jupyter-PHP のソースコード](https://github.com/Litipk/Jupyter-PHP) を見てみると `composer.json` があるので、`composer require` でインストールできるのかと思いきや、エラーでできません。

- [Jupyter-PHP 本体](https://github.com/Litipk/Jupyter-PHP) @ GitHub

`composer.json` を覗くと `"require":{"php":">=7.0"}` と PHP のバージョンを制限していました。そこで `--ignore-platform-reqs` オプションを付けてみたところ、今度はエラーは出ないものの、やはり動きません。Jupyter にカーネルが現れないのです。どうやら、肝心のカーネルがしかるべきパスに設置されないようです。

つまり、`Jupyter-PHP` のカーネルをスコープ（許された範囲）外に設置する必要があります。しかし、`composer` だけでは、どこのパスに Jupyter のカーネル設置先のディレクトリがあるか分からず、検索するにしても OS にも依存するため `composer.json` 単体では完結できないのです。

そこで、次に [README にあるようにインストーラー](https://github.com/Litipk/Jupyter-PHP#getting-started)を実行してみましたが、やはりエラーでインストールできませんでした。

- [Jupyter-PHP-Installer](https://github.com/Litipk/Jupyter-PHP-Installer) @ GitHub

インストーラーの中身を確認してみると、カーネルのインストール先ディレクトリを検索して見つけたのち、内部で `composer` コマンドを呼び出して `composer install` していました。

- [/src/Installer/Installer.php#L159-L166](https://github.com/Litipk/Jupyter-PHP-Installer/blob/3ed473805a8de2d6f57d95b1488930ae88784c60/src/Installer/Installer.php#L159-L166)

この時に `--ignore-platform-reqs` オプションが指定されていないため「PHP8 ではバージョンが合わない」とエラーが出ていたのです。

そのため、Jupyter-PHP を PHP8 で動かすためには、主に２カ所の修正が必要です。

- Jupyter-PHP-Installer の `composer.json` にある PHP バージョン指定の削除
- Jupyter-PHP-Installer の `src` にあるインストール・スクリプト内に `--ignore-platform-reqs` を加え、依存パッケージの PHP バージョンを無視させる

そして、これらの修正を行うパッチを作成しました。[Jupyter-PHP-Installer のリポジトリ](https://github.com/Litipk/Jupyter-PHP-Installer)をクローンかダウンロードして適用します。

- [diff.jupyter-php.patch](https://github.com/KEINOS/Jupyter-PHP8/blob/docker/diff.jupyter-php.patch) @ GitHub

また、JupyterLab に表示されるカーネルの選択パネル（アイコン？）の表記が「PHP」だったので、わかりやすいように「PHP8」に変更するパッチも作成しました。

Jupyter-PHP-Installer でインストール後、[PHP のカーネル `kernel.json` を検索](https://github.com/KEINOS/Jupyter-PHP8/blob/docker/Dockerfile#L246-L250)して、パッチをあてて下さい。

- [diff.kernel.patch](https://github.com/KEINOS/Jupyter-PHP8/blob/docker/diff.kernel.patch) @ GitHub

PHP に関する注意点としては、`composer` のバージョンと、Jupyter-PHP が依存する PECL パッケージの [zmq](https://www.php.net/manual/ja/book.zmq.php) の PHP エクステンション（PHP 拡張機能）です。

1. `composer` のバージョンを v1系にすること。最新の v2 系だと、追随していない Composer パッケージが多いため、エラーがでまくります。
    - [Dockerfile の指定箇所](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L68) @ GitHub
2. `zmq` はソースから PECL パッケージをインストールすること。`pecl install zmq` で入るコンパイル済みパッケージは古すぎるため動きません。また、`zmq` が依存する `zip` の PHP エクステンションも一緒に入れておく必要があります。
    - [Dockerfile の指定箇所](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L55-L59) @ GitHub

あとは、JupyterLab や Classic Jupyter Notebook を動かすのに必要な Python3 と、インストールに使う `pip` を入れます。

1. Python3 を Alpine Docker に入れる
    - [Dockerfile の指定箇所](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L109-L211) @ GitHub
2. `pip` を Alpine に入れる
    - [Dockerfile の指定箇所](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L222-L240) @ GitHub
3. Jupyter 一式をインストールする
    - [Dockerfile の指定箇所](https://github.com/KEINOS/Jupyter-PHP8/blob/e418cfa6204f266698a522e175e0a33001c5d86a/Dockerfile#L245-L268) @ GitHub

ここまで来て、やっと出来上がったのですが、1つだけ後悔していることがあります。いつものクセで Docker のベースイメージを Alpine にしたことです。

Docker の Alpine Linux ベースのイメージは軽量であるため、複数のコンテナを起動して利用するのには向いています。つまり、１コンテナに1つの機能（サービス）を動かして使う場合です。しかし、軽量である反面、Alpine はクセが強いため、1つのコンテナにアレコレとインストールするのには向いていません。

Alpine にも `apk` と言うパッケージマネージャーがあるのですが、お世辞にも Ubuntu や Debian などの `apt` ほど素直にインストールできません。そのため、PHP やら Python やら node.js を入れてアレコレとアプリをインストールするなら、サイズは巨大になるものの Ubuntu や Debian ベースの Docker イメージの方が慣れていない人には楽だったのかもしれないと感じました。

## 参考文献

- [Installation](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html) | Jupyter lab @ readthedocs.io
- [jupyter-php環境を構築してみた](https://qiita.com/yes_dog/items/0751555e8f6c7f614d14) @ Qiita
- [dockerでjupyter notebookが動く環境を付け加える作業](https://qiita.com/tand826/items/0c478bf63ead75427782) @ Qiita
- [Jupyter-PHP](https://github.com/Litipk/Jupyter-PHP) | Litipk @ GitHub
- [jupyter-alpine](https://github.com/nbgallery/jupyter-alpine) | nbgallery @ GitHub