🤔

A to Bな変換系WebアプリをFlaskで作ろう!ゼロから…

2020/11/03に公開

最初に

Flaskを理解するために奮闘した記事です。
ここ間違ってるよ、こう解釈すると良いよなどのアドバイスがあればコメントいただけると無茶苦茶嬉しいです。
出来上がったものがこちらになります。
https://neutrino-converter.herokuapp.com/
では、始めます。

概要

  • A to B の形で文字列を変換する系のWebアプリを制作する
  • NEUTRINOで作業するときに文字にスペースを入れる作業がめんどくさいので、これを利用してめんどくさくなくす

127.0.0.1_5000_.png

第一部 「Flaskを準備する」

使用する言語は僕がそれしか使えないのでPythonです。
Flaskというモジュールを使用しますが、実はこれが初体験です。
1から手順を追って説明していきましょう。

pipでFlaskをインストール

今回の開発にはpipenvを使用ましたが、本記事はpipで説明します。
以下コマンドを実行してFlaskを導入します。

pip install Flask

ディレクトリを準備する

  • projectのディレクトリにapp,modelsフォルダを作成します。
  • modelsフォルダ内に__init__.py
  • appフォルダ内にtemplates,staticフォルダを作成します。
  • appフォルダ内にapp.pyを作成します。
  • projectディレクトリにrun.pyを作成します。

気がつくとこうなってると思います。

project
 ├app/
 │ ├templates/
 │ ├static/
 │ └app.py
 ├models/
 │ └__init__.py
 └run.py

htmlを用意する

app/templatesフォルダにindex.htmlを用意します。
だいたい中身はこんな感じとなっています。
これが出力されるページのモトとなります。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Musescore流し込み用文字変換</title>
    </head>
    <body>
        <h1>ここはindex.htmlだよ!</h1>
    </body>
</html>

Pythonファイルを用意する

先程作った〜.pyの中身を組み立てていきます。

run.py

サーバーを立ち上げる際に実行します。
単純にappを呼び出してrunする、という書き方で。

run.py
from app.app import app

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

app.py

Webアプリの根幹となる部分です。
とりあえずHello World.を表示させときましょう。

app.py
from flask import Flask, render_template, request

# Flaskオブジェクトの生成
app = Flask(__name__)

@app.route('/')
def index():
    # 単純に文字列を表示するだけ
    return 'hello world.'


# 〜/index にアクセスがあった場合、index.htmlを描写する
@app.route('/index')
def post():
    return render_template('index.html')

中身の解説については後ほど行います。
とりあえずこれで準備が整いました。

第二部 「とりあえず実行してみよう」

最小構成が出来上がりました。
ではこの構成で一度サーバーを立ち上げてみましょう。
立ち上げ方は、flaskが実行できる環境でrun.pyを実行する、です。

python run.py

実行するとターミナルにいろいろ文字が出てきますが、注視する点はこれです。

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

表示されているURLを開くと、このような画面が出てくるはずです。

スクリーンショット 2020-11-03 2.04.56.png

この画面とPythonコードを見比べてみましょう。

app.py
@app.route('/')
def index():
    return 'Hello World'

このコードの意味は以下の通りとなります。

  • /、つまりhttp://127.0.0.1:5000/にアクセスすると関数を呼び出す。
  • 関数では単純に、文字列Hello Worldが返されているだけ。
  • この関数の返り値であるHello Worldがページに反映される。

といった感じです、なんとなく理解はできましたでしょうか?

確か、こういったコードも入力しましたよね。

app.py
# 〜/index にアクセスがあった場合、index.htmlを描写する
@app.route('/index')
def post():
    return render_template('index.html')

このコードの意味は以下の通りとなります。

  • /index、つまりhttp://127.0.0.1:5000/indexにアクセスすると関数を呼び出す。
  • 関数では、render_template('index.html')が返されている。
  • render…描写する
  • template…テンプレート
  • つまり、render_template('index.html')は、テンプレートとしてindex.htmlを描写するといった意味です。
  • 即ち、http://127.0.0.1:5000/indexにアクセスすると、index.htmlが表示されます。

