📐

サプーさんの動画を参考に、FlaskでWebアプリ作成

2024/11/21に公開

はじめに

サプーさんの下記の動画を参考に、Flaskの入門を実施します。
公開が数年前なので、最新状況で動作確認しつつ、入門していきます。

https://www.youtube.com/watch?v=EQIAzH0HvzQ

環境構築

作業はMacOSで、VSCodeを使って作業しています。
バージョン管理ツールにasdfを使っています。

  • python -V => Python 3.12.4
  • which python => python: aliased to python3
  • which python3 => $HOME/.asdf/shims/python3
  • cat $HOME/.asdf/shims/python3
#!/usr/bin/env bash
# asdf-plugin: python 3.12.0
# asdf-plugin: python 3.12.4
exec /opt/homebrew/opt/asdf/libexec/bin/asdf exec "python3" "$@" # asdf_allow: ' asdf '

な感じ。pythonの最新ではないけど、上げようとするとエラーになるので、とりあえずこのバージョンで。

作業ディレクトリ作成と仮想環境の導入とアクティベート

作業用ディレクトリ作成と仮想環境(venv)の導入をして、仮想環境に入ります。

  • mkdir test_app
  • cd test_app
  • python -m venv .venv
  • source .venv/bin/activate
    • 抜ける時は、deactivate
  • pip install --upgrade pip

Flaskのインストール

仮想環境内でFlaskをインストールし、最小限のファイルを準備します。

  • pip install flask
    • pip install jinja2 => 入ってる?
  • 作業フォルダとアプリのフォルダを作成
    • アプリのフォルダ内のファイル構成ルール
      • [アプリのフォルダ]
        • templates
          • index.html
        • __init__.py
        • main.py

動作させるための最小限のファイルを用意する

Ruby on Railsのように、ジェネレーターのようなものはない‥とはいえ、すべてのファイルの役割を把握しつつ習得していけるのでお得と考えよう。

  • mkdir flask_app <= アプリのフォルダを作成
  • mkdir flask_app/templates <= テンプレートのフォルダを作成
  • touch flask_app/templates/index.html
  • touch flask_app/__init__.py <= 設定ファイル
  • touch flask_app/main.py <= アプルの起動スクリプト

ファイルの編集

動作させるために、ファイルの中身を埋めていきます。

  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  <p>今月の新刊情報はありません。</p>
</body>
</html>
  • test_app/flask_app/__init__.py
from flask import Flask
app = Flask(__name__)
  • test_app/flask_app/main.py
from flask_app import app

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

一旦ここまでで、基本はOKです。
ブラウザに表示させるために、設定ファイルをさらに編集します。

  • test_app/flask_app/__init__.py
from flask import Flask
app = Flask(__name__)
import flask_app.main

起動の準備

一時的に環境変数を設定します。

  • export FLASK_APP=flask_app
  • export FLASK_DEBUG=1
    永続的に設定する方法は、.zshrcとでいいのか?仮想環境でそれは有効なのかは不明。やり方はあとで調べるとして、いまはこのまま動画どおりにして進む。動画では、上記すこし違うけど、非推奨になったらしい(動画のコメント欄で言及されてる)

起動

下記のコマンドでサーバーを起動します。

  • flask run
 * Serving Flask app 'flask_app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 101-406-689

ブラウザで、http://127.0.0.1:5000 へアクセスしてindex.htmlが表示されていることを確認します。
Debug mode: on と表示されているのがミソです。ここがOFFだと、毎回サーバーを手動でリスタートする必要があります。

スクリプトからテンプレートへのデータの受け渡し

現状では、静的なHTMLを表示しているだけですので、アプリらしく、スクリプト(ここでは、起動用に用意した main.py)から、データをテンプレートに受け渡しして、そのデータを表示するようにします。

  • test_app/flask_app/main.py
from flask_app import app
from flask import render_template

@app.route("/")
def index():
    book = {"title": "はらぺこあおむし", "price": 1200, "arrival_day": "2020年7月12日"}
    return render_template("index.html", book=book)
  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  <p>タイトル: {{ book.title }}</p>
  <p>価格: {{ book.price }}</p>
  <p>入荷日: {{ book.arrival_day }}</p>
</body>
</html>

埋め込むデータを複数にする

  • 繰り返し構文を使って複数データを表示する
  • test_app/flask_app/main.py
from flask_app import app
from flask import render_template

