So-net無料ブログ作成
検索選択

Python CGI で 掲示板みたいなものを作る~sqlite3モジュール編~ [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000365.png

Python CGI で 掲示板みたいなものを作る~Ajax編~では、Ajaxという仕組みを利用してページ遷移を起こさない掲示板システムを作成しました。 しかし、SQLiteとのインターフェイスは、相変わらず"system"関数によるコマンドを呼び出しで、標準入力を通じてSQLコマンドを送り込み、標準出力から結果を受け取る方式になっていました。 Python CGI で作るアクセスカウンタ~sqlite3モジュール編~でsqlite3モジュールが使えるようになったので、掲示板もsqlite3モジュールを使用するように変更してみました。

データベースの構成

今までの掲示板は、一つのデータベースを使いまわしてきましたが、今回は別のデータベースを作成しました。 その理由は、文字エンコーディングの扱いが異なってきたからです。

カラム名タイプ内容
timeNUMBER記録時刻を表す数値です。
descriptionTEXTメッセージの内容をユニコードで表現した文字列です。

このテーブルに "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("&amp;");
  str = str.split("<").join("&lt;");
  str = str.split(">").join("&gt;");
  str = str.split('"').join("&quot;");
  str = str.split("{").join("&#123;");
  str = str.split("}").join("&#125;");
  str = str.split("'").join("&#039;");
  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"モジュールが標準で装備されたため、プログラムが楽にはなりましたが、マニュアルは、必要です。

参考文献

Python クックブック 第2版

Python クックブック 第2版

  • 作者: Alex Martelli
  • 出版社/メーカー: オライリー・ジャパン
  • 発売日: 2007/06/26
  • メディア: 大型本
Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

nice!(0)  コメント(0)  トラックバック(0)  このエントリーを含むはてなブックマーク#

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この記事のトラックバックURL:
※言及リンクのないトラックバックは受信されません。