pythonでbottleとsqlite3を使ってWEBアプリケーションを作るには?
血圧データcsvをWindowsのファイルダイアログを使って読み込みます。
データはDBに格納し、新しいファイルを読み込んだらデータをアペンドしてゆきます。
csvのフィールドは、”日付”、”時間”、”最高血圧”、”最低血圧”、その他いくつかの不定数の項目。
血圧データCSVには、同じ日付で、1回から10回まで、不特定回数のデータが含まれています。
次に、グラフ出力をします。
ボタンで選択して2種類のグラフ出力ができるようにします。
1.日付を選択し、最高血圧、最低血圧それぞれの、一番高い値と、一番低い値と、一日の中の平均三項目をグラフにします。
日付はスピンドルボタンで変更できるようにしてグラフを出力しなおします。
2.カレンダーで、始めと終わりの期間を指定して、最高血圧の平均値と最低血圧の平均値を連続的に描画します。
変数名関、数名は日本語で、詳細なコメントをつけて出力してください。
```python
# -*- coding: utf-8 -*-
import bottle
from bottle import route, run, template, request, view, static_file
import sqlite3
import csv
import os
import tkinter as tk
from tkinter import filedialog
import datetime
import matplotlib.pyplot as plt
import japanize_matplotlib # 日本語対応
# アプリケーション設定
app = bottle.Bottle()
app.config['db.file'] = '血圧データ.db'
# データベース初期化
def データベース初期化():
"""データベースを初期化する。"""
conn = sqlite3.connect(app.config['db.file'])
c = conn.cursor()
c.execute('''
CREATE TABLE IF NOT EXISTS 血圧 (
日付 TEXT,
時間 TEXT,
最高血圧 REAL,
最低血圧 REAL,
その他 TEXT
)
''')
conn.commit()
conn.close()
データベース初期化() # アプリ起動時に初期化
# ファイルアップロード関数
def ファイルアップロード():
"""ファイルダイアログを開いてCSVファイルを選択し、データベースにデータを追加する。"""
root = tk.Tk()
root.withdraw() # メインウィンドウを表示しない
file_path = filedialog.askopenfilename(
title="血圧データCSVファイルを選択してください",
filetypes=[("CSV files", "*.csv")]
)
if file_path:
データベースにCSVデータを追加(file_path)
return file_path
def データベースにCSVデータを追加(ファイルパス):
"""CSVファイルからデータを読み込み、データベースにアペンドする。"""
try:
conn = sqlite3.connect(app.config['db.file'])
c = conn.cursor()
with open(ファイルパス, 'r', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
next(reader, None) # ヘッダー行をスキップ
for row in reader:
日付 = row[0]
時間 = row[1]
最高血圧 = float(row[2]) # 数値に変換
最低血圧 = float(row[3]) # 数値に変換
その他 = ','.join(row[4:]) # その他の項目を結合
c.execute('INSERT INTO 血圧 VALUES (?, ?, ?, ?, ?)', (日付, 時間, 最高血圧, 最低血圧, その他))
conn.commit()
conn.close()
print(f"CSVファイル '{ファイルパス}' からのデータをデータベースに追加しました。")
return True
except Exception as e:
print(f"エラーが発生しました: {e}")
return False
# 静的ファイル配信 (CSS, JavaScript など)
@app.route('/static/<filename:path>')
def static_dir(filename):
"""静的ファイルを提供する。"""
return static_file(filename, root='static') # 'static' ディレクトリに配置
# トップページ
@app.route('/')
@view('index')
def index():
"""トップページを表示する。"""
return {} # テンプレートに渡す変数は空
# ファイルアップロード処理
@app.route('/upload', method='POST')
def upload():
"""ファイルアップロード処理を行う。"""
ファイルパス = ファイルアップロード()
if ファイルパス:
bottle.redirect('/') # アップロード後にリダイレクト
else:
return "ファイルが選択されませんでした。"
# 日付選択グラフ
@app.route('/日別グラフ')
@view('日別グラフ')
def 日別グラフ表示():
"""日付選択グラフのページを表示する。"""
conn = sqlite3.connect(app.config['db.file'])
c = conn.cursor()
c.execute('SELECT DISTINCT 日付 FROM 血圧 ORDER BY 日付')
日付リスト = [row[0] for row in c.fetchall()]
conn.close()
return {'日付リスト': 日付リスト}
@app.route('/日別グラフ表示')
def 日別グラフ():
"""選択された日付のグラフを表示する。"""
日付 = request.query.日付
if not 日付:
return "日付が選択されていません。"
conn = sqlite3.connect(app.config['db.file'])
c = conn.cursor()
c.execute('SELECT 最高血圧, 最低血圧 FROM 血圧 WHERE 日付 = ?', (日付,))
データ = c.fetchall()
conn.close()
if not データ:
return "データがありません。"
最高血圧リスト = [row[0] for row in データ]
最低血圧リスト = [row[1] for row in データ]
最高血圧最大値 = max(最高血圧リスト)
最高血圧最小値 = min(最高血圧リスト)
最高血圧平均値 = sum(最高血圧リスト) / len(最高血圧リスト)
最低血圧最大値 = max(最低血圧リスト)
最低血圧最小値 = min(最低血圧リスト)
最低血圧平均値 = sum(最低血圧リスト) / len(最低血圧リスト)
# グラフ作成
plt.figure(figsize=(10, 6))
plt.plot([1, 2, 3], [最高血圧最大値, 最高血圧最小値, 最高血圧平均値], marker='o', label='最高血圧')
plt.plot([1, 2, 3], [最低血圧最大値, 最低血圧最小値, 最低血圧平均値], marker='o', label='最低血圧')
plt.xticks([1, 2, 3], ['最大値', '最小値', '平均値'])
plt.title(f'{日付} の血圧')
plt.xlabel('項目')
plt.ylabel('血圧 (mmHg)')
plt.legend()
plt.grid(True)
# グラフを画像として保存
グラフファイルパス = f'static/{日付}_血圧.png'
plt.savefig(グラフファイルパス)
plt.close()
return template('<img src="{{グラフファイルパス}}" alt="血圧グラフ">', グラフファイルパス=グラフファイルパス)
# 期間指定グラフ
@app.route('/期間指定グラフ')
@view('期間指定グラフ')
def 期間指定グラフ表示():
"""期間指定グラフのページを表示する。"""
return {}
@app.route('/期間指定グラフ表示')
def 期間指定グラフ():
"""指定された期間のグラフを表示する。"""
開始日 = request.query.開始日
終了日 = request.query.終了日
if not 開始日 or not 終了日:
return "開始日と終了日を選択してください。"
try:
開始日Obj = datetime.datetime.strptime(開始日, '%Y-%m-%d').date()
終了日Obj = datetime.datetime.strptime(終了日, '%Y-%m-%d').date()
except ValueError:
return "日付の形式が正しくありません (YYYY-MM-DD)。"
if 開始日Obj > 終了日Obj:
return "開始日は終了日より前にしてください。"
conn = sqlite3.connect(app.config['db.file'])
c = conn.cursor()
# 期間内の日付リストを作成
日付リスト = []
現在の日付 = 開始日Obj
while 現在の日付 <= 終了日Obj:
日付リスト.append(現在の日付.strftime('%Y-%m-%d'))
現在の日付 += datetime.timedelta(days=1)
最高血圧平均値リスト = []
最低血圧平均値リスト = []
for 日付 in 日付リスト:
c.execute('SELECT 最高血圧, 最低血圧 FROM 血圧 WHERE 日付 = ?', (日付,))
データ = c.fetchall()
最高血圧リスト = [row[0] for row in データ]
最低血圧リスト = [row[1] for row in データ]
if 最高血圧リスト:
最高血圧平均値 = sum(最高血圧リスト) / len(最高血圧リスト)
最高血圧平均値リスト.append(最高血圧平均値)
else:
最高血圧平均値リスト.append(None) # データがない場合はNoneを追加
if 最低血圧リスト:
最低血圧平均値 = sum(最低血圧リスト) / len(最低血圧リスト)
最低血圧平均値リスト.append(最低血圧平均値)
else:
最低血圧平均値リスト.append(None) # データがない場合はNoneを追加
conn.close()
# グラフ作成
plt.figure(figsize=(12, 6))
plt.plot(日付リスト, 最高血圧平均値リスト, marker='o', label='最高血圧 (平均)')
plt.plot(日付リスト, 最低血圧平均値リスト, marker='o', label='最低血圧 (平均)')
plt.xlabel('日付')
plt.ylabel('血圧 (mmHg)')
plt.title(f'{開始日} から {終了日} までの血圧 (平均)')
plt.legend()
plt.grid(True)
plt.xticks(rotation=45) # x軸ラベルを回転
# グラフを画像として保存
グラフファイルパス = f'static/{開始日}_{終了日}_血圧.png'
plt.savefig(グラフファイルパス)
plt.close()
return template('<img src="{{グラフファイルパス}}" alt="血圧グラフ">', グラフファイルパス=グラフファイルパス)
# 実行
if __name__ == '__main__':
run(app, host='localhost', port=8080, debug=True, reloader=True)
```
**必要なもの:**
1. **Bottle:** `pip install bottle`
2. **SQLite3:** (Python に標準で含まれていることが多い)
3. **matplotlib:** `pip install matplotlib`
4. **japanize\_matplotlib:** `pip install japanize_matplotlib`
5. **tkinter:** (Python に標準で含まれていることが多い)
**ファイル構成:**
```
my_app.py # メインの Python スクリプト
static/ # CSS, JavaScript, 画像などを置くディレクトリ
templates/ # Bottle テンプレートを置くディレクトリ
```
**テンプレート (templates/) の例:**
* **index.tpl:**
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>血圧データ管理</title>
<link rel="stylesheet" href="static/style.css">
</head>
<body>
<h1>血圧データ管理</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<button type="submit">CSVファイルを選択してアップロード</button>
</form>
<p><a href="/日別グラフ">日別グラフ</a></p>
<p><a href="/期間指定グラフ">期間指定グラフ</a></p>
</body>
</html>
```
* **日別グラフ.tpl:**
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>日別グラフ</title>
<link rel="stylesheet" href="static/style.css">
</head>
<body>
<h1>日別グラフ</h1>
<form action="/日別グラフ表示" method="get">
日付:
<select name="日付">
% for 日付 in 日付リスト:
<option value="{{日付}}">{{日付}}</option>
% end
</select>
<button type="submit">表示</button>
</form>
<p><a href="/">トップページに戻る</a></p>
</body>
</html>
```
* **期間指定グラフ.tpl:**
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>期間指定グラフ</title>
<link rel="stylesheet" href="static/style.css">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$(function() {
$("#開始日").datepicker({ dateFormat: 'yy-mm-dd' });
$("#終了日").datepicker({ dateFormat: 'yy-mm-dd' });
});
</script>
</head>
<body>
<h1>期間指定グラフ</h1>
<form action="/期間指定グラフ表示" method="get">
開始日: <input type="text" id="開始日" name="開始日">
終了日: <input type="text" id="終了日" name="終了日">
<button type="submit">表示</button>
</form>
<p><a href="/">トップページに戻る</a></p>
</body>
</html>
```
* **static/style.css:** (必要に応じて)
```css
body {
font-family: sans-serif;
}
```
**解説:**
1. **ライブラリのインポート:** 必要なライブラリをインポートします。
2. **データベース初期化:** `データベース初期化()` 関数は、データベースファイルが存在しない場合に、`血圧` テーブルを作成します。
3. **ファイルアップロード:**
* `ファイルアップロード()` 関数は、`tkinter` の `filedialog` を使用してファイル選択ダイアログを表示し、選択されたファイルのパスを返します。
* `データベースにCSVデータを追加()` 関数は、指定された CSV ファイルからデータを読み込み、データベースの `血圧` テーブルに追加します。 エラーハンドリングを追加しています。
4. **ルーティング:**
* `@app.route('/')`: トップページを表示します。
* `@app.route('/upload', method='POST')`: ファイルアップロード処理を行います。
* `@app.route('/日別グラフ')`: 日付選択グラフのページを表示します。
* `@app.route('/日別グラフ表示')`: 選択された日付のグラフを表示します。
* `@app.route('/期間指定グラフ')`: 期間指定グラフのページを表示します。
* `@app.route('/期間指定グラフ表示')`: 指定された期間のグラフを表示します。
5. **テンプレート:** Bottle のテンプレートエンジンを使用して HTML を生成します。 `templates/` フォルダにテンプレートファイルを配置します。
6. **グラフ作成:**
* `matplotlib` を使用してグラフを作成します。
* グラフは一時的なファイルに保存され、HTML テンプレートで表示されます。
7. **実行:** `run(app, host='localhost', port=8080, debug=True, reloader=True)` で Bottle アプリケーションを開始します。 `debug=True` と `reloader=True` は開発時に便利です。
**使い方:**
1. 上記のコードを `my_app.py` という名前で保存します。
2. `templates/` フォルダを作成し、上記のテンプレートファイルを保存します。
3. `static/` フォルダを作成し、CSS ファイル (必要に応じて) を保存します。
4. コマンドラインで `python my_app.py` を実行します。
5. ブラウザで `http://localhost:8080` にアクセスします。
**改善点:**
* **エラーハンドリング:** より詳細なエラーハンドリングを追加することを検討してください (例えば、不正なデータ形式の CSV ファイルを処理するなど)。
* **データの検証:** CSV ファイルから読み込んだデータの検証を行うことを推奨します (例えば、血圧の値が妥当な範囲内にあるかを確認するなど)。
* **UI の改善:** CSS を使用して UI を改善し、より使いやすくすることができます。
* **JavaScript:** JavaScript を使用して、クライアントサイドで日付ピッカーなどを実装し、よりリッチな UI を提供できます。例として、期間指定グラフのテンプレートに jQuery UI の Datepicker を組み込んでいます。
* **グラフライブラリ:** matplotlib だけでなく、Plotly, Bokeh など、インタラクティブなグラフを作成できるライブラリも検討してください。
* **データベース:** 大量のデータを扱う場合は、SQLite よりも PostgreSQL などのより強力なデータベースを使用することを検討してください。
* **日付スピンドル:** 日付スピンドル(数値の増減ボタンで日付を操作する UI)は、HTML の標準機能にはないため、JavaScript ライブラリを使用するか、自作する必要があります。 jQuery UI の Spinner などが利用できます。
このコードはあくまで出発点です。必要に応じて、機能を追加したり、コードを改善したりしてください。
@app.route・・・はデコレータですか?
どういう仕組みで、何をどのようにデコレーションしていますか。
また、デコレータを使わずにコードを書くことは可能ですか?
はい、`@app.route` は Python のデコレータです。 Bottle フレームワークにおけるルーティングの定義に用いられます。
**デコレータの仕組み:**
デコレータは、関数 (またはクラス) を引数として受け取り、その関数を別の関数でラップして、元の関数の機能を拡張したり、変更したりするものです。 デコレータは、`@` 記号を使って関数の定義の上に記述します。
**`@app.route` のデコレーション:**
`@app.route` デコレータは、Bottle フレームワークの `Bottle` クラスの `route` メソッドを使って、関数をデコレートしています。
1. **`app.route(パス, メソッド)`**: `route` メソッドは、URL パス (例: "/") と HTTP メソッド (例: "GET", "POST") を引数として受け取ります。
2. **デコレーション:** デコレータが関数 (例: `index()`) に適用されると、`route` メソッドはその関数を Bottle アプリケーションのルーティングテーブルに登録します。 ルーティングテーブルは、URL パスとそれに対応する関数を結びつけるためのものです。
3. **呼び出し:** ユーザーが特定の URL にアクセスすると、Bottle フレームワークはルーティングテーブルを検索し、その URL に対応する関数を呼び出します。
**例:**
```python
@app.route('/')
def index():
return "Hello, World!"
```
この例では、`@app.route('/')` デコレータが `index()` 関数に適用されています。 これにより、`index()` 関数は、URL パス "/" に関連付けられます。 ユーザーがブラウザで `http://localhost:8080/` にアクセスすると、Bottle フレームワークは `index()` 関数を呼び出し、その戻り値 ("Hello, World!") をブラウザに表示します。
**デコレータを使わずにコードを書く:**
デコレータを使わずに、同じ機能を実装することは可能です。 ただし、コードが少し冗長になります。
デコレータを使わない場合、以下のように書くことができます。
```python
def index():
return "Hello, World!"
app.route('/', method='GET', callback=index) # 明示的に関数を登録
```
この例では、`app.route()` メソッドを直接呼び出し、URL パス、HTTP メソッド、および関数 (`index`) を引数として渡しています。
**デコレータの利点:**
* **可読性:** デコレータを使用すると、コードがより簡潔で読みやすくなります。
* **再利用性:** デコレータは複数の関数に適用できるため、コードの再利用性が向上します。
* **保守性:** デコレータを使用すると、ルーティングの定義が一箇所に集中するため、コードの保守が容易になります。
Bottle フレームワークでは、デコレータを使用することで、ルーティングを簡単に定義できるため、非常に便利です。 デコレータを使用することで、コードの可読性、再利用性、および保守性を向上させることができます。
コメント