Help us understand the problem. What is going on with this article?

Flaskで自然言語処理を使ったWebアプリを作りherokuでデプロイした

説明

Flaskで英文を入れると時制を解析するWebアプリを作りました。

環境

Python3がインストールされていることとGitHubで作成していることを前提に進めます。

Flaskアプリケーションの作成

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
└── analyzer.py

htmlを作成

templates/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>TENSE ANALYZER</title>
</head>

<body>
    <div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>
    </div>
</body>

</html>

Flaskのインストール

pip install Flask

main.pyを作成

main.py
from flask import *

app = Flask(__name__)


@app.route("/")
def init():
    return render_template('index.html')


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    app.run(host='127.0.0.1', port=5000, debug=True)

アプリケーションを立ち上げる

python main.py

http://127.0.0.1:5000/ を開いて"TENSE ANALYZER"の文字が表示されていれば成功。

フォームを作る

htmlにフォームを追加

templates/index.html
...
<div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>

        <div id="analyze">
            <h2>英文を入力してEnterキーを押してください</h2>
            <form id="form" action="/input" method="post" name="formStr">
                <input id="textField" class="input" type="text" name="str" value="{{value_str}}">
                <input class="submit" type="submit" name="" value="送信">
            </form>
        </div>

        <div class="result">
            <h2>結果</h2>
            <p>{{value_str}}</p>
        </div>
    </div>
...

formタグにaction="/input"
inputタグにvalue="{{value_str}}", name="str"
の属性を追加。

フォームから送信した値を受け取る処理をmain.pyに追加

flask_socketioをインストール
pip install flask_socketio

main.py
from flask import *
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)  # [変更] socketioを追加

value_str = ""  # [変更] value_strを定義


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', value_str=value_str)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""
    # index.html内にて{{ value_str }}で挿入できる
    return render_template('index.html', value_str=value_str)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # [変更] 


index.htmlの<p>{{value_str}}</p>の部分にフォームから送信した値が表示されれば成功。

NLPで解析部分を実装

今回はNLTK(Natural Language Toolkit)というライブラリを使用しました。

NLTKをインストール

pip install nltk

analyzer.pyを作成

analyzer.py
analyzer.py
import re
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')


class Analyzer:
    def tense_analyze(self, value_str):
        if(len(value_str) > 0 and "." != value_str[-1]):
            value_str = value_str+"."
        s = value_str
        morph = nltk.word_tokenize(value_str)
        result = nltk.pos_tag(morph)
        tense = ''
        q = 0
        will = re.search('will', value_str)
        for i in range(len(s.split())):
            b = int(i)
            if result[b][1] != 'VBP' and result[b][1] != 'VBZ' and result[b][1] != 'VBG' and result[b][1] != 'VB' and result[b][1] != 'VBD' and result[b][1] != 'VBN':
                q = q+1
            elif (result[b][1] == 'VBP' or result[b][1] == 'VBZ' or result[b][1] == 'VBD' or result[b][1] == 'VB') and (result[b][0] != 'am' and result[b][0] != 'are' and result[b][0] != 'was' and result[b][0] != 'were' and result[b][0] != "be" and result[b][0] != 'is'):
                if result[b][0] == 'have' or result[b][0] == 'has' or result[b][0] == 'had':
                    if result[b+1][1] == 'VBN' or result[b+2][1] == 'VBN' or result[b+1][1] == 'VBD' or result[b+2][1] == 'VBD':
                        if result[b+1][0] == 'been':
                            if result[b+2][1] == 'VBG' or result[b+3][1] == 'VBG':
                                tense = 'PerfectContiuous'
                                break
                            else:
                                tense = 'Perfect'
                                break
                        else:
                            tense = 'Perfect'
                            break
                    else:
                        tense = 'Simple'
                        break
                else:
                    tense = 'Simple'
            elif result[b][1] == "VBP" or result[b][0] == "was" or result[b][0] == "were" or result[b][1] == "VBZ" or result[b][0] == "be" or result[b][0] == "is" or result[b][0] == "VBD":
                if result[b+1][1] == 'VBG':
                    tense = 'Continuous'
                    break
                else:
                    tense = 'Simple'
                    break

        for j in range(len(s.split())):
            c = int(j)
            if tense == 'Simple':
                if result[c][1] == 'VBD':
                    tense = 'PastSimple'
                    break
                elif result[c][1] == 'VBD' and (result[c-1][1] == 'VBP' or result[c-1][1] == 'VBD' or result[c-1][1] == 'VBZ'):
                    tense = 'PastSimple'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentSimple"
                elif will and (result[c-1][1] != 'VBP' or result[c-1][1] != 'VBD' or result[c-1][1] != 'VBZ'):
                    tense = "FutureSimple"
                else:
                    q = q+1
            elif tense == "Perfect":
                if result[c][0] == 'had':
                    tense = 'PastPerfectSimple'
                    break
                elif not will and (result[c][0] == "have" or result[c][0] == "has"):
                    tense = "PresentPerfectSimple"
                elif will and result[c][0] == "have":
                    tense = "FuturePerfectSimple"
            elif tense == "Continuous":
                if result[c][0] == 'was' or result[c][0] == 'were':
                    tense = 'PastContinuous'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentContinuous"
                elif will:
                    tense = "FutureContinuous"
                else:
                    q = q+1
            elif tense == "PerfectContiuous":
                if result[c][0] == 'had':
                    tense = "PastPerfectContinuous"
                elif not will and (result[c][0] == 'have' or result[c][0] == "has"):
                    tense = 'PresentPerfectContinuous'
                elif will:
                    tense = "FuturePerfectContinuous"
            else:
                q = q+1
        value_str.strip()

        return value_str, tense

