ローカル Web アプリでグラフ描画 【Python】

Python

ブラウザ上での入力に応じてグラフなどの画像を返すローカル Web アプリについて、Python のスクリプトをまとめ、公開しておきます。
ローカル Web サーバーから Python を実行していますので、ローカルファイルの処理を行うなど、自由なカスタマイズが可能になると思います。

以下の環境で動作確認をしています。
環境: Windows パソコン、Python 3.x (+Anaconda 3)

背景 ~ ブラウザで動くローカルアプリを作りたい!

ローカルでファイルの読み書きなどもできる GUI アプリをできるだけ速攻で、簡単に作りたくなることがあります。
設定やインストールは最小限に済ませ、できたアプリはブラウザ上で動くとベストです。
とはいえ、ネットで探すと、こういった要求を満たしたものは少ないようです。また、ローカルアプリを作るとなると、設定が煩雑なものがほとんどです。

これまでこのサイトでは、ブラウザ上で動く、テキストボックスの入出力程度のスクリプトについてまとめてきています。
そこで今回は、この構成を発展させて、ブラウザ上の入力に応じて、画像を描画するスクリプトについてまとめ、公開しておくことにします。
プログラムの内容は、ブラウザ経由で Python を実行している程度です。したがって、データの読み書きや、他の画像を生成する機能を追加するなど、カスタマイズも容易だと思います。

画像については、通常よく使われるラスター画像を使うと、計算を実行するたびにデータの転送や画像の保存が発生し処理が重くなってしまいます。
そこで、jpg、bmp、HTML の canvas などの使用は避け、ベクター形式である SVG を使って表示させることにします。

なお、以下のサンプルスクリプトは、個人など限られたユーザーのみで使用することを想定しています。
不特定の人がアクセスする環境で必要となる機能(パスワード画面、いたずら防止、…)などは入っておりません(別途、検討が必要です)ので、ご理解をいただきますようお願いいたします。

設定手順

ローカル Web アプリの設定

① 以下を参考に、Windows パソコンに公開フォルダ(ドキュメントルート)として使うフォルダ(例:”webserver1″)を作ります。
c:\user\webserver1\
② ①のフォルダ内にさらに “cgi-bin” という名前でフォルダを作ります。
③ ②のフォルダ内に、chart1.py というファイル名でテキストファイルを作成し、末尾のサンプルスクリプトの内容をコピー&ペーストし、保存します。
c:\user\webserver1\cgi-bin\chart1.py

起動用バッチファイルの設定

④ ①のフォルダ内に、”chart1.bat” という名前でテキストファイルを作成し、下記のバッチファイルの内容をコピー&ペーストして保存してください。
※ このバッチファイルを入れたフォルダ内がドキュメントルートとなるよう、フォルダを移動して、サーバーとブラウザを起動するよう記載してあります。
※ ①~③で、フォルダ名やファイル名などを変更した場合は、バッチファイルの内容も修正してください。
※ 画面を最小化してサーバーを起動したのち、ブラウザを起動するようにしています。
ブラウザの起動が不要であれば、最後の timeout 以後の2行を削除してください。
※ また、サーバーの起動コマンドを変更したい場合も、適宜、修正をしてください。

⑤ ④のバッチファイルの記載で、[username1] の記載のある行は、Anaconda を使う場合に必要なコマンドです。
Anaconda を使っていない場合は、この1行は削除して問題ありません。Anaconda を使う場合は、Anaconda の activate.bat のありかを確認して、記載を修正してください。通常、[username1] の部分は、パソコン設定時のユーザー名となっていると思います。

起動・停止方法

⑥ 起動する場合は、⑤のバッチファイル chart1.bat をダブルクリックしてください。
パソコン上で、Python のローカルサーバーが起動し、その後、ブラウザが起動して Web アプリが表示されます。

→ ブラウザ上で入力をしてみて、画像が更新されたら成功です!

※ バッチファイルの中にコマンドを記載していますので、コマンドプロンプト(または Anaconda Prompt)から手入力で動かしても同様に動作すると思います。

