12
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[前編] 文系初学者がPython×Flask×データベースをつかって検温記録WEBアプリをつくってみたメモ

Last updated at Posted at 2021-03-29

この記事は、忘れっぽい自分のための覚え書きメインですが、初学者/文系/非エンジニア/趣味レベルの同胞にも参考になればと、丁寧めに書き留めていきます。

特に初学者は汎用的な情報から察したり応用する力が弱いので、あえて写実的な書きっぷりにしています。結果、鬼のように長くなったので前・後編に分けました。(それでも長いですが)

後編はコチラ

ちなみにこの時からアップデートを重ねて、いまではLINEでサクッと記録できるようになりました。一般公開してますので、よろしければお試しください。
LINE公式アカウント「毎日検温くんβ」
image.png

#0.環境・前提
・macOS Catalina10
・Python3.7.7
・Flask
制作当時の参考文献は3.7系が多かったので、3.8までは上げませんでした。
PythonのWEBフレームワークとしてはDjangoが有名ですが、やりたいことと習得レベル的に手軽ぽいFlaskを選択しました。文献はDjangoの方が豊富ですが、ここはシンプルさ重視。

その他つかったサービス
・コード書くのに「VS Code」
”コードエディタ”でググったら新しめの記事で推されてた印象。つかったらエディタどころじゃなく超多機能。特にGithub連携とか素人レベルでも便利でした。

・ファイルの管理に「Github」
よくわからんけど、エンジニアといえばって感じで使ってみたかった。純粋な憧れ。

・WEBを公開するため「Heroku」
これまたよくわからんけど、有名なので文献も多いかなと。

・データベース管理は「Heroku Postgres」
Herokuを導入したら行きがかり上これになりました。いまだにデータベースまわりは一番苦手です。

#1.プロジェクト用のフォルダを作る
さて最初の手順です。
これからいろいろファイルを保存していくフォルダを作ります。

どこでもいいですが、iCloud Driveは避けた方がよさげです。このあとターミナル操作とかいろいろ作業進めるときに、iCloud Driveだとパスの指定(=フォルダ場所の記載)がかなりややこしそう。

おすすめはローカルのユーザーフォルダ直下あたり。ターミナル操作にせよFinderから開くにせよ、アクセスしやすいです。
image.png
筆者はユーザーフォルダ'kazuyamano'の下に、'Dev'という個人開発用の親フォルダをつくって、その下に'ken-on-kun'というプロジェクト個別の子フォルダを作りました。(日々の体温を記録するWEBアプリなので「検温くん」です)

この時点では、フォルダ内はスッカラカンでいいです。焦らずいきます。

#2.ターミナルで初期作業もろもろ
初学者はターミナルそのものに苦戦すると思います。
筆者はProgateで初等教育を受け、あとはGoogle先生に教わりながらなんとか凌げています。
###Gitを設定

ターミナルで、さっき作ったPJTフォルダへ移動 (cd = change directory)。
gitのローカルリポジトリとして設定(git init) 。
1行ずつEnter叩いて実行していきます。 ←ここ初心者がわからず不安になるポイント

% cd ~/Dev/ken-on-kun
% git init

見た目には何も起きません。エラーが起きなければ成功です。

###環境を仮想化(venv)
プロジェクトフォルダ「ken-on-kun」内での作業が、他のプロジェクトやPC全体に影響を与えないように、仮想環境(vertual enviroment) を作成します。

って言ってもよくわかりませんね。内側から結界張るようなイメージ?
まぁわからなくてもいいです。以下おまじないをまんまコピペで1行ずつ実行します。 

% python3 -m venv venv
% source venv/bin/activate

うまくいくと、ターミナルの行頭に(venv)って表示が入ってきます。
↓ 筆者の実例

(venv) kazuyamano@MacBookPro ken-on-kun % 

ここまでやると「ken-on-kun」の下に「venv」フォルダが作られ、venv環境に必要と思しきファイル群が自動で追加されています。
image.png
中身はよくわからんけど気にせずすすみます。

