短期集中:PythonでWeb名簿管理

Python
この記事は約21分で読めます。

Python勉強企画、7日目もCGIです。 名簿管理をネタに、データベース処理をためしてみます。

仕様を決定しよう

全体の仕様

とある学校の生徒名簿という体のプログラムです。

機能

機能は、

・生徒情報の一覧表示
・生徒情報の新規追加
・生徒情報の編集
・生徒情報の削除

という、データベースアプリケーションに求められる機能をひととおり付けることにします。

各機能は別のページ(スクリプト)で実装することにします。

管理する情報

管理する生徒情報は以下のようにします。

項目データの仕様データ例
学籍番号生徒を一意に判別できる。5桁の半角数字文字列“01001”
専攻1~10文字の文字列。日本語可。“普通科”
学年1~3の整数値2
氏名1~20文字の文字列。日本語可。“山田太郎”
生年月日日付2005-07-02

生徒情報の一覧表示

今日はまず、生徒情報の一覧表示ページを作ります。

機能はシンプルに、

・全生徒の情報が表形式で表示される
・各生徒毎に『編集』『削除』ボタンがあり、クリックすると生徒情報の編集機能、生徒情報の削除機能の画面へ遷移する
・新規追加機能の画面へは一覧画面からリンクする

とします。

画面のイメージはこんな感じです。CSSなど一切使っていないのでシンプルというより殺風景ですが…。

まずは、準備

ライブラリのインストール

今回は、xampp環境でインストールされるデータベース MySQL(XAMPPの最近のバージョンでは MariaDB に変更されていますが、MySQL互換なので同じ設定で動きます)を利用します。PythonからMySQLを利用するためには外部ライブラリが必要です。MySQL用の外部ライブラリは何種類かあるのですが、今回は mysql-connector-python を使うことにします。MySQLの公式ライブラリのようです。さっさとインストールしてしまいましょう。

C:\> py -m pip install mysql-connector-python
Collecting mysql-connector-python
  Downloading mysql_connector_python-8.0.26-cp39-cp39-win_amd64.whl (799 kB)
     |████████████████████████████████| 799 kB 3.3 MB/s
Collecting protobuf>=3.0.0
  Downloading protobuf-3.17.3-cp39-cp39-win_amd64.whl (909 kB)
     |████████████████████████████████| 909 kB ...
Requirement already satisfied: six>=1.9 in d:\programs\python\lib\site-packages (from protobuf>=3.0.0->mysql-connector-python) (1.16.0)
Installing collected packages: protobuf, mysql-connector-python
Successfully installed mysql-connector-python-8.0.26 protobuf-3.17.3

C:\>

データベースの準備

データベースの作成

MySQLで、以下のデータベースを作成します。

データベース名:webdb
照合順序:utf8_general_ci

具体的な方法についてはここでは触れませんが、XAMPP環境ならphpmyadminを使えば簡単です。

テーブルの作成

『管理する情報』の仕様にしたがって、以下のようにテーブルを作成します。

テーブル名:studentlist

カラム名データ型照合順序意味備考
idvarchar(5)utf8_general_ci学籍番号主キー
coursevarchar(10)utf8_general_ci専攻
gradeint学年
namevarchar(20)utf8_general_ci氏名
birthdaydate生年月日

初期値として、適当なデータを入れておきます。

テーブル作成&サンプルデータ用のSQLを置いておきます。
サンプルデータには突っ込まないで下さい。いろいろな意味で(苦笑)

