見出し画像

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

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

[こんな方を想定]
・ググってがんばるけど、根本の文系感は否めない
・汎用化された例じゃなく、現物そのもの実例で見たい
・写経でもなんでもいいから、全体ひと回しやり切ってみたい

ただ、鬼のように長くなったので2つに分けました。(それでも長いです)

↓↓  後編はコチラ ↓↓

ちなみにこの時からアップデートを重ね、いまはLINEでサクッと記録できるようになりました。一般公開してますのでよろしければお試しください。


PCと言語の環境

■ macOS Catalina10
■ Python3.7.7
■ Flask

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

なお筆者はいまだに「フレームワーク?何それテクノ神?」状態ですが、ざっくり概念は下の動画がわかりやすかったです。


その他つかったサービス

■ コード書くのに「VS Code」
ググったら割と新しめの記事で推されてた印象。つかったらエディタどころじゃなくいろいろできて便利でした。(それでも1割も使い切れてないと思う)

■ ファイルの管理に「Github」
よくわからんけど、有名だし使ってみたかった。純粋な憧れ。

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

■ データベース管理は「Heroku Postgres」
Herokuで制作していくと、行きがかり上これになりました。データベースはカンペキに無知無学のゼロスタートです。


1.プロジェクト用のフォルダを作る

これから色々とファイルをつくって保存していく親フォルダを作ります。どこでもいいですが、iCloud内じゃないい方がよさげです。(このあとターミナルを操作するときに、iCloudDriveはパスの指定がややこしかった)

おすすめはローカルにあるユーザーフォルダの下あたり。ターミナル操作にせよFinderから開くにせよ、アクセスしやすいです。

スクリーンショット 2020-06-08 0.57.19

筆者はユーザーフォルダ'kazuyamano'の下に、'Dev'という開発もろもろ用の親フォルダをつくって、その下に'ken-on-kun'というプロジェクト個別の子フォルダを作りました。(日々の体温を記録するWEBアプリ=「検温くん」)

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


2.ターミナルで初期作業もろもろ

初学者はターミナルそのものに苦戦すると思いますが、筆者はProgateで初等教育受けてから、あとはGoogle先生で凌いでます。

 Gitを設定

ターミナルで、さっき作ったPJTフォルダへ移動(change directory)。gitのローカルリポジトリとして設定(git init) 。

1行ずつ実行します。

% 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) フォルダ名 % 

↓ 筆者の実例

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

ここまでやると「ken-on-kun」の下に「venv」フォルダが作られ、venv環境に必要と思しきファイル群が追加されています。

スクリーンショット 2020-06-08 1.17.01

*中身はよくわからんけど気にせずすすみます。


 Flaskをインストール

ひきつづきターミナルで作業。

pipをアップグレードします。
 *pip=Pythonの便利なパッケージをインストールするためのツール。

$ pip install --upgrade pip

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

$ pip -V

つづいてPythonの便利なパッケージ(フレームワーク)、その名もFlaskをインストールします。

$ pip install flask

↓一連の作業をすると、こんな感じになります

スクリーンショット 2020-06-11 1.25.48

字!字!

--upgrade pipも、install flaskも、'Successfully installed ●●' って出てるのでどっちも成功してます。


3.VSCodeで初期作業もろもろ

VS Codeを開いて、[ファイル] → [開く.…] → [ken-on-kun] を開きます。

スクリーンショット 2020-06-13 12.23.48

↑ ターミナルと並べて作業しがち。便利です ↑

この時点で、VSCode左メニューのgit管理アイコン(枝分かれ的なやつ)に、未読バッチみたいなのが860件とかついてビビります。まだ何もしてないのに!

スクリーンショット 2020-06-08 0.30.57

これはさっき追加された「venv」フォルダ内のファイル数を表していて(たぶん)、「新しいファイルが860件追加されたよ!」と通知が働いた模様(たぶん)。

とりあえず、ken-on-kunの下に「.gitignore」というファイルをつくって、中に'venv'と平文で打ち込んで保存しましょう。860件の通知が消えます(厳密には「.gitignoreってファイルが追加されたよ!」の通知1件に変わります)

スクリーンショット 2020-06-08 0.32.39

gitはバージョン管理の仕組みなのでほっとくとあらゆる変更履歴を拾って通知してくれるのですが、「.gitignore」ファイルをつくってそこにフォルダ名を登録すると、例外的に変更履歴と見なさない決まりのようです。(たぶん)

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


いよいよプログラムをつくっていきます

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

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


[Special Thanks文献]

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


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

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

 ~/run.py

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

from main import app

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

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

スクリーンショット 2020-06-14 10.59.01

[役割1]
トップのURLにアクセスした時、サイトを表示するために真っ先に動く.pyスクリプト。1行目の「mainパッケージ(≒フォルダ)をimportしなはれや!」の指示で、後述の~/main/__init__.py を走らせる。

[役割2]
2行目は、__init.pyで定義されているFlaskクラス(変数app)がアプリケーションとして起動されたのかインポートされたのかによる条件分岐。
3行目は、アプリケーションとして起動された場合は、デバッグモードを適用

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

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


 ~/main/__init__.py

