ソースコードで学ぶWebプログラミング

[Python]簡易メモ帳の作り方

Webプログラミングではデータの入力処理、データの変換処理、データの出力処理が基本となります。またデータをエスケープするポイントや扱い方もパターン化しているため、効率良く学べば直ぐに習得することも可能です。このページでは、Python言語によるCGIプログラミングの学習として、簡易メモ帳の作り方を説明します。

Pythonで簡易メモ帳のCGIを作成する

まず初めに、簡易メモ帳に必要となる機能をピックアップします。続いて、ピックアップした機能の詳細を決めていきます。システム開発では、制作する物の仕様を決定する工程を要件定義と言います。プログラミング学習ではあまり重視されませんが、実務では必須となります。

1.データ「ID、テキスト入力、日時」を受け取る ・データ「テキスト入力」は、複数行テキスト ・データ「日付」は、「2018/01/01 00:00」の書式(YYYY/mm/dd HH:ii) ・データ「ID」は、メモ毎に異なる英数字 2.データ「ID、テキスト入力、日時」を保存する ・保存は、テキストファイルとして保存 3.保存したデータ「ID、テキスト入力、日時」を表示する ・表示は、日時の新しい順に表示する 4.指定したデータを削除する ・削除は、チェックボックスでチェックしたものを削除

今回は、上の構成で作成することにします。

簡易メモ帳の入力データをPythonで受け取る方法

処理するデータは「ID、テキスト入力、日時」です。この内、テキスト入力は、ユーザーから受け取る必要があります。まずCGIプログラムの処理として、ブラウザからの入力を受け取れるようにします。ブラウザからの入力はエンコードされています。デコード用のモジュール cgi_decodeを使うので、こちらからダウンロードして下さい。

また受け取るテキスト入力のnameを決めておきます。このnameは、受け取り時のハッシュのキーになり、また表示タグのname属性になります。今回は memo にします。また入力フォームからはPOST送信で受け取ることにします。

#!/usr/bin/python3 import os import cgi_decode cgi_decode.Set() # GET,POST,COOKIE,FILES が使えるように # POST送信の時の処理 if os.environ.get('REQUEST_METHOD')=='POST': # memoをキーとして取得できる memo = POST.get('memo','')

またID、日時も受け取る処理を行います。Pythonでは、ユニークなIDを作成する関数は標準ではないため、他と重複しない一意の文字列を作成します。今回は、UnixタイムスタンプとプロセスIDで作成することにします。タイムスタンプは、timeモジュール、日時は、datetimeモジュールで取得できます。

ここでIDの変数名について注意点があります。Pythonには、関数id()が存在しますが、変数名でidを使用した場合、この関数が使えなくなります。エラーなどは出ないため、変数名や関数名の付け方には注意が必要です。

#!/usr/bin/python3 import os import time from datetime import datetime import cgi_decode cgi_decode.Set() # GET,POST,COOKIE,FILES が使えるように # UnixタイムスタンプとプロセスIDで作成 13文字 def uniqid(): return '{:010d}{:03d}'.format(int(time.time()), int(str(os.getpid())[-3:])) # 現在日時を取得 def gDate(): return datetime.now().strftime('%Y/%m/%d %H:%M') # POST送信の時の処理 if os.environ.get('REQUEST_METHOD')=='POST': # memoをキーとして取得できる memo = POST.get('memo','') # IDを作成する mid = uniqid() # 現在日時を作成 date = gDate()

Unixタイムスタンプは、time.time()で取得できますが、小数点以下も含まれるためint()で整数化しています。またプロセスIDをos.getpid()で取得し、文字列にしてからスライスで末尾の3文字を取得、再度整数にしています。

簡易メモ帳のデータをPythonで変換する方法

続いて、保存できるようにデータを整形したり、ブラウザへ表示できるようにデータを整形する処理を実装します。保存時のデータ構成は、取り出しやすいようにする事が基本となります。今回は、通常のテキストファイルで保存し、区切り文字としてタブを使うことにします。

テキストファイルの処理では、改行毎に処理が行われるため、改行は1レコードに1つにする必要があります。今回、複数行を入力として受け取るため入力データ内の改行は、保存用に別のデータに変換する必要があります。またタブも別のデータに変換します。それぞれ\\nと\\tのエスケープ表現に変換することにします。\が2つの場合は、その文字列自体として処理されます。

# 改行コードを\nに統一する memo = memo.replace("\r\n","\n") memo = memo.replace("\r","\n") # 改行コードを別の記号に変換する memo = memo.replace("\n","\\n") # タブを別の記号に変換する memo = memo.replace("\t","\\t") # 保存するレコードとなる文字列 line = mid +"\t"+ memo +"\t"+ date +"\n";

ブラウザへの表示では、HTMLとして表示させるため、入力データをエスケープする必要があります。また保存時のエスケープ表現から復元する処理も必要です。

# HTMLエスケープする関数 def h(s): s = s.replace('&','&amp;') s = s.replace('<','&lt;') s = s.replace('>','&gt;') s = s.replace('"','&quot;') s = s.replace("'",'&#39;') return s # 改行を改行タグとして復元する memo = memo.replace("\\n","<br>") # タブを復元する memo = memo.replace("\\t","\t")

簡易メモ帳のデータをPythonで出力する方法

具体的に保存を行ったり、ブラウザに表示を行なう処理を実装します。実装の流れとして、変換と出力を分けているのは、フレームワークなどを利用した場合の開発と同じ流れにするためです。オブジェクト指向と関連しますが、フレームワークではデータ保存、保存データの取得はモデルと呼ばれるオブジェクトで行います。保存データの作成は、一般的にコントローラと呼ばれるオブジェクトに記述します。

# モジュールインポート import codecs # メモデータを取得する def file_get_memo(fn): a = [] with codecs.open(fn,'r','utf-8') as f: for li in f: (mid,memo,date) = li.split("\t") a.append({ "id":mid, "memo":memo, "date":date }) return a

ファイルから保存したデータを取得しています。今回は、python2、python3の両方の環境で動作するように、codecsモジュールを利用して、utf-8で読み込んでいます。

タブ区切りで保存したので、li.split("\t")で配列(リスト)にしています。Pythonでは、() = で配列を項目ごとに受け取れます。また表示用の配列に連想配列(辞書)で追加しています。

# バージョン確認用に import sys # ファイル名を定数に FILE_NAME = 'memo.txt' # 表示データを取得 DATA = file_get_memo(FILE_NAME) # 表示させるメモ memos = '' # データがある時 if len(DATA): memos = '''<form method="post"> <table> ''' # 配列で順番に処理 for li in DATA: # 表示用の変換 memo = h(li['memo']) memo = memo.replace("\\n","<br>") memo = memo.replace("\\t","\t") # python2の時 if sys.version_info[0]==2: memo = memo.encode('utf-8') memos += '''<tr> <td>{}</td> <td>{}</td> <td><label><input type="checkbox" name="id" value="{}">削除</label></td> </tr> '''.format(h(li['date']),memo,li['id']) memos += '''</table> <input type="submit" value="メモを削除"> </form>'''

表示用の文字列を変数memosに代入するかたちで実装しています。Pythonでは、複数行の文字列をヒアドキュメントと呼ばれる方法で埋め込むことが出来ます。'''から始まって'''で終わります。

# ヘッダー情報、HTMLを指定 print("Content-Type: text/html; charset=utf-8\n") print('<title>[Python]簡易メモ帳</title>') print(memos) html = ''' <form method="post"> <textarea name="memo"></textarea> <input type="submit" value="記録"> </form> <style> table { width: 100%; } form { margin: 50px auto; width: 80%; } input[type='submit'] { display: block; margin: 20px auto; padding: 5px; width: 50%; } textarea { height: 100px; width: 100%; } td { border-bottom: 1px solid #333; } tr:first-child td { border-top: 1px solid #333; } .c2 { width: 160px; } .c3 { width: 70px; } </style> ''' print(html)

簡易メモ帳のデータをPythonで削除する方法

最後に、指定したデータを削除する処理を実装します。チェックボックスからの入力は複数チェックされていた場合、cgi_decodeでは値が配列になっています。

ファイルの変更や削除を行なう場合は、読み書きモードで開いてデータを取得した後、再度ファイルの先頭に移動してからデータを書き込みます。また変更後にファイルサイズが小さくなっている場合もあるため、truncateでファイルサイズを調節します。

# メモデータを保存する def file_put_memo(h): import fcntl # ファイル名の指定がない時 if not 'f' in h: return 0 # ファイルが存在しない場合、作成 if not os.path.isfile(h['f']): with open(h['f'],'w') as f: f.write('') # 保存するデータ lines = h['line'] if 'line' in h else '' with codecs.open(h['f'],'r+','utf-8') as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) for li in f: # 削除指定がある時 if 'del' in h: (mid,memo,date) = li.split("\t") # 削除指定の配列にIDが含まれない時、保存 if not mid in h['del']: lines += li # 削除指定がない時 else: lines += li f.seek(0) f.write(lines) f.flush() f.truncate()

あとはPOST送信を受け取った際に、作成した関数にデータが渡るようにします。またPOST送信で作成された画面は、再読み込みを行なうとPOSTの再送信が行われてしまいます。対策として、GETアクセスの状態にするためにリダイレクトします。

# POST送信の時の処理 if os.environ.get('REQUEST_METHOD')=='POST': h = {'f':FILE_NAME} if 'id' in POST: h['del'] = POST.get('id','') else: # memoをキーとして取得できる memo = POST.get('memo','') # IDを作成する mid = uniqid() # 現在日時を作成 date = gDate() # 改行の統一とエスケープ memo = memo.replace("\r\n","\n") memo = memo.replace("\r","\n") memo = memo.replace("\n","\\n") memo = memo.replace("\t","\\t") h['line'] = '' if memo=='' else mid +"\t"+ memo +"\t"+ date +"\n" file_put_memo(h) print("Status: 301 Moved Permanently") print('Location: '+os.environ.get('SCRIPT_NAME')+"\n") sys.exit()