CREATE TABLE meibo(
    id        VARCHAR(5),
    course    VARCHAR(10),
    grade     INT,
    name      VARCHAR(20),
    birthday  DATE,
    PRIMARY KEY(id)
);
INSERT INTO studentlist(id,course,grade,name,birthday)
VALUES('01001','普通科',          2,'上原歩夢',    '2005-03-01'),
('11001','普通科',          1,'中須かすみ',  '2006-01-23'),
('14001','国際交流科',      1,'桜坂しずく',  '2005-04-03'),
('95001','ライフデザイン科',3,'朝香かりん',  '2003-06-29'),
('03001','情報処理科',      2,'宮下愛',      '2004-05-30'),
('95002','ライフデザイン科',3,'近江彼方',    '2003-12-16'),
('01003','普通科',          2,'優木せつ菜',  '2004-08-08'),
('94001','国際交流科',      3,'エマヴェルデ','2004-02-05'),
('13001','情報処理科',      1,'天王寺璃奈',  '2005-11-13'),
('01002','普通科',          2,'高咲侑',      '2005-03-05'),
('12001','音楽科',          1,'嵐千砂都',    '2006-02-25');

全ソースコード

#!C:/Programs/Python/python.exe

import io
import sys
import mysql.connector

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
print("Content-Type: text/html; charset=utf-8")
print()
print("""<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>生徒一覧</title>
</head>
<body>
    <h1>生徒一覧</h1>
    """)

con = mysql.connector.connect(host="localhost", port=3306, user="root", password="", db="webdb", charset="utf8")
cur = con.cursor(dictionary=True)
cur.execute("SELECT * FROM studentlist")
rows = cur.fetchall()
    print("""
    <tr><td>%(id)s</td><td>%(course)s</td><td>%(grade)s</td><td>%(name)s</td><td>%(birthday)s</td>
    <form action="studentedit.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">編集</button></td>
    </form>
    <form action="studentdelete.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">削除</button></td>
    </form>
    </tr>""" % row)
cur.close()
con.close()

print("""<hr>
<a href="studentinsert.py">生徒の新規追加</a>
</body>
</html>""")

コード解説

冒頭部分

#!C:/Programs/Python/python.exe

import io
import sys
import mysql.connector

例によって1行目はpython実行ファイルのパスを記述しています。

importするモジュールは、前回と同じく文字化け対策の ioモジュールと sysモジュール、そしてデータベースMySQLを使用するための mysqlモジュールです。

HTML出力(前半)

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
print("Content-Type: text/html; charset=utf-8")
print()
print("""<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>生徒一覧</title>
</head>
<body>
    <h1>生徒一覧</h1>
    """)

最初の行は出力文字コードを utf-8 にするための処理です。

それ以降はすべてHTMLを出力するためのprint()関数(複数行文字列)です。

データベースアクセス

con = mysql.connector.connect(host="localhost", port=3306, user="root", password="", db="webdb", charset="utf8")
cur = con.cursor(dictionary=True)
cur.execute("SELECT * FROM studentlist")
rows = cur.fetchall()

この部分が今回製作したプログラムの中心、データベースアクセスのための処理です。

データベースにアクセスする基本的な手順は以下の通りです。

  1. データベースに接続し、”データベース接続オブジェクト”を取得する
  2. “カーソル”を取得する
  3. SQLを実行する
  4. (実行したSQLがSELECTの場合)カーソルからレコードを取得する
データベースに接続し、”データベース接続オブジェクト”を取得する

データベースに接続するには、mysql.connector.connect() メソッドを使用します。

書式は以下の通りです。

mysql.connector.connect( 接続情報 )

接続情報には、今回は以下のものを指定しています。

プロパティ名意味デフォルト値
hostデータベースのホスト名localhost
portデータベースの接続ポート3306
userデータベース接続ユーザ名カレントユーザー
passwordデータベース接続パスワードパスワードなし
dbデータベース名データベース選択なし
charset文字セットデフォルトの文字セット
con = MySQLdb.connect(host="localhost", port=3306, user="root", password="", db="webdb", charset="utf8")

