2015/04/09

Python & PostgreSQL のポータブルな Web クライアント(1)

[Python][実行環境][PostgreSQL]
一昨日、ポータブル版 QGIS から Python 2.7 だけを単独で使えるようにしました。今日は、同梱のパッケージだけで、同じくポータブルな PostgreSQL 9.4 に接続する簡単な Web サーバ & クライアントを作る一回目。とりあえず psycopg2 と CGIHTTPServer を利用。最近は WSGI が主流のようですが、静的な HTML を単純に返すのが意外に面倒そうだったので(2)に回します。

↓ これまでと同様 Windows 7 32bit 上のゲストユーザで実行した様子で、動作確認のブラウザは Firefox 36 ポータブル版を使用。遅い USB メモリでは使い勝手が悪いですが、外部ディスクで DB サーバ & Web サーバ & クライアントをまるごと持ち運べます。スクリプト等のソースは少し下です。


↓ 今回使った三つのポータブルアプリと、それぞれのセットアップに関する記事へのリンク。この QGIS の中に Python 2.7 があります。

・ Firefox
・ PostgreSQL
・ QGIS


先に全体のファイル構成について書き、その後ソースを載せます。上の QGIS フォルダの直下は ↓ こんな感じで、今回作ったのは python_cgiserver というフォルダと、PostgreSQL および Web サーバを一緒に起動するバッチファイル。


↓ フォルダ python_cgiserver の中には HTML, CSS, JavaScript が各一つ。favicon.ico はなくてもいいですが、ブラウザが自動的に読みに行って Web サーバに 404 Not Found のログが出るのでその対策。フォルダ htbin に、CGI で実行される Python スクリプト(今回は一つ)を置きます。



以上のファイルを 20150409_pgpy_cgiclient.zip にまとめました。以下、ソースです。

■ Start PG94 and Python CGI Server.bat
@echo off
title PostgreSQL 9.4.1 and Python 2.7 CGI Server
chcp 932 > nul

set PGDIR=../PostgreSQLPortable-9.4
set CGIDIR=python_cgiserver

:: set up paths for PostgreSQL / PostGIS
::
set PGSQL=%PGDIR%/App/PgSQL
set PGDATA=%PGDIR%/Data/data
set PGLOG=%PGDIR%/Data/log.txt
set PGLOCALEDIR=%PGSQL%/share
set GDAL_DATA=%PGSQL%/gdal-data
set PATH=%PGSQL%/bin

:: set up variables for PostgreSQL / PostGIS
::
set PGDATABASE=postgres
set PGUSER=postgres
set POSTGIS_GDAL_ENABLED_DRIVERS=ENABLE_ALL
set POSTGIS_ENABLE_OUTDB_RASTERS=1

:: startup postgres server
::
echo.
"%PGSQL%/bin/pg_ctl" -D "%PGDATA%" -l "%PGLOG%" -w start

:: set up paths and startup Python CGI server
::
cd %CGIDIR%
set PYTHONHOME=../QGIS/apps/Python27
set PYTHONPATH=../QGIS/apps/Python27/Lib
set PYTHONBIN=../QGIS/bin
set PATH=%PYTHONHOME%
"%PYTHONBIN%/python" -m CGIHTTPServer

:: terminate
::
"%PGSQL%/bin/pg_ctl" -D "%PGDATA%" stop


このバッチを実行すると ↓ 最初に PostgreSQL を、次に Python の CGIHTTPServer を起動して待機状態になります。コンソールを閉じると両サーバの強制終了になるので注意。


上のバッチファイルでは、Web サーバのドキュメントルートを python_cgiserver に設定しました。そこに置いた index.html ↓ がクライアントの玄関で、CSS と JavaScript を各一つ読み込みます。

■ python_cgiserver/index.html
<html>
<head>
<title>Portable psycopg2 on CGIServer</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="main.css">
<script src="main.js" type="text/javascript"></script>
</head>
<body>
<div>
Connection :<input id="con" type="text" /><br />
<textarea></textarea><br />
<button>実行</button>
</div>
<div id="res"></div>
</body>
</html>


■ python_cgiserver/main.css
body {
font-family: monospace
; font-size: 12px
; margin: 20px 0 0 20px
}

input {
font-family: monospace
; font-size: 12px
; margin-left: 8px
; padding: 2px 4px
; width: 320px
}

textarea {
font-family: monospace
; font-size: 12px
; height: 250px
; width: 400px
; margin: 5px 0 0 0
; padding: 5px
}

button {
margin: 0 0 10px 300px
; width : 80px
}

table {
border-collapse: collapse
; border-top: 1px solid black
; border-left: 1px solid black
; margin: 0 0 20px
}

th {
background: wheat
}

th, td {
border-bottom: 1px solid black
; border-right: 1px solid black
; font-family:monospace
; font-size:12px
; padding:2px 4px
}