⑦ サーバーが起動すると、Windows のタスクバー(右下あたり)に、”python.exe” のアイコン(白紙のアイコン)が表示された状態になります。最小化された画面で起動した状態となります。
サーバーを終了する場合は、この “python.exe” のアイコンをクリックしてコマンドプロンプトの画面を表示させて、画面(黒画面)上で [ctrl] + [c] をキー入力すると停止します。
ブラウザ側も閉じてしまって問題ありません。
※ なお、⑥~⑦でサーバーを起動させておけば、今回と同様な Python のスクリプトを追加することで、ブラウザ上で動かすことが可能です。

スクリプトの説明

・ スクリプトの内容は、下記の関連リンクとほぼ同様です。
関連リンクの要素技術を単純に組み合わせたものになっています。
Python のスクリプトに HTML を埋め込んで、入出力をブラウザで表示させるようにしています。
・ ローカルサーバーの起動など、今回、バッチファイル化して一発で起動できるようにしました。各コマンドの詳細については、関連リンクでまとめていますので、必要により参照してください。
・ SVG ファイルについても同様です。以前作っている基本図形のスクリプトのコピー&ペースト程度で、上記のグラフを生成しています。

・ Python のスクリプトについて、ローカルサーバーの CGI として動かしたいため、冒頭で cgi モジュールなどをインポートしています。
・ 多項式の計算のため、numpy モジュールもインポートしています。
・ def polynomial_function1( … ) としたところで、多項式を計算する関数を定義しています。HTML の入力を受け取ったら、この関数に入れて、座標計算をしています。
・ def svg_… () となっている部分は、SVG の文字列を生成するための関数です。
HTML の文字列を埋め込み、座標などを差し込んで文字列を生成します。

・ ユーザーがブラウザから、cgi-bin/chart1.py にアクセスすると、sys.stdout 以降のスクリプトが実行されます。
・ form1 = … とした行で、ブラウザに入力されたデータを受け取ります。
・ str11 = …、…、str14 = … とした各行で、ブラウザのテキストボックスに入力した文字列を取得します。
初回のアクセスでは、HTML が存在しないので、”0.1″、”0.0″ … 等の文字列が設定されます。
取得した文字列を浮動小数点の値 v1 ~ v4 に変換し、str25 = svg_chart1( ) とした関数に入力することで、SVG の文字列を生成します。
・ 生成した SVG の文字列を、str26 = … で定義した HTML に挿入して、出力用の HTML を生成します。その後、print( str26 ) としたところで、生成した HTML をブラウザに返します。

うまく動いたら

・ うまく動いたら、内部の処理を変えてみてください。
いまは、多項式のグラフ表示としていますが、入力したデータに応じて、自由にカスタマイズできると思います。
・ なお、末尾でコメントアウトしている3行について、コメントアウトを外すと、HTML ファイルを書き出します。
計算を実行するたび、HTML で書き出すようにしてもよいですし、たとえば、サーバーを使わずに、コンソールから Python のスクリプト chart1.py を実行することで、SVG のグラフを HTML ファイルで書き出す、などが可能です。
また、今回は SVG 画像を使っていますが、多項式のデータは生成されていますので、matplotlib などを使って、jpg, png などの画像を生成して保存する、なども可能です。

少し考察:ローカル GUI アプリはどんなツールで作るべきか?

・ ローカルのパソコンで、手軽に GUI を作るとなると、たとえば Python の場合、tkinter が知られています。描画では、matplotlib が知られています。ところが、これらのコマンドには統一性や汎用性があまりありません。
・ また、入出力を GUI 化したい、結果は手軽にブラウザで表示したい、可能であれば Web アプリ化していきたい、となってくると、将来的な発展性という点で疑問が残ります。
・ Windows 用のコンパイラなどをインストールしてコンパイル&実行を繰り返すのも避けたいです。ちょっとしたアプリを思いついたタイミングで瞬時に作成できるようにしたいです。
・ 計算を実行するたびに、ハードディスクや SSD に画像データを書き出して、その後表示する、というのも避けたいです。データの保存は、保存する必要があるときのみ実行できるようにしたいです。
また、matplotlib の画像表示のように、計算のたびにマウスクリックなどの手作業が発生してしまって、操作性を改善しにくい、GUI を洗練させにくい、というのも避けたいです。
・ 最近は、いろいろなサービスが Web アプリ化されてきています。ユーザーも HTML 上での入出力に慣れています。すると、GUI アプリを作るとなった場合、インターフェースはブラウザに揃えておくことがベストです。
・ ブラウザでの画像表示は、現在のところ、HTML の canvas が一般的なようです。しかし、画像表示の都度、プログラムを作るのも煩雑です。