XAMPP環境で実験する場合はだいたいこの設定で接続できると思います。

  • host=”localhost” は、このCGIスクリプトが実行されているWebサーバとデータベースサーバが同じであることを表します。
  • XAMPPでインストールされたMySQLでは、localhostからの接続の場合はこの例のように rootユーザがパスワードなしで接続できます。レンタルサーバの場合には管理ページなどから接続ユーザとパスワードを確認・設定して下さい。
  • charsetがhttpやPythonでの指定と違い、”utf-8″ ではなく “utf8″(ハイフンがない)であることに注意して下さい。

※host と port はデフォルト値のままなので省略しても動きそうです。

“カーソル”を取得する

カーソルを取得するには、データベース接続オブジェクトの cursor() メソッドを使用します。

書式は以下の通りです。

カーソル.cursor()

引数は必須ではありませんが、このプログラムでの例のように dictionary=True を指定すると、SELECT文で読み出したデータをfetchone()/fetchall() したときにカラム名で参照できるようになります。これを指定しないとインデックス番号でしか参照できません。

cur = con.cursor(dictionary=True)
SQLを実行する

SQLを実行するには、カーソルのメソッド execute() を使用します。書式は以下の通りです。

カーソル.execute( SQL文 )

SELECTで読みだしたレコードは、execute()メソッドの返却値ではなく、次のfetchone()/fetchall()メソッドで取得します。

カーソルからレコードを取得する

SELECTを実行した場合、JavaやPHPではSQLを実行するメソッドの返却値から結果のレコードを取り出すのですが、Pythonではカーソルから取り出します。

SELECTの結果からレコードを1件だけ取り出すには fetchone() メソッドを使用します。書式は以下の通りです。

カーソル.fetchone()

返却値は各カラムの値を格納した dict(辞書)型オブジェクトです。dict型とは添字(番号)の代わりに文字列をキーとして使用する配列のようなものです。PHP や JavaScript の連想配列とだいたい同じです。

例として、サンプルデータの id=’01001′ のレコードを取得すると、下記のようになります。

{‘id’: ‘01001’, ‘course’: ‘普通科’, ‘grade’: 2, ‘name’: ‘上原歩夢’, ‘birthday’: datetime.date(2005, 3, 1)}

『 キー : 値 』を列挙する、JSON型式とよく似た記述です。この形式でPythonのプログラム中に直接データを記述することもできますが、詳細は別の機会に。

SELECT文で取り出されたレコードをすべて読み出すには、fetchall() メソッドを使用します。書式はfetchone()と同じです。

カーソル.fetchall()

返却値は、レコードをdict型で表したものを複数まとめたタプル型のオブジェクトです。タプルとは変更のきかない配列のようなものです。例えばサンプルデータをすべて読み出し、fetchall() した場合の返却値は、

 (
{‘id’: ‘01001’, ‘course’: ‘普通科’, ‘grade’: 2, ‘name’: ‘上原歩夢’, ‘birthday’: datetime.date(2005, 3, 1)},
{‘id’: ‘01002’, ‘course’: ‘普通科’, ‘grade’: 2, ‘name’: ‘高咲侑’, ‘birthday’: datetime.date(2005, 3, 5)},

(略)

)

のようになります。

結果表示

print("<table>")
print("<tr><th>学籍番号</th><th>専攻</th><th>学年</th><th>生徒名</th><th>生年月日</th></tr>")
for row in rows:
    print("""
    <tr><td>%(id)s</td><td>%(course)s</td><td>%(grade)s</td><td>%(name)s</td><td>%(birthday)s</td>
    <form action="studentedit.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">編集</button></td>
    </form>
    <form action="studentdelete.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">削除</button></td>
    </form>
    </tr>""" % row)
print("</table>")

データベースから読みだした結果を表示しているのがこの部分です。

fetchall()で取得した複数のレコードを順に処理する

fetchall()の返却値はタプル型です。タプル型は『配列のようなもの』なので、for文を使って要素(この場合はレコードを表すdict型オブジェクト)を1つずつ取り出し、繰り返し処理を行うことができます。

for row in rows:

