2009年1月27日火曜日

web2py

これほどまでに万能(変態)の名がふさわしいフレームワークがあるだろうか。

・フルスタック
・1からアプリケーションを開発できるエディタつき万能WEBADMIN
・GUIでER図を描くSQL Designer
・Stan-likeなHTML BuilderとDjangoとPSPのいいとこどりテンプレート
・とりあえずBEAUTIFY
・Railsのflash
・Model連動Form
・SQLAlchemyに対抗心メラメラなORM
・gae-likeモデル連動自動スキーマ変換
・GQL生成するので、GAEでもModel変更不要。
・ついでにCASも提供しておきます。
・Exceptionから自動Ticket発行
・Rails-likeのURL-RoutingにReverseがつき、Regex機能も併せ持つ完ぺき主義。
・request,responseなど、実用性だけを考えた大量のglobalオブジェクト
・ValidatorまでGlobalはマジでやめてくれ、IS_EXPR … 恐ろしい子

必要な機能はすべてそろっている。
後は勇気だけ。

変態だから人に勧めにくい。
GAEで使うフレームワークを探していたときに見つけた。
この豊富な機能は感動。

個人でいくつかGAEように作ったけど、会社では使いたくない。


------
ようやく書いた。
書こうと思ってメモ書きだけして、放置してあるのが多数。
ブログの使い方を間違えているな。

もういいやと思ってメモを消してしまったことも多いし、
メモでもいいからとりあえず公開して、その後追加、あるいはまとめを書くスタイルに変えなければ。


書きかけで放置してあるメモはあと5つ。
・django command拡張
・django-things
・Seasar2
・ipython
・本文抽出

2009年1月23日金曜日

download mass images 3

download mass images 2のつづき

前回で問題としてあがったこと。
1. タプル
2. 出力の乱れ

まず、q.put((download, urljoin(url, a[-1].get('href'))))を書き換えます。
今は引数ひとつしか受け入れられないので、
q.put(func, args, kwargs)

func, args, kwargs = q.get()
になればいいですね。

  1. q = Queue()  
  2. def worker(func):  
  3.     def inner(*args, **kwargs):  
  4.         print func, args, kwargs  
  5.         q.put((func, args, kwargs))  
  6.     return inner  
  7.  
  8. @worker  
  9. def download(url):  
  10.     print 'down  '+url  
  11.     urllib.urlretrieve(url, url[url.rfind('/')+1:])  
  12.  
  13. @worker  
  14. def parse(url):  
  15.     print 'parse '+url  
  16.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  17.     a = soup('a', href=re.compile(r'^/images/pub/\d+/\w+\.jpg$'))  
  18.     download(urljoin(url, a[-1].get('href')))  
  19.  
  20. @worker  
  21. def images(url):  
  22.     print 'image '+url  
  23.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  24.     a = soup('a', {'class':'image_a'}, href=re.compile(r'^/backgrounds/\d+$'))  
  25.     map(lambda a:parse(urljoin(url, a.get('href'))), a[:4])  
  26.   
  27. def pages(url):  
  28.     print 'pages '+url  
  29.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  30.     a = soup('a', href=re.compile(r'^/desktop/\w+\.php$'))  
  31.     map(lambda a:images(urljoin(url, a.get('href'))), a[:4])  
  32.   
  33. if __name__ == '__main__':  
  34.     def loop():  
  35.         while 1:  
  36.             func, args, kwargs = q.get()  
  37.             try:  
  38.                 func(*args, **kwargs)  
  39.             except Exception, e:  
  40.                 print e  
  41.             finally:  
  42.                 q.task_done()  
  43.     for i in range(THREAD_MAX):  
  44.         w = Thread(target=loop)  
  45.         w.daemon = True  
  46.         w.start()  
  47.     pages(sys.argv[1])  
  48.     q.join()  

なんとなく思いついたので、デコレータにしてみました。
これなら、呼び出しがシングルスレッドと同じになります。
引数も可変です。
読みやすいですね。
欠点はシングルスレッドと同じと言うことでしょう。
似たようなものは似た書き方をすべきではないかもしれません。

