2015/04/10

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

[PostgreSQL][実行環境][Python]
昨日 CGIHTTPServer を使った部分を、今日は wsgiref で置き換えました。CGI だとクエリ一回ごとに Python インタプリタを起動しますが、WSGI はそれがサーバ起動時だけで済みます。外形的には昨日と同じで、DB サーバ & Web サーバ & クライアントをまるごとポータブルに持ち運ぶもの。Windows 7 32bit 上のゲストアカウントで動作確認しました。↓ が使用例。ソースは中ほどです。


↓ 昨日の再掲で、使った三つのポータブルアプリと、それぞれのセットアップに関する記事へのリンク。この QGIS の中に Python 2.7 があり、標準で付いている wsgiref パッケージを使って簡易な WSGI 用サーバを立てられます。

■ Firefox
■ PostgreSQL
■ QGIS

↓ ファイル構成の大枠は昨日と同じ。QGIS フォルダ直下に、起動用バッチとフォルダを各一つ作り、後者の中にファイル一式を入れました。



上記フォルダがドキュメントルートになり、ブラウザが直接アクセスするのは index.py だけです。昨日使った CGIHTTPServer と違って静的な HTML 等をそのまま返せないため、index.py 内で、リクエストされたファイルを読み込んで中身を返送します。

他のスクリプト parse_query.py と return_re.py は、PostgreSQL へのクエリ実行時に使用。初回の呼び出し時に自動的にコンパイルされ ↓ 二回目以降の処理が効率的になります。


以下、各ファイルのソースです。一式は 20150410_pgpy_wsgiclient.zip に置きました。

まず起動用バッチファイル ↓ は、最初に PostgreSQL を、次に Python による Web サーバを立ち上げます。前者については昨日と全く同じ。

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

set PGDIR=../PostgreSQLPortable-9.4
set WSGI_DIR=python_wsgi
set ROOT_SCRIPT=/index.py
:: 上、CGI から変更

:: 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

:: startup Python WSGI server
::
set PYTHONHOME=QGIS/apps/Python27
set PYTHONPATH=QGIS/apps/Python27/Lib
set PATH=%PYTHONHOME%
set PYTHONBIN=QGIS/bin
"%PYTHONBIN%/python" %WSGI_DIR%%ROOT_SCRIPT%
:: 上、CGI から変更

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


index.html は TITLE タグを変えただけ、main.css は昨日と同じなのでソースは省略します(先ほどの ZIP には有り)。PostgreSQL へのクエリに使う main.js は ↓ リクエスト先アドレスを変更。昨日は CGI 用の Python スクリプトを直接指定していました。WSGI では、どんなリクエストも必ず同じスクリプト(今日の場合は index.py)を呼ぶので、そこで適宜処理できれば何でもいいです。

■ 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', './'); // ここだけ昨日から変更
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 スクリプト。サーバ起動時に実行され、ブラウザからのリクエストに応答します。静的な HTML、CSS、JavaScript 等を返すのもこのスクリプト経由となり、リクエスト URI に応じて実ファイル(ルートなら index.html)を読み込み、拡張子から MIME タイプを判断して返送。何かクエリストリングがあれば DB への要求と仮定して parse_query.py に渡します。

■ index.py
# -*- coding: utf-8 -*-

import os
import parse_query

def app(environ, start_response):
path = environ['PATH_INFO']
if path == '/':
path = '/index.html'
path = os.environ.get('wsgi_dir') + path

try:
que = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
except:
que = ''

if que != '':
return proc_query(que, start_response)
elif os.path.exists(path):
return static_app(environ, start_response, path)
else:
return show_404_app(environ, start_response)

def static_app(environ, start_response, path):
headers = [('Content-type', content_type(path))]
start_response('200 OK', headers)

h = open(path, 'r')
content = h.read()
h.close()
return content

def proc_query(que, start_response):
headers = [('Content-type', 'text/plain')]
res = parse_query.parse(que)
start_response('200 OK', headers)
return res

def show_404_app(environ, start_response):
headers = [('Content-type', 'text/plain')]
start_response('404 Not Found', headers)
return '404 Not Found'

def content_type(path):
MIME_TABLE = {
'.html': 'text/html',
'.text': 'text/plain',
'.css': 'text/css',
'.js': 'application/javascript',
}
name, ext = os.path.splitext(path)
if ext in MIME_TABLE:
return MIME_TABLE[ext]
else:
return 'application/octet-stream'

if __name__ == '__main__':
try:
from wsgiref.simple_server import make_server
httpd = make_server('', 8000, app)
print('Serving on port 8000...')
httpd.serve_forever()
except KeyboardInterrupt:
print("\n" + 'Server was stopped.' + "\n")