変数rowsに代入されたタプルの中から、dict型のレコードが1件ずつ変数rowに代入され、処理が繰り返されます。各カラムの値は row[カラム名] で参照できます。

表示する
    print("""
    <tr><td>%(id)s</td><td>%(course)s</td><td>%(grade)s</td><td>%(name)s</td><td>%(birthday)s</td>
    <form action="studentedit.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">編集</button></td>
    </form>
    <form action="studentdelete.py" method="post">
        <input type="hidden" name="id" value="%(id)s">
        <td><button type="submit">削除</button></td>
    </form>
    </tr>""" % row)

この部分でレコード1件の内容、html としては tr 要素一つ分を表示しています。長いですが1つのprint()関数です。
print() の引数の文字列から、Pythonとは無関係なただの文字列を省略してまとめると、

print(“””… %(id)s … %(course)s … %(grade)s … %(name)s … %(birthday)s …
… %(id)s … %(id)s …””” % row)

となっています。これはおみくじの回でも使用した、%演算子を利用した書式文字列(他言語のsprintfに相当する機能)を拡張したものです。

以前使用した記述法は、

“…%s…%s…” % (値1, 値2)

という型式で、書式文字列中の %s や %d の部分に、後述の値1、値2…が順に当てはめられるというものでした。当然、%sや%dの個数と値の個数は一致している必要がありました。

これを拡張したのが今回の記述法です。

“… %(キー1)s … %(キー2)s …” % { キー1 : 値1, キー2 : 値2 }

書式文字列の中では %s の代わりに %(キー)s と記述し、文字列中に挿入する値も『 ( 値1, 値2…) 』の型式(タプル型)ではなく『 { キー1:値1, キー2:値2 … } 』のdict型になっています。こうすると、記述した順番とは無関係に、書式文字列中の対応するキーの部分に値が挿入されます。

%s や %d の個数と値の個数が異なっていても問題はなく、書式文字列中で使用されているキーがすべて辞書型に含まれていれば正常に動作します。

たとえば、

“プログラミング言語 %(lang)s の使用人口は世界第 %(rank)d 位です”
% {“lang”:”Python”, “rank”:3}

という記述は、

プログラミング言語 Python の使用人口は世界第 3 位です

となります。

“プログラミング言語 %(lang)s の使用人口は世界第 %(rank)d 位です”
% {“rank”:3, “lang”:”Python”}

のようにdict型内での記述の順番を変えても、

“プログラミング言語 %(lang)s の使用人口は世界第 %(rank)d 位です”
% {“rank”:3, “lang”:”Python”, “country”:”Japan”}

のようにdict型内に書式文字列で使われていないキーがあっても、結果は変わりません。

ではこのプログラムでの記述をもう一度観てみましょう。

print(“””… %(id)s … %(course)s … %(grade)s … %(name)s … %(birthday)s …
… %(id)s … %(id)s …””” % row)

このプログラムの例では、値としてdict型の書式 {キー:値, … } を直接記述するのではなく、dict型の値が代入された変数rowを使用しています。また、書式文字列中にキー”id” が3回使われていますが、これはすべて同じ値(dict型内のキーidの値)に置き換わります。

データベースの切断

cur.close()
con.close()

データベースを利用する処理が終わったら、カーソル及びデータベースを切断します。切断にはカーソル及びデータベース接続オブジェクトの close() メソッドを使用します。

カーソル.close()
データベース接続オブジェクト.close()

記述しなくてもプログラム終了時に自動的に切断されるのですが、使い終わったら早めに切断する方がよいのではないかと思います。

HTMLの表示(後半)

print("""<hr>
<a href="studentinsert.py">生徒の新規追加</a>
</body>
</html>""")

後半というか、あと4行のHTMLを出力するだけですが。

今宵はここまでにしとうございまする

編集、削除、追加は次回。

コメント

タイトルとURLをコピーしました