🐍

HTML内にPythonコードを書いて実行できるPyScriptで遊んでみた!

2022/05/22に公開

こんにちは👋

突然ですがみなさん、PyScriptというフレームワークをご存知でしょうか。

https://twitter.com/anacondainc/status/1520447158603890691

リンク先の記事などを読むに、どうやら、PyScriptというのはJavaScriptのようにHTML内にPythonコードを直接書き、その処理を実行できるというフレームワークらしいです。

今回はそんなPyScriptを使って少し遊んでみたいと思います。

PyScriptのインストール方法

なんとこのPyScript、インストールを必要としません。HTMLファイルのheadタグ内に以下を追加するだけですぐ使用できます。

<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>

ちなみに、公式サイトからpyscript.csspyscript.jsをダウンロードして読み込むことも可能です。

遊んでみる

文字の出力

まずは文字の出力を行いたいと思います。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <title>PyScript Playground</title>
  </head>

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center text-7xl"></p>
      </div>
    </main>

    <py-script output="title">
print('I love PyScript')
    </py-script>
  </body>

</html>

実際のページが以下になります。

ちゃんとI love PyScriptの文字が表示されています。

bodyタグの中を見ながら少し解説したいと思います。

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center text-7xl"></p>
      </div>
    </main>
    <py-script output="title">
print('I love PyScript')
    </py-script>
  </body>

まず、Pythonのコードを書いているのはpy-scriptタグで囲まれた部分になります。この中は完全にPythonのコードになるので、当然ですがインデントもしっかりしなければエラーになります。また、py-scriptタグのoutputで指定されているのは、Pythonコードの実行内容をhtmlのどの部分に表示するかを指定しています。html要素のidの値を使用して任意の要素にPythonコードの実行結果を渡す事ができます。今回の例では、<py-script output="title">...</py-script>と書かれてるので、<p id="title" class="text-center text-7xl"></p>にPythonコードの実行結果であるprint('I love PyScript')が渡されています。

ちなみに、pyscript.write(idの値, 渡したい値)としても同じ結果を得ることができます。その場合は以下のようになります。

<body>
  <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
    <p id="title" class="text-center text-7xl"></p>
    </div>
  </main>
  <py-script output="title">
title = 'I love PyScript'
pyscript.write('title', title)
  </py-script>
</body>

そして、スタイリングについてですが、どうやらTailwind CSSで使用されているクラス名がそのまま使用できるようです。ただ、これについてはどこかに書いてあったわけでもなく、自分で気付いただけなので、もっと調べる必要があると思います。

Pythonの標準ライブラリを使う

Pythonコードなので当然ですが標準ライブラリを使うことができます。
それでは、コードを以下のように変更して、使用されているPythonのバージョンを確認してみましょう。Pythonのバージョン確認にはplatformを使用したいと思います。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <title>PyScript Playground</title>
  </head>

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center text-7xl"></p>
    </main>

    <py-script output="title">
import platform # 追加

print('I love PyScript')
print('Python version: {}'.format(platform.python_version())) # 追加
    </py-script>

  </body>

</html>

はい、ちゃんとPythonのバージョンが確認できました。

コードの変更を加えたpy-scriptの中身について解説したいと思います。

<py-script output="title">
import platform # 追加

print('I love PyScript')
print('Python version: {}'.format(platform.python_version())) # 追加
</py-script>

ライブラリのインポートはpy-scriptの中で行います。そしてその後は普段どおりに処理を記述するだけです。本当にPythonのコードをそのまま書けるのでとても楽です。

外部ライブラリを使う

Pythonの強みといえば優秀な外部ライブラリの存在だと思います。PyScriptでも一部ではありますが、Pythonの外部ライブラリを使用することができます。使用可能なライブラリは以下のPyodideのサイトで確認できるようになっていて、numpyやpandas、matplotlib、Pillow、scikit-learnなどが使用可能になっています。

https://pyodide.org/en/stable/usage/packages-in-pyodide.html

それでは、今回はmatplotlibを使用してグラフの表示をしてみたいと思います。
コードは以下のように変更します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <!-- 追加 ここから -->
    <py-env>
      - numpy
      - matplotlib
    </py-env>
    <!-- 追加 ここまで -->
    <title>PyScript Playground</title>
  </head>

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center"></p>
      <!-- 追加 ここから -->
      <py-button id="graph-button" label="グラフを表示"></py-button>
      <div id="graph-container"></div>
      <!-- 追加 ここまで -->
    </main>

    <py-script output="title">
