Python CGI で 掲示板みたいなものを作る~sqlite3モジュール編~ [プログラム三昧]
Python CGI で 掲示板みたいなものを作る~Ajax編~では、Ajaxという仕組みを利用してページ遷移を起こさない掲示板システムを作成しました。 しかし、SQLiteとのインターフェイスは、相変わらず"system"関数によるコマンドを呼び出しで、標準入力を通じてSQLコマンドを送り込み、標準出力から結果を受け取る方式になっていました。 Python CGI で作るアクセスカウンタ~sqlite3モジュール編~でsqlite3モジュールが使えるようになったので、掲示板もsqlite3モジュールを使用するように変更してみました。
データベースの構成
今までの掲示板は、一つのデータベースを使いまわしてきましたが、今回は別のデータベースを作成しました。 その理由は、文字エンコーディングの扱いが異なってきたからです。
カラム名 | タイプ | 内容 |
---|---|---|
time | NUMBER | 記録時刻を表す数値です。 |
description | TEXT | メッセージの内容をユニコードで表現した文字列です。 |
このテーブルに "visitor" という名前をつけて、 "visitor-world9.sqlite" というレンタル・サーバ上のファイルに格納します。
今までのデータベースでは、メッセージを "url.quote" を通すことによって、 "ascii" すなわち7ビットのバイト列で表現していました。 今回は、 Python から直接データを入れることが出来るので、 Python の標準文字列エンコーディングである、ユニコードに変更したというわけです。 ところが、そのために、かなり苦労をすることになってしまいました。 その話は、後ほど。
データベース初期化CGI : visitor-world9-init.cgi
データベースの初期化も、"sqlite3"モジュールを使用しました。 要するに、"CREATE"文を使って"visitor"表を"cisitor-world9.swlite"データベースに作成しているだけです。 すでにデータベースファイルが存在したり、表が存在したりした場合は、エラーが発生しますが、最初の一回だけしか使用しないので、なんらエラー処理を行っていません。
#!/usr/local/bin/python # $Id: visitor-world9-init.cgi,v 1.1 2010/01/16 08:41:14 noritan Exp $ import sys import cgi import sqlite3 import cgitb # Parameters db_file = "visitor-world9.sqlite" # Enable debug output cgitb.enable() # Issue SQL con = sqlite3.connect(db_file) cur = con.cursor() cur.execute( "CREATE TABLE visitor (time NUMBER, description TEXT)" ) con.commit() cur.close() con.close() # Execute command print """Content-type: text/plain OK"""
このCGIは、最後に"OK"と返答します。 まあ、こんなものでいいでしょう。
アプリケーションページ HTML : visitor-world9.html
HTMLファイルは、以前作成した"Ajax"版から呼び出すべきCGIファイルを変更しただけです。 あ、タイトルも変更してますね。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <!-- $Id: visitor-world9.html,v 1.2 2010/01/16 09:05:20 noritan Exp $ --> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" > <head profile="http://www.w3.org/2005/10/profile"> <title>VISITOR WORLD 9</title> </head> <body> <h1>VISITOR WORLD 9</h1> <form action="#"> <div> <textarea name="message" id="message" rows="4" cols="40"></textarea> </div> <p> <input type="button" value="Submit" onclick="submit_message();" /> <input type="reset" value="Clear" /> </p> </form> <div id="table"> </div> <script src="xmlhttprequest.js" type="text/javascript"></script> <script src="visitor-world9.js" type="text/javascript"></script> <p> <a href="http://validator.w3.org/check?uri=referer"><img src="http://www.w3.org/Icons/valid-xhtml11" alt="Valid XHTML 1.1" height="31" width="88" /></a> </p> </body> </html>
メッセージの記録と表示に必要な処理は、すべて、”JavaScript"と"CGI"に入れてあるので、"HTML"ファイルには、ロジックは入っていません。
メッセージ記録および表示 JavaScript : visitor-world9.js
"HTML"ファイルから呼び出される"submit_message()"関数がこの中に記述されています。
// // BBS using Ajax technique // // $Id: visitor-world9.js,v 1.1 2010/01/16 08:41:14 noritan Exp $ // // Submit a message to the server // function submit_message() { var element = document.getElementById('message'); var query = 'message='+encodeURIComponent(element.value); xmlhttp = new XMLHttpRequest(); xmlhttp.open('POST', 'visitor-world9.cgi', true); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { update_table(xmlhttp.responseXML); } } xmlhttp.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded;charset=utf-8' ); xmlhttp.setRequestHeader("Content-Length", query.length); xmlhttp.send(query); } // // Get a table of messages. // function get_table() { xmlhttp = new XMLHttpRequest(); xmlhttp.open('GET', 'visitor-world9.cgi', true); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { update_table(xmlhttp.responseXML); } } xmlhttp.send(null); } // // Escape a string with entity references // function escapeHTML(str) { str = str.split("&").join("&"); str = str.split("<").join("<"); str = str.split(">").join(">"); str = str.split('"').join("""); str = str.split("{").join("{"); str = str.split("}").join("}"); str = str.split("'").join("'"); return str; } // // Update a table of message with a received XML // function update_table(doc) { var str = ""; var element = document.getElementById('table'); var topnode = doc.getElementsByTagName("visitor-memo")[0]; var mes_list = topnode.getElementsByTagName("message"); str += '<dl>\n' for (var i = 0; i < mes_list.length; i++) { var date = escapeHTML(mes_list[i].getAttribute("date")); var message = escapeHTML(mes_list[i].firstChild.nodeValue); str += "<dt>" + date + "</dt>\n"; str += "<dd>" + message + "</dd>\n"; } str += "</dl>\n" element.innerHTML = str; } // // Initialize the table visualization // get_table()
このファイルも、呼び出している"CGI"以外は、"Ajax"版と同じですね。
メッセージ記録兼取り出し CGI : visitor-world9.cgi
大幅に変更されたのは、この"CGI"ファイルです。 単に"sqlite3"モジュールを使うだけで終わりかと思っていたら、文字エンコーディングでかなり苦労しました。
#!/usr/local/bin/python # $Id: visitor-world9.cgi,v 1.1 2010/01/16 08:41:14 noritan Exp $ import cgi import cgitb import codecs import exceptions import sqlite3 import sys import time # Parameters db_file = "visitor-world9.sqlite" # Enable debug output cgitb.enable() # Get a POST data. form = cgi.FieldStorage() # Get Current time now = time.time() # Get and escape a MESSAGE # At first, confirm as UTF-8 encoding # and then trying SJIS encoding # at last, give up encodings. message_key = 'message' if message_key in form: try: message = form.getvalue(message_key) message = unicode(message, 'utf-8') except exceptions.UnicodeDecodeError as ex: try: message = unicode(message, 'sjis') except exceptions.UnicodeDecodeError as ex: message = "ILLEGAL MESSAGE %s" % type(message) else: message = "" # Connect to the DATABASE con = sqlite3.connect(db_file) con.text_factory = sqlite3.OptimizedUnicode cur = con.cursor() # INSERT if expected if len(message) > 0: cur.execute( "INSERT INTO visitor VALUES (?,?)", (now, message) ) con.commit() # SELECT messages cur.execute( "SELECT time, description FROM visitor ORDER BY time DESC" ) # Create UTF writer as BROWSER expected writer = codecs.getwriter('utf-8')(sys.stdout) # Show HTML header writer.write("""Content-type: text/xml; charset="utf-8" <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE visitor-memo [ <!ELEMENT visitor-memo (message)* > <!ELEMENT message (#PCDATA) > <!ATTLIST message date CDATA #REQUIRED > ]> """) # Show a list of visitor record writer.write("""<visitor-memo> """) # Make a list of messages for field in cur.fetchall(): asctime = time.strftime( "%Y-%m-%d (%A) %H:%M:%S", time.localtime(float(field[0])) ) message = cgi.escape(field[1]) writer.write("<message date=\"%s\">%s</message>\n" % (asctime, message)) # Show footer writer.write("""</visitor-memo> """) # Close the writer writer.close() # Close DATABASE cur.close() con.close()
"CGI"が受け取った"FORM"情報は、"URI"形式にエンコードされていますが、"cgi.FieldStorage"の作用で文字列に変換されて受け渡されます。 ところが、この時の文字エンコーディングは、指定されていません。 "JavaScript"が送り込んだ文字エンコーディングは、"JavaScript"だけが知っているのです。 ただ、このシステムの場合には、"HTML"ファイルで指定されている"UTF-8"エンコーディングが使用されているはずです。 この"UTF-8"エンコーディングを"Python"の内部エンコーディングである"Unicode"に変換するのが、"unicode"関数です。
try: message = form.getvalue(message_key) message = unicode(message, 'utf-8') except exceptions.UnicodeDecodeError as ex: try: message = unicode(message, 'sjis') except exceptions.UnicodeDecodeError as ex: message = "ILLEGAL MESSAGE %s" % type(message)
このプログラムでは、まず、'utf-8'エンコーディングであると仮定してメッセージを'Unicode'に変換します。 このとき、受け取ったメッセージが'utf-8'エンコーディングではなかった場合、 "exceptions.UnicodeDecodeError" 例外が発生します。 'utf-8'エンコーディングではなかった場合には、「特別サービス」として、'sjis'エンコーディングと仮定して変換を行います。 それでも、変換に失敗した場合には、"ILLEGAL MESSAGE"というメッセージを記録します。
もし、ここで'Unicode'に変換されなかった場合、'Unicode'ではない文字列がデータベースに記録されてしまい、表示するときにエラーを発生させてしまいます。 そのため、この入り口部分で、しっかりとエンコーディングを確認しておく必要があります。
cur.execute( "INSERT INTO visitor VALUES (?,?)", (now, message) ) con.commit()
メッセージが'Unicode'になったら、しめたものです。 "sqlite3"モジュールに"SQL"を発行してもらうだけで、データベースに文字列が入ります。 以前の版では、SQL文をコマンドの一部として発行するために、”cgi.encode"などで文字列をエンコードする操作が入っていたのですが、もう必要ありません。 単純明快でしょ。
# SELECT messages cur.execute( "SELECT time, description FROM visitor ORDER BY time DESC" )
メッセージの記録が終わったら、データベースにアクセスして、掲示板の内容を取り出します。 この部分もSQL文を直接渡すだけで、データベースへのアクセスができます。
# Create UTF writer as BROWSER expected writer = codecs.getwriter('utf-8')(sys.stdout)
データベースから取り出した情報を元にXMLを作成するのですが、ここで一苦労ありました。 データベースに記録した文字列は、Python標準の'Unicode'です。 ところが、XML文書は、'UTF-8'で作成しようとしています。 このため、「'Unicode'の文字列を'UTF-8'に変換する」作業が必要になってきます。
そこで、使用したのが、"codecs.StreamWriter"です。 この"factory"と呼ばれる「関数」は、'Unicode'で渡した文字列を任意のエンコーディング(ここでは、'UTF-8')に変換して関数の引数として渡した"file"に書き込んでくれる"Writer"というオブジェクトを返してくれます。 ここでは、標準出力(sys.stdout)に対して'UTF-8'エンコーディングで書き出してくれる"Writer"オブジェクトを作成しています。
# Make a list of messages for field in cur.fetchall(): asctime = time.strftime( "%Y-%m-%d (%A) %H:%M:%S", time.localtime(float(field[0])) ) message = cgi.escape(field[1]) writer.write("<message date=\"%s\">%s</message>\n" % (asctime, message))
あとは、すべてのレコードに対応するXMLエレメントを表示してやれば、XML文書の出来上がりです。
参考サイト
- 8.8. codecs — Codec registry and base classes
- Pythonでのエンコーディングについては、ここに書いてあるはずなのですが、読んだだけではわかりませんでした。 何本かプログラムを書いているうちに、哲学が見えてきます。
- 12.13. sqlite3 — DB-API 2.0 interface for SQLite databases
- Python2.6になって、"sqlite3"モジュールが標準で装備されたため、プログラムが楽にはなりましたが、マニュアルは、必要です。
コメント 0