こんにちは、七宮さん(@shichinomiya_s)です。
今回の記事では、FlaskベースでモバイルファーストなWeb会計アプリケーションを構築したお話を書こうと思います。American Expressのクレジットカード明細を手動でインポートして管理できる仕組みも実装しました。
最近、家計簿アプリは色々ありますが、「自分でデータを管理したい」「スマホでサクッと入力したい」「セキュリティもしっかりしたい」という要望を満たすために、完全自作してみました。
はじめに:なぜ自作したのか
家計簿アプリは市販のものも多いですが、以下の理由で自作することにしました:
- データの完全な所有権:クラウドサービスではなく、自分のサーバー(NAS等)で完全に管理したい
- カスタマイズ性:カテゴリや機能を自由に追加・変更したい
- スマホ対応:外出先でもサクッと入力できるモバイルファーストなUI
- セキュリティ:最新のWebAuthn(パスキー)認証を実装してみたい
- AMEXとの連携:クレジットカード明細を効率的に取り込みたい
既存のツールでは痒いところに手が届かないことが多かったので、思い切って自作することにしました。
アプリケーションの機能概要
このWeb会計アプリには以下の機能を実装しています:
主要機能
- 支出記録の入力・表示(スマートフォン最適化)
- カテゴリ別支出集計(食費、日用品、医療費など27カテゴリ)
- 月次レポート
- WebAuthn(パスキー)認証
- 管理者機能(アカウント管理、カテゴリ管理)
- ログイン試行制限・IP BANシステム
- レシート画像アップロード
- 月次精算機能
- 公共料金管理ダッシュボード
- AMEXデータ手動インポート(CSVファイル経由)
画面構成
主な画面は以下の通りです:
- ログイン画面:WebAuthn(パスキー)による生体認証
- 支出記録入力画面:日付、店舗、カテゴリ、金額等を入力
- 記録一覧画面:月別の支出一覧とカテゴリ別集計
- 管理画面:カテゴリ管理、アカウント管理、BAN管理
WebAuthnによるパスキー認証
モバイルファーストなUI
カテゴリ別の支出集計
カテゴリのカスタマイズ
技術スタック
このアプリケーションは以下の技術で構築しています:
Backend
- Flask:Pythonの軽量Webフレームワーク
- Pandas:データ処理とCSV操作
- WebAuthn:パスキー認証(
webauthn>=2.0.0) - Cryptography:暗号化処理(
cryptography>=41.0.0)
Frontend
- Bootstrap 5:レスポンシブUIフレームワーク
- Font Awesome:アイコン
- Apple Design System:Apple風のカスタムCSS
- Progressive Web App (PWA):スマホのホーム画面に追加可能
データ管理
- CSV:支出データの保存(年月別)
- JSON:設定データ、ユーザー情報、カテゴリ情報
セキュリティ
- HTTPS:SSL/TLS通信
- WebAuthn:FIDO2準拠のパスキー認証
- セッション管理:Secure Cookie、HTTPOnly、SameSite属性
- IP BAN:ログイン試行回数制限
システム構成図