表示の乱れに関しては、
  1. from threading import Lock  
  2. lock = Lock()  
  3. def p(s):  
  4.     lock.acquire()  
  5.     try:  
  6.         print s  
  7.     finally:  
  8.         lock.release()  

ロックされた関数pを使って表示するようにします。
あとは、ご存知loggingモジュールはスレッドセーフなので、
loggingを使うのがもっとよい方法です。

ここまでを統合して、Workerクラスを書くとすれば、
このあたりでしょうか。
  1. class Worker:  
  2.     def __init__(self, num):  
  3.         self.q = q = Queue()  
  4.         def loop():  
  5.             while 1:  
  6.                 func, args, kwargs = q.get()  
  7.                 try:  
  8.                     func(*args, **kwargs)  
  9.                 except Exception, e:  
  10.                     logging.exception(e)  
  11.                 finally:  
  12.                     q.task_done()  
  13.         for i in range(num):  
  14.             w = Thread(target=loop)  
  15.             w.daemon = True  
  16.             w.start()  
  17.     def put(self, func, *args, **kwargs):  
  18.         logging.debug(e)  
  19.         self.q.put((func, args, kwargs))  
  20.     def join(self):  
  21.         self.q.join()  
  22.   
  23. workers = Worker(THREAD_MAX)  

workersになにかputして作業が始まったらjoinで終了を待ってください。

これも大きな欠点があります。
Ctrl+Cで終了させることができません。

さてと、最後にeventletバージョン
  1. from eventlet import coros, httpc, util  
  2. util.wrap_socket_with_coroutine_socket()  
  3. pages = coros.CoroutinePool(max_size=THREAD_MAX)  
  4. def download(url):  
  5.     a = httpc.get(url)  
  6.     # aにデータ入ってます。  
  7. pages.execute(alljpg, n, depth - 1)  
  8. pages.wait_all()  


ごめん、最後は面倒なんで手抜き。

2009年1月14日水曜日

django2php

PHPの仕事が来てしまったので、
Djangoと似た環境をPHPで探しました。

Djangoの基本要素は以下の4つかな?
・ORM
・Template
・Routing
・Form
DjangoのFormとRoutingは絶品だと思っています。
実際は両方とも書き換えてしまったので、絶品というのは変だけど、
基本設計は秀逸。今も感動しています。

あとの認証、セッション、Middleware(Filter)あたりは、
最初に書けば、あとはあまりいじらないから適当なライブラリから引用すればOK。

あとはディレクトリ構造。
appごとに分けられるDjangoは編集範囲が限定されていて見通しがよい。
rails風はコントローラとモデルが遠くて大変。

ORM


Propel
Doctrine
がいいらしい
Comparing Propel, Doctrine and sfPropelFinder
これはよい比較

YAMLでSchemaを書けて、HQLみたいなのもあるDoctrineがいい感じ。
でもやっぱりDjango-ORMがほしい。

途中、DoctrineでfindOneByTitleという、いかにもmethod_missingを使いました的な、メソッド名を発見。
調べてみたら、__callなんてあるんですね。
面白い。

Propelをメソッドチェーンで動くようにするなど拡張したsfPropelFinderもなかなか努力のあとがにじみ出ていていいですね。

php-django-like-db-abstraction/
「素晴らしい!」と思って、開いたら空っぽでした。
名前だけ取って手をつけない人は、それがどれぐらい迷惑な行為か早く学んでほしい。

template


生PHPが流行らしいです。
レイアウトとかどうするんだろう。
ヘッダとフッタを別々に読み込むの?

form


symfonyのformはDjangoと似ていていいですね。
まだ面倒に感じるのはMetaclassがないからかな?
単体で探したけど、似たものはないので、symfonyを使うか、単体用に取り出すか実装するか。
あるいは別の道を探すか。
inputタグなどを出力するためのHelper関数は多く準備されているようだけど、
ValidationやDBからの出力をこちらで考慮しなければならないのは面倒。
DjangoのFormは優秀。

Routing


Rails-likeが多いですね。
/controller/action/id
というやつね。
正規表現Routing、おまけに間接参照のやつがあれば素敵。
難しいものじゃないし、移植しようかな。

あと、ほしいのがpythonのimport
パスを考慮しながらのrequire_onceは難しい。
あと、まとめて数ファイルimportしたいときもある。