pre {
background: wheat
; padding: 5 10px
; width: 380px
}


■ python_cgiserver/main.js
// encoding: UTF-8

window.addEventListener('load', function(){
var b = document.getElementsByTagName('button')[0];
b.addEventListener('click', execQuery, false);
}, false);

execQuery = function() {
var con = document.getElementsByTagName('input')[0].value;
var sql = document.getElementsByTagName('textarea')[0].value;
var dat = 'con=' + con + '&sql=' + encodeURIComponent(sql);
var req = new XMLHttpRequest();
req.open('POST', '/htbin/psycopg2test.py');
req.onreadystatechange = function() {
if (req.readyState == 4) {
setRespose(req.responseText);
}
}
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.send(dat);
}

setRespose = function(txt) {
var res = document.getElementById('res');
res.innerHTML = txt;
}


Python の CGIHTTPServer はデフォルトで 8000 番ポートを使用。localhost:8000 を開くと ↓ 最小限のフォームが出ます。


Connection に PostgreSQL への接続文字列を入れ、その下に自由にクエリを入力します。実行ボタンを押すと XMLHttpRequest で下の Python スクリプトを呼び出し、戻り値をそのまま InnerHTML で画面内にセットする簡単な仕組み。

■ python_cgiserver/htbin/psycopg2test.py
# coding: UTF-8

import os
import sys
import cgi
os.chdir(os.environ.get('pythonbin')) # need for next statement
import psycopg2

print "Content-type: text/plain; charset=UTF-8\n"

fld = cgi.FieldStorage()
con = fld.getvalue('con', '')
sql = fld.getvalue('sql', '')
if con == '' or sql == '' :
print 'input for connection or query.'
sys.exit()

try :
connect = psycopg2.connect(con)
cur = connect.cursor()
except :
print 'unable to connect to the database.'
sys.exit()

try :
cur.execute(sql)
mes = connect.notices
col = cur.description
except psycopg2.Error as e :
print e.pgerror
sys.exit()

res = []

if col is not None :
columns = [d[0] for d in col]
res.append('<table>')
tr = []
for c in columns :
tr.append('<th>' + str(c).decode('utf-8').encode('utf-8') + '</th>')
res.append(''.join(tr))
for r in cur.fetchall() :
tr = ['<tr>']
for c in r:
tr.append('<td>' + str(c).decode('utf-8').encode('utf-8') + '</td>')
tr.append('</tr>')
res.append(''.join(tr))
res.append('</table>')

if len(mes) > 0 :
res.append('<pre>')
for m in mes :
res.append(m)
res.append('</pre>')

cur.close()
connect.close()
print ''.join(res)


↓ 冒頭の再掲で、クエリ結果がテーブル表示されたところ。接続文字列の各フィールドには psycopg2 のデフォルト値があり(例えば dbname は postgres)省略時はそれが使われます。


↓ Web サーバへのアクセスがコンソールに出て、フォームからリクエストするたび Python スクリプトが起動されている様子が分かります。


クエリ結果だけでなく、RAISE INFO などのメッセージも表示するようにしました。そのテストと、日本語を使った様子。↓ まだ検証が浅いですが、一応動いているようです。

-- 無名コードブロックはテキストで出力
DO $$
BEGIN
RAISE INFO 'テスト%', E'\n' ;
FOR i IN 1..5 LOOP
RAISE INFO '%', random() ;
END LOOP ;
END $$ ;

-- SQL の結果はテーブル
SELECT random() AS "一様乱数"
, repeat(' ', 6) || ceil(random() * 6) AS "サイコロ"
FROM generate_series(1, 10) ;


↓ クエリが間違った場合、PostgreSQL のエラーメッセージをそのまま表示。ただし改行を処理していないので(BR タグに未変換)、問題箇所を示すキャレットは無意味になります。


↓ コンソールで Ctrl-C を打つと、PostgreSQL と CGIHTTPServer の両サーバが終了します。


制作にあたり、下記のサイトを参考にしました。m(_)m

psycopg2 :
■ Psycopg2 Tutorial - PostgreSQL wiki
■ python - Get warning messages through psycopg2 - Stack Overflow

Python いろいろ :
■ Python カレントディレクトリの移動などosモジュールについて - 笑うクジラ
■ Pythonで簡易CGIサーバーを利用する - yummy-yummy
■ 環境変数の取得 - Python入門から応用までの学習サイト

クエリ結果のテーブルデザイン :
■ 基本となるシンプルなtableの装飾:CSS小技

なお当初は From Postgres to JSON strings - Peterbe.com にあるサンプル(psycopg2 で取得したクエリ結果を Python 内で JSON に変換)を試しましたが、一部のデータ型がうまく変換されずエラーになるので、弥縫策として直接 HTML タグを出力するようにしました。
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。