プロジェクト構成
プロジェクトのディレクトリ構成は以下のようになっています:
web_accounting/
├── app.py # メインアプリケーション
├── auth.py # 認証モジュール(WebAuthn実装)
├── requirements.txt # Python依存関係
├── cert.pem / key.pem # SSL証明書(HTTPS用)
├── templates/ # HTMLテンプレート
│ ├── base.html # ベーステンプレート
│ ├── index.html # 支出記録入力画面
│ ├── view.html # 記録一覧画面
│ ├── login.html # ログイン画面
│ ├── register.html # ユーザー登録画面
│ ├── manage_categories.html # カテゴリ管理画面
│ ├── manage_accounts.html # アカウント管理画面
│ └── manage_bans.html # BAN管理画面
├── static/ # 静的ファイル
│ ├── apple-design.css # Apple風カスタムCSS
│ ├── icons/ # PWA用アイコン
│ ├── manifest.json # PWAマニフェスト
│ └── sw.js # Service Worker
└── data/ # データディレクトリ
├── categories.json # カテゴリ設定
├── users.json # ユーザー情報
├── login_attempts.json # ログイン試行記録
├── csv/ # 会計データ(年月別CSV)
│ └── 2025/
│ ├── 202501.csv
│ └── 202502.csv
└── receipts/ # レシート画像保存先
セットアップ手順
1. 依存関係のインストール
まず、必要なPythonパッケージをインストールします:
cd web_accounting
pip install -r requirements.txt
requirements.txt の内容はこんな感じです:
flask
pandas
python-dateutil
webauthn>=2.0.0
cryptography>=41.0.0
シンプルですね。
2. データディレクトリの準備
初回起動時に自動的に作成されますが、手動で作成する場合は以下のコマンドで:
mkdir -p data/csv data/receipts
3. SSL証明書の生成(HTTPS用)
WebAuthn(パスキー)認証はHTTPS環境が必須です。自己署名証明書を生成します:
python generate_cert.py
これで cert.pem と key.pem が生成されます。
本番環境では、Let’s Encryptなどで正式な証明書を取得することをおすすめします。
4. アプリケーションの起動
python app.py
デフォルトではポート5000でHTTPSサーバーが起動します。
* Running on https://0.0.0.0:5000
5. ブラウザでアクセス
スマートフォンやPCのブラウザから以下のURLでアクセス:
https://[サーバーのIPアドレス]:5000
自己署名証明書の場合、ブラウザで警告が出ますが、「詳細設定」→「このサイトにアクセスする」で進めます。
6. 初回ユーザー登録
初回アクセス時に、ユーザー登録画面が表示されます。WebAuthnに対応したデバイス(指紋認証、顔認証、セキュリティキー等)を使用して登録します。
パスキーを掌握されると困るので、私の環境では専用ページを作成し、Cloudflare Access で上位レイヤーで防御しています。
ユーザー名入力
パスキー登録
登録完了
主要機能の実装解説
ここからは、主要な機能の実装について詳しく見ていきます。
WebAuthn(パスキー)認証
このアプリの目玉機能の一つが、WebAuthn(パスキー)認証です。従来のパスワード認証ではなく、FIDO2準拠の生体認証やセキュリティキーを使った認証を実装しました。
WebAuthnとは?
WebAuthnは、Webサイトやアプリで生体認証(指紋、顔認証)やセキュリティキーを使って安全にログインできる仕組みです。パスワードを覚える必要がなく、フィッシング攻撃にも強いという特徴があります。
最近では、Apple、Google、Microsoftが推進している「パスキー」という名前でも知られています。
認証フロー