ken-on-kunの下に[main]フォルダを新設(↓のあたり右クリックで作れます)

スクリーンショット 2020-06-14 11.10.53

mainの下に[__init__.py]ファイルを追加。

from flask import Flask 
app = Flask(__name__)
import main.views
スクリーンショット 2020-06-14 11.28.45

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

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

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

_init_.pyについて掘り下げるならコチラなど。


 ~/main/views.py

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

import flask 
from main import app

@app.route('/')
def show_entries():
   return 'Hello, World!'
スクリーンショット 2020-06-15 19.57.45

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

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

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


 [テスト] ローカルで実行、ブラウザで確認

ターミナルでrun.pyを実行

$ python run.py

実際のターミナル画面こんな感じです

スクリーンショット 2020-06-18 20.51.21

Running on http://127.0.1:5000/ って書いてあるので、ブラウザでアクセスしてみると

スクリーンショット 2020-06-18 20.54.48

はい、成功です。わーい

ローカルサーバーに眠っているプログラム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してくれました。

スクリーンショット 2020-06-18 21.01.06

勉強なりました。以下参考文献。


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


5.データベースの下準備

引き続きこちらのサイトを参考に進めます

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

 flask_sqlalchemyをインストール

$ pip install flask_sqlalchemy 
スクリーンショット 2020-06-18 21.52.02

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

データベースまわりはムズイのでまずは実践で進みます。参考文献。


 ~/main/__init__.py を編集

インストールしたSQLALchemyを、基礎工事プログラムに組み込んでいきます。

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]ファイルを追加。

import os

SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL'or "sqlite:///test.db"
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY="secret key"
スクリーンショット 2020-06-18 22.36.21

[役割]  *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の環境とローカルの環境で書き換える必要がありません。

だそうです。むずい。次いってみよー

 ~/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は従業員番号)

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()
スクリーンショット&nbsp;2020-06-21&nbsp;11.52.32

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

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

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

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

わからないなりに以下サイトが参考になりました。



6.データベースをつくる

ターミナルで以下実行します。

python -c "import main.modelsmain.models.init()"
スクリーンショット&nbsp;2020-06-19&nbsp;11.53.35

~/main/config.pyで設定した

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

この指定場所にデータベースが作成されます。今回はローカルでの実行なので 'or' 以降が適用され、~/mainフォルダの下にtest.dbが生成されているはずです。

スクリーンショット&nbsp;2020-06-19&nbsp;12.04.45

test.dbできてました。成功です。ちなみにVSCodeでは中身は見れません。

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


 [テスト] 箱にデータを入れてみる

ターミナルを「インタラクティブシェル」モードにします

$ python
スクリーンショット&nbsp;2020-06-19&nbsp;12.09.57

>>>が出れば成功です。(ctrl+Dで戻れます)

>>> 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

上記を1行ずつEnterしていきます。.pyファイルのコードを1行ずつ実行する感じですね。

・models.pyのEntryクラスをインポート
・dbをインポート    *__init__.pyの db = SQLAlchemy(app) 
・Entryクラスからインスタンス(entry1とentry2)をつくってデータベースに追加
・Entryクラスに対応するテーブルから全件を配列として抽出(というクエリ)

スクリーンショット&nbsp;2020-06-21&nbsp;11.59.25

最後の2行、2件のデータが配列として取り出され、entry1・2としてハンド追加したデータがちゃんとDBに収まってたことがわかります。deteもちゃんと作業時間が入ってます。テスト成功!

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


7.ブラウザにDBを表示する

さっき基礎工事したプログラムを、データベースとつないでいきます。

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

Before:世界に呼びかける壮大なプロジェクトですが、それだけです。

スクリーンショット&nbsp;2020-06-20&nbsp;7.02.39

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

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]フォルダの下です。

スクリーンショット&nbsp;2020-06-20&nbsp;7.58.55

 ~/main/templates/entries.html

[templates]フォルダの下に、entries.htmlを作ります。さきほどのviews.pyの最後に呼び出してた、トップページの表示内容を決める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
スクリーンショット&nbsp;2020-06-20&nbsp;12.33.08

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

スクリーンショット&nbsp;2020-06-21&nbsp;13.21.00

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

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


8.ブラウザからDBに書き込む

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

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

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関数を定義
@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 を編集

<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>

いったん見栄えをチェックしましょう。ターミナルで

python run.py

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

スクリーンショット&nbsp;2020-06-21&nbsp;14.04.28

いい感じぶぁい。

 [テスト] 入力フォームからデータを送信

スクリーンショット&nbsp;2020-06-21&nbsp;14.22.03

(履歴が増えてますが気にしないで下さい)

適当にフォームに入れて送ってみます。えいっ!

スクリーンショット&nbsp;2020-06-21&nbsp;14.22.52

わーい。 

見た目はさて置き、ローカル環境ではひと通り機能ができあがりました。

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


つづきは後編へ

ここまでで11,619文字・・・長い・・・

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

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

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


↓↓後編はコチラ ↓↓

この時からアップデートを重ねて、いまではLINEでサクッと記録できるようになりました。一般公開してますのでよろしければお試しください。


この記事が参加している募集

つくってみた

やってみた

この記事が気に入ったらサポートをしてみませんか?