・ このように考えてくると、入出力のインターフェースはブラウザとし、HTML 上での描画は SVG とするとデータ量も軽くなり現実的です。
加えて、必要に応じて、ローカルでのデータの読み書きも自由に行えるようにしたいとなると、HTML や JavaScript では制約が強すぎるため、Web サーバーを動かすことになります。
・ すると、ローカル Web サーバーを Python で動かすこととなり、今回の構成のようになります。
つまり、必要な要素技術は Python に集約してしまって、その中で Web サーバーを動かし、HTML や SVG の生成を実行する、となります。
サーバーの起動はバッチファイル化してしまって、1アクションで実行できたとします。するとあとは、Python で作った CGI のスクリプトをブラウザのお気に入りに登録していけば、いくらでも機能追加が可能となります。ローカルファイルの読み書きも自由に実行できることになります。
・ GUI についても、一般の Web サービスやソフトウェアなどを参照し、デザインがよく使いやすいインターフェース設計を参考に、都度、取り込んでいくことが可能です。外観を洗練させていくことが可能になります。

まとめ

ブラウザ上で動く GUI アプリについて、Python のスクリプトをまとめました。
ローカルで自作したくなる GUI アプリの多くは、この構成で作成できるのではないかと思います。

100行少々の Python のスクリプトを書くだけで、ブラウザベースでの入出力、アプリ開発が可能になります。
Web サーバーに限らず、ローカルのアプリについても、GUI プログラミングは今後、ブラウザベースでもよいのではないかなという気がしてきます。

関連リンク
・ HTML で図形を描く 【Python】
・ Python で Web サーバーを動かす 【Windows】
・ Python で自由曲線を描く 【HTML & SVG】
・ Python で掲示板を作ってみる 【Windows 版】
・ Webサーバーで動くPythonアプリ 【Windows 版】
・ バッチファイルで Anaconda から Python を実行する 【Windows】

サンプルスクリプト

グラフ描画 Web アプリ chart1.py


#!/usr/bin/python3
# coding: utf-8
import cgi
import sys 
import io 
import numpy as np1 
import os 

# python -m http.server 8000 --cgi 
# http://localhost:8000/cgi-bin/chart1.py


def write1( file1, str1 ): 
    with open( file1, 'w', encoding='utf-8' ) as f1: 
        f1.write( str1 ) 
    return 0 

def svg_axis1(): 
    str1 = ''' 
  <path d="M 020.0 350.0 L 380.0 350.0" stroke="#000000" stroke-width="1" fill="none" />" 
  <path d="M 370.0 347.0 L 380.0 350.0 370.0 353.0 " stroke="#000000" stroke-width="1" fill="none" />" 
  <path d="M 200.0 020.0 L 200.0 390.0" stroke="#000000" stroke-width="1" fill="none" />" 
  <path d="M 197.0 030.0 L 200.0 020.0 203.0 030.0 " stroke="#000000" stroke-width="1" fill="none" />" 
  <text x="213.0" y="380.0" font-family="meiryo" font-size="20">0</text> 
  <text x="370.0" y="380.0" font-family="meiryo" font-size="20">X</text> 
  <text x="213.0" y="030.0" font-family="meiryo" font-size="20">Y</text> 

'''
    return str1 

def str2float( str1 ): 
    try: 
      v1 = float( str1 ) 
    except: 
      v1 = 0.0 
    return v1 

def float2str( v1 ): 
    if v1.is_integer(): 
      str1 = '{:.1f}'.format( v1 ) 
    else: 
      str1 = str( v1 ) 
    return str1 

def np2list( np0 ): 
    return np0.tolist() 

