MatplotlibをFlaskで拡張して、誰でもPythonで作成したグラフを見られるようにする

製造業やWeb制作会社だとPythonをインストールしているのが自分のPCだけだったりします。そんな時、PythonをインストールしていないPCでもMatplotlibでグラフを生成し、ブラウザを介して画像としてダウンロードする方法をメモします。

これにより、

* 誰にでもMatplotlibで生成したグラフにアクセスしてもらえる環境を構築する
* 自分のPCが非力な時、性能の良いサーバで画像を生成して効率化を図る
* めちゃめちゃ重いデータをあらかじめグラフ作成しておいてすぐ提供できるようにする

ことができるようになります。

## 方法と項目

方法は下記のとおりです。

* WebAPIを作成し、あるURLを叩いたらPythonの関数が走るようにする
* その関数の中でMatplotlibで画像を生成し、returnする

この記事ではそれぞれについて説明した後、さらに

* 本番環境でも耐えられるようにクラウドサービスの一つであるGCFにデプロイする方法と、
* そのAPIを叩いてcanvas要素に描写するHTMLのサンプル作成

までまとめます。
実際のところ、グラフの生成はユーザー側でJavaScriptで描写することの方が望ましいとは思いますが、Matplotlibで凝ったグラフを作るとこういうことやりたくなると思いますので、Geekな気分の時にお役立ていただけるとうれしいです。

## FlaskでWebAPIを作成し、あるURLを叩いたらPythonの関数が走るようにする

これはFlaskの基本的な使い方なので簡単に説明します。とりあえず自分だけに公開します。

```main.py
# -*- coding: utf-8 -*-
from flask import Flask
from flask_cors import CORS
import json

app = Flask(__name__)
CORS(app)

@app.route('/',methods=["GET","POST"])
def hello():
    name = "Hello World"
    return json.dumps({'name':name})

if __name__ == "__main__":
    app.run(host='127.0.0.1',debug=True)
```

これを実行するとこうなります。

```
* Serving Flask app "main" (lazy loading)
* Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 280-448-684
```

http://127.0.0.1:5000　にアクセスするとこう表示されます。

![スクリーンショット 2020-11-24 21.31.51.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550742/84f36379-3fb8-5e82-9bd1-c7d954e851ca.jpeg)

もし同一ネットワーク内の他のPCからも見られるようにしたいなら、最後の部分をこうします。

```main.py
app.run(debug=False, host='0.0.0.0', port=5000）
```
host=''0.0.0.0'をしていることで可能になります。あとはネットワーク内の自身のIPを調べて、http://xx.xx.xx.xx:5000でアクセスできるようになります。

以上がFlaskでの簡単のWebAPIの作成です。これについてはQiitaにたくさん記事あると思いますので関数の中でグラフを作成する部分に行きます。

## 関数の中でMatplotlibで画像を生成し、returnする

先ほどの関数の中でグラフを生成して、文字列の代わりにグラフをリターンすることにします。

```main.py
# -*- coding: utf-8 -*-
import io
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from flask import Flask, send_file
from flask_cors import CORS
import json

app = Flask(__name__)
CORS(app)

@app.route('/',methods=["GET","POST"])
def hello():
    image = io.BytesIO()
    x = np.linspace(0, 10)
    y = np.sin(x)
    plt.plot(x, y)
    plt.savefig(image, format='png')
    image.seek(0)
    return send_file(image,
                     attachment_filename="image.png",
                     as_attachment=True)

if __name__ == "__main__":
    app.run(host='127.0.0.1',debug=True)
```

ポイントは２つあって、一つはmatplotlib.use('Agg')という部分と、二つ目はsavefigの部分でメモリ上でファイルを書き出していることです。

#### matplotlib.use('Agg')
FlaskはGUIをサポートしていないので、Aggがないとエラーで落ちます。

#### ioモジュールを使ってメモリ上でファイルを書き出し
ローカルで動かす場合は、普通にHDD（SSD？）にjpgなどで書き出しても問題ないと思いますが、クラウドなどだとローカルへのアクセスが禁止されていることが往々にしてあります。この場合に備えて、メモリ上でファイルの読み書きを行うioモジュールを使って、ローカルに書き出すことなく画像を出力します。
これを実行してhttp://127.0.0.1:5000　にアクセスすると勝手にファイルをダウンロードしてくれます。

![スクリーンショット 2020-11-24 21.41.39.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550742/131a71b5-810a-e099-ca3f-8100a41975b4.jpeg)

開くとちゃんと画像になってます。

![スクリーンショット 2020-11-24 21.44.25.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550742/65a37514-29f5-3dd8-128e-f1f30f349a91.jpeg)

以上でとりあえず動作するようになりましたが、下記の２つの問題があります。

* Flaskの簡易サーバで実行していること
* 通信は暗号化されていないこと

の２点です。Flaskの簡易サーバで運用することは元々推奨されていないですし、httpsではなくhttpでアクセスしていることからわかるように通信は暗号化されていません。これでは実用に耐えられるものではありません。

そこで、この辺の本番環境への移行を簡単にしてくれるサービスとして、AWS,Herokuなどのクラウドサービスが登場するわけですが、今回はGoogle Cloud PlaftformのCloudFunctionsでデプロイします。勝手にSSL化もしてくれます。AWSならlambdaがGCFに相当します。

## GCPのCloud Functionsでホスティングする

Cloud Functionsでホスティングするには、GCPを契約した後、SDKをダウンロードして、GCFの設定を完了する必要があります。最初は訳分からなくて少し大変でしたがクラウドは応用範囲広いので頑張りたいところです（自分に言ってる）。

GCFについてはこちら
https://cloud.google.com/functions/docs/quickstart-console

GCFですが、欠点としてデプロイに時間がかかることがあります。したがって、開発時は自分のローカル環境でエミュレート（クラウドの環境を再現）して開発し、うまく行ったらデプロイする流れになります。エミュレートしないと開発効率が非常に落ちました。

### ローカルでエミュレートする
ローカルでエミュレートするには、少し特殊な設定が必要です。SDKに加えて、pipでfunctions-frameworkというものをインストールします。
```
pip install functions-framework
```

デプロイしたいファイルを用意します。GCFの場合、必ずmain.pyという名前で作成する必要があります。

```main.py
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import io
import json
import numpy as np
import matplotlib
matplotlib.use('Agg')
from flask import Flask, send_file

def hello(request):
    headers = {
        'Access-Control-Allow-Origin': '*',
        #'Access-Control-Allow-Origin': 'http://localhost:8080',
    }
    image = io.BytesIO()
    x = np.linspace(0, 10)
    y = np.sin(x)
    plt.plot(x, y)
    plt.savefig(image, format='png')
    image.seek(0)
    return send_file(image,
                     attachment_filename="image.png",
                     as_attachment=True)
    #return (json.dumps({'rtrn1':'rtrn1'}))
```

main.pyのファイルのあるフォルダでターミナルを開いて、下記を打ち込むとエミュレートしてくれます。

```
functions-framework --target=hello  --port=8080
```

あとはhttp://localhost:8080/にアクセスするだけです。
なお、WindowsでAnacondaの場合は、anaconda prompt経由でcdで移動してコマンド打つ必要があります。また、 http://0.0.0.0:8080/ではなく localhost:8080/でアクセスします。

詳しくはこちら。
https://github.com/GoogleCloudPlatform/functions-framework-python

いよいよ本番環境へデプロイします。

### 本番環境へデプロイする

本番環境でデプロイする場合、ファイルの存在するフォルダで下記を実行します。もし標準モジュール以外のものを使う場合、同じ階層にrequirements.txtというテキストファイルを作成し、使用するライブラリを一つずつ記載する必要があります。今回で言うとmatplotlibとnumpy、Flaskの記載が必要です。

```requirements.txt
Flask==1.1.2
matpolotlib==3.3.2
numpy1.19.2
```

基本的には、pip listで表示した自分の使ったライブラリのバージョンを記載すればOKですが、ランタイム環境が開発とデプロイ先で異なる場合は注意が必要です。例えば自分はPython３.６で開発していて標準のランタイムであるPython３.７にデプロイする場合、使っていたライブラリのバージョンがデプロイ先のランタイムではサポートされていないことがあります。この場合、意味不明のエラーでデプロイできませんので注意してください。特にnumpyとか注意。

####　その他はまった点

gcfから他のサービスと連携する際、認証はいらないです。

#### gcf上でローカルにファイルを書き出すようなプログラムをかくと意味不明なエラーでcrashする

ioモジュールでメモリに書き出す必要があります


諸々準備したら、下記のコマンドでデプロイします。

```
gcloud functions deploy hello --runtime python37 --trigger-http --allow-unauthenticated
```
あとはコンソールにデプロイ先のURLが表示されるので環境です。httpsでアクセスできるようになって、SSL化もバッチリです。

最後にHTML側のコードの紹介です。

## APIを叩いてcanvas要素に描写する

これまではURLを直接叩いてましたが、APIを叩いてcanvas要素に描写するHTMLも載せておきます。

```index.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

    <title>bootstrapとjquery</title>

  </head>
  <body>

<main>
<article>
  <section>
       <input type="button" value="Start" onclick="main();"/>
        <canvas style="height: 30vw;width:50vh"></canvas>
  </section>
</article>
</main>

<script language="javascript" type="text/javascript">

    function main(){
      var canvas = document.getElementsByTagName('canvas');
      var ctx = canvas[0].getContext('2d');

      var img = new Image();
      img.src = 'http://0.0.0.0:5000';//FlaskでホスティングしたURL

      img.onload = function() {
        img.style.display = 'none'; // ようわからん
        console.log('WxH: ' + img.width + 'x' + img.height)

        ctx.drawImage(img, 0, 0);
        var imageData = ctx.getImageData(0, 0, img.width*2, img.height*2)

        for(x = 0 ; x < 1000 ; x += 10) {
          for(y = 0 ; y < 1000 ; y += 10) {
             ctx.putImageData(imageData, x, y);
          }
        }
      };    }
</script>

  </body>
</html>
```
ボタンを押したらURLを叩いて、canvas要素へ描写してくれます。

## 最後に

実際のところ、Flaskで返すのはデータにして、ユーザー側でJavaScriptのChart.jsで描写することの方が望ましいとは思います。でも、結構Matplotlibで凝ったグラフを作るとこういうことやりたくなりますよね。Geekな気分の時にお役に立てるとうれしいです。

この記事が役に立ったと思ったらLGTMお願いいたします:thumbsup: