2011年06月16日

GAE/PでAspyctを使ってAOP(アスペクト指向プログラミング)

ある処理の前後に本来のビジネスロジックとは関係ない処理を挟みたいことはよくある話である。
そこで今回はGAE for Pythonでアスペクト指向プログラミング(AOP)する方法を紹介する。

まずAspyctというAOP用のサードパーティ製外部モジュールをダウンロードする。ちなみに今回は Aspyct-3.0_beta_4 というバージョンをダウンロードした。こいつを解凍すると aspyct というフォルダがあるのでGAEアプリのプロジェクトフォルダへフォルダごとコピーする。
これで下準備はOK。

早速コードを書いてみよう。
以下のサンプルは、計算機クラスの足し算処理に割り込んで、勝手に引数を10倍してくれる迷惑なアプリだ。

まずは割り込むクラスから書いてみよう。
MyAspect.py
# -*- coding: utf-8 -*-
import cgi
import os

import logging
from Aspyct.aop import *

class MyAspect(Aspect):
def atCall(self, cd):
logging.info('atCall:' + str(cd.args))
#
# 二つの引数を勝手にx10してみる。
#
cd.change() # 編集不可な引数cd(tuple)を編集可に。
p1 = cd.args[0] * 10
p2 = cd.args[1] * 10
return (p1, p2), {}

def atReturn(self, cd):
logging.info('atReturn:' + str(cd.returned))

def atRaise(self, cd):
logging.info('Exception raised!')

上記のソースで大切なのは cd.change() だ。これを呼び出すことで、tupleである引数cdが編集可能となる。これをやらないと引数を変更できないので注意。そして最後の return (p1, p2), {} で、書き換えた二つの引数を返している。

次にお馴染みのhelloworld.pyだ。
helloworld.py
# -*- coding: utf-8 -*-
import cgi
import os

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template
import logging
import MyAspect
from Aspyct.aop import *

#
# 計算機クラス
#
class Calc(object):
@MyAspect.MyAspect()
def add(self, p1, p2):
return p1 + p2

class MainPage(webapp.RequestHandler):
def get(self):
logging.info("MainPage#get")
calc = Calc()
ans = calc.add(1, 2) # 1+2=3 な筈が、、、
logging.info("Ans:%s" % ans) # 10+20=30になっている。

template_values = {}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

application = webapp.WSGIApplication([('/', MainPage)], debug=True)

def main():
run_wsgi_app(application)

if __name__ == "__main__":
main()


大切なのは以下の処理。
from Aspyct.aop import *

@MyAspect.MyAspect()
def add(self, p1, p2):

先頭でAspyctをインポートし、処理を差し込みたいメソッドの頭にJavaで言うところのアノテーションである @MyAspect.MyAspect() を記述している。これは MyApsect.py ファイルの中の MyAspect クラスを差し込みますよ、という宣言だ。

index.html は適当でいいので今回は省略。

さて、これを実行すると以下のようにログが出力される。
INFO 2011-06-16 00:46:46,763 helloworld.py:20] MainPage#get
INFO 2011-06-16 00:46:46,765 MyAspect.py:10] atCall:(1, 2)
INFO 2011-06-16 00:46:46,766 MyAspect.py:20] atReturn:30
INFO 2011-06-16 00:46:46,767 helloworld.py:23] Ans:30


Calc#add の処理の前後に MyAspect の処理が割り込んでいるのが分かるだろう。

このように、Javaでお馴染みのAOPをPythonでも実現でき、汎用性の高いコードを書くことが出来るようになるのだ。

ここはひとつポチっとよろしく。
人気ブログランキングへ

プログラミング Google App Engine
Dan Sanderson
オライリージャパン
売り上げランキング: 40082



posted by 寄り道退屈男 at 10:19 | Comment(0) | TrackBack(0) | GAE for Python

2011年06月15日

GAE/Pでカスタムタグを作って日付をUTC→JST変換する方法。

GAE for Pythonでカスタムタグ(カスタムフィルタ)を使って、テンプレートHTML内で日付をUTCからJSTへ変換・表示する方法を紹介する。

その前にまず、dateutil という便利な外部モジュールをダウンロードしておこう。これは読んで字のごとく日付変換ユーティリティである。

dateutilのインストールは簡単で、解凍したフォルダ内の dateutil フォルダをそのままGAEアプリのプロジェクトフォルダにコピーするだけだ。

dateutilを準備できたら、いよいよサンプルアプリの作成だ。
今回は単純な掲示板アプリをカスタムタグを利用して実装してみる。

まずはカスタムタグ templatefilters.py を実装する。
templatefilters.py
#!-*- coding:utf-8 -*-

# カスタムフィルタ作成に必要
from google.appengine.ext import webapp

import datetime
import dateutil.parser
import dateutil.tz

# レジストリを取得し、最後に自作フィルタを登録する。
register = webapp.template.create_template_register()

# UTC→JST変換フィルタ定義
def jst (value):
return value.replace(tzinfo=dateutil.tz.tzutc()).astimezone(dateutil.tz.gettz('Asia/Tokyo'))

# UTC→JST変換フィルタの登録
register.filter(jst)


次にお馴染みのhelloworld.pyだ。
helloworld.py
# -*- coding: utf-8 -*-
import cgi
import os

from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template
from google.appengine.ext import db
from datetime import datetime
import dateutil.parser
import logging

# カスタムタグの登録
webapp.template.register_template_library('templatefilters')

class Greeting(db.Model):
content = db.StringProperty(multiline=True)
date = db.DateTimeProperty(auto_now_add=True)

class MainPage(webapp.RequestHandler):
def get(self):
greetings = db.GqlQuery("SELECT * FROM Greeting ORDER BY date DESC LIMIT 10")
template_values = {
'greetings': greetings
}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

class Guestbook(webapp.RequestHandler):
def post(self):
greeting = Greeting()
greeting.content = self.request.get('content')
greeting.date = datetime.now()
greeting.put()
self.redirect('/')

application = webapp.WSGIApplication(
[('/', MainPage),
('/sign', Guestbook)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == "__main__":
main()


上記のコードで大切なのは、カスタムタグ(templatefilters.py)を登録している以下の部分だ。
webapp.template.register_template_library('templatefilters')


ここでちょっと話が逸れる。
日付を文字列で受け取りたい時もあると思う。そんな時のためにdateutilで日付文字列をdatetime型へ変換する方法も紹介しておこうと思う。以下のようにすれば日付文字列のフォーマットから自動的に判断してdatetimeに変換してくれる。
dateutil.parser.parse('2011-06-15T23:55:59+09:00')


さて、気を取りなおして、最後にテンプレートHTMLであるindex.htmlだ。
index.html
<html>
<body>
{% for greeting in greetings %}
<blockquote>
{{ greeting.date|jst|date:"Y-m-d H:i:s" }}
:
{{ greeting.content|escape }}
</blockquote>
{% endfor %}
<form action="/sign" method="post">
<div><textarea name="content" rows="3" cols="60"></textarea></div>
<div><input type="submit" value="Sign Guestbook"></div>
</form>
</body>
</html>

上記のコードで大切なのは以下の部分。
{{ greeting.date|jst|date:"Y-m-d H:i:s" }}

UTC な datetime で格納されている greeting.date を先程自作したフィルタのjst関数を通して JST へ変換している。

以上だ。
今回は、GAE/Pでの「カスタムタグの作り方」と「dateutil」の使い方の両方を学習できて一石二鳥のエントリだったね。

ちなみに、SEOとかを気にしないのであれば、JavascriptでJST変換する方法もある。まぁTPOで使い分けるといいだろう。


ここはひとつポチっとよろしく。
人気ブログランキングへ

プログラミング Google App Engine
Dan Sanderson
オライリージャパン
売り上げランキング: 40082



posted by 寄り道退屈男 at 10:41 | Comment(0) | TrackBack(0) | GAE for Python

2011年06月12日

GAE/PでBeautifulSoupを使ってXMLやHTMLをパース。

以前にGAE for PythonでPythonの標準APIだけを使ってXMLパースする方法を紹介したが、今回は Beautiful Soup という、かなり便利な外部モジュールを利用してHTMLパース(Webスクレイピング)する方法を紹介する。

BeautifulSoupのインストールは至って簡単で、インストールと言っても BeautifulSoup.py をダウンロードしてきてGAEアプリのプロジェクトフォルダにコピーするだけだ。

現時点のGAEはPython2.x系で動作しているので、Beautiful Soupもそれに対応したバージョン3.0系のモジュールをダウンロードする必要がある。ちなみに俺は、v3.0.8をダウンロードした。

さて、BeautifulSoup.py をプロジェクトフォルダにコピーできたら、早速サンプルコードを書いてみよう。以下はお馴染みの helloworld.py だ。

helloworld.py
# -*- coding: utf-8 -*-
import cgi
import os
import logging
import urllib2
from BeautifulSoup import BeautifulSoup
from google.appengine.ext import webapp, db
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template

#
# メインハンドラ
#
class MainHandler(webapp.RequestHandler):
def get(self):
# html = urllib2.urlopen("http://取得したいページのURL")
html = '''<html>
<p>aaa</p>
<p id="hoge">bbb</p>
<p>ccc</p>
</html>'''

soup = BeautifulSoup(html)

logging.info(u'全てのPタグのText')
tags = soup.findAll('p')
for idx, tag in enumerate(tags):
logging.info("#%s %s" % (str(idx), tag.text))

logging.info(u'id="hoge" なPタグのText')
tags = soup.findAll('p', attrs={'id':'hoge'})
for idx, tag in enumerate(tags):
logging.info("#%s %s" % (str(idx), tag.text))

logging.info(u'id="hoge" なタグのText')
tags = soup.findAll(attrs={'id':'hoge'})
for idx, tag in enumerate(tags):
logging.info("#%s %s" % (str(idx), tag.text))

logging.info(u'全てのタグのid属性')
tags = soup.findAll()
for idx, tag in enumerate(tags):
try:
logging.info("#%s %s" % (str(idx), dict(tag.attrs)['id']))
except KeyError:
logging.info("#%s %s" % (str(idx), ""))

template_values = {}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

application = webapp.WSGIApplication([('/', MainHandler)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == "__main__":
main()


index.html
<html>hello world!</html>


上のサンプルを実行すると以下のようにログが出力される。

INFO 2011-06-12 12:16:09,815 helloworld.py:25] 全てのPタグのText
INFO 2011-06-12 12:16:09,818 helloworld.py:28] #0 aaa
INFO 2011-06-12 12:16:09,819 helloworld.py:28] #1 bbb
INFO 2011-06-12 12:16:09,822 helloworld.py:28] #2 ccc
INFO 2011-06-12 12:16:09,823 helloworld.py:30] id="hoge" なPタグのText
INFO 2011-06-12 12:16:09,828 helloworld.py:33] #0 bbb
INFO 2011-06-12 12:16:09,831 helloworld.py:35] id="hoge" なタグのText
INFO 2011-06-12 12:16:09,835 helloworld.py:38] #0 bbb
INFO 2011-06-12 12:16:09,836 helloworld.py:40] 全てのタグのid属性
INFO 2011-06-12 12:16:09,839 helloworld.py:46] #0
INFO 2011-06-12 12:16:09,842 helloworld.py:46] #1
INFO 2011-06-12 12:16:09,842 helloworld.py:44] #2 hoge
INFO 2011-06-12 12:16:09,845 helloworld.py:46] #3
INFO 2011-06-12 12:16:09,897 dev_appserver.py:4151] "GET / HTTP/1.1" 200 -


Beautiful Soup を使えば非常にシンプルにHTMLやXMLの解析ができることが分かったかな。

とりあえず findAll メソッドだけ覚えておけば何とかなるだろう。

Beautiful Soup APIの詳細を知りたいなら本家サイトのドキュメントをご参照あれ


★追記 2011/06/22
BeautifulSoupで文字コードに悩まされたら、BeautifulSoupに食わせるhtmlを明示的にdecodeしよう。


ここはひとつポチっとよろしく。
人気ブログランキングへ

プログラミング Google App Engine
Dan Sanderson
オライリージャパン
売り上げランキング: 40082



posted by 寄り道退屈男 at 21:33 | Comment(0) | TrackBack(0) | GAE for Python

GAE/Pでログインが必要なページを取得する方法

ログイン状態でないと見れないページや、Cookieがセットされていないと表示できないページを取得・表示する方法を以下に紹介する。

今回の肝は、自作した URLOpener というクラスである。
こいつは setCookie と open という二つのメソッドを持つ。
setCookie で「ログインページのURL」と「ログインID」と「パスワード」をセットすると URLOpener のインスタンスはログイン状態になり、open でログインが必要なページを取得することができる。

helloworld.py
# -*- coding: utf-8 -*-
import cgi
import os
import urllib

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template
from google.appengine.api import urlfetch