Analyzerクラスに文章の時制の手がかりとなる部分と時制を返す関数def tense_analyze(self, value_str):を作成。
アルゴリズムの説明等は省略します。

main.py内でanalyzer.pyをimportして使う

main.py
from flask import *
from flask_socketio import SocketIO
# analyzer.pyをimport
import analyzer

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)

# [削除]value_str = ""
result = []  # [変更] 結果を格納する配列


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    # [変更] value_str -> result
    return render_template('index.html', result=result)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""

    # tense_analyze関数の実行
    Analyzer = analyzer.Analyzer()
    value_str, tense = Analyzer.tense_analyze(value_str)

    # 結果をresultに追加
    appendList = []
    if(value_str):
        appendList.append(value_str)
    if(tense):
        appendList.append(tense)
    result.append(appendList)

    # [変更] value_str -> result
    # index.html内にて{{ result }}で挿入できる
    return render_template('index.html', result=result)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # 変更


index.htmlでresultを表示する。

Python用のテンプレートエンジンJinja2をインストール
pip install Jinja2

Jinja2をインストールすることでhtml内でif文やfor文が使えます。
Jinja2のドキュメント

templates/index.html
...
  <div class="result">
            <h2>結果</h2>
            <!-- result配列の長さが0より大きい場合表示 -->
            {% if result|length > 0 %}
            <div class="result-content">
                <div class="items">
                    <!-- resultの中身ををループで表示(ここではreverseで逆順にしています) -->
                    {% for r in result|reverse %}

                    <!-- r[0](文章)が存在するなら表示する -->
                    {% if r[0] %}

                    <div class="item">
                        <p class="value_str">{{ r[0] }}</p>

                        <!-- r[1](時制)が存在するなら表示する -->
                        {% if r[1] %}
                        <p class="tense">{{ r[1] }}</p>
                        {% endif %}
                        <!--  -->
                    </div>

                    {% endif %}
                    <!--  -->

                    {% endfor %}
                    <!--  -->
                </div>
            </div>
            {% endif %}
            <!--  -->
        </div>
...

リセットボタンを作成

index.html

templates/index.html
...
<div class="result-content">
        <form id="reset" action="/reset" method="post">
                <button name="reset_result">結果をリセットする</button>
        </form>
<div class="items">
...

formにaction="/reset"属性を追加

main.py

main.py
...

@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', result=result)

#  action="/reset"
@app.route("/reset", methods=["GET", "POST"])
def reset_result():
    global result
    result = []
    return render_template('index.html', result=result)


#  action="/input"
@app.route("/input", methods=["GET", "POST"])
...


CSSとJavaScript

CSS

style.css
static/css/style.css
/* css reset */

body {
    font-family: 'Inconsolata', monospace;
    background-color: #fdfeff;
    display: flex;
    padding: 0 1rem;
}

h1, h2, h3 {
    color: rgb(70, 70, 70);
    margin-bottom: 0;
}

h1 {
    font-size: 4vw;
}

h2 {
    font-size: 3vw;
}

h3 {
    font-size: 2vw;
}

p {
    margin: 0;
    color: dimgrey;
    font-size: 1.6vw;
    font-weight: bold;
}

ul, li {
    padding: 0;
}

li {
    list-style-type: none;
    font-size: 1.6vw;
    font-weight: bold;
    color: dimgrey;
}

li::before {
    content: "・";
}

input {
    background: white;
}