import platform

print('I love PyScript')
print('Python version: {}'.format(platform.python_version()))
    </py-script>
    <!-- 追加 ここから -->
    <py-script>
import numpy as np
from numpy import pi, sin, cos
import matplotlib.pyplot as plt

graph_button = Element('graph-button')

def display_graph(*ags, **kwgs):
    t = np.arange(0,2*pi,0.01)
    x = 16*sin(t)**3
    y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
    fig, ax = plt.subplots()
    ax.fill_between(x, y, facecolor='red', alpha=0.5)
    pyscript.write('graph-container', fig)

graph_button.element.onclick = display_graph
    </py-script>
    <!-- 追加 ここまで -->

  </body>

</html>

実際に動かすと以下のようになります。

グラフを表示というボタンをクリックすると素敵なグラフが表示されます。では、追加部分のコードを解説したいと思います。

<py-env>
  - numpy
  - matplotlib
</py-env>

まず、headタグ内のこの部分ですが、py-envタグ内で、使用したい外部ライブラリ名を記述します。これにより、py-scriptタグ内で任意のライブラリをインポート可能になります。

<py-button id="graph-button" label="グラフを表示"></py-button>
<div id="graph-container"></div>

次に上記の部分、py-buttonという見慣れないタグがありますが、PyScriptにはVisual component tagというすでにボタンや入力ボックスの見た目が整えられたものが用意されており、それらを使用することで簡単に実装が可能になっていて、py-buttonではボタンコンポーネントの実装を行っています。以下のリンクからVisual component tagの一覧を見ることができます。(まだ種類は少ないですが...)

https://github.com/pyscript/pyscript/blob/main/docs/tutorials/getting-started.md#visual-component-tags

そして、divタグのid="graph-container"の部分を記述し、このあと実行するPythonコードから得られたグラフを表示するエリアの用意をしています。

  <py-script>
import numpy as np
from numpy import pi, sin, cos
import matplotlib.pyplot as plt

graph_button = Element('graph-button')

def display_graph(*ags, **kwgs):
    t = np.arange(0,2*pi,0.01)
    x = 16*sin(t)**3
    y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
    fig, ax = plt.subplots()
    ax.fill_between(x, y, facecolor='red', alpha=0.5)
    pyscript.write('graph-container', fig)

graph_button.element.onclick = display_graph
  </py-script>

py-scriptタグ内ではまず、headタグ内で記述したライブラリのインポートを行います。graph_button = Element('graph-button')でボタン要素の取得を行い、graph_button.element.onclick = display_graphでボタンクリック時にdisplay_graph関数が実行されるようになっています。この部分はまさにJavaScriptで行えるDOM操作って感じで感動しました。display_graphの中身は普通にpltでグラフを作成し、pyscript.writeで指定のdivタグ内に描写しているだけなので、特に説明不要かと思いますが、巷で話題の三角関数を使用して素敵なハート型のグラフを作成している点は注目ポイントかもしれません。

scikit-learnでデータを見る

Pythonを使用している方の中にはデータ分析をメインとしている方も多いのではないでしょうか。

というわけで、scikit-learnで用意されているデータセットから王道のThe Iris Datasetを使用してデータの中身の確認やグラフでデータの特徴を見てみたいと思います。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <py-env>
      - numpy
      - matplotlib
      # 追加 ここから
      - scikit-learn
      - pandas
      # 追加ここまで
    </py-env>
    <title>PyScript Playground</title>
  </head>

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center"></p>
      <py-button id="graph-button" label="グラフを表示"></py-button>
      <div id="graph-container"></div>
      <!-- 追加 ここから -->
      <py-button id="iris-button" label="Irisデータセットを分析"></py-button>
      <div id="iris-container">
        <p id="data-description"></p>
        <div id="plot-area"></div>
      </div>
      <!-- 追加 ここまで -->
    </main>

    <py-script output="title">
import platform

print('I love PyScript')
print('Python version: {}'.format(platform.python_version()))
    </py-script>
    <py-script>
import numpy as np
from numpy import pi, sin, cos
import matplotlib.pyplot as plt

graph_button = Element('graph-button')

def display_graph(*ags, **kwgs):
    t = np.arange(0,2*pi,0.01)
    x = 16*sin(t)**3
    y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
    fig, ax = plt.subplots()
    ax.fill_between(x, y, facecolor='red', alpha=0.5)
    pyscript.write('graph-container', fig)

graph_button.element.onclick = display_graph
    </py-script>
    <!-- 追加 ここから -->
    <py-script>