統合するとこんな感じか
urls.php
  1. import('bloggy.views.*');  
  2. import('bloggy.form.EntryForm');  
  3.   
  4. add_patterns(array(  
  5. '' => 'index',  
  6. ));  
  7. add_patterns(array(  
  8. 'new' => 'EntryForm.create',  
  9. 'entry/(?P<id>[0-9]+)' => 'EntryForm.edit',  
  10. ), 'admin_required');  
  11. </id>  

forms.php
  1. class EntryForm extends Form {  
  2.     var $conf = array(  
  3.         'name' => Form::TextField(),  
  4.     );  
  5. }  

PHPの構文が分からない、これでコンパイルとおるかな。
EntryFormをimportしたときに継承して拡張したEntryFormを渡すことってできるのかな。
同名だから無理かな。
じゃあ、定義は_EntryFormか。

やだねー、ほかの言語仕様に縛られて無理やり持ち込む人。
もっと柔軟になったほうがいいと思います。
symfonyおぼえよっと。

あと、Kohanaも結構優秀でした。
程よいimport(loading)、routing

2009年1月13日火曜日

download mass images 2

download mass images 1のつづき

トップページから巡回して4ジャンルから3枚ずつのリストの取得。
  1. def parse(url):  
  2.     print 'parse '+url  
  3.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  4.     a = soup('a', href=re.compile(r'^/images/pub/\d+/\w+\.jpg$'))  
  5.     return urljoin(url, a[-1].get('href'))  
  6.   
  7. def images(url):  
  8.     print 'image '+url  
  9.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  10.     a = soup('a', {'class':'image_a'}, href=re.compile(r'^/backgrounds/\d+$'))  
  11.     return map(lambda a:parse(urljoin(url, a.get('href'))), a[:3])  
  12.   
  13. def pages(url):  
  14.     print 'pages '+url  
  15.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  16.     a = soup('a', href=re.compile(r'^/desktop/\w+\.php$'))  
  17.     return map(lambda a:images(urljoin(url, a.get('href'))), a[:4])  
  18.   
  19. print pages(sys.argv[1])  

前回の設定を引き継ぎ、上記のようなコードを書けば一覧が取得できます。
% python wally.py http://www.backgroundsarchive.com/desktop/|xargs -n1 -P6 wget
このコマンドがが動いて、取得してくるように変更することは難しいことではないでしょう。

しかし、このコマンドは遅いですね。
ダウンロードリストを生成する時間がかかりすぎです。
Python内でもスレッドを使いたい。


そして本題
これが書きたかっただけですが、前置き長い。

まずは、Queue。
Queue#getが呼ばれると待ちます。
中身が入るまで、死ぬまで待ち続けます。
中身が入ったら、嬉々として値を返します。
格納できる値はひとつずつです。
複数の値を入れたい場合は、タプルを使います。
これを利用して、Queueに呼び出してほしい関数と引数をタプルにして詰め込んでやります。
そして一定数のスレッドを、Queueから取り出しては実行する無限ループにします。
これでThreadPool完成です。
実装してみましょう。
  1. import sys, re, urllib  
  2. from urlparse import urlparse, urljoin  
  3. from Queue import Queue  
  4. from threading import Thread, Lock  
  5. from BeautifulSoup import BeautifulSoup  
  6. THREAD_MAX = 6  
  7. class FFURLopener(urllib.FancyURLopener):  
  8.     version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5'  
  9. urllib._urlopener = FFURLopener()  
  10. q = Queue()  
  11.   
  12. def download(url):  
  13.     print 'down  '+url  
  14.     urllib.urlretrieve(url, url[url.rfind('/')+1:])  
  15.   
  16. def parse(url):  
  17.     print 'parse '+url  
  18.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  19.     a = soup('a', href=re.compile(r'^/images/pub/\d+/\w+\.jpg$'))  
  20.     q.put((download, urljoin(url, a[-1].get('href'))))  
  21.   
  22. def images(url):  
  23.     print 'image '+url  
  24.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  25.     a = soup('a', {'class':'image_a'}, href=re.compile(r'^/backgrounds/\d+$'))  
  26.     map(lambda a:q.put((parse, urljoin(url, a.get('href')))), a[:3])  
  27.   
  28. def pages(url):  
  29.     print 'pages '+url  
  30.     soup = BeautifulSoup(urllib.urlopen(url).read())  
  31.     a = soup('a', href=re.compile(r'^/desktop/\w+\.php$'))  
  32.     map(lambda a:q.put((images, urljoin(url, a.get('href')))), a[:4])  
  33.   
  34. if __name__ == '__main__':  
  35.     def loop():  
  36.         while 1:  
  37.             func, args = q.get()  
  38.             try:  
  39.                 func(args)  
  40.             except Exception, e:  
  41.                 print e  
  42.             finally:  
  43.                 q.task_done()  
  44.     for i in range(THREAD_MAX):  
  45.         w = Thread(target=loop)  
  46.         w.daemon = True  
  47.         w.start()  
  48.     pages(sys.argv[1])  
  49.     q.join()  