といった感じです。
なんとな〜く、仕組みが理解できてきたと思います。
なんとなくで良いのです、僕も詳しい意味はわからないので…

第三部 「ページを作っていこう」

おさらい

今回作りたいのはこんなページです。

127.0.0.1_5000_.png

このページに必要な要素を組んでいきましょう。

index.htmlを組む

さっきまで触っていたhtmlは普通のhtmlでしたが、ここから少し違ったhtmlの書き方をします。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Musescore流し込み用文字変換</title>
</head>
<body>
    <!-- form -->
    <form action="/" method="POST">
        <textarea name="before" rows="4" cols="40">ここにテキストを入れてね</textarea>
        <input type="submit" value="変換">
        {% if body %}
        <textarea name="after" rows="4">{{body}}</textarea>
        {% else %}
        <textarea name="after" rows="4">ボタンを押すとこっちに出力されるよ</textarea>
        {% endif %}
    </form>
    <!-- form -->
</body>
</html>

見慣れない文字が出てきましたね、順を追って説明しましょう。

  • {% if A %},{% endif %}に挟まれたhtmlは、条件Aを満たさないと表示されない。

これの書き方がちょっとよくわからないんですけども、Webアプリ詳しい方いたら解説いただきたいです…

  • Flask側からbodyという要素を受け取ると、{{body}}にそれが挿入される。

といった感じです、プログラミングかじったことあるならば見慣れたような気がしますね。
では、以下の点に注目しましょう。

Flask側からbodyという要素を受け取ると、{{body}}にそれが挿入される。

これはどういった感じなのでしょうか?
実際にFlask側、からbodyという要素を渡すapp.pyがこちらになります。

app.py
@app.route('/', methods=['post'])
def post():
    body = 'hoge'
    return render_template('index.html', body=body)

簡単な話ですね、返り値の引数にbodyを用意してあげればいいのです。
逆に、html側からFlask側に要素を渡す際はこうなります。

index.html
<form action="/" method="POST">
    <textarea name="before" rows="4" cols="40">ここにテキストを入れてね</textarea>
    <input type="submit" value="変換">
</form>
app.py
body = request.form['before']
  • POSTするformを組み、このフォームを実行するとアクションが起こるページをactionで指定します。
  • textareaname="before"を設定します。
  • inputtype="submit"を設定します。
  • この状態でボタンを押すと、Flask側で指定した要素をhtmlから取得した、という形でページが表示されます。

つまり、

これが
スクリーンショット 2020-11-03 2.35.53.png

こうなります
スクリーンショット 2020-11-03 2.36.48.png

…おや、なんか手順がすっ飛んでますね。
すっとんだ手順について追っていきましょう。

第四部 「文字列を変換する機構を作る」

そうです、文字列をどのように変換したかがすっ飛んでいます。

自作モジュールはmodelsフォルダへ入れる

自作のモジュールをFlaskで使用する場合、modelsフォルダにまとめておきましょう。
フォルダの中身はこんな感じにしておきます。

models/
 ├__init__.py
 └convert.py

__init__.pyを置くことで、modelsからimportできるようにします。

参考: Python 3でのファイルのimportのしかたまとめ - Qiita

それでは、convert.pyをいじっていきましょう。

正規表現で文字列を変換する

今回は正規表現で攻めていきます。
各々関数にまとめたかったのですが、正直行数節約にもならなそうだったので一つにまとめちゃいました。

手順を説明するとこういった感じです。

  • (.)で文字列を検索し、\1 で文字列に変更を加えないまま、スペースを後方に配置する。
  • \s(ゃ|ゅ|ょ|〜) 拗音となっている文字列を検索し、余分なスペースを削除する。

これについてもっとスマートな方法がある気がするのですが、知ってたら教えてください。

  • \s{2,}で2つ以上連続しているスペースを検索し、余分なスペースを削除する。
convert.py
import re

