説明
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を作成
<!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を作成
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にフォームを追加
...
<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
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
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して使う
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のドキュメント
...
<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
...
<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
...
@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
/* 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
// ウィンドウ読み込み時にフォームにfocusする
window.onload = function () {
document.formStr.str.focus()
}
html内で読み込む
html内でcssとJavaScriptを読み込むのを忘れずに
<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
web: gunicorn [ファイル名]:app --log-file=-
mainの部分は自分で作ったファイル名に合わせる(今回はmain.pyだからmain)
web: gunicorn main:app --log-file=-
requirements.txtを作成
以下のコマンドを実行すると、requirements.txt何に必要なライブラリ等が自動的に書き込まれる。
$ pip freeze > 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アプリケーションが開きます。
終わりに
- 今回作ったもの: https://english-tense-analyzer.herokuapp.com/
- GitHub: https://github.com/mk668a/flask_nlp_app
感想
使用してみて、とても軽く、簡単に手軽に作れるという点で学習コストが低く、使いやすいフレームワークだと思いました。
機械学習を使ってWebアプリケーションを作りたい方や少し試したいという方には丁度いいかと思います。
Djangoなどの他のPythonのWebフレームワークはまだ使用したことがないので試してみたいです。