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

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

- 今回作ったもの: https://english-tense-analyzer.herokuapp.com/
- GitHub: https://github.com/mk668a/flask_nlp_app


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

# Flaskアプリケーションの作成
## ディレクトリ構成
```
.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
└── analyzer.py
```

## htmlを作成
``` 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を作成
```python3: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にフォームを追加
``` 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```

```python3: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)](https://www.nltk.org/)というライブラリを使用しました。

### NLTKをインストール
```pip install nltk```

### analyzer.pyを作成
<details><summary>analyzer.py</summary><div>

```python: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
```
</div></details>


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


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

```python: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のドキュメント](https://jinja.palletsprojects.com/en/2.10.x/)

``` html: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

```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
``` python: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

<details><summary>style.css</summary><div>

```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;
}

```
</div></details>

### JavaScript

```javascript:static/js/main.js

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

```

### html内で読み込む

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

```html: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に登録する

[このページ](https://www.heroku.com/)でアカウントを作成する。

## Herokuをインストールする

 ``` brew tap heroku/brew && brew install heroku```

## Herokuにログインする

``` heroku login ```

## Herokuアプリを作成

``` heroku create [アプリ名]```

今回は、tenseにします。

``` heroku create tense```

```terminal

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

```

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


``` heroku create english-tense-analyzer```

```terminal

$ 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アプリケーションが開きます。


## 終わりに

- 今回作ったもの: https://english-tense-analyzer.herokuapp.com/
- GitHub: https://github.com/mk668a/flask_nlp_app

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

## 参考
- <a href="https://dev2prod.site/python/flask-screen/" target="_blank">Python Flaskでサクッと簡単に画面を作成する</a>

- <a href="https://qiita.com/hitoribucho/items/9f5dd087b3d784af6b73" target="_blank">Flaskにおける基本的なフォームの使い方</a>

- <a href="https://tanuhack.com/deploy-flask-heroku/" target="_blank">HerokuにFlaskアプリ(hello world)をデプロイする方法まとめ</a>

- <a href="https://qiita.com/gogotanaka/items/a30764001d9f312465d8" target="_blank">Python+Flask+Herokuで簡単webアプリ</a>