###Flaskをインストール
ひきつづきターミナルで作業。
pipをアップグレードします。 *pip = Python用のパッケージ(≒便利道具たち)をインストールするソフト。

% pip install --upgrade pip

成功したら、バージョンを確認

% pip -V

つづいてPythonベースのWEBアプリ開発用フレームワーク「Flask」をインストールします。

% pip install flask

↓一連の作業をすると、こんな感じになります
image.png
字ちっさ!
"--upgrade pip"も、"install flask"も、"Successfully installed ●●" って出てるのでどっちも成功してます。

#3.VSCodeで初期作業もろもろ
VS Codeを開いて、[ファイル] → [開く.…] → [ken-on-kun] を開きます。
image.png
この時点で、VSCode左メニューのgit管理アイコン(枝分かれ的なやつ)に、未読バッチみたいなのが860件とかついてビビります。まだ何もしてないのに!
image.png
これはさっき追加された「venv」フォルダ内のファイル数を表していて(たぶん)、「新しいファイルが860件追加されたよ!」と通知が働いた模様(たぶん)。

とりあえず、ken-on-kunの下に「.gitignore」というファイルをつくって、中に'venv'と平文で打ち込んで保存しましょう。860件の通知が消えます(厳密には「.gitignoreってファイルが追加されたよ!」の通知1件に変わります)
image.png
gitはバージョン管理の仕組みなのでほっとくとあらゆる変更履歴を拾って通知してくれるのですが、「.gitignore」ファイルをつくってそこにフォルダ名を登録すると、例外的に変更履歴と見なさない決まりのようです。(たぶん)

*[venv]フォルダの中は環境設定のためのファイル群で、これから開発するプログラムそのものとは関係ない。だからignore(無視)していいよ、ということのようです(たぶん)。

###いよいよプログラムをつくっていきます
Flaskの公式ドキュメントにクイックスタートやチュートリアルもありますし、ググれば立派なエンジニアさんのちゃんとした文献もモリモリ出てきます。正しい知識・詳しい情報は、そっち見てもらった方がいいです。

ただ、エンジニアリング界隈の予備知識がない我々にはハードル高く心折れがちなので、この記事では「理解するよりも、前に進めたいマジで」の精神で、必要な作業となんとなくの意味合いを、初心者目線で記録していきます。

####[Special Thanks文献]
Flaskチュートリアル + herokuにデプロイ - Qiita

全体の流れはこちらのチュートリアルを完コピさせていただいてます。これがなかったら無理でした。本当にありがたい。インターネット最高。

#4.VSCodeでひと通りファイルとフォルダを基礎工事

[目的]
・Flaskを使ったWEBアプリをつくってみる
・ブラウザでちゃんと表示されるとこまでもっていく(=バグをつぶす)
・ひとまず、ローカル環境で (PCの中の世界で)実現する
[手段]
文字列「Hello, World」が表示されるだけのサイトを、超まわりくどくWEBアプリっぽく実装。あとで動的なサイトに膨らますための骨格基礎工事として。

###~/run.py
ken-on-kunの下に[run.py]ファイルを追加。コードは以下。

~/run.py
from main import app

if __name__ == '__main__':
    app.run(debug=True)

完了したら、VS Code上の見た目はこんな感じ
image.png

[役割1]
トップのURLにアクセスした時、サイトを表示するために真っ先に動く.pyスクリプト。1行目の「mainパッケージ(≒フォルダ)をimportしなはれや!」の指示で、後述の~/main/__init__.py を走らせる。
[役割2]
2行目は、__init__.pyで定義されているFlaskクラス(変数app)がアプリケーションとして起動されたのかインポートされたのかによる条件分岐。
3行目は、アプリケーションとして起動された場合は、デバッグモードを適用

って意味わかんないですよね。わからなくてもとりあえず大丈夫なので、心を無にして写経→次進みましょう。

