2018年09月05日

言語処理100本ノックでPython入門 #69 - FlaskでWebアプリケーションを作成

  

とうとう言語処理100本ノック 2015の第7章・問題69です。やっと7章の最後の問題まできました。

■ 問題
69. Webアプリケーションの作成
ユーザから入力された検索条件に合致するアーティストの情報を表示するWebアプリケーションを作成せよ.アーティスト名,アーティストの別名,タグ等で検索条件を指定し,アーティスト情報のリストをレーティングの高い順などで整列して表示せよ.

PythonでWebアプリケーションってどうやって作成するのかそのそも経験がないのでわからない。 しらべたところ、DjangoとFlaskというフレームワークがあるらしい。 今回は、手軽そうなFlaskを使って問題を解いてみようと思います。
conda list flask
とやると、

flask                     0.12                     py36_0
flask-cors                3.0.2                    py36_0
って表示されたので、anacondaには、すでにflaskが入っているみたい。
なので、これを使うことにします。

■ HTMLレイアウトファイルの作成

あちこちのサイトの見よう見まねで、テンプレートファイルを作成します。
まずは、大本となるlayout.htmlファイル。

<!DOCTYPE html>
<html>
<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <title>言語処理100本ノック:No69</title>
    <style>
        body {
            margin: 20px;
        }
    </style>
<body>
    <div class="container-fluid">
    {% block content %}
    <!-- ここにメインコンテンツが挿入される  -->
    {% endblock %}
    </div>
</body>
</head>


とりあえず、bootstrapを使います。デザインに凝るつもりはないですが...
cssファイルを別に作るまでもないので、styleタグでbodyのmarginだけ指定しています。

■ 検索ページのhtmlファイル

次に作成するのは、検索用のhtmlファイルです。

{% extends "layout.html" %}
{% block content %}

<form method="post" class="form-horizontal">
    <div  class="form-group">
        <label class="control-label col-xs-2">名前: </label>
        <input name="name" type="text" >
    </div>
    <div  class="form-group">
        <label class="control-label col-xs-2">地域: </label>
        <input name="area" type="text" >
    </div>
    <div  class="form-group">
        <label class="control-label col-xs-2">タグ: </label>
        <input name="tag" type="text" >
    </div>
    <div  class="form-group">
        <div class="control-label col-xs-2"></div>
        <button class="btn btn-primary" type="submit">検索</button>
    </div>    
</form>
{% endblock %}
※ あれ、なんかHTMLコードの色付けがおかしいけど、許してください。

先頭と最後に、flask特有の記述がありますが、それ以外は普通のhtmlファイルです。
class名には、bootstrap専用のクラス名を指定しています。

■ 結果表示用のhtmlファイルの作成

次に結果表示用のhtmlファイルを作成します。

{% extends "layout.html" %}
{% block content %}
<h3>結果一覧</h3>
<p>
<a href="/">戻る</a>
</p>
<div class="table-responsive">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>名前</th>
                <th>地域</th>
                <th>性別</th>
                <th>タグ</th>
                <th>別名</th>
                <th>タイプ</th>
            </tr>
        </thead>
        <tbody>
            {% for artist in artists %}
            <tr>
                <td>{{ artist.name }}</td>
                <td>{{ artist.area }}</td>
                <td>{{ artist.gender }}</td>
                <td>
                    {% for tag in artist['tags'] %}
                        {{ tag.value }}<br>
                    {% endfor %}
                </td>
                <td>
                    {% for alias in artist['aliases'] %}
                        {{ alias.name }}<br>
                    {% endfor %}
                </td>
                <td>{{ artist.type }}</td>
                </tr>
            {% else %}
                <td>書籍が登録されていません</td>
            {% endfor %}
        </tbody>
    </table>
</div>
{% endblock %}

{{}}でくくっているのが、モデルとバインドする項目です。

{% for artist in artists %}など、{% %}で括られているのは、flaskのテンプレートエンジンが処理するための記述です。forにelseが使えるのがいかにもPython用のフレームワークですね。

ちなみに、MongoDBから検索した結果(複数)がartistsです。これを forで回して、artist.nameとかartist['aliases']といった記述で取り出しています。まあ、artist.aliasesでもいいんですけど、気分で両方の書き方をしてみました。

このアプリは、検索ページ「検索」ボタンをしたら、その結果を表形式で表示するだけなので、定義するページはこれで終わりです。

■ Pyrthonのコード

それでは、Pythoのコードです。

import pymongo
from flask import Flask, render_template, request

class SearchInfo:
    def __init__(self, name, area, tag):
        self.name = name
        self.area = area
        self.tag = tag

class Searcher:
    def __init__(self):
        self.client = pymongo.MongoClient('localhost', 27017)
        self.db = self.client['MusicBrainzDb']
        self.co = self.db['artists']

    def search(self, info):
        query = { '$and' : [ ]}
        if info.name != '':
            cond = []
            cond.append({ 'aliases.name': info.name })
            cond.append({ 'name': info.name })
            query['$and'].append({ '$or': cond })
        if info.area != '':
            query['$and'].append({ 'area': info.area })
        if info.tag != '':
            query['$and'].append({ 'tags.value': info.tag })
        q2 = self.co.find(query).sort('rating.count', pymongo.DESCENDING)
        return list(q2)[:100]

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        searcher = Searcher()
        results = searcher.search(SearchInfo(request.form['name'], request.form['area'], request.form['tag']))
        return render_template('result.html', artists=results) 
    else:
        return render_template('search.html') 

if __name__ == "__main__":
    app.run(debug=True)

SearchInfoクラスは、Flaskとは直接は関係していないクラスなので、普通に書けばOK.

flask関連のコードは、これ以降のコードです。

app = Flask(__name__)
これで、Flaskオブジェクトを生成して、runメソッドでWebアプリを起動しています。

def index():

が、アクセスした時に呼び出される関数です。今回のアプリでは、URLは、ルート(/)だけです。

@app.route('/', methods=['GET', 'POST'])
で、get/post両方で、この関数が呼ばれるようにしています。

getならば、render_template関数を使って、search.htmlをレンダリングした結果を返しています。
postならば検索してから、result.htmlをレンダリングして返しています。このとき、
artists=results

で、htmlテンプレートのartistsと検索結果を関連付けています。

■ 実行してみる

MongoDBが起動してある前提です。
めんどくさいので、Visla Studio Codeのデバッグ機能をつかって実行ました。

コンソールに以下のようなメッセージが表示されます。

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 148-857-886

なので、http://127.0.0.1:5000/ にアクセス。

すると、以下のようなページが表示されるので、ここで適当に検索ワードを入れます。

スクリーンショット 2018-09-02 15.31.25
検索ボタンを押します。
すると、無事検索されて、以下の結果が得られました。
スクリーンショット 2018-09-02 15.31.49
これだけだと、ルーティング、リダイレクト、認証...などわからないことも多いですが、Flaskの基礎の基礎はわかったような気がします。まあ、HTMLテンプレートでサーバー側のモデルとバインドさせてHTMLをレンダリングするってのは、ASP.NET MVCと考え方は同じですね。ただ、Controller作る必要がないので、どう設計するかは開発者に任されているということなんですかね。実際にアプリ作成するとなるとベストプラクティスとか知る必要がありそうです。 でも、慣れないことやったので、さすがに疲れました。