from sklearn import datasets
import pandas as pd

iris_button = Element('iris-button')
dataset = datasets.load_iris()

def display_irisdata(*ags, **kwgs):
    df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    pyscript.write('data-description', df.head())

    fig, ax = plt.subplots()
    ax.scatter(x=dataset.data[dataset.target==0, 0],
              y=dataset.data[dataset.target==0, 1],
              label=dataset.target_names[0],
              c='blue')
    ax.scatter(x=dataset.data[dataset.target==1, 0],
              y=dataset.data[dataset.target==1, 1],
              label=dataset.target_names[1],
              c='red')
    ax.scatter(x=dataset.data[dataset.target==2, 0],
              y=dataset.data[dataset.target==2, 1],
              label=dataset.target_names[2],
              c='green')

    ax.legend(loc='best', fontsize=14)

    ax.set_title('Iris SepalLength / SepalWidth', size=16)
    ax.set_xlabel(dataset.feature_names[0], size=14)
    ax.set_ylabel(dataset.feature_names[1], size=14)
    pyscript.write('plot-area', fig)

iris_button.element.onclick = display_irisdata
    </py-script>
    <!-- 追加 ここまで -->

  </body>

</html>

今までの説明と同じことをしているので、細かい説明は不要かと思いますが、簡単に流れを説明すると、py-envタグ内で使用する外部ライブラリを追加、mainタグ内にPythonコードの処理結果を表示するエリアを追加、py-scriptタグ内でIrisデータセットのロードから、データを表示しやすいようpandas.DataFrameへの変換、アヤメの種類とがくの長さ、幅の関係を散布図でプロット、その結果をhtmlに描写という流れになっています。pandas.DataFrameの表示はJupyter Labなどと違って少し見にくい感じがありますが、このようなデータ分析のプログラムがHTML内に記述できるようになるのはなかなか夢がありますね。

PythonコードをモジュールとしてインポートしてHTML内で使用する

HTMLってコードが長くなりがちで、スタイリングなんかも合わせるとごちゃごちゃ度が増して嫌だ。という方も多いのではないでしょうか。自分もHTMLファイルとにらめっこするのはあまり好きではありません。そんなHTMLファイル内にPythonコードなんて書いていたら余計複雑になる...と頭を抱えている方に朗報です。なんとこのPyScript、Pythonコードは.pyファイルとして記述しておき、それをHTML内でモジュールとしてインポートして使用できます。

まずはディレクトリ構成を以下のようにします。

.
├── index.html
└── modules
    ├── create_graph.py
    └── create_irisdata.py

py-script内で記述していたdisplay_graphdisplay_irisdataについて、それぞれグラフ作成処理の部分を切り出し、create_graph.pycreate_irisdata.py内に以下のように記述します。

create_graph.py
import numpy as np
from numpy import pi, sin, cos
import matplotlib.pyplot as plt

def create_graph(*ags, **kwgs):
    t = np.arange(0,2*pi,0.01)
    x = 16*sin(t)**3
    y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
    fig, ax = plt.subplots()
    ax.fill_between(x, y, facecolor='red', alpha=0.5)

    return fig

create_irisdata.py
from sklearn import datasets
import pandas as pd
import matplotlib.pyplot as plt

dataset = datasets.load_iris()

def create_irisdata(*ags, **kwgs):
    df = pd.DataFrame(dataset.data, columns=dataset.feature_names)

    fig, ax = plt.subplots()
    ax.scatter(x=dataset.data[dataset.target==0, 0],
              y=dataset.data[dataset.target==0, 1],
              label=dataset.target_names[0],
              c='blue')
    ax.scatter(x=dataset.data[dataset.target==1, 0],
              y=dataset.data[dataset.target==1, 1],
              label=dataset.target_names[1],
              c='red')
    ax.scatter(x=dataset.data[dataset.target==2, 0],
              y=dataset.data[dataset.target==2, 1],
              label=dataset.target_names[2],
              c='green')

    ax.legend(loc='best', fontsize=14)

    ax.set_title('Iris SepalLength / SepalWidth', size=16)
    ax.set_xlabel(dataset.feature_names[0], size=14)
    ax.set_ylabel(dataset.feature_names[1], size=14)

    return df, fig