*以降こんな感じで、初心者的 ”わかってないけど自分なりの解釈” をメモっていきます。正しい知識や解説は、公式ドキュメントやちゃんとしたエンジニアさんの文献ググってください。

###~/main/__init__.py
ken-on-kunの下に[main]フォルダを新設(↓のあたり右クリックで作れます)
image.png
mainの下に[__init__.py]ファイルを追加。

~main/__init__.py
from flask import Flask 
app = Flask(__name__)
import main.views

image.png

[役割]
・flaskモジュールからFlaskクラスをインポート
・変数appにFlaskクラスを代入
・~/main/views.pyにバトンタッチ

run.pyの1行目「from main」を受けて、mainフォルダ内にある__init__.pyって名前のファイルが走る、で「import app」の'app'に当たるものが、__init__.pyの中で定義されている、という感じかと。

このあたり難しいですが、Flaskの基本的なお作法っぽいので、ひとつひとつの意味を深く考えず、一旦「そういうもんだな」で進んだ方がいいと思います。

__init__.pyについて掘り下げるならコチラなど。
Python __init__.py の機能について - 寒月記

###~/main/views.py
mainの下に[views.py]ファイルを追加。

~/main/views.py
import flask 
from main import app

@app.route('/')
def show_entries():
    return 'Hello, World!'

image.png

[役割]
・トップページにアクセスしたとき
・show_entries()って関数を実行する→'Hello, World!'って文字列を返す

@app.route()のくだりは「ルーティング」といって、URLアクセス時の動きを定めるパーツのようです。()内にURLの末尾部分を指定します。'/' はいわゆるトップページのことですね。

例)http://www.hogehoge.jp/  ←この最後の'/'を表してる

###[テスト] ローカルで実行、ブラウザで確認
ターミナルでrun.pyを実行

$ python run.py

実際のターミナル画面こんな感じです
image.png
Running on http://127.0.1:5000/ って書いてあるので、ブラウザでアクセスしてみると
image.png
はい、成功です。わーい

ローカルサーバーに眠っているプログラムken-on-kunは、run.pyに着火すると「127.0.0.1」っていうIPアドレス(=自分のPC)の「5000番ポート」を通して実行結果を返す(=Hello,World!を表示する)、って感じで理解してます。
ちなみに「自分のPC」っていうIPアドレスを表す時のお決まりとして
・127.0.0.1
・0.0.0.0
・localhost
3通りあるみたいです。試しに打ち込んだら全部Helloしてくれました。
image.png
勉強なりました。以下参考文献。
localhost:3000とは何か - Qiita

この時点で、ターミナルが「Python実行モード」になってるので、'ctrl+C' 押して通常モードに戻しておきましょう

#5.データベースの下準備
引き続きこちらのサイトを参考に進めます

Flaskチュートリアル + herokuにデプロイ - Qiita

サイトではブログを題材に「タイトル」と「本文」をDB化しますが、こちらは検温記録アプリなので少しアレンジして「誰」「いつ」「何」をDBに記録するWEBアプリを目指します。

###flask_sqlalchemyをインストール

$ pip install flask_sqlalchemy 

image.png

[役割]
・SQLAlchemyはPython用のデータベース使いやすくするやーつ
・特に今回のはFlask用にカスタマイズされてるやーつ

データベースまわりはムズイのでまずは実践で進みます。参考文献。
SQLAlchemyの基本的な使い方 - Qiita

###~/main/__init__.py を編集
インストールしたSQLALchemyを、基礎工事プログラムに組み込んでいきます。

~/main/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy  #ここ追加

app = Flask(__name__)
app.config.from_object('main.config')  #ここ追加

db = SQLAlchemy(app)  #ここ追加
import main.views

###~/main/config.py (DBの配置とか設定)
mainの下に[config.py]ファイルを追加。

~/main/config.py
import os

SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or "sqlite:///test.db"
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY="secret key"

image.png

