0. これはなに
- Webアプリなんも知らん人間が、勉強の一環として家計簿アプリを作ってみたので、その過程などを備忘録として残す
- 言語はPython、アプリケーションのフレームワークはFlask、データベース管理システムとしてはSQLiteを使った
- 他にもCSSフレームワークとしてBootStrapを使って、オシャレ(?)な感じの見た目にした
※実装内容はGitHubで公開しています
1. 概要・モチベ
- オタク活動用の出金管理アプリを作りたいと思ったので、アプリやHTMLの勉強がてら作ってみようと思った
- サービスごとに対してお金をどれくらい使っているかを記録し、それが設定したラインより超えている・超えていないかが判断できればよいと考えていた
- 自分が毎日使えるような機能や、いい感じのデザインにできれば良いと思っていた。またそんなに労力をかけたくなかったので、高望みはせずに必要な機能だけを実装する方針にした
- Monthly Otakatsu Cash Administrator、略してMOCA(モカ)という名前にした 自分の作った何かに名前、つけてみたかったんですよね
- これをやる前の自分のスキルセットとしては以下のような感じ
- 分かる:Python
- ちょっと分かる:FastAPI(APIを作るのに使ったことある)・Streamlit(Webアプリもどきを作ったことがある)
- 分からん:データベース(に関すること。SQLなど)・HTML関連
2. 使う技術
アプリとして実装するには何が必要なのか(ツール)、どうすればいいのか(知識)が分からなかったが、少なくとも今回実装した分については以下の5つがあれば十分だった。
言語:Python(3.9.13)
アプリケーションフレームワーク:Flask(2.3.3)
- Webアプリを動作させるために必要な機能をまとめたもの。プログラミング言語によって様々なものが存在し、例えばJavaだったらSpring Framework、RubyだったらRuby on Railsなどがある。PythonだとWebアプリケーションフレームワークの代表的な候補として、Flask以外にDjangoとFastAPIがある。モデルの推論機能だけ欲しいっすみたいな時にFastAPIとか使って、ガッツリ商用でもイケるアプリ作りたいですみたいな時にDjangoを使うみたいな認識(?)
- (軽量・高速・小規模向け)FastAPI ≤ Flask << Django (重量・低速・大規模向け)
フロントエンドフレームワーク:BootStrap(5.3)
- ブラウザにアプリの内容を表示するとき、HTMLでページの遷移とか入力フォームの作成とかの機能を作ったり、CSSで見た目を整えたりする必要がある。BootStrapはCSSのフレームワークであり、あらかじめいい感じのボタンとかページのレイアウトとかを提供してくれたり、ブラウザの幅を変えたり閲覧する端末を変えたりしても画面を自然にレイアウトしなおしてくれる機能(レスポンシブデザイン)があったりと便利で、自分でHTML・CSSを書く労力が少なくなる嬉しさがある
テンプレートエンジン:Jinja2(3.1.2)
- テンプレートエンジンはデータとテンプレートを繋ぐ役割を果たす。Jinjaは、PythonのデータとHTMLテンプレートを繋ぎ、Pythonコードの中で扱うデータを画面に表示させる役割を担っている。Flaskは、テンプレートエンジンとしてJinja2を活用している。
- HTMLファイルの中で.pyファイルから受け渡されたデータを処理する部分を書く必要があり、この記法はテンプレートエンジン(今回はJinja)特有のものである
データベース管理システム:SQlite
- 今回は家計簿アプリを作るので、登録した商品のデータなどを管理する必要がある。それをデータベース(拡張子が".db"のファイル)として格納し、データの取り出しや編集、削除などの操作を行うシステムが、データベース管理システムである
- システム自体は色々あるが、軽量のデータを扱うならSQLiteが使われる。表形式(リレーショナルデータベース)で扱うものが多い
- DBの操作として必須のものをまとめてCRUD(Create, Read, Update, Delete)と呼んだりする。これらの操作をするときはSQL文(データの読み出しならselect、データの削除ならdeleteなど)を使う
3. 実際のスケジュール進行
一週間強かけて行った。最初の3日はUdemyの講座でFlaskを使ったアプリ作りとはなんぞやという部分を学習し、その後の約一週間(50時間程度)で家計簿アプリを作った。家計簿アプリを作っていた期間の内訳は以下の通り
- 要件の洗い出し・画面のレイアウト・画面遷移図の作成・BootStrapを簡単に触る:6時間
- サービスの登録・編集・削除画面を作る(DBとは繋げない):4時間
- サービスの登録・編集・削除画面を作る(DBと繋げる):8時間
- 商品の登録・編集・削除画面を作る(DBと繋げる):8時間
- サービス詳細閲覧機能・合計金額のグラフ表示機能を作る:8時間
- デザイン調整・リファクタリング・バグ修正:10時間
4. 要件
画面の様子や、どの画面からどの画面(URL)に遷移するのかをまずは紙に書き起こしながら考えていった。この時点ではだいぶざっくりとした感じであり、最終的に実装した内容とは異なる点もあるが、大体は同じ内容になっている。
(当初はログイン画面が欲しいと思っていたが、自分一人で使うなら不要と判断した)
実装の結果できた画面とともに、どういった機能がアプリに欲しいと思っていたのかを簡単に述べる。
a. ポートフォリオ
b. 各サービスの情報確認
c. サービスの登録・変更・削除
d. 商品の登録・変更・削除
e. データベース(DB)
- データベースは、商品の情報を管理するテーブルと、サービスの情報を管理するテーブルを用意する必要がある
- 商品のテーブルは、購入した日・商品名・購入したサービス名・金額・ジャンルを保持する。全ての商品を一つのテーブルに格納する
- サービスのテーブルは、どの月の情報であるのかを示す年月・上限金額を保持する
f. その他の機能(節約に関する機能)
余力があれば実装したい、と思っていたものをいくつか実装した
5. 2週間で学んだこと・知ったことのまとめ
Webアプリを作るには何が必要なのか(ツール)、どうすればいいのか(知識)という部分はざっくりと理解できたような気がする。
またアプリの実装(Flask)、 HTML関連(CSS・BootStrap・Jinja)、データベース(SQL・SQLite)に関することを色々知った。デザインについても少しだけ調べてみた
5-1. 作るプロセス
- 画面遷移図をまずは手で書いてみる
- 画面を設計してみる(FlaskとHTMLを使う。BootStrapを活用してもよい)
- 画面を作ったら、DBとは繋げない状態で、DBでデータを受け取った想定で辞書型のデータを使ってシミュレーションしてみる
- リンクの遷移がうまくできるか・コードの中でうまく処理ができるかなどを確認する
- 画面遷移や内部の処理がうまくできると分かったら、DBと接続する機能を付け加える
5-2. Flask
アプリケーションフレームワークとしてFlaskを使ったWebアプリの作り方を今回は学習した。公式のドキュメントがあるので、そこを見れば大体のことは書いてある(英語・日本語)、のだが、どうしても読む気になれなかったのでUdemyの講座をやってみた。基本的な部分は分かったのでやった意味はあったと思う
- ルーティング:違うURLで違うページにアクセスする(違うHTMLを表示する)処理
- ルーティングを行うためには、Flaskでは関数の前に@[アプリ名].route("/[127.0.0.1/の後の部分]")と書く。これはデコレータと呼ばれる。いまいちこうする理由がわかっていないが、デコレータは既存の関数に自分でカスタムした処理を加えるためのものだと思われるので、異なる機能を持つ関数が出力した結果を、共通の流れで前処理・後処理するために使うのだと思う
- うまく説明できないが、HTMLファイルでいうところのblock contentが同じような感じなのではないだろうかと妄想している。デコレータが付いた各関数部分はblock contentの内側の部分で、それがルーティングによって外側の部分(サーバとの通信とかの共通の処理を行う役割を担う部分)に渡されるのではないか
- また、デコレータは複数書ける(ネストできる?)ので、たとえば@auth.login_requiredというデコレータを入れれば、先ほど述べたルーティングの共通の処理に加え、ログインしているかしていないかでデコレータに渡された関数を実行する・しないを分岐させる、みたいなことをしているのだと思われる
- ルーティングを行うためには、Flaskでは関数の前に@[アプリ名].route("/[127.0.0.1/の後の部分]")と書く。これはデコレータと呼ばれる。いまいちこうする理由がわかっていないが、デコレータは既存の関数に自分でカスタムした処理を加えるためのものだと思われるので、異なる機能を持つ関数が出力した結果を、共通の流れで前処理・後処理するために使うのだと思う
- Flaskではどこにテンプレートファイル(.htmlのファイル)を置くべきか
- アプリケーション本体(app.py)と、テンプレートファイルが入ったフォルダ(template)は同じ階層に置くと多分うまくいく。というか、変な場所に設置したり、フォルダの名前をtemplate以外にしたりするとうまくテンプレートファイルを読んでくれない、みたいなことが起こるくらいの認識しかない(ので、ちゃんと調べたほうが良い)
- render_templateとredirectの違い
- rendor_templatesの引数はhtmlファイル。rendor_templatesは、引数にとったhtmlファイルを表示するだけの機能しかない。つまりurlの遷移は行わないので、例えばpage_a(urlが"/page_a")でボタンを押した際にpage_b(urlが"/page_b")に飛ぶ処理をするときは、html側のaタグでbutton要素を包み、page_bのURLに遷移するように書かなければならない。これを怠ると、URLが"/page_a"のままなのに、page_bの内容が表示されるということが起きてしまう
- redirectの引数はURL。pythonコード側でURLを遷移させたければ、redirectを使う
- g
- 正直よくわかっていない。ここを見る限り、アプリを起動させてから落とすまでの間がコンテキストと呼ばれ、そのコンテキストの中でGET・POSTといったHTTP通信(リクエスト)の度に更新されるグローバル変数のことらしい
- これを使うことでどういう嬉しさがあるのか(逆に使わなかったらどうなるのか)がよく分からない。DBから取ってきたデータを一時的にgに保存したり、ログイン時に送られてきたユーザ情報を格納したりといったことに使われるらしいが、別にローカル変数でも良いのでは? と思ってしまう…。セッションを跨いで保持したくない(残しておくとマズい)センシティブな情報とかを保持するために使うのかしら
- 今回の家計簿アプリの実装では、Udemyの講義で作っていたメモアプリに倣った形で、DBのデータを一時的にgに保存するという利用のみしていた。他にgを使うべき局面がどういう時なのか、未だにわからないままでいる
5-3. HTML・Jinja
HTML
- 「タグ」は文字(要素と呼ばれる)をどういうふうに表示するかを制御するために使われる。色々あるが、今回よく使ったのは以下の通り。BootStrapの公式ページからコピペが多かったので、細かく覚えてなくてもなんとかはなると思う
- div:要素を区切る。なんか要素と要素の間に余白欲しいな〜と思った時に使える
- a:リンクを貼る時に使う
- form:いろいろ入力できるフォームを作れる。methodにPOSTを指定すると、URLに入力内容が表示されない
- input:formタグの内側で、テキストを入力する部分を作れる
- select:formタグの内側で、選択肢の中から一つ(もしくは複数)選ぶ部分を作れる。選択肢はoptionタグで括る
- button:ボタンを表示できる
- trとかtd:テーブル作るときに使える
- input属性のname・id・forについて(こちらのページが参考になった)
- HTMLファイルのformタグで囲われたところに入力された値を、app.pyで受け取るには、HTMLのinputタグの内側にnameを定義しないといけない(name=hogeとしたら、hogeという変数名でサーバーにリクエストが渡る)
- inputタグで括られた部分のid属性と、その上に表示されるラベル(labelタグ)のfor属性は一対一に対応していて、同じ名前をつける必要がある。同じにしておくと、ラベルをクリックしたときに、フォームの中の値にフォーカスが当たり、その部分を選択してコピーできるみたいな状態になる
- HTMLファイルの雛形を使い回すには、雛形のファイルをhoge.htmlとしたとき、その中で使いまわしたい部分以外(ファイルによって変わる部分)を{% block content %} ~ {% endblock %}でくくり、その内側の部分を他のHTMLファイル(例えばfuga.html)で書く。fuga.htmlの冒頭には{% extends "hoge.html" %}と書き、{% block content %} ~ {% endblock %}で括った内側に、hoge.htmlで記述しなかった部分を記述する
- フォームを作る時のtips
- disabledとreadonlyの違い
- どちらもテキスト入力欄に用いることができ、値の変更をできなくするもの。用途としては、要素の削除の時に変えて欲しくない値を見せるとかがある
- disabledした値は、フォームをsubmitしても送信されない。しかしreadonlyにした値は、フォームをsubmitすれば送信される
- select(選択肢)だと、すでに選んである選択肢をreadonlyにするという処理ができないが、optionをdisabledにすることができるので、すでに選んでいる選択肢しか実質選べないようにするかたちでreadonlyを実現できる(参考)
- placeholder:フォームに記入する例を示したい時に、入力欄に例を薄い字で書ける(ユーザが入力すると消える)
- disabledとreadonlyの違い
-
GETとPOSTの違い
- GET:検索とかで使う。URLに生のデータが入る。例えばamazonの検索結果のリンクみたいなやつ。これはそのままブクマすれば、直接このパラメータを入力した結果に飛べる。
- POST:書き込みとかで使う。URLに生のデータが入らないので、URLからどういうリクエストをしたかを推定できない(盗聴はできる)。ログイン画面とかで使われる。POSTメソッドを使った画面で前に戻ろうとした場合、「入力した内容を送信してもいいですか?」みたいなポップアップがでることがある。
- コメントアウトは以下のように記述する
<!--コメントアウトしたい文字列-->
Jinja
- HTMLファイルの中にPythonっぽくfor文やif文を書くことができて、Pythonコードから受け渡されたデータを使って、Webページに何を表示するかを制御したりできる
- flask.rendor_templateは引数として、htmlファイルの他に、Pythonコード内の辞書型とかsqlite3.Rowオブジェクトを受け取ることができる。例えばhoge={"fuga":2}を受け取ったら、HTMLファイル内で{{hoge.fuga}}と書くことで2というデータを扱うことができる(二重波括弧で引数を受け取れる)
- こちらのページに詳しく載っている。例えばif文は{% if hoge == 2 %}(処理){% endif %}みたいな感じでHTMLファイル内に書ける
5-4. BootStrap
- ※最新版はBootStrap5.xみたいな感じになっているが、BootStrap4.x.xの情報が結構転がっていて、しかもBootStrap4と5で違うところも多くあったりするので、バージョンはちゃんと見たほうが良い(これ終盤も終盤で気づいてしまったので、実装の中でBootStrap4のままになったままになっているところがいくつか残ってしまっている)
- 例:HTMLのformの中で、選択肢を選ぶ部分(select)のクラスを、BootStrap4まではform-controlと書いていたが、BootStrap5からはform-selectとすることにした。BootStrap5をCDNで使った時にform-controlをそのまま残してあると、選択肢を表示する部分にドロップダウンであることを示す下向き矢印が表示されないみたいなことが起こる(参考)
- BootStrapをとりあえず触ってみたい時:BootStrapのクイックスタートがあるのでそれをコピペしてみる
- CDN(Content Delivery Network):普通にBootStrapの機能を使う場合、BootStrapのファイル一式をダウンロードして、ダウンロードしてきたCSSやJavascriptを読み込む部分をHTMLファイル内に書かないといけない。しかしローカルにダウンロードしなくても、HTMLファイル内にBootstrap用のCSSやJavaScriptをネット上から読み込むコードを書くだけで、BootStrapの色々な機能が使えるようになるのがCDNである(こちらのページの説明が参考になる)
- その際はこちらにあるように、HTMLファイルの中でheadで括られた部分でCSSを、bodyで括られた部分でJavaScriptを落としてくるコードを書く必要がある
- 以下のコードは上のリンクに貼ったBootStrapのチュートリアルにあるコード。headとbodyの中にそれぞれCSSとJSを読み込む部分がある
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<h1>Hello, world!</h1>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>
- ページのレイアウト:BootStrapはグリッドシステムというのを採用しており、これのおかげでいい感じのレイアウトにできる。配置したいコンテンツを包む箱みたいなのがあり、大きい順にcontainer > row > columnという名前が付いている。これらを組み合わせて、どのコンテンツをどこに置くかを決めていく(※こちらの動画がイメージを掴むのに参考になった)
- container・row・column
- container:ページの真ん中にドドンとコンテンツを大きく置ける。containerがないと、ページの端から端までフルに使う間延びしたデザインになる。
- row:containerの内側に、長さ12のrowがある。同じrowに包まれた要素は、container内で同じ高さ(行)に配置される。
- column:同じcolumn内にある要素は、同じ列に配置される。containerの中にrowを作った上で、columnの幅を4(col-md-4と書く)にすると、rowの幅が12なので、12÷4で3つの要素が横一列に並ぶレイアウトになる
- 幅の調整とかこれ読んでおくべきかも…と思ったが読まなくてもなんとかなった。多分もっと思い通りにレイアウトしたいみたいな時には必要になると思う
- container・row・column
- ダークテーマ:ページをダークテーマにすることができる。これはBootStrap5から実装された機能
BootStrapの実装で使ったコンポーネント
「あ〜あれを表示させたいな〜」と思った時に、CSSを書いて自分でイチから作らずとも、BootStrapのページからソースをコピーしてぺたっと貼るだけで実現できて大変助かった
- ナビバー:画面の一番上に固定して表示しておけるバーのこと。ここによく使うページへのリンクとか置ける。
- ジャンボトロン:画面の上の方に大きく大事なものを表示する。これはBootStrap5では存在しないので、こちらのように別の方法で実装する必要がある
- テーブル:表を作れる
- カード:いい感じの四角い要素を表示できる
- ボタン:いい感じのボタンを表示できる
- モーダル:ポップアップを表示できる
- プログレスバー:進行度合いを表示する。今回は上限金額のうちどのくらい使用したのかを可視化するために使った
-
折れ線グラフ
- .htmlファイルにJavaScriptのコード片を埋め込むためにはscriptタグを使う
- PythonのコードからHTML内のJavaScriptに変数を受け渡す必要があったため、こちらのページを参考にした
- ピクトグラム
5-5. SQL・SQLite(sqlite3)
Pythonに関係ない話
- データベースはあくまでデータの入れ物の総体であり、実際にデータを入れるのはテーブルと呼ばれるもの
- 一つのデータベースに、名前の違う複数のテーブルを保存できる(今回はitem・service・extra_itemというテーブルを、kakeibo.dbというデータベースに保存していた)
- 今回の実装で、SQL文としてはselect・insert・update・deleteだけしか使わなかった
- トランザクション:SQLでDBを操作する一連の流れ
- データベースと接続したときにbeginを送り、SQL文を実行した後で実行前の状態に戻したければrollback、操作を確定させたければcommitを送る
- 曖昧検索:正規表現などを使って、所望の文字列が入っているかどうかを条件に入れたりすることができる
- SQLiteで使えるデータ型:NULL, INTEGER, REAL, TEXT, BLOBの5種類がある
Pythonに関係ある話
- Python3でSQLiteを使うライブラリはsqlite3という名前
- sqlite3では、insertやupdate(DML文)の前に暗黙的にbeginが行われているが、commitは行われないので、明示的にPythonコード内でcommitする必要がある
- commitをしないとDBに変更が反映されない
- 今回の家計簿アプリの実装で、DBからデータを取得するために使ったsqlite3のメソッドはfetchoneとfetchall
- fetchone→クエリに合致する要素があれば、タプルが返ってくる。合致する要素がない場合、Noneが返る
- fetchall→クエリに合致する要素があれば、タプルのリストが返ってくる。合致する要素がない場合、空のリストが返る
- Connectionオブジェクトのrow_factoryをSQLite3.Rowにすると、SQLiteのselect文で取り出したデータはSQLite3.Rowオブジェクト(辞書型っぽく扱えて、キーで値にアクセスできるもの)として受け取れるようにできる
def connect_db():
db = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
5-6. デザイン
- Webページとかスライドとか作るときには、ベースカラー70%、メインカラー25%、アクセントカラー5%にすると良い、みたいなことはよく言われているが正直何をしたら良いか分からん
- Webページを作るときに考えることは多分こんな感じで、実際どうしたかも併せて書いておく
- ベースカラー(背景色):ダークグレー(BootStrapのダークテーマのデフォルト)
- プライマリカラー(メインカラー):#03dac5 (淡い緑・teal)→ボタンの色として使
- セカンダリカラー(プライマリカラーに近い色):今回は使わない
- アクセントカラー(メインカラーと対になる色):今回は使わない
- メイン文字色:白(BootStrapのデフォルト)
ダークテーマに関すること
material designというページを見ながら少し調べた
- ダークテーマの大半を占める背景色は、真っ黒(#FFFFFF)ではなくダークグレー(#121212)
- 真っ黒だと白い文字とのコントラストが強すぎるとのこと
- ダークテーマでは明度の差で高低差を作る。明るい(白に近い)ほど上に見える
- ダークモードで使う色(プライマリカラー・セカンダリカラー)は、彩度の高いビビッドな色にすると、背景色に飲まれてしまい、視認性が悪くなる。なので、彩度の低い(淡い・白に近い)色を使うべき
- それに伴い、彩度の高い色の上に文字を載せるなら、文字の色は黒にする(ダークモードだとデフォで文字色が白になっていることが多いので、敢えて変える必要がある)
- また、プライマリカラーなどで使う色が画面を占有すると逆に眩しくなってしまうので、画面を占有しないようにする
- プライマリカラーとセカンダリカラーは色として近いものを選ぶが、その際にTone(明度 (Brightness) や彩度 (Saturation) を調整した色調)は同じものを選ぶと良い。
- material designのページだと、Toneは0~1000の値を取り、ダークテーマではだいたい50~200が望ましいとされていた
- 色のバランスやトーンを見られる良さげなサイト
6. 将来的にやりたいこと
ORマッパー
- SQLAlchemyというのが使える(FlaskだとFlask-SQLAlchemyというやつ)。Pythonを扱うみたいにSQLのリクエストを飛ばせるようになるっぽい
- あまり詳しいことがわかっていないのでアレだけど、SQLを生でPythonコードの中に書くのはセキュリティ上よろしくないらしいので、何がよくないのかとかちゃんと理解した上で実装したい
flask blueprintなどを使ってモジュール分割
- アプリ本体のpythonコード(app.py)がメチャクチャ長くなってしまっているので、なんとかしたい
ログイン画面・ユーザ登録画面・アカウント設定画面(ユーザID・パス表示)・ユーザIDとパスワード変更画面
- ログイン機能はflask-loginを使う
- UserMixin, LoginManager, login_required, login_user, logout_user
- ログイン状態でなければログイン画面に無条件で遷移(リダイレクト)し、入力された情報が適切でなければログイン画面にリダイレクト、適切ならトップ画面に移動、という感じ
- ログインとログアウトの処理、ログインに失敗した際にログイン画面に戻ってくる処理、ログインがなされていない状態でデータの更新などを行おうとした際に無条件でログイン画面へリダイレクトするような機能が必要
- データベースでユーザー名とパスワードを管理する方法
- パスワードをそのままDBに保存してはいけない(DBをそのまま除いたらユーザー名とパスワードの組みがわかってしまい、セキュリティ上危ない)→パスワードはハッシュ化して保存する
- ハッシュ化では、文字列をハッシュ関数によってハッシュ値に変換する。元の文字列が同じならハッシュ化された後のハッシュ値も同じになる。ハッシュ値から元の文字列に変換することができないので、ハッシュ値が流出してもパスワードの文字列を推測することはできない。
- ユーザーごとにユニークな数字・ユーザーID・ハッシュ化したパスワードの3つのカラムが必要
- ユーザの情報をPythonのapp側で管理するには、werkzeugというライブラリが必要
- generate_password_hash, check_password_hashを使うことで、パスワードをハッシュ化したり、ハッシュ化した文字列のチェックを行える
データベースの保護
kakeibo.dbがrmコマンド一発で消えてしまうリスクがあるので、誤って消してしまわないようにする(書き込み可+削除不可)