続いて、index.htmlファイルを以下のように変更します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <py-env>
      - numpy
      - matplotlib
      - scikit-learn
      - pandas
      - paths:
        - ./modules/create_graph.py
        - ./modules/create_irisdata.py
    </py-env>
    <title>PyScript Playground</title>
  </head>

  <body>
    <main class="py-5 grid gap-y-4 grid-cols-1 place-items-center">
      <p id="title" class="text-center"></p>
      <py-button id="graph-button" label="グラフを表示"></py-button>
      <div id="graph-container"></div>
      <py-button id="iris-button" label="Irisデータセットを分析"></py-button>
      <div id="iris-container">
        <p id="data-description"></p>
        <div id="plot-area"></div>
      </div>
    </main>

    <py-script output="title">
import platform

print('I love PyScript')
print('Python version: {}'.format(platform.python_version()))
    </py-script>
    <py-script>
from create_graph import create_graph

graph_button = Element('graph-button')

def display_graph(*ags, **kwgs):
    fig = create_graph()
    pyscript.write('graph-container', fig)

graph_button.element.onclick = display_graph
    </py-script>
    <py-script>
from create_irisdata import create_irisdata

iris_button = Element('iris-button')

def display_irisdata(*ags, **kwgs):
    df, fig = create_irisdata()
    pyscript.write('data-description', df.head())
    pyscript.write('plot-area', fig)

iris_button.element.onclick = display_irisdata
    </py-script>

  </body>

</html>

py-scriptタグの中が結構スッキリした気がすると思います。それでは、index.htmlについてコードの解説をしたいと思います。

<py-env>
  - numpy
  - matplotlib
  - scikit-learn
  - pandas
  - paths:
    - ./modules/create_graph.py
    - ./modules/create_irisdata.py
</py-env>

まず、別ファイルのPythonコードを使用するには、py-envタグ内で- paths:として、それ以降に使用したいPythonファイルのパスを指定します(インデントに注意)。そして、これも重要なのですが、各Pythonファイル内で使用されている外部ライブラリも全て記述する必要があります。

<py-script output="title">
import platform

print('I love PyScript')
print('Python version: {}'.format(platform.python_version()))
</py-script>
<py-script>
from create_graph import create_graph

graph_button = Element('graph-button')

def display_graph(*ags, **kwgs):
    fig = create_graph()
    pyscript.write('graph-container', fig)

graph_button.element.onclick = display_graph
</py-script>
<py-script>
from create_irisdata import create_irisdata

iris_button = Element('iris-button')

def display_irisdata(*ags, **kwgs):
    df, fig = create_irisdata()
    pyscript.write('data-description', df.head())
    pyscript.write('plot-area', fig)

iris_button.element.onclick = display_irisdata
</py-script>

あとはいつも通りにpy-scriptタグ内で各モジュールをインポートし、任意の処理を書いていくだけになります。

これでHTMLファイルがPythonコードでグチャグチャになるのが少しは回避できるのかなと思います。それと、これはPyScriptが発表されて間もないことも影響していますが、HTMLファイルでPythonコードを書いていると自動補完や自動整形が効かないので.pyファイル内で記述することで快適にコーディングすることができます。記事の中でも言及したようにpy-scriptタグは完全にPythonコードなので、コメントアウトも#を使用しますし、インデントにも注意しなければならないので、.pyファイルでコードが書けるのはとても楽でした。

まとめ

さて、ここまでPyScriptで遊んできましたが、実際のところ、現状のPyScriptを使用したページは表示速度がかなり遅く(今回のページで読み込みに10秒ほどかかる)、対応している外部ライブラリもOpenCVやseabornがないなどまだまだ本番環境での使用は遠いかなという感じもします。ただ、Pythonの優秀なライブラリの恩恵をHTML形式で簡単に色々な人に届けられるようになることを考えると、今後のPyScriptはかなり楽しみだなと思いました。

それでは、最後に今回使用したファイルや公式サイトのサンプルなどを紹介して終わりたいと思います。

使用ファイルや公式サイトなどの紹介

  • 今回使用したファイル
    ファイルをダウンロードしてVSCodeのLive Serverなどで簡単にページを見ることができます。

https://github.com/zoniha/pyscript-playground

  • PyScript公式サイト

https://pyscript.net

  • PyScriptサンプルページ集
    PyScriptを使用して作成された様々なデモページを見ることができます。

https://pyscript.net/examples

  • PyScriptのGitHub
    docsの中などに、公式サイトなどに記載のない使用方法などについて記述されていたりするので、より深くPyScriptを知りたい方は見てみることをオススメします。

https://github.com/pyscript/pyscript

以上、PyScriptで遊んでみた話でした。
最後までお読みいただきありがとうございました。

GitHubで編集を提案

Discussion