↓ PostgreSQL へのクエリを受け取り psycopg2 経由で結果を返す部分。実質的に昨日と同じですが、例外処理等で長くなったので二つに分割しました。

■ parse_query.py
# -*- coding: utf-8 -*-

def parse(que):
import os
import return_res

tmpdir = os.getcwd()
os.chdir(os.environ.get('pythonbin'))
import psycopg2
os.chdir(tmpdir)

dic = parse_cgi(que)
try:
con = dic['con'][0]
sql = dic['sql'][0]
except:
return 'input for connection or query.'

try:
connect = psycopg2.connect(con)
except :
return 'unable to connect to the database.'

try:
cur = connect.cursor()
cur.execute(sql)
except psycopg2.Error as e:
return e.pgerror

try:
mes = connect.notices
except:
mes = []

try:
col = cur.description
dat = cur.fetchall()
except:
col = []
dat = []

cur.close()
connect.close()
return return_res.output(col, dat, mes)

def parse_cgi(que):
import cgi
return cgi.parse_qs(que)


■ return_res.py
# -*- coding: utf-8 -*-

def output(col, dat, mes):
res = []
add_dat(col, dat, res)
add_mes(mes, res)
if len(res) == 0:
res.append('No Result')
return ''.join(res)

def add_dat(col, dat, res):
if len(col) == 0:
return

columns = [d[0] for d in col]
res.append('<table>')
tr = []

for c in columns :
s = str(c).decode('utf-8').encode('utf-8')
tr.append('<th>' + s + '</th>')
res.append(''.join(tr))

for r in dat:
tr = ['<tr>']
for c in r:
s = str(c).decode('utf-8').encode('utf-8')
tr.append('<td>' + s + '</td>')
tr.append('</tr>')
res.append(''.join(tr))
res.append('</table>')

def add_mes(mes, res):
if len(mes) == 0:
return
res.append('<pre>' + ''.join(mes) + '</pre>')


以下、動作の例です。起動用バッチファイルを実行すると ↓ こんなコンソールが出て、PostgreSQL と Web サーバが待機状態になります。localhost:8000 で最初に表示されるフォーム(二枚目の画像)は昨日と全く同じ簡素なもの。



↓ 使い方も昨日と同じです。SELECT 文ならテーブルで、RAISE INFO などメッセージは PRE タグに背景を付けて表示。


↓ 使用中のコンソールで、POST の行が PostgreSQL へのクエリ。昨日の CGI ではクエリのたびに実行コマンドが出ましたが、今日はリクエスト URI、ステータス、バイト数だけです。


↓ 普通の文字列なら日本語がそのまま使える一方、なぜか配列だとエスケープされたまま。psycopg2 の仕様なのか分かりませんが、とりあえず SQL の時点でキャストすれば戻ります。


↓ SELECT 以外の作成・更新系クエリは、成功時に何も結果が返りません。psql や pgAdmin のように何か表示すると親切なので、いずれ検討する予定。あと今回 Python スクリプトでは commit コマンドを入れなかったので、下のように最後に COMMIT を付けないと結果が破棄されます。


今回に限らず PostgreSQL へのクエリに共通する動作ですが、複数の文をまとめて実行すると、最後の結果だけが ↓ 返ります。ただし SELECT の後に DO 文で何かメッセージを出力させると(先ほどの乱数の例)、DO 文には結果がないので両方が表示されます。


pgAdmin のクエリツールでは、pg_sleep と RAISE INFO を組み合わせて任意の時間間隔でメッセージを表示できましたが、同じことを psycopg2 で出来るのか分からず、未対応。↓ のように一秒おきに三行出力させたくとも、三秒後にまとめて返ってしまいます。
【追記 2015/4/13】psycopg2 の非同期モードを使えばいいと判明しました。詳細はこちら


↓ コンソールで Ctrl-C を押すと Web サーバが終了し、バッチファイルの最終行(PostgreSQL の終了処理)を実行してから「バッチ ジョブを終了しますか」プロンプトになるようです。このタイミングの正確なところは、まだ調べていません。


作る前は wsgiref で静的なファイルを返すのが面倒そうな印象でしたが、それより psycopg2 との連携部分が昨日のままではエラー連発で、情報もなく苦労しました。ともかく一応動いたので、CGIHTTPServer による昨日のバージョンはおそらく不要。

制作にあたり、下記を参照しました。m(_)m

■ Python標準ライブラリだけでWebフレームワークっぽくしてみる - 清水川Web
■ Getting Started with WSGI - O'Reilly Media
■ python - How to catch POST using WSGIREF - Stack Overflow
■ Serving Static Content - Python Web Programming (Steve Cassidy)
×

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