SSブログ

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

トラックバックの受付は締め切りました

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。