これで4ジャンルから3枚ずつ最大サイズを取得します。

問題がいくつかあります。
1. タプル
2. 出力の乱れ

まずタプル。
読みづらいですね。
私はたいてい括弧を読み飛ばしてしまい、悩みます。
あと、引数の数の変更にも対応できません。

q.put((func, [a1], {}))

func, args, kwargs = q.get()
func(*args, **kwargs)
にすれば引数に対応できますが、煩雑です。
ここは、ラッパーを書きましょう。

次へ続く

rst2a

django-lockのトップページで使われていたのを見て知った。

rst2a

reStructuredTextをHTMLやPDFに変換してくれるサービス。

そこのAPIを使ってこんなブックマークがあると便利。
rst2html

2009年1月9日金曜日

download mass images 1

突然壁紙がたくさんほしくなったので、
壁紙がいっぱい集まっているところからちょいといただきます。

少し眺めていれば、以下のURLのように連番が発見できます。
http://www.backgroundsarchive.com/backgrounds/9124

まあ、ざっと見て20000以下なので、そこからbashを使います。
まずは10ほどで試してみましょう。
% echo http://www.backgroundsarchive.com/backgrounds/{1..10}
なんかたくさん表示されましたね。
OK、続けて
% echo http://www.backgroundsarchive.com/backgrounds/{1..10}|xargs -n1 echo
Bravo! これで分かりましたね。

あとはBeautifulSoupでいただいてしまいましょう。
  1. import sys, re, urllib  
  2. from urlparse import urlparse, urljoin  
  3. from BeautifulSoup import BeautifulSoup  
  4.   
  5. def download(url):  
  6.    print 'down  '+url  
  7.    urllib.urlretrieve(url, url[url.rfind('/')+1:])  
  8.   
  9. def parse(url):  
  10.    print 'parse '+url  
  11.    soup = BeautifulSoup(urllib.urlopen(url).read())  
  12.    a = soup('a', href=re.compile(r'^/images/pub/\d+/\w+\.jpg$'))  
  13.    map(lambda a:download(urljoin(url, a.get('href'))), a)  
  14.   
  15. if __name__ == '__main__':  
  16.    parse(sys.argv[1])  

wally.pyとでも命名して即実行。
% time echo http://www.backgroundsarchive.com/backgrounds/{1..10}|xargs -n1 python wally.py
echo http://www.backgroundsarchive.com/backgrounds/{1..10} 0.00s user 0.00s system 42% cpu 0.001 total
xargs -n1 python wally.py 1.52s user 0.84s system 3% cpu 1:16.55 total
うーむ、信じられないほど遅い。
ブラウザだと少し早いからUAによる制限でしょうか。
まあ、しょうがない。

同時にダウンロードしちゃえば早いじゃんというのは自然な発想です。
コネクションが多すぎるとダメって聞いたことがあるので6つぐらいにしましょう。
% time echo http://www.backgroundsarchive.com/backgrounds/{1..10}|xargs -n1 -P6 python wally.py
echo http://www.backgroundsarchive.com/backgrounds/{1..10} 0.00s user 0.00s system 40% cpu 0.001 total
xargs -n1 -P6 python wally.py 1.48s user 1.45s system 11% cpu 26.645 total
これで六本同時に走ります。
うん、多少早くなりましたね。