[役割]  *config = 設定、構成、配置、構造
・データベース(ファイル)をどこに生成するか指定する
 → os.environ.get('DATABASE_URL') : 環境変数DATABESE_URLに生成
   or
  "sqlite:///test.db" : ローカルの同じフォルダにtest.dbを生成
・セッション情報を暗号化するためのキーを設定する
 → 実際に運用する場合には、SECRET_KEYは必ず変更して下さい

os.environ.get('DATABASE_URL')が空文字だった場合、右辺を代入するようにしています。os.environ.get('DATABASE_URL')はherokuのpostgresで使用し、
右辺はsqliteのデータベースです。こうすることでherokuの環境とローカルの環境で書き換える必要がありません。

参考文献
Flaskチュートリアル + herokuにデプロイ - Qiita

むずい。次いってみよー

###~/main/models.py (どんなDBか設計)
mainの下に[models.py]ファイルを追加。

ググってよく出てくるチュートリアルは”ブログを作ろう”系で、「連番=id」「タイトル=title」「本文=text」を定義していますが(下記)

チュートリアルでよくあるやーつ
from main import db
from flask_sqlalchemy import SQLAlchemy

class Entry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)
   
    def __repr__(self):
        return "<Entry id={} title={!r}>".format(self.id, self.title)

def init():
    db.create_all()

今回は”検温を記録”なので、「連番=id」「誰=jcode」「体温=temp」「いつ=date」という感じでアレンジしてみます。(jcodeは従業員番号)

~/main/models.py
from main import db
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta, timezone

class Entry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    jcode = db.Column(db.String)
    temp = db.Column(db.Float)
    date = db.Column(db.DateTime, default=datetime.now()) 

def __repr__(self):
    return "<Entry id={} jcode={!r} temp={!r} date={}>".format(self.id, self.jcode, self.temp, self.date)

def init():
    db.create_all()

image.png
3,8,9行目のdatetimeまわりは、データが作られた日時が日本時間で自動登録されるよう仕込んでます。なかなか思うようにデータ反映されずいろんなやり方を試行錯誤しましたが、ここに落ち着きました。

__repr__と{!r}の意味がわからなくて調べましたが、なんとなくの理解まで。

__repr__(self) とは
Pythonの特殊メソッド、クラスの性質として型変換を定義。print()とか組込み関数format()とかの引数にそのクラスから作られたオブジェクトが指定されると呼び出される。類似品の__str__は可読性が高い文字列に変換する(ユーザー向け)。__repr__は可読性よりも正確な情報を文字列として返す(デバッグ向け)

{!r} とは
置換フィールド{}に入った値に対して、repr()を呼んで書式変換する。今回のコードだと、String型のjcodeは ' ' 付きで「文字列だよー」という風に返ってきて、Froat型のtempは ' ' なしで「数値だよー」という感じで返ってきます。これもデバッグ用? いまは深く考えない。。。
*他にstr()を呼ぶ '!s' 、ascii()を呼ぶ '!a' がある模様。

わからないなりに、以下サイトが参考になりました。
strrepr について - もぐもぐプログラミング
string --- 一般的な文字列操作 — Python 3.8.3 ドキュメント
組み込み型 — Python 3.8.3 ドキュメント

#6.データベースをつくる
ターミナルで以下実行します。

% python -c "import main.models; main.models.init()"

実際の画面
image.png

ターミナル上では何もおきませんが、この時、~/main/config.pyで設定した以下の指定場所

SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or "sqlite:///test.db"

ここにデータベースが作成されます。今回はローカルでの実行なので or "sqlite:///test.db" が適用され、~/mainフォルダの下にtest.dbが生成されているはずです。
image.png
test.dbできてました。成功です。ちなみにVSCodeではファイルの中身までは見れません。
と言ってもまだこの時点ではただの空き箱です。

*流れと関係ないけど補足*
ふとVSCodeのエクスプローラーを見ると、いつの間にか「__pycache__」なるフォルダができてますが、気にしなくていいです。おそらくファイルの変更履歴とかを自動でキャッシュしてるだけだと思います(たぶん)。