#
# URLオープナー
#
class URLOpener(object):
def setCookie(self, login_url, login_id = None, password = None):
# パラメータはサイトに合わせて編集が必要。
params = urllib.urlencode({
"login_id" : login_id,
"password" : password
})
f = urlfetch.fetch(
login_url,
payload=params,
method=urlfetch.POST,
headers={}
)
self.cookie = f.headers['Set-Cookie'].replace("path=/,", "").replace("path=/", "")

def open(self, url, charset = "utf-8"):
f = urlfetch.fetch(
url,
payload = "",
method=urlfetch.POST,
headers={"Cookie" : self.cookie,
"content-type" : "text/html"}
)
return unicode(f.content, charset, "ignore")

#
# メインハンドラ
#
class MainHandler(webapp.RequestHandler):
def get(self):
LOGIN_URL = 'http://ログインページのURL'
URL = 'http://ログインが必要なページのURL'
LOGIN_ID = "hoge"
PASSWORD = "0123456789"
CHARSET = "shift_jis"

urlopener = URLOpener()
urlopener.setCookie(LOGIN_URL, LOGIN_ID, PASSWORD)
content = urlopener.open(URL, CHARSET)

template_values = {"content" : content}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

application = webapp.WSGIApplication([('/', MainHandler)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == "__main__":
main()


index.html
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
</head>
<body>
<h2>読み込んだページ</h2>
<blockquote>{{ content }}</blockquote>
</body>
</html>


これを使えば、「ログインは必要ないけど、一度アクセスしてCookieを取得しないとページを見られない」というサイトにも対応できる。たまに見るよね、そういうサイト。


ここはひとつポチっとよろしく。
人気ブログランキングへ

プログラミング Google App Engine
Dan Sanderson
オライリージャパン
売り上げランキング: 40082



posted by 寄り道退屈男 at 12:18 | Comment(0) | TrackBack(0) | GAE for Python

2011年06月07日

GAE/Pでファイルアップロード。

前回、GAE for Pythonでのリクエストデータの受け取り方を紹介したが、今回はファイルアップロードの方法も紹介しようと思う。

先ずは画面HTMLからだ。

index.html
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
</head>
<body>
<form enctype="multipart/form-data" method="post" action="/">
<input type="file" name="file" />
<input type="submit" value="アップロード" />
</form>

<p>結果</p>
<p>ファイル名: {{ filename }}</p>
<p>ファイルサイズ(byte): {{ filesize }}</p>
<p>MIMEタイプ: {{ mimetype }}</p>
</body>
</html>


そして、お馴染みの helloworld.py を以下のようにする。

helloworld.py
# -*- coding: utf-8 -*-
import cgi
import os

from google.appengine.ext import webapp, db
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template
import logging

#
# メインハンドラ
#
class MainHandler(webapp.RequestHandler):
def get(self):
template_values = {}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

def post(self):
file = self.request.get("file")

if file:
# ファイルサイズ取得
blob = db.Blob(file)
filesize = len(blob)

# ファイル名、MIMEタイプ取得
body_file = self.request.body_file.vars['file']
filename = body_file.filename
mimetype = body_file.headers['content-type']

# 実際はここでデータストアへ格納したりする。。。

template_values = {
"filename" : filename,
"filesize" : filesize,
"mimetype" : mimetype
}
else:
template_values = {}

path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))

application = webapp.WSGIApplication([('/', MainHandler)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == "__main__":
main()


ポイントは、二つある。

一つ目は、ファイルサイズを取得するところだ。
self.request.get("file") で取ってきた file を db.Blob 型にキャストし、len() でサイズを取得する。
file = self.request.get("file")
blob = db.Blob(file)
filesize = len(blob)


二つ目のポイントは、ファイル名とMIMEタイプを取得するところだ。
self.request.body_file.vars['file'] で取得した body_file の filename プロパティからファイル名が、headers['content-type'] でMIMEタイプがそれぞれ取得できる。
body_file = self.request.body_file.vars['file']
filename = body_file.filename
mimetype = body_file.headers['content-type']




ここはひとつポチっとよろしく。
人気ブログランキングへ

プログラミング Google App Engine
Dan Sanderson
オライリージャパン
売り上げランキング: 40082



posted by 寄り道退屈男 at 09:50 | Comment(0) | TrackBack(0) | GAE for Python