def polynomial_function1( x1, a1, b1, c1, d1 ): 
    y1 = a1*x1**3 + b1*x1**2 + c1*x1 + d1 
    return y1 

def svg_conv1( x1, y1, x1_max): 
    y1_max = x1_max 
    x2 = 200.0/x1_max*x1 + 200.0 
    y2 = 350.0 - 200.0/y1_max*y1 
    x3 = np2list( x2 ) 
    y3 = np2list( y2 ) 
    str4 = str(x2[0]) + " " + str(y2[0]) + " " 
    str5 = "" 
    for i1 in range( len( x2 )-1 ): 
        str5 = str5 + str(x2[i1+1]) + " " + str(y2[i1+1]) + " " 
    str6 = ''' 
  <path d="M {str4} L {str5}" stroke="#ff0000" stroke-width="2" fill="none" />" 
'''.format( str4=str4, str5=str5 ) 
    return str6 

def svg_chart1( v1, v2, v3, v4 ): 
    x1_max = 5.0 
    str5 = svg_axis1() 
    x1 = np1.arange( -x1_max, x1_max, 0.1 )
    y1 = polynomial_function1( x1, v1, v2, v3, v4 ) 
    str6 = svg_conv1( x1, y1, x1_max ) 
    str7 = ''' 
<svg x="0" y="0" width="400" height="400" viewbox= "0 0 400 400" style="background-color:#ffffff" > 
{str5} 
</svg>
'''.format( str5=str5+str6 ) 
    return str7 

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
form1 = cgi.FieldStorage() 

str11 = form1.getvalue( 'TextBox11', '0.1' )
str12 = form1.getvalue( 'TextBox12', '0.0' )
str13 = form1.getvalue( 'TextBox13', '0.0' )
str14 = form1.getvalue( 'TextBox14', '0.0' )

v1 = str2float( str11 ) 
v2 = str2float( str12 ) 
v3 = str2float( str13 ) 
v4 = str2float( str14 ) 

str21 = float2str( v1 ) 
str22 = float2str( v2 ) 
str23 = float2str( v3 ) 
str24 = float2str( v4 ) 
str25 = svg_chart1(v1, v2, v3, v4) 

str26= '''
<html>
<head>
<meta charset="UTF-8"/>
</head> 
  <title>chart v0.1</title>
  <body style="background-color:#efefef" >
    <form name="Form" method="POST" action="/cgi-bin/chart1.py">
      <table> 
        <tr> 
          <td> 
          {str35} 
          </td> 
          <td name="table1 width="50%"> 
          &nbsp y = a1&nbspx³ + b1&nbspx² + c1&nbspx + d1 <br><br> 
          &nbsp a1 = &nbsp <input type="text" size="20" name="TextBox11" value={str31}><br> 
          &nbsp b1 = &nbsp <input type="text" size="20" name="TextBox12" value={str32}><br> 
          &nbsp c1 = &nbsp <input type="text" size="20" name="TextBox13" value={str33}><br> 
          &nbsp d1 = &nbsp <input type="text" size="20" name="TextBox14" value={str34}><br><br> 
          &nbsp <input type="submit" value="計算" name="button1"> 
          </td> 
        </tr> 
      </table> 
    </form> 
  </body> 
</html> 
'''.format( str31=str21, str32=str22, str33=str23, str34= str24, str35=str25 ) 

print("Content-Type: text/html; charset=UTF-8;\n")
print( str26 ) 


# path1 = os.path.dirname(__file__) + "/" 
# file1 = path1 +  "chart1.html" 
# write1( file1, str26 ) 

バッチファイルの記載例 chart1.bat


rem ---- press ctrl + c to stop the server ---- 
@echo off 
@if not "%~0"=="%~dp0.\%~nx0" ( 
  start /min cmd /c,"%~dp0.\%~nx0" %* 
  exit 
) 

call C:\Users\[username1]\Anaconda3\Scripts\activate.bat 
cd %~dp0 

start /min python -m http.server 8000 --cgi

timeout /t 2 
start "" http://localhost:8000/cgi-bin/chart1.py


タイトルとURLをコピーしました