###【テスト】 空き箱にデータを入れてみる
ターミナルを「インタラクティブシェル」モードにします (=ターミナル上で1行ずつコード記述→実行できる)

$ python

image.png
>>>が出れば成功です。ctrl+Dで戻れます)
下記を1行ずつEnterしていきます。.pyファイルのコードを1行ずつ実行する感じですね。

>>> from main.models import Entry 
>>> from main import db

>>> entry1 = Entry(jcode='1111111', temp='36.1')
>>> db.session.add(entry1)
>>> db.session.commit()

>>> entry2 = Entry(jcode='2222222', temp='36.2')
>>> db.session.add(entry2)
>>> db.session.commit()

>>> entries = Entry.query.all()
>>> entries

(実行している内容)
・models.pyで定義したEntryクラスをインポート
・__init__.pyで定義した変数dbをインポート  *db = SQLAlchemy(app)
Entryクラスからインスタンスentry1entry2をつくってデータベースに追加
Entryクラスに対応するテーブルから全件を配列として抽出(というクエリ)

image.png
最後の2行を見ると、配列データが2件取り出されています。
entry1entry2として追加したデータがちゃんとDBに収まってたってことですね。
deteもちゃんと作業時間が入ってます。テスト成功!

忘れずに、ctrl+Dで、インタラクティブシェルモードを終了させておきましょう

#7.ブラウザにDBを表示する
すでに基礎工事済みのプログラムを、データベースとつないでいきます。

###~/main/views.py を編集 (その1)
Before:世界に「こんちわ!」と呼びかける壮大なプロジェクトですが、それだけです。
image.png

After:データベースの中身をトップページに表示します。
データが変われば、トップページも変わる =いわゆる動的サイトになります。

~/main/views.py
import flask 
from main import app
from main.models import Entry # 追加

@app.route('/')
def show_entries():
    entries = Entry.query.all() # 追加
    return flask.render_template('entries.html', entries=entries) # 変更

Entryクラスをインポート
・テーブルからデータ全件抽出するクエリを追加(変数entriesに代入)
・トップページに返す内容を、単純な文字列からhtmlにグレードアップ 

上2つは、さきほどインタラクティブシェルでやったことの移植。
3つめは、flask.render_templateという仕組みを使って.pyファイルから.htmlファイルを呼び出してトップページに表示します。

最終的には、トップページに検温履歴を表示する機能として動きます。

###~/main/templates
flask.render_templateという仕組みは、[templates]というフォルダに入っている.htmlファイルを認識して呼び出すきまりのようです。(たぶん)

なので、とりあえず[templates]フォルダをつくります。
場所はviews.pyと同列になるよう、[main]フォルダの下です。

image.png

###~/main/templates/entries.html
[templates]フォルダの下に、entries.htmlを作ります。さきほどのviews.pyの最後に呼び出してた、トップページの表示内容を決めるhtmlファイルです。

~/main/templates/entries.html
<html>
<head>
  <title>検温くん_第一形態</title>
</head>
<body>
  <ul class="entries">
  {% for entry in entries %}
    <li>社員コード:{{entry.jcode}} / 体温:{{entry.temp}} / 検温日時:{{entry.date}}</li>
  {% endfor %}
  </ul>
</body>
</html>

20年前に阿部寛ばりのHPを手書きしてた身としては { } ってヤツが違和感たっぷりですが、見るとfor文が入ってたりするので、Pythonのコードを埋めこんでサイトを動的にする'テンプレート'ということなのかと思います(たぶん)

views.pyで「テーブルからデータ全件抽出するクエリを追加→変数entriesに代入」を行っているので、
{% for entry in entries %}
でひとつずつデータを取り出し
<li>{{entry.jcode}} / {{entry.temp}} / {{entry.date}}</li>
でリスト表示する感じですね。