念のため、User-Agentをかえてみます。
こんなのをimport urllibの下に書いておけばいいでしょう。
  1. class FFURLopener(urllib.FancyURLopener):  
  2.     version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5'  
  3. urllib._urlopener = FFURLopener()  

そして実行
% time echo http://www.backgroundsarchive.com/backgrounds/{1..10}|xargs -n1 -P6 python wally.py
echo http://www.backgroundsarchive.com/backgrounds/{1..10} 0.00s user 0.00s system 45% cpu 0.001 total
xargs -n1 -P6 python wally.py 1.52s user 1.32s system 82% cpu 3.428 total
げええ。
というわけで最適化に教科書なし。
遅いところに適切に対応しましょう。

さて、単純な連番ならこれでいいのですが、今回は全件取るつもりはありません。
序盤を集めたところマーブルばっかりで、動物とか風景がもっとほしい。
トップ画面からクロールしてほしい画像の一覧を作ったほうが好みの画像が集まりそうです。
トップページを見ると21ジャンルほどありますね。
巡回して21ジャンルから100枚ずつぐらいのリストを作るプログラムを書いて、
% python wally.py http://www.backgroundsarchive.com/desktop/|xargs -n1 -P6 wget
でうまくいきそうです。

予想外に長くなってしまってので次回へ続く。

2009年1月8日木曜日

Ajax file upload

byteflowのデコレータから引用していたけど、FileUploadに対応させました。

Ajaxでは
request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
が成り立つのですが、FileUploadはiframeを使いますのでつきません。
iframeでsubmitした場合、application/jsonにすると、ダウンロードが始まってしまうため、逃げます。
  1. def ajax_request(func):  
  2.     def wrapper(request, *args, **kw):  
  3.         if request.method == 'POST':  
  4.             response = func(request, *args, **kw)  
  5.         else:  
  6.             response = {'error': {'type'403,  
  7.                 'message':'Accepts only POST request'}}  
  8.         if isinstance(response, dict):  
  9.             if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':  
  10.                 class JsonResponse(HttpResponse):  
  11.                     def __init__(self, data):  
  12.                         super(JsonResponse, self).__init__(  
  13.                                 content=simplejson.dumps(data),  
  14.                                 mimetype='application/json')  
  15.                 return JsonResponse(response)  
  16.             else:  
  17.                 # for file upload  
  18.                 class IframeResponse(HttpResponse):  
  19.                     def __init__(self, data):  
  20.                         super(IframeResponse, self).__init__(  
  21.                                 content='<textarea>%s</textarea>'%simplejson.dumps(data))  
  22.                 return IframeResponse(response)  
  23.         else:  
  24.             return response  
  25.     return wraps(func)(wrapper)  


jquery.form.jsを使うと、具合が大変よろしいです。
  1. $('#image_form').ajaxForm({  
  2.     dataType: 'json',  
  3.     beforeSubmit: function() {if (!$('#icon').val()) return false},  
  4.     success: function(data) { render(data) },  
  5.     resetForm: true  
  6. })  

dataTypeは'json'で。
beforeSubmitの第1引数のformDataはnameで引けないので不便。
第2引数のjqFormで、jqForm[0].icon.valueでもOK。
もっといい指定方法は知りたい。
clearFormがなぜか効かなかったのでresetForm。

jquery.blockUI.jsで画像選択やアップロード画面を出すときれい。

jquery.js必須

ブログ作成3個のヒント

10 Killer WordPress Hacks

1. 広告は検索エンジンからの訪問にのみ表示しろ
ブックマークから来る人は広告なんて見ない、押さない。
でもどうせRSSで読むから関係ないけど。

2. 連投を防げ
連投かっこ悪い。
でもどうせならタイトルをSlugFieldとかにして、同じタイトルのものがないほうがインデックスとかもろもろで気持ちいいな。

3. NextやPrevはやめて、Paginationにしよう。
1,2,3,...Lastみたいなやつね。
AutoPagerが効かなくなる可能性がありそうだけど、<link rel="next">とかに対応してくれたら幸せだね。

など。
ほかはどうでもよかった。

2009年1月5日月曜日