def convert(arg: str):
    pattern_before = r'(.)'
    pattern_after = r'\1 '
    s = arg
    s = re.sub(pattern_before, pattern_after, s)

    pattern_before = r'\s(ゃ|ゅ|ょ|ぁ|ぃ|ぅ|ぇ|ぉ|ャ|ュ|ョ|ァ|ィ|ゥ|ェ|ォ)'
    pattern_after = r'\1'
    s = re.sub(pattern_before, pattern_after, s)

    pattern_before = r'\s{2,}'
    pattern_after = r' '
    s = re.sub(pattern_before, pattern_after, s)

    return s

convert.pyをapp.pyで呼び出す

それでは、app.pyからconvert.pyを呼び出して使用します。

app.py
from models import convert

@app.route('/', methods=['post'])
def post():
    body = request.form['before']
    body = convert.convert(body)
    return render_template('index.html', body=body)
  • from models import convertでconvertをimportする。
  • フォームのボタンを押すとbefore要素を取得する
  • beforeに対してconvert関数を実行する。
  • 変換した文字列bodyを描写する

といった感じです。

つまり、これでようやく

これが
スクリーンショット 2020-11-03 2.35.53.png

こうなる
スクリーンショット 2020-11-03 2.36.48.png

とちゃんと説明できたわけです。

第五部 「機能ができたら次は外見」

このままだと見た目がちょっとダサいので、bootstrapを使って見た目を整えます。

appフォルダ内にstaticというフォルダがあります。
ここに、CSSやJSなどを入れていくわけです。
今回、CSSのみを使用するのでJSフォルダは用意しません。

app/
 └static/
   └css
     └bootstrap.min.css

といった構成でいきましょう。
bootstrapをいじるわけですから…index.htmlですよね。

index.html
<!DOCTYPE html>
<html>

<head>
    <title>Musescore流し込み用文字変換</title>
    <link href="../static/css/bootstrap.min.css" rel="stylesheet">

</head>

<body>
    <div class="jumbotron text-center">
        <h1 class="display-4">Lyrics -> Musescore</h1>
        <p class="lead">for NEUTRINO</p>
        <hr class="my-4">
        <p>NEUTRINO用にMusescoreを用意する際に流し込む歌詞にスペースを自動で追加するやつです。</p>
        <p>あいうえおきゃきゅきょ→あ い う え お きゃ きゅ きょ </p>
    </div>

    <!-- form -->
    <form action="/" method="POST">
        <div class="container-fluid">
            <div class="row justify-content-around form-group">
                <textarea class="col-11 col-md-4 form-control" name="before" rows="4" cols="40">ここにテキストを入れてね</textarea>
                <div class="col-md-1 my-md-auto">
                    <input type="submit" class="btn btn-primary btn-lg" value="変換">
                </div>
                {% if body %}
                <textarea class="col-11 col-md-4 form-control" name="after" rows="4">{{body}}</textarea>
                {% else %}
                <textarea class="col-11 col-md-4 form-control" name="after" rows="4">ボタンを押すとこっちに出力されるよ</textarea>
                {% endif %}
            </div>
        </div>
    </form>o

    <!-- form -->
    <footer class="text-muted ">
        <div class="container">
            <p class="text-center">
                連絡はこちらへ:<a href="https://twitter.com/_Sotono">@_Sotono</a>
            </p>
        </div>
    </footer>


</body>

</html>

これでようやく見た目がこうなりました。

127.0.0.1_5000_.png

長い旅路だった…
あとはこれを公開するなり煮るなり焼くなりして自由に取り扱うことができます。

番外編 「詰まったところと解決策」

  • Q.テストでapp.pyを実行してるのに、No module named appとか表示されて動かない…

  • A.run.pyを実行しましょう、全部解決します。

  • Q.なんかよくわからない長文エラーを吐き出しています…jinja2.exceptions.TemplateNotFoundとか末端に書いてある…

  • A.index.htmlapp.pyがうまく噛み合ってないかもしれないです、コードをちゃんと確認しましょう。存在しない要素呼び出してませんか?

おわりに

長い文章になりました。
今回これを作ったのは、趣味で作曲をしているときに「あーめんどくせ!!!」って思ったからです。
NEUTRINOはすごい技術だと思うので今後の発展に期待しています。
おわり。

Discussion