実装のポイント
auth.py に認証ロジックを実装しています。主なポイントは以下の通りです:
from webauthn import (
generate_registration_options,
verify_registration_response,
generate_authentication_options,
verify_authentication_response,
)
# ユーザー登録時のチャレンジ生成
def start_registration(username, user_id):
"""WebAuthn登録開始"""
options = generate_registration_options(
rp_id=get_rp_id(),
rp_name="家計簿システム",
user_id=user_id.encode('utf-8'),
user_name=username,
authenticator_selection=AuthenticatorSelectionCriteria(
user_verification=UserVerificationRequirement.REQUIRED,
resident_key=ResidentKeyRequirement.REQUIRED,
),
)
return options
# ログイン時のチャレンジ生成
def start_authentication(username):
"""WebAuthn認証開始"""
user = get_user_by_username(username)
if not user:
return None
options = generate_authentication_options(
rp_id=get_rp_id(),
allow_credentials=[
PublicKeyCredentialDescriptor(id=base64.b64decode(cred['id']))
for cred in user['credentials']
],
)
return options
WebAuthnライブラリの webauthn>=2.0.0 を使うことで、比較的簡単に実装できました。
認証の流れ
認証の流れはこんな感じです:
- 登録フロー
- ユーザーがユーザー名を入力
- サーバーがチャレンジ(ランダムな文字列)を生成
- ブラウザがWebAuthn APIを呼び出し、生体認証を実行
- 公開鍵がサーバーに保存される
- ログインフロー
- ユーザーがユーザー名を入力
- サーバーがチャレンジを生成
- ブラウザがWebAuthn APIを呼び出し、生体認証を実行
- サーバーが署名を検証してログイン成功
パスワードを一切使わないので、とても安全ですね。
Apple風UIデザイン
UIデザインには、Apple Design Systemを参考にしたカスタムCSSを作成しました。
デザインコンセプト
Appleのデザインといえば、シンプルで洗練された印象ですよね。以下の要素を取り入れました:
- SF Pro Displayフォント風のシステムフォント
- グラスモーフィズム効果(半透明のカード)
- 鮮やかなアクセントカラー(Apple Blue、Apple Orange等)
- 滑らかなアニメーション(cubic-bezierイージング)
- 適度な余白と角丸
カスタムCSSの例
static/apple-design.css には、Apple風のスタイルを定義しています:
:root {
--apple-blue: #007AFF;
--apple-gray: #F2F2F7;
--apple-border: #E5E5EA;
--apple-text: #1C1C1E;
--apple-red: #FF3B30;
--apple-green: #34C759;
--apple-orange: #FF9500;
}
/* グラスモーフィズム効果 */
.glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border: 0.5px solid rgba(255, 255, 255, 0.3);
}
/* Apple風のホバー効果 */
.apple-hover {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.apple-hover:hover {
transform: scale(1.02);
}
.apple-hover:active {
transform: scale(0.98);
}
backdrop-filter を使った半透明のグラスモーフィズム効果は、最近のAppleのデザインでよく見られますね。スマホで見ると特に美しいです。
レスポンシブデザイン
モバイルファーストで設計しているので、スマホでの操作性を重視しています:
- タッチ操作に最適化されたボタンサイズ
- スワイプジェスチャーに対応
- フローティングアクションボタン(FAB)で素早く入力
- 大きめのフォントサイズで視認性向上
Bootstrap 5のグリッドシステムと組み合わせることで、画面サイズに応じて最適なレイアウトを提供しています。
支出記録機能
家計簿アプリのコア機能である支出記録について見ていきます。
入力フォーム
templates/index.html には、支出記録の入力フォームを実装しています:
<form action="{{ url_for('add_expense') }}" method="POST" enctype="multipart/form-data">
<div class="row">
<!-- 日付 -->
<div class="col-md-6 mb-3">
<label for="date" class="form-label">
<i class="fas fa-calendar me-1"></i>日付
</label>
<input type="date" class="form-control" id="date" name="date" required>
</div>
<!-- 利用店舗 -->
<div class="col-md-6 mb-3">
<label for="shop" class="form-label">
<i class="fas fa-store me-1"></i>利用店舗
</label>
<input type="text" class="form-control" id="shop" name="shop" required>
</div>
<!-- カテゴリ -->
<div class="col-md-6 mb-3">
<label for="category" class="form-label">
<i class="fas fa-tags me-1"></i>カテゴリ
</label>
<select class="form-select" id="category" name="category" required>
<option value="">カテゴリを選択</option>
{% for cat in categories %}
<option value="{{ cat }}">{{ cat }}</option>
{% endfor %}
</select>
</div>
<!-- 支払方法 -->
<div class="col-md-6 mb-3">
<label for="payment_method" class="form-label">
<i class="fas fa-credit-card me-1"></i>支払方法
</label>
<select class="form-select" id="payment_method" name="payment_method" required>
<!-- 省略 -->
</select>
</div>
<!-- 金額 -->
<div class="col-md-6 mb-3">
<label for="amount" class="form-label">
<i class="fas fa-yen-sign me-1"></i>決済金額
</label>
<input type="number" class="form-control" id="amount" name="amount" required>
</div>
</div>
</form>
Font Awesomeのアイコンを各入力項目に付けることで、視覚的に分かりやすくしています。
データ保存処理
app.py の add_expense() 関数で、入力されたデータをCSVファイルに保存します:
@app.route('/add', methods=['POST'])
@login_required
def add_expense():
"""支出記録を追加"""
# フォームデータを取得
date = request.form.get('date')
shop = request.form.get('shop')
category = request.form.get('category')
payment_method = request.form.get('payment_method')
amount = request.form.get('amount')
# 年月を抽出
year_month = datetime.strptime(date, '%Y-%m-%d').strftime('%Y%m')
# CSVファイルパスを生成
csv_path = os.path.join(CSV_BASE_DIR, f"{year_month[:4]}", f"{year_month}.csv")
# 新しいレコードを作成
new_record = {
'日付': date,
'店舗': shop,
'カテゴリ': category,
'支払方法': payment_method,
'金額': amount,
}
# CSVに追記
# (詳細なコードは省略)
flash('記録を追加しました', 'success')
return redirect(url_for('index'))
データはCSVファイルとして年月別に保存されるので、後からExcelなどで分析することもできますね。
カテゴリ管理
カテゴリは data/categories.json で管理しています:
{
"categories": [
"食費 (家庭)",
"食費 (外食)",
"日用品",
"消耗品",
"医療費",
"交通費",
"その他"
],
"payment_method": [
"クレジットカード",
"現金",
"電子マネー"
]
}
管理画面からカテゴリの追加・削除・並び替えができるようになっています。
カテゴリ一覧
ドラッグ&ドロップで並び替え
ログイン制限・BAN機能
セキュリティ対策として、ログイン試行回数制限とIP BANシステムを実装しました。
仕組み
- 最大試行回数:5回まで
- BAN期間:24時間
- 試行回数リセット:30分間ログイン試行がない場合
auth.py に実装されています:
# ログイン制限設定
MAX_LOGIN_ATTEMPTS = 5
BAN_DURATION_HOURS = 24
ATTEMPT_RESET_MINUTES = 30
def is_ip_banned(ip_address):
"""IPアドレスがBANされているか確認"""
ban_data = load_ban_list()
current_time = datetime.now()
for ban_entry in ban_data['banned_ips']:
if ban_entry['ip'] == ip_address:
ban_time = datetime.fromisoformat(ban_entry['banned_at'])
ban_until = ban_time + timedelta(hours=BAN_DURATION_HOURS)
if current_time < ban_until:
return True, ban_until
return False, None
def record_login_attempt(ip_address, success=False):
"""ログイン試行を記録"""
attempts_data = load_login_attempts()
if success:
# 成功したら試行記録をクリア
if ip_address in attempts_data['attempts']:
del attempts_data['attempts'][ip_address]
else:
# 失敗したら試行回数を増やす
if ip_address not in attempts_data['attempts']:
attempts_data['attempts'][ip_address] = []
attempts_data['attempts'][ip_address].append({
'timestamp': datetime.now().isoformat()
})
# 最大試行回数を超えたらBAN
if len(attempts_data['attempts'][ip_address]) >= MAX_LOGIN_ATTEMPTS:
ban_ip(ip_address)
save_login_attempts(attempts_data)
これにより、ブルートフォース攻撃を防ぐことができます。管理画面から、BANされたIPアドレスの確認や解除もできるようにしています。

AMEXデータの手動インポート
American Expressのクレジットカード明細を手動でインポートする仕組みも実装しました。

フロー
- AMEXサイトから明細をダウンロード
- American Expressの公式サイトにログイン
- 「利用明細」→「ダウンロード」
- CSV形式でダウンロード(ファイル名:
activity.csv)
- データ変換スクリプトを実行
- ダウンロードしたCSVを
AMEX/raw_data/フォルダに配置 - 変換スクリプトを実行:
- ダウンロードしたCSVを
cd AMEX
python convert_activity.py
- 変換されたデータを確認
AMEX/data/フォルダに年月別のCSVファイルが生成されます- ファイル名例:
activity_converted_202501.csv
convert_activity.py の処理内容
変換スクリプトは以下の処理を行います:
import pandas as pd
import os
def convert_activity_data():
"""AMEXデータの変換処理"""
# 元データを読み込み(Shift-JISエンコーディング)
df = pd.read_csv('raw_data/activity.csv', encoding='shift_jis')
# 必要な列のみ抽出
required_columns = ['ご利用日', 'ご利用内容', 'カード会員様名', '金額']
df_filtered = df[required_columns].copy()
# 金額を数値に変換(カンマ削除)
df_filtered['金額'] = df_filtered['金額'].str.replace(',', '').astype(float)
# マイナス金額を除外(返金データ等)
df_filtered = df_filtered[df_filtered['金額'] > 0]
# 日付でフィルタリング(指定日以降のデータのみ)
df_filtered['ご利用日'] = pd.to_datetime(df_filtered['ご利用日'])
df_filtered = df_filtered[df_filtered['ご利用日'] >= '2025-07-01']
# 年月ごとにグループ化して保存
df_filtered['年月'] = df_filtered['ご利用日'].dt.strftime('%Y%m')
grouped = df_filtered.groupby('年月')
for year_month, group_df in grouped:
output_path = f"data/activity_converted_{year_month}.csv"
group_df.to_csv(output_path, index=False, encoding='shift_jis')
print(f"保存完了: {output_path}")
Web会計アプリへの取り込み
変換されたCSVファイルは、以下の方法でWeb会計アプリに取り込めます:
方法1:手動でデータをマージ
- 変換後のCSVファイルを
web_accounting/data/csv/[年]/[年月].csvにコピー - 既存のデータがある場合は、Excelなどで開いて手動でマージ
方法2:アプリ内で個別に入力
- 変換後のCSVファイルを参照しながら、Web会計アプリの入力画面から手動で入力
- カテゴリを選択しながら入力できるので、より細かい分類が可能
私は通常、方法2を使っています。AMEXの明細をざっと確認しながら、適切なカテゴリを振り分けられるので便利ですね。
セキュリティ対策
個人的な金融データを扱うアプリなので、セキュリティには特に気を使いました。
実装したセキュリティ対策
| 対策項目 | 実装内容 |
|---|---|
| HTTPS通信 | SSL/TLS証明書によるHTTPS通信を必須化 |
| WebAuthn認証 | FIDO2準拠のパスキー認証、パスワード不要 |
| セッション管理 | Secure Cookie、HTTPOnly、SameSite属性を設定 |
| IP BAN | 5回のログイン失敗で24時間BAN |
| CSRF対策 | FlaskのセッションとSameSite属性で対策 |
| ファイルアップロード制限 | 画像ファイルのみ許可、サイズ制限16MB |
| 管理者認証 | 管理画面は別途パスワード認証を実装 |
| データの暗号化 | セッションキーは環境変数で管理 |
セキュリティ設定のコード例
app.py でのセキュリティ設定:
# セッション設定
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'default_secret_key')
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS必須
app.config['SESSION_COOKIE_HTTPONLY'] = True # JS無効化
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF対策
# ファイルアップロード制限
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
本番環境では、FLASK_SECRET_KEY を環境変数で設定し、推測不可能なランダム文字列を使用することをおすすめします。
管理者パスワードの設定
管理画面にアクセスするための管理者パスワードは、SHA256ハッシュで保存しています:
# デフォルトパスワードのハッシュ
DEFAULT_PASSWORD_HASH = "7c4b006def035d37fd5d9f8a67a2fb73626ae2c3a502573bc026d80aebda1930"
def hash_password(password):
"""パスワードをSHA256でハッシュ化"""
return hashlib.sha256(password.encode('utf-8')).hexdigest()
本番環境では、環境変数 ADMIN_PASSWORD_HASH で独自のハッシュ値を設定してください。
実際に使ってみた感想
このアプリを数ヶ月間実際に運用してみた感想をまとめます。
良かった点
- スマホでサクッと入力できる:外出先でもその場で入力できるのが便利
- パスキー認証が快適:指紋認証でログインできるので、パスワード入力が不要
- カスタマイズ性が高い:カテゴリや機能を自由に追加できる
- データの所有権:NASに保存しているので、完全に自分で管理できる
- AMEXとの連携:クレジットカード明細を効率的に取り込める
- Apple風UIが綺麗:見た目が良いとモチベーションが上がる
改善したい点
- CSVインポート機能の強化:Web画面から直接CSVアップロードできると便利
- グラフ機能の追加:カテゴリ別の支出推移をグラフで可視化したい
- 予算管理機能:月ごとの予算を設定して、超過を警告する機能
- 複数ユーザーでの共有:家族で支出データを共有する仕組み
- 自動バックアップ:定期的にデータをバックアップする仕組み
といった感じで、まだまだ改善の余地はありますね。今後のアップデートで順次追加していこうと思います。
パフォーマンス
Synology NAS(DS723+)上でDockerコンテナとして動かしていますが、レスポンスは非常に良好です。Flaskは軽量なので、NASのような非力なハードウェアでも快適に動作します。
データ量が増えても、CSVファイルを年月別に分割しているので、パフォーマンスの劣化は今のところ感じていません。
さいごに
今回の記事では、FlaskベースのモバイルファーストなWeb会計アプリケーションを構築したお話を書きました。
WebAuthn(パスキー)認証、Apple風UI、AMEXデータのインポートなど、実用的な機能を詰め込んだアプリになったかと思います。
自分でデータを管理したい、カスタマイズ性の高い家計簿アプリが欲しい、という方にはおすすめです。コード全体はGitHubで公開する予定はありませんが、この記事が参考になれば幸いです。
今後の展開
今後は以下の機能を追加していく予定です:
- Web画面からCSVインポート機能
- Chart.jsを使ったグラフ可視化
- 予算管理・警告機能
- 月次レポートのPDFエクスポート
- 複数ユーザー対応(家族アカウント)
- 定期支出の自動入力機能
- データの自動バックアップ
もし「こんな機能があったらいいな」というアイデアがあれば、コメントで教えていただけると嬉しいです。
いかがだったでしょうか。この記事が、Web会計アプリを自作してみたい方の参考になれば幸いです。