###【テスト】 ローカルで実行、ブラウザで確認
ターミナルでrun.pyを実行

% python run.py

image.png

Running on http://127.0.1:5000/なので、ブラウザでアクセスしてみます。

image.png

キター! さっき追加したデータがちゃんと表示されました。成功です。

*ターミナルがPython実行モードなので、ctrl+Cで忘れず戻しておきましょう

#8.ブラウザからDBに書き込む
いよいよブラウザからデータベースに書き込む仕掛けをつくっていきます。
ザ・WEBアプリっぽい。動的感出てきますね。

###~/main/views.py を編集 (その2)

~/main/views.py
import flask 
from main import app, db  #インポート対象にdbを追加
from main.models import Entry

@app.route('/')
def show_entries():
    entries = Entry.query.all()
    return flask.render_template('entries.html', entries=entries)

###DBにデータを追加するadd_entry関数を定義

~/main/views.py
@app.route('/add', methods=['POST'])
def add_entry():
    entry = Entry(jcode = flask.request.form['jcode'],temp = flask.request.form['temp'])
    db.session.add(entry)
    db.session.commit()
    return flask.redirect(flask.url_for('show_entries'))

###~/main/templates/entries.html を編集

~/main/templates/entries.html
<html>
<head>
  <title>検温くん_第二形態</title>
</head>
<body>
  <br>
  <h1>検温結果おしえてクリクリ</h1>
  <form action="{{ url_for('add_entry') }}" method=post>
    <p>社員コードは? <input type="text" name="jcode" maxlength="7">  ※半角英数字7桁で!</p>
    <p>今朝の体温は? <input type="text" name="temp" maxlength="4">  ※半角数字○○.○で!</p>
    <p><input type="submit" value="送るのねん"></p>
  </form>
  <br>
  <p>[送信履歴]</p>
  <ul class="entries">
  {% for entry in entries %}
    <li>社員コード:{{entry.jcode}} / 体温:{{entry.temp}} / 検温日時:{{entry.date}}</li>
  {% endfor %}
   </ul>
</body>
</html>

いったんブラウザで確認しましょう。ターミナルでrun.py実行

% python run.py

http://127.0.1:5000/ にブラウザでアクセス。

image.png

おーー、できとるできとる。
さっきターミナルから直接放り込んだデータ2件、ちゃんと表示されましたね。

###【テスト】 入力フォームからデータを送信
いよいよ、ブラウザから体温データ登録してみます。ドキドキ。
image.png

えいっ!送信!
image.png
よしゃー。登録されました。
(データ件数が急に増えてるのは見逃してください。何回かテストした後にスクショ撮ったので。。)

殺風景なUIはさて置き、ローカル環境でちゃんと動くWEBアプリができあがりました!

(補記)
なお検温日時は送信した時間じゃなく、直近のローカルサーバー起動時間が登録されています。なので、ターミナルでctrl+Cでサーバー切らない限り、何回送っても同じ時間が記録されてしまいました。
仕様としてはイマイチですが、実用シーンとしては1日に1・2回のことですし、本番環境なら毎回セッションが切れればサーバ再起動するので問題ないのかなと。いったん割り切って進みます。

#つづきは後編へ
ここまでで10,000文字以上・・・長い・・・

いよいよローカル環境からインターネットの世界に飛び出していきますが、これ以降は別の記事に分けます。

Git、Github、Heroku、Heroku Postgresなど使うツールも増えますし、トラブルやバグも多発、ググっても答えが見つからず、途方に暮れることが多くなってきます。
引きつづき実例写実主義で、「わからなくてもとにかく前に進めたい」の参考になればと思います。

ここまで読んでいただいた皆さま、まずはありがとうございましたm(_ _)m

そして後編はコチラです。

###再掲
この時からアップデートを重ねて、いまではLINEでサクッと記録できるようになりました。一般公開してますので、よろしければお試しください。
LINE公式アカウント「毎日検温くんβ」
image.png

12
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?