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

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

Python勉強企画、8日目もCGIです。 前回の名簿管理に機能を追加します。データベースへデータを新規追加(挿入)するやり方と、入力内容のチェックのための正規表現の使い方を試してみます。

生徒情報の新規追加機能

新規追加機能の仕様

最初にしっかり設計します。

画面設計

新規追加機能の画面は、このようなフォームにします。

画面上の各要素については以下の通りです。

要素名種類内容・動作
学籍番号1行テキスト入力studentlist テーブルの id カラムに対応する
専攻1行テキスト入力studentlist テーブルの course カラムに対応する
学年1行テキスト入力studentlist テーブルの grade カラムに対応する
氏名1行テキスト入力studentlist テーブルの name カラムに対応する
生年月日1行テキスト入力studentlist テーブルの birthday カラムに対応
登録ボタンクリックすると、後述の入力値の値チェックを行う
エラーがない場合:
入力データをstudentlistテーブルに挿入して一覧画面に遷移する
エラーがある場合:
エラーを表示し、フォームを再表示する
戻るリンクデータベースへの登録は行わずに一覧画面に遷移

登録データの書式

前回の『全体の仕様』で決定した登録データの各項目の書式のチェックを行います。

各項目の書式は以下の通りです(再掲)

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

書式に合わない入力があった場合、下の画面のようにエラーを表示し、入力済のデータを保存した状態でフォームを再表示します。

書式以外の条件

学籍番号は主キーなので、既存データと重複してはいけません。

エラー時の処理

重複があった場合、下の画面のようにエラーを表示し、入力済のデータを保存した状態でフォームを再表示します。

全ソースコード

いつもどおり、最初に全ソースコードを見てみましょう。

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

import io
import sys
import cgi
import mysql.connector
import re

form = cgi.FieldStorage()
error=[]

con = mysql.connector.connect(user="root", password="", host="localhost", db="webdb", charset="utf8")
cur = con.cursor(dictionary=True)

if "ac" in form and form["ac"].value=="insert":
    id       = form["id"].value       if "id" in form       else ""
    course   = form["course"].value   if "course" in form   else ""
    grade    = form["grade"].value    if "grade" in form    else ""
    name     = form["name"].value     if "name" in form     else ""
    birthday = form["birthday"].value if "birthday" in form else ""
    
    if not re.match("^[0-9]{5}$", id):
        error.append("学籍番号が異常です")
    if not re.match("^.{1,10}$", course):
        error.append("専攻は1~10文字です")
    if not re.match("^[123]$", grade):
        error.append("学年は1~3の半角数字です")
    if not re.match("^.{1,20}$", name):
        error.append("氏名は1~20文字です")
    if not re.match("^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$", birthday):
        error.append("誕生日は0000-00-00の形式です")

    if len(error)==0:
        try:
            sql = "INSERT studentlist(id,course,grade,name,birthday) VALUES(%s,%s,%s,%s,%s)" 
            cur.execute(sql, (id, course, grade, name, birthday))
            con.commit()
            cur.close()
            con.close()
            print("Location: studentlist.py\n")
            exit()

        except mysql.connector.errors.IntegrityError:
            error.append("学籍番号が重複しています")

else:
    id = course = grade = name = birthday = ""

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

if len(error) != 0:
    print("<ul style='color:red'>")
    for mes in error:
        print("<li>%s</li>" % mes)
    print("</ul>")

print("""<form action="" method="post">
<input type="hidden" name="ac" value="insert">
<table>
    <tr><th>学籍番号</th><td><input type="text" name="id" value="%(id)s"></td></tr>
    <tr><th>専攻</th><td><input type="text" name="course" value="%(course)s"></td></tr>
    <tr><th>学年</th><td><input type="text" name="grade" value="%(grade)s"></td></tr>
    <tr><th>氏名</th><td><input type="text" name="name" value="%(name)s"></td></tr>
    <tr><th>生年月日</th><td><input type="text" name="birthday" value="%(birthday)s"></td></tr>
</table>
<button type="submit">追加</button>
</form>
<hr>
<a href="studentlist.py">追加せずに一覧に戻る</a>
</body>
</html>
"""  % {"id":id, "course":course, "grade":grade, "name":name, "birthday":birthday})

コード解説

冒頭部分

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

import io
import sys
import cgi
import mysql.connector
import re

文字化け対策の io モジュールと sys モジュール、CGI処理のための cgi モジュール、データベース利用のための mysqlモジュールのconnector、そして今回初めて使う正規表現処理のための reモジュールをimportしています。

前処理

form = cgi.FieldStorage()
error=[]

con = mysql.connector.connect(user="root", password="", host="localhost", db="webdb", charset="utf8")
cur = con.cursor(dictionary=True)

まず、フォームから送信されたデータがあった場合、取得して変数formに代入します。

変数error は検出したエラー(書式と違う入力データ)を代入する変数です。空のリストとして初期化します。

mysql.connector.connect() メソッドでデータベースに接続し、con.cursor() メソッドでカーソルを所得します(詳しいことは前回を参照して下さい)。

入力データの書式チェック

if "ac" in form and form["ac"].value=="insert":
    id       = form["id"].value       if "id" in form       else ""
    course   = form["course"].value   if "course" in form   else ""
    grade    = form["grade"].value    if "grade" in form    else ""
    name     = form["name"].value     if "name" in form     else ""
    birthday = form["birthday"].value if "birthday" in form else ""
    
    if not re.match("^[0-9]{5}$", id):
        error.append("学籍番号が異常です")
    if not re.match("^.{1,10}$", course):
        error.append("専攻は1~10文字です")
    if not re.match("^[123]$", grade):
        error.append("学年は1~3の半角数字です")
    if not re.match("^.{1,20}$", name):
        error.append("氏名は1~20文字です")
    if not re.match("^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$", birthday):
        error.append("誕生日は0000-00-00の形式です")

フォームでsubmitした場合、form[“ac”]に”insert”という値が代入されています。その場合は入力データの書式をチェックします。

三項演算子

変数formから、既に各入力項目の値をいったんcourse、grade、name、birthdayに代入します。
cgi.FieldStorage() で取得したデータは、入力欄が空欄だった場合はエントリ自体がないという欠点があるためです。たとえば氏名欄が空欄だった場合、form[“name”].value==”” ではなくform[“name”]自体が存在しないため、後の処理でエラーが発生してしまう可能性があるのです。

ここで利用するのが三項演算子です。三項演算子の書式は以下の通りです。

値1 if 条件 else 値2

この式は、条件が成立した場合は値1を、成立しなかった場合は値2を返します。

id =  form["id"].value  if "id" in form   else  ""

この例では、

変数formにキー”id”が存在する場合は form[“id”].value の値
変数formにキー”id”が存在しない場合は空文字列

を、変数 id に代入します。

正規表現による書式チェック

正規表現による書式のチェックには、re.match() メソッドやre.search()メソッドを使用します。書式は以下の通りです。

re.match(正規表現, 文字列)

re.search(正規表現, 文字列)

re.match()メソッドでは文字列の先頭の正規表現にマッチしている部分
re.search()メソッドでは文字列中の正規表現にマッチする部分
を探します。マッチしている部分がある場合は対応するマッチオブジェクトを、マッチしている部分がない場合は None を返します。

このプログラムで使用している正規表現は以下の通りです。

文字意味
^文字列の先頭。
$文字列の末尾
.任意の1文字
[123]1か2か3のいずれかの文字
[0-9]0~9のいずれかの文字
{n}直前の文字またはパターンがn回つづく
{m,n}直前の文字またはパターンがm回以上n回以下つづく
re.match("^[0-9]{5}$", id)

例えばこの例では、変数id の内容が5桁の数字文字列だとマッチします。

※なお、re.match()メソッドは『文字列の先頭』のみ判定するため、実は正規表現に『^』がなくても動作します。他言語の正規表現メソッドはだいたいre.search()と同様の動作なので思わずつけてしまいました…。

※Python 3.4以降では、『文字列全体が正規表現とマッチするか』を判定する re.fullmatch() メソッドも利用できます。その場合は『^』や『$』は不要です。

if not re.match("^[0-9]{5}$", id):
    error.append("学籍番号が異常です")

re.match()メソッドは True/False を返すメソッドではないのですが、
Pythonでは

0・None・空文字列・空のリストetc は False
上記以外(0や空ではない値)は True

と読み替える仕様なので、『マッチした部分があれば True、なければFalse』として条件判断に利用できます。

not は条件の否定です(他言語のような!演算子は使えません)。
よってこのコードでは、正規表現にマッチしなかった場合に変数errorにエラーメッセージを追加します。

データの挿入

    if len(error)==0:
        try:
            sql = "INSERT studentlist(id,course,grade,name,birthday) VALUES(%s,%s,%s,%s,%s)" 
            cur.execute(sql, (id, course, grade, name, birthday))
            con.commit()
            cur.close()
            con.close()
            print("Location: studentlist.py\n")
            exit()

        except MySQLdb._exceptions.IntegrityError:
            error.append("学籍番号が重複しています")

書式のチェックでエラーがなかった場合は、データベースへの登録処理を行います。

    if len(error)==0:

書式のチェックでエラーがあったかどうか、変数errorの要素数で判定しています。

テーブルへのレコード挿入
sql = "INSERT studentlist(id,course,grade,name,birthday) VALUES(%s,%s,%s,%s,%s)" 
cur.execute(sql, (id, course, grade, name, birthday))
con.commit()
cur.close()
con.close()

レコードを挿入、修正、削除などテーブルの内容に変更を加える処理の手順は、

  1. データベースに接続し、”データベース接続オブジェクト”を取得する
  2. “カーソル”を取得する
  3. SQLを実行する
  4. コミット(変更を確定)する

となります。1~3までは読み出し(SELECT)の場合と同じですが、4のコミットを行わないとデータベースに変更が反映されません。

SQLの実行

SELECTの場合と同様、SQL(INSERT INTO文)を cur.execute() メソッドで実行していますが、SQL内にフォームから投稿された値を含めるため、execute()メソッドのプレースホルダー機能を使用しています。

プレースホルダー機能は%演算子を利用した書式文字列とよく似ています。execute()メソッドでプレースホルダーを使用する場合は、

cur.execute( “SQL文… %s … %s …”, (値1, 値2 …) )

とします。%演算子ではなく、第1引数に書式文字列を記述し、第2引数にはあてはめたい値をタプルとして列挙します。%s の部分が第2引数のタプル内の値1,値2…と置き換わります。

%演算子を利用する書式文字列と同様、書式文字列中でキーを記述し、第2引数をタプルではなくdict型で記述することも可能です。

そして execute()メソッドの場合、値1,値2…の中に SQLインジェクションに使われる危険のある文字(シングルクォート『 ‘ 』等)が含まれる場合は自動的にエスケープしてくれます

また、このプログラムでの使用例を見れば判るとおり、 『%s』の部分は値を当てはめるときに自動的にシングルクォートで囲まれるようで、書式文字列中には『 ‘ %s ‘ 』とは書きません。(実はこれで10分悩んだ)

データベースのコミット

コミット(データ変更の確定)には、データベース接続オブジェクトのcommit()メソッドを使用します。書式は、

データベース接続オブジェクト.commit()

です。他言語では明示的にコミットする必要がないものも多いですが、Python(+mysql.connector)ではコミットが必須なので注意して下さい。PHPなどに慣れていると忘れます。(実はこれで10分悩んだ)

※もしかしたら自動コミットするライブラリもあるのかもしれませんが…

データベースの切断

挿入が成功した場合はそれ以上のデータベースの処理は不要のため、すぐにカーソルとデータベース接続をclose()しています。

画面の遷移とプログラムの終了

このプログラムでは、処理結果画面は用意せず、挿入が成功した場合はすぐに http の Location ヘッダを出力して生徒一覧画面に遷移します。ヘッダ出力後は exit() メソッドでプログラムを終了します。

例外処理
try:
    (INSERT INTO文の実行)

except mysql.connector.errors.IntegrityError:
    error.append("学籍番号が重複しています")

SQL(INSERT INTO文)実行時、idが重複していた等の理由で実行に失敗した場合は mysql.connector.errors.IntegrityError 例外が発生します。try文によってそれをキャッチし、except句で変数errorに”学籍番号が重複しています”というエラーメッセージを追加します。

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

io.TextIOWrapper()メソッドで出力文字コードをutf-8に設定し、httpヘッダとhtmlの前半を出力しています。

エラーの表示

if len(error) != 0:
    print("<ul style='color:red'>")
    for mes in error:
        print("<li>%s</li>" % mes)
    print("</ul>")

書式チェックなどでエラーがあった場合(変数errorが空でなかった場合)、エラー表示をします。

変数errorはエラーメッセージ文字列を要素とするリストなので、for文でエラーメッセージ文字列の個数分だけループしながら箇条書き要素(li要素)として出力しています。

htmlの出力(後半)

print("""<form action="" method="post">
<input type="hidden" name="ac" value="insert">
<table>
    <tr><th>学籍番号</th><td><input type="text" name="id" value="%(id)s"></td></tr>
    <tr><th>専攻</th><td><input type="text" name="course" value="%(course)s"></td></tr>
    <tr><th>学年</th><td><input type="text" name="grade" value="%(grade)s"></td></tr>
    <tr><th>氏名</th><td><input type="text" name="name" value="%(name)s"></td></tr>
    <tr><th>生年月日</th><td><input type="text" name="birthday" value="%(birthday)s"></td></tr>
</table>
<button type="submit">追加</button>
</form>
<hr>
<a href="studentlist.py">追加せずに一覧に戻る</a>
</body>
</html>
"""  % {"id":id, "course":course, "grade":grade, "name":name, "birthday":birthday})

この部分ではhtmlの後半(フォーム部分以降)を出力しています。長いですがPythonのプログラムとしてはprint()関数1つだけの簡単なものです。

print()の中身は複数文字列+%演算子を利用した書式文字列になっています。最後の行にdict型のデータが記述されており、書式文字列中の %(id)s などの部分にdict型のキーが一致する値が挿入されます。詳しくは前回を参照して下さい。

動作確認

では、新しく、次の生徒のデータを登録してみましょう。

学籍番号:11002
専攻:普通科
学年:1
氏名:三船栞子
生年月日:2005-10-05

『追加』ボタンをクリックします。

すぐに一覧画面に遷移して、新しいデータが追加されていれば成功です。

・書式に合っていないデータ(空欄、長すぎる、数字が要求されているのに数字以外の文字、等)を入力して、エラーになることを確認しましょう

・学籍番号が既存のものと重複するデータを入力したら、エラーになることを確認しましょう

長くなってしまった

本当は更新・削除も一気に作るつもりだったのですが…というかプログラムは作ってあるのですが、解説が長くなりすぎたので残りは次回に。

コメント

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