@app.route("/")
def index():
    books = [
        {"title": "はらぺこあおむし", "price": 1200, "arrival_day": "2020年7月12日"},
        {"title": "ぐりとぐら", "price": 990, "arrival_day": "2020年7月13日"},
    ]
    return render_template("index.html", books=books)
  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  {% for book in books %}
    <p>タイトル: {{ book.title }}</p>
    <p>価格: {{ book.price }}</p>
    <p>入荷日: {{ book.arrival_day }}</p>
  {% endfor %}}
</body>
</html>

表示をテーブルにして見やすくする

  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  <table>
    <tr>
      <th>タイトル</th>
      <th>価格</th>
      <th>入荷日</th>
    </tr>
    {% for book in books %}
    <tr>
      <td>{{ book.title }}</td>
      <td>{{ book.price }}円</td>
      <td>{{ book.arrival_day }}</td>
    </tr>
    {% endfor %}
  </table>
</body>
</html>
  • テーブルの見栄えがあれなので一時的にCDNのCSSを追加

データのない場合の表示を切り替える

  • 条件分岐を使う
  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  {% if books == [] %}
    <p>今月の新刊情報はありません。</p>
  {% else %}
    <table>
      <tr>
        <th>タイトル</th>
        <th>価格</th>
        <th>入荷日</th>
      </tr>
      {% for book in books %}
      <tr>
        <td>{{ book.title }}</td>
        <td>{{ book.price }}円</td>
        <td>{{ book.arrival_day }}</td>
      </tr>
      {% endfor %}
    </table>
  {% endif %}
</body>
</html>
  • test_app/flask_app/main.pyを編集して表示を確認

レコード登録画面を作成

  • touch flask_app/templates/form.html
  • test_app/flask_app/templates/form.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body>
  <h1>サトー書店</h1>
  <p>新しい画面です</p>
</body>
</html>
  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  {% if books == [] %}
    <p>今月の新刊情報はありません。</p>
  {% else %}
    <table>
      <tr>
        <th>タイトル</th>
        <th>価格</th>
        <th>入荷日</th>
      </tr>
      {% for book in books %}
      <tr>
        <td>{{ book.title }}</td>
        <td>{{ book.price }}円</td>
        <td>{{ book.arrival_day }}</td>
      </tr>
      {% endfor %}
    </table>
  {% endif %}
  <a href="{{ url_for('form') }}">編集</a>
</body>
</html>
  • test_app/flask_app/main.py
from flask_app import app
from flask import render_template

@app.route("/")
def index():
    books = [
        {"title": "はらぺこあおむし", "price": 1200, "arrival_day": "2020年7月12日"},
        {"title": "ぐりとぐら", "price": 990, "arrival_day": "2020年7月13日"},
    ]
    return render_template("index.html", books=books)

@app.route("/form")
def form():
    return render_template("form.html")
  • 変数bookをbooksにし、連想配列に改造
  • 動作確認

表示データをSQLiteから取得するように変更する

データをテンプレートにわたせば、表示できるところまで確認することができました。だだし現在は、スクリプト内にデータがあるため、不便です。なので、データベースに情報を格納し、そこから引っ張り出して表示するようにします。データベースはSQLiteを使います。

  • SQLiteはデフォルトでインストールされている
  • touch flask_app/db.py <= DB操作用のスクリプトファイルを作成
  • test_app/flask_app/db.py
import sqlite3

DATABASE = "database.db"

def create_books_table():
    con = sqlite3.connect(DATABASE)
    con.execute("CREATE TABLE IF NOT EXISTS books (title, price, arrival_day)")
    con.close()

設定ファイルにデータベース用の設定を追加

  • test_app/flask_app/__init__.py
from flask import Flask

app = Flask(__name__)
import flask_app.main

from flask_app import db

db.create_books_table()

main.pyに「DBからデータを引き出すコード」と「データ登録用のHTMLへのアクセスするためのコード」を追加

  • test_app/flask_app/main.py
from flask_app import app
from flask import render_template
import sqlite3

DATABASE = "database.db"

@app.route("/")
def index():
    con = sqlite3.connect(DATABASE)
    db_books = con.execute("SELECT * FROM books").fetchall()
    con.close()
    books = []
    for row in db_books:
        books.append({"title": row[0], "price": row[1], "arrival_day": row[2]})

    return render_template("index.html", books=books)

@app.route("/form")
def form():
    return render_template("form.html")

データ登録画面を作成する

新しく、データ登録用の画面を作成します。

  • touch flask_app/templates/form.html
  • test_app/flask_app/templates/form.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
    <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body>
  <h1>サトー書店</h1>
  <form method="post" action="{{ url_for('register')}}">
    <table>
      <tr>
        <th>入荷日</th>
        <th>タイトル</th>
        <th>価格</th>
      </tr>
      <tr>
        <td><input type="date" name="arrival_day"></td></td>
        <td><input type="text" name="title"></td>
        <td><input type="number" name="price"></td>
      </tr>
    </table>
    <br>
    <input type="submit" value="登録">
  </form>
</body>
</html>

main.pyに、データ登録用のコード(フォーム表示と登録処理)を追加します。

  • test_app/flask_app/main.py
from flask_app import app
from flask import render_template, request, redirect, url_for
import sqlite3

DATABASE = "database.db"

@app.route("/")
def index():
    con = sqlite3.connect(DATABASE)
    db_books = con.execute("SELECT * FROM books").fetchall()
    con.close()
    books = []
    for row in db_books:
        books.append({"title": row[0], "price": row[1], "arrival_day": row[2]})
    return render_template("index.html", books=books)

@app.route("/form")
def form():
    return render_template("form.html")

@app.route("/register", methods=["POST"])
def register():
    title = request.form["title"]
    price = request.form["price"]
    arrival_day = request.form["arrival_day"]

    con = sqlite3.connect(DATABASE)
    con.execute("INSERT INTO books VALUES (?, ?, ?)", [title, price, arrival_day])
    con.commit()
    con.close()
    return redirect(url_for("index"))
  • form.htmlを表示、入力後、register()を実行し、indexへリダイレクトするように設定
  • 動作を確認

CSSを適用して見栄えを変更する

  • mkdir flask_app/static <= HTMLや画像などのリソースを管理するためのディレクトリ
  • touch flask_app/static/style.css
  • test_app/flask_app/static/style.css
body {
  margin: auto;
  max-width: 650px;
}

h1 {
  position: relative;
  background: #dfefff;
  box-shadow: 0px 0px 0px 5px #dfefff;
  border: dashed 2px white;
  padding: 0.2em 0.5em;
  color: #454545;
}

h1:after {
  position: absolute;
  content: '';
  left: -7px;
  top: -7px;
  border-width: 0 0 15px 15px;
  border-style: solid;
  border-color: #fff #fff #a8d4ff;
  box-shadow: 1px 1px 1px rgba(0,0,0,0.15);
}

h2 {
  border-bottom: 5px solid #dfefff;
}

table {
  margin: 20px auto;
  width: 80%;
  border-collapse: separate;
  border-spacing: 0px;
}

table th {
  background: #dfefff;
  border: 1px solid #cccccc;
  padding: 5px;
}

table td {
  padding: 3px;
  border: 1px solid #cccccc;
}

a {
  display: block;
  margin: 20px 0px;
  padding: 3px;
  max-width: 4em;
  text-decoration: none;
  text-align: center;
  border: 1px solid #cccccc;
}

a:hover{
  background: #dfefff;
}

cssのリンクをCDNから、style.cssに変更

  • test_app/flask_app/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>サトーアプリ</title>
  <link rel="stylesheet" href="{{url_for('static',filename='style.css')}}">
</head>
<body>
  <h1>サトー書店</h1>
  <h2>今月の新刊一覧</h2>
  {% if books == [] %}
    <p>今月の新刊情報はありません。</p>
  {% else %}
    <table>
      <tr>
        <th>タイトル</th>
        <th>価格</th>
        <th>入荷日</th>
      </tr>
      {% for book in books %}
      <tr>
        <td>{{ book.title }}</td>
        <td>{{ book.price }}円</td>
        <td>{{ book.arrival_day }}</td>
      </tr>
      {% endfor %}
    </table>
  {% endif %}
  <a href="{{ url_for('form') }}">編集</a>
</body>
</html>
  • test_app/flask_app/templates/form.htmlも同様に編集

と、動画の内容はここまで。ここからは、成果物をベースに少しつづ改造していきます。

テンプレートの分割

HTMLコードに重複する部分が出てきたので、共通部分を別のテンプレートに切りだします。

  • 共通テンプレート
  • touch flask_app/templates/base.html
  • test_app/flask_app/templates/base.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{% block title %}{% endblock  %}</title>
  <link rel="stylesheet" href="{{url_for('static',filename='style.css')}}">
</head>
<body>
  <div class="header">
      {% block header %}{% endblock  %}
  </div>
  <div class="content">
    {% block content %}{% endblock  %}
  </div>
  <div class="footer">
      {% block footer %}{% endblock  %}
  </div>
</body>
</html>

index.html、form.htmlを共通テンプレート対応に変更します。

  • test_app/flask_app/templates/index.html
{% extends 'base.html' %}

{% block title %}
サトーアプリ
{% endblock  %}

{% block header %}
<h1>サトー書店</h1>
{% endblock header %}

{% block content %}
  <h2>今月の新刊一覧</h2>
  {% if books == [] %}
    <p>今月の新刊情報はありません。</p>
  {% else %}
    <table>
      <tr>
        <th>タイトル</th>
        <th>価格</th>
        <th>入荷日</th>
      </tr>
      {% for book in books %}
      <tr>
        <td>{{ book.title }}</td>
        <td>{{ book.price }}円</td>
        <td>{{ book.arrival_day }}</td>
      </tr>
      {% endfor %}
    </table>
  {% endif %}
  <a href="{{ url_for('form') }}">編集</a>
{% endblock  %}

{% block footer %}
&copy 2023 サトーbookstore
{% endblock  %}
  • test_app/flask_app/templates/form.html
{% extends 'base.html' %}

{% block title %}
サトーアプリ
{% endblock  %}

{% block header %}
<h1>サトー書店</h1>
{% endblock header %}

{% block content %}
  <form method="post" action="{{ url_for('register')}}">
    <table>
      <tr>
        <th>入荷日</th>
        <th>タイトル</th>
        <th>価格</th>
      </tr>
      <tr>
        <td><input type="date" name="arrival_day"></td></td>
        <td><input type="text" name="title"></td>
        <td><input type="number" name="price"></td>
      </tr>
    </table>
    <br>
    <input type="submit" value="登録">
  </form>
{% endblock  %}

{% block footer %}
&copy 2023 サトーbookstore
{% endblock  %}
  • bootstrapなどを使うときは、この辺でやるんだと思う。
  • 念の為、ブラウザで表示確認(変わらないけど‥)

登録ボタンが編集ボタンと違うのを揃える

  • 片方はaタグ、片方はsubmitボタンなので、見た目が違うので違和感ある
  • formにname属性を追加
    • <form method="post" action="{{ url_for('register')}}" name="form1">
  • submitボタンをaタグをJavascriptに変更
    • <a href="javascript:form1.submit()">登録</a>
  • 動作確認

Flaskインストール直後のpip list

ここで、機能を拡張するにしても、方針が定まらないので、Flaskが依存しているライブラリをざっくり把握するために以下を調査。

% pip list
Package      Version
------------ -------
blinker      1.9.0 
click        8.1.7
Flask        3.1.0
itsdangerous 2.2.0
Jinja2       3.1.4
MarkupSafe   3.0.2
pip          24.3.1
Werkzeug     3.1.3
  • blinker => 簡単に言うとオブジェクトの間のシグナルの送信/受信の仕組みを提供
  • click => clickはコマンドラインツールを作成する
  • Flask => Webアプリケーションフレームワーク
  • itsdangerous => cookieベースのセッションを実装
  • Jinja2 => テンプレートエンジン
  • MarkupSafe => 文字をエスケープするテキスト オブジェクトを実装
  • pip => パッケージ管理システム
  • Werkzeug => WSGIアプリケーションを簡単に作成できるライブラリ

WSGI(ウィズギー)とは、PythonでWebアプリケーションとWebサーバーを接続する際に考案されたインターフェース定義

  • サーバー間で通信するための標準的なインターフェースを統一
    • WSGIアプリケーションは、呼び出し可能なオブジェクトとして定義する。
    • オブジェクトが呼び出される際、第一引数に環境変数が渡され、第二引数にステータスコードとレスポンスヘッダを受け取る呼び出し可能なオブジェクトが渡される。
    • 第二引数に渡されたオブジェクトを呼び出して、ステータスコードとレスポンスヘッダ情報を渡す。
    • 戻り値として、バイト文字列をyieldするiterableなオブジェクトを返す。

完全に理解した!

※以下、作業中

個別の本のページを追加してみる(作業中)

管理画面とユーザー画面を追加する(作業中)

Flask-AdminとFlask-LoginによるDB管理者画面の実装

デプロイ&デプロイ先の選択(作業中)

Discussion