input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active {
    -webkit-text-fill-color: rgb(70, 70, 70);
    -webkit-box-shadow: 0 0 0px 1000px white inset;
    box-shadow: 0 0 0px 1000px white inset;
}

input:focus {
    outline: 0;
}

button {
    color: dimgrey;
}

button:hover {
    cursor: pointer;
}

button:focus {
    outline: 0;
}

/*  */

/* container */

#container {
    margin: auto;
    width: 80vw;
}

@media screen and (max-width:768px) {
    #container {
        width: 90vw;
    }
}

#container>div {
    margin-bottom: 2rem;
}

/*  */

/* nlp */

#nlp {
    /* border-radius: 5px; */
    /* background: #34456314; */
}

#form {
    margin: 0.5rem 0;
}

#form .input {
    height: 25px;
    width: calc(100% - 15px);
    padding: 5px;
    margin-left: 1px;
    background: transparent;
    font-size: 1.8vw;
    border: 2px solid rgb(70, 70, 70);
}

#form .submit {
    display: none;
}

.result-content {
    margin: 0.5rem 0;
    padding: 5px 10px;
    background-color: #f1f1f1;
    border-radius: 5px;
}

.result-content #reset {
    padding: 5px 0;
}

.result-content #reset button {
    margin: auto 0 0 0;
    padding: 5px 10px;
    display: block;
    border: 1px solid #fdfeff;
    background: #fdfeff;
    border: 2px solid dimgray;
    border-radius: 5px;
    font-size: 1.6vw;
    font-weight: bold;
    transition: all 0.3s ease;
}

.result-content #reset button:hover {
    background: dimgrey;
    color: #fdfeff;
}

.result-content .items {
    overflow: scroll;
    max-height: 50vh;
}

.result-content .items .item {
    display: flex;
    flex-wrap: wrap;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid #34456314;
    margin: 1rem 0;
}

.result-content p {
    padding: 5px 0;
    font-size: 1.8vw;
}

.result-content .tense {
    background: #fdfeff;
    border-radius: 15px;
    padding: 5px 10px;
}

/*  */

#task li {
    color: aliceblue;
    padding: 5px;
    display: inline-block;
    border-radius: 5px;
    background: #344563;
}

JavaScript

static/js/main.js
// ウィンドウ読み込み時にフォームにfocusする
window.onload = function () {
    document.formStr.str.focus()
}

html内で読み込む

html内でcssとJavaScriptを読み込むのを忘れずに

templates/index.html
<head>
...
        <script src="{{ url_for('static', filename='js/main.js') }}"></script>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

Herokuにデプロイ

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
├── analyzer.py
├── Procfile
└── requirements.txt

Procfileを作成

gunicornをインストール

pip install gunicorn

Procfile

Procfile
web: gunicorn [ファイル名]:app --log-file=-

mainの部分は自分で作ったファイル名に合わせる(今回はmain.pyだからmain)

Procfile
web: gunicorn main:app --log-file=-

requirements.txtを作成

以下のコマンドを実行すると、requirements.txt何に必要なライブラリ等が自動的に書き込まれる。

$ pip freeze > requirements.txt

requirements.txt
autopep8==1.4.4
Click==7.0
Flask==1.1.1
Flask-SocketIO==4.2.1
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==2.10.3
MarkupSafe==1.1.1
nltk==3.4.5
pycodestyle==2.5.0
python-engineio==3.10.0
python-socketio==4.4.0
six==1.13.0
Werkzeug==0.16.0

Herokuに登録する

このページでアカウントを作成する。

Herokuをインストールする

brew tap heroku/brew && brew install heroku

Herokuにログインする

heroku login

Herokuアプリを作成

heroku create [アプリ名]

今回は、tenseにします。

heroku create tense


$ heroku create tense
Creating ⬢ tense... !
 ▸    Name tense is already taken

既に使用されてたため変更

heroku create english-tense-analyzer


$ heroku create english-tense-analyzer
Creating ⬢ english-tense-analyzer... done
https://english-tense-analyzer.herokuapp.com/ | https://git.heroku.com/english-tense-analyzer.git

デプロイ

以下のコマンドを実行するだけで完了です

git push heroku master

heroku openを実行するとブラウザでWebアプリケーションが開きます。

終わりに

感想

使用してみて、とても軽く、簡単に手軽に作れるという点で学習コストが低く、使いやすいフレームワークだと思いました。
機械学習を使ってWebアプリケーションを作りたい方や少し試したいという方には丁度いいかと思います。
Djangoなどの他のPythonのWebフレームワークはまだ使用したことがないので試してみたいです。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした