Webアプリ

最近の状況

前回の記事でWebアプリの開発のためにPythonのマイクロフレームワークであるFlaskについて学んでいますと書きましたが、ネット上のサンプルをいじりながら簡単なものなら書けるようになりました。ですので自分なりの知識の整理をするためにFlaskでつくったアプリと実際にHerokuで公開するまでを簡単にまとめてみたいと思います。

経緯について

自分の所属している研究室ではデータの解析をNgraphExcelで行っています。当研究室に代々受け継がれているデータ解析用のExcel等があるので皆さんそれを使ってらっしゃるのですが、Excelは重いしデータ解析の前に無駄な動作があるなと思っておりました。無駄な動作というのは生データの入れ替えをExcelでいちいち行わなければならないということです。入れ替えの動作自体は単純な作業なのですが、逆に言えば単純な作業ならプログラムに任せるべきでもあります。そこで生データの入れ替えを行うWebアプリを開発して公開しようと思い立ちました(データの入れ替えだけならそんなに難しくないだろうと思ったので笑)。

Flask Web app

つくったWebアプリですが自分で一からつくったわけではなく、ネット上のサンプルをいじりながら作りました。参考サイトは以下です。

qiita.com

この例では

<input type=text>

に入力された名前をHTMLに埋め込んで返すということを行っております。自分の場合だと十数行×十数行のタブ区切りのテキストデータなのでtextareaにする必要がありました。また変更後のデータも十数行×十数行となるためtextareaに埋め込んでレンダリングしようと考えました。また変更後のデータはコピペのみができたら良いのでreadonly=“true"にすることと、大量のデータはwrapされると読みにくいのでwrap="off"としています。pythonファイル側はデータのならび変えをするだけなのでなにか特別なことをしているわけではないです。なにはともあれとりあえず以下に載せます。

"""filename : main.py"""

# Flask などの必要なライブラリをインポートする
from flask import Flask, render_template, request, redirect, url_for
from operator import methodcaller
import pandas as pd


def group(dataframe):
    dataframe_col = dataframe.shape[1]
    RANGE = range(1, int(dataframe_col/4)+1)
    dv = ['Voltage_1 ({})'.format(i) for i in RANGE]
    dc = ['Current_1 ({})'.format(i) for i in RANGE]
    gv = ['Voltage_2 ({})'.format(i) for i in RANGE]
    gc = ['Current_2 ({})'.format(i) for i in RANGE]
    drain_v = dataframe[dv]
    drain_c = dataframe[dc]
    gate_v = dataframe[gv]
    gate_c = dataframe[gc]

    return pd.concat([drain_v, drain_c, gate_v, gate_c], axis=1)


def toNestedList(data):
    data = data.splitlines()
    splitedData = list(map(methodcaller("split", "\t"), data))

    return splitedData


# 自身の名称を app という名前でインスタンス化する
app = Flask(__name__)


# ここからウェブアプリケーション用のルーティングを記述
# index にアクセスしたときの処理
@app.route('/')
def index():
    title = "Transform data"
    # index.html をレンダリングする
    return render_template('index.html',
                           title=title)


# /post にアクセスしたときの処理
@app.route('/transformed', methods=['GET', 'POST'])
def transformed():
    title = "Data is transformed!"
    if request.method == 'POST': 
        data = request.form['textarea']  # データを取得
        NestedList = toNestedList(data)
        df = pd.DataFrame(NestedList[1:], columns=NestedList[0])
        df2 = group(df)

        return render_template('index.html',
                               title=title,
                               originalData=data,
                               transformedData=df2.to_csv(
                                          index=False, sep='\t'))
    
    else:
        # エラーなどでリダイレクトしたい場合はこんな感じで
        return redirect(url_for('index'))


if __name__ == '__main__':
    app.debug = True  # デバッグモード有効化
    app.run()  # どこからでもアクセス可能に

個人的に悩んだのはHTMLのtextareaから取得するのはtsvではなくただのstring型なのでどうやってpandasのDataFrameに読み込ませるかということですね。最初はそもそもファイルをアップロードしてもらう方式を取ろうかとも考えていたのですが、HTML5についてあまり良く知らなかったため諦めました笑 結局stringデータとなったものをsplitを二回して2次元配列とし、それをDataFrameとするように実装しました。

また出力に関しても悩んだのですがこれはpandas公式ドキュメントを読むと、pandas.DataFrame.to_csvメソッドで引数を与えないと文字列として返すようになるのを利用しました。

この他にindex.htmlとlayout.htmlも書く必要があります。以下に記載します。

<!-- index.html -->
{% extends "layout.html" %}
{% block content %}
  <!-- Form
  ================================================== -->
<div class="form">
  <div class="container">
    <div class="row">
      <div class="col-md-12">
      <h2>Transform data for mobility of linear or saturation region</h2>
      <h4><a href="https://www.youtube.com/watch?v=xAH6VFGCX6Q">Tutorial video</a></h4>
        <form action="/transformed" method="post">
          <h3>Original data</h3>
          <textarea class="form-control" rows="5" id="textarea" name="textarea" placeholder="Original data" wrap="off">{% if originalData %}{{originalData}}{% endif %}</textarea>
          <button type="submit" class="btn btn-default">Transform</button>
        </form>
        <h3 for="comment">Transformed data</h3>
        <form>
          <textarea class="form-control" rows="5" id="textarea2" placeholder="Transformed data" readonly="true" wrap="off">{% if transformedData %}{{transformedData}}{% endif %}
          </textarea>
        </form>
      </div>
    </div>
  </div>
</div>
{% endblock %}
<!-- layout.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">
    {% if title %}
        <title>{{title}}</title>
    {% else %}
        <title>Bootstrap 101 Template</title>
    {% endif %}
    <!-- Bootstrap -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <![endif]-->
  </head>
  <body>
    {% block content %}{% endblock %}
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> 
    </body>
</html>

layout.htmlは上に挙げた参考サイトのものをほとんどそのまま引用してます。参考サイトの方もBootstrapの公式をそのまま使ったというようなことが書いてあったはずです。layout.html内の{% block content %}{% endblock %}にindex.htmlの内容を埋め込むようなかたちでレンダリングしているようです。index.htmlの<form action=/transformed method="POST">でボタンを押すといった動作が検知されたときにどこに移動先のURLを示すようです。

たぶんまだ色々書いたほうが良いのでしょうが、文章ですべてを説明しようとするのはなかなか難しいですね。main.pyのFlaskのルーティングのこととかの説明は他サイトを見て頂くと分かるかと思われます。質問していただければ自分も分かる範囲で答えさせて頂きます。

長くなったのでHerokuでデプロイするところはまた別記事で書きます。それでは