2008/04/28

はてな
床屋。もし結果同じ髪型に仕上がる床屋が2軒あったとして、片方は5分で片方は40分で仕上がる店があったとしたら。私は迷わず40分掛かる床屋に行くだろう。

髪切り落とされる様をただ見る時間。髭をそるクリームを塗られ、髭を剃られるまでの「じらし」。これも床屋の魅力であると思う。
別にしっかり肩を「パンパン」叩いてくれとも思わない。しっかり叩いたら床屋で無くなる気がする。

もしかしたら、コンピュータ業界も同じなのかもしれない。
要件定義から、すぐさま答えの出る業界ならば面白くもなんともないかもしれない。
ああでもない。こうでもないと考えてこそ面白いのであって、仕様が、そして納期が決められているからこそ面白いのだ。別の担当者が作る部品の完成をドキドキしながら待つ時間。それも床屋と同じと考えれば気が楽になったりしないだろうか。しないか...
もしかしたら気の効きすぎた、仕事を100%こなすソフトウェアというのは、実は魅力的でないのかもしれない。

プログラミング言語を取ってみても、各言語毎にそれぞれライブラリがあり目的を達成させる為の近道になる物が沢山ある。
それも良い事だとは思うけれど、気の効いてない言語もまた楽しかったりするんだろうな。

でも、髭くらいはちゃんと剃れ。

2008/04/24

はてな
書き換えてしまえ...
*** elementtree/ElementTree.py.orig Thu Apr 24 21:02:09 2008
--- elementtree/ElementTree.py  Thu Apr 24 21:34:42 2008
***************
*** 1252,1254 ****
--- 1252,1267 ----
          tree = self._target.close()
          del self._target, self._parser # get rid of circular references
          return tree
+
+ try:
+     from xml.parsers import expat
+ except ImportError:
+     import xmllib
+     import SimpleXMLTreeBuilder
+     def __init__fake(self, html=0):
+         self.__init__orig(html)
+         xmllib.XMLParser.__init__(self, accept_utf8=1)
+     clazz = SimpleXMLTreeBuilder.TreeBuilder
+     clazz.__init__orig = clazz.__init__
+     clazz.__init__ = __init__fake
+     XMLTreeBuilder = SimpleXMLTreeBuilder.TreeBuilder
ちなみにpatch当てなくても、ElementTree.pyの最下行に

try:
    from xml.parsers import expat
except ImportError:
    import xmllib
    import SimpleXMLTreeBuilder
    def __init__fake(self, html=0):
        self.__init__orig(html)
        xmllib.XMLParser.__init__(self, accept_utf8=1)
    clazz = SimpleXMLTreeBuilder.TreeBuilder
    clazz.__init__orig = clazz.__init__
    clazz.__init__ = __init__fake
    XMLTreeBuilder = SimpleXMLTreeBuilder.TreeBuilder
を足せば出来上がる。こうすると大概のものは小細工無しに動くと思う。たぶん
エラーが出てたGoogle App Engineのサンプル、muvmuvのレビュービルド「http://localhost:8080/build」も問題なく通った。
良いやり方ではないですが...

はてな
クァーってサービスが出来たみたいです。
Qaa(クァー):Twitterで簡単アンケート!(まだまだ開発中)
Qaa

Qaa(クァー)は、Twitterを利用して簡単にアンケートをとることができるサービスです。

Qaa(クァー)を通じて投稿されたアンケートはYes or No形式で回答する事ができ、その結果はアンケート毎に用意されるページでパイチャートで確認する事ができます。


http://qaa.orig.jp/h#about
twitterを使ったソーシャルアンケートサービスですね。
でもいちいちtwitterやIMから「@qaa qid14 YES」とか入力するの面倒臭いのでMinibufferコマンド作ってみました。
アンケートエントリページに行って、「q y」もしくは「q n」で「YES」もしくは「NO」の回答をtwitter経由で投稿します。
qaa_yes_or_no.user.js
よろしければどうぞ。
※一応、firefox3/greasemonkeyからBasic認証のダイアログが出ない件は対応してあります。

はてな
こんなのあるんだ...
freshmeat.net: Project details for pytumblr - ロックスターになりたい
pytumblr is a Python library for the tumblr.com API. freshmeat.net: Project details for pytumblr
Google App Engine そう言えば前に「Windowsのエクスプローラで「送る」からShareOnTumblr」なんてのも作ったなぁ。 pythonで作ってあってlinuxなんかでも動くように作ったはず。

でpytumblrですが、ソース見たら簡単なソースだったのでGoogle App Engineで動くように改造してみました。以下パッチ
--- pytumblr.py.orig    Thu Apr 24 03:15:26 2008
+++ pytumblr.py Thu Apr 24 12:02:32 2008
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
+#!-*- coding:utf-8 -*-
 
-import string, httplib, urllib2, urllib
-from xml.dom import minidom
+import urllib2, urllib
+from google.appengine.api import urlfetch
+from BeautifulSoup import BeautifulSoup
 
 class pytumblr(object):
    """Tumblr API Object.
@@ -75,14 +77,11 @@
 
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST",'/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) == 201):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) == 201):
            return 'Success'
-       elif( int(response.status) != 201):
-           raise 'Error - Status %s (%s) returned' %(response.status, response.reason)
+       elif( int(response.status_code) != 201):
+           raise 'Error - Status %s (%s) returned' %(response.status_code, 'something wrong')
 
    def auth(self):
        values = {
@@ -92,13 +91,10 @@
            }
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST", '/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) != 200 ):
-           return '< There was a tiny problem: %s (%s) >' %(response.status, response.reason)
-       if ( int(response.status) == 200 ):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) != 200 ):
+           return '< There was a tiny problem: %s (%s) >' %(response.status_code, 'something wrong')
+       if ( int(response.status_code) == 200 ):
            return '< Authenticated! >'
    
 
@@ -112,25 +108,8 @@
            opts = opts + "?type=" + type
        if ( id != 'None' ):
            opts = opts + "?id=" + id
-       return urllib.urlopen('http://%s.tumblr.com/api/read%s' %(self.user, opts)).read()
+       return urlfetch.fetch('http://%s.tumblr.com/api/read%s' %(self.user, opts)).content
        
    def getblog(self):
-       rxml = minidom.parseString(self.blogread())
-       titles = rxml.getElementsByTagName('regular-title')
-       postid = rxml.getElementsByTagName('post')
-       i = 0
-       n = 0
-       posts = {}
-       while ( i < len(postid)):
-           if ( postid[i].attributes['type'].value == 'regular' ):
-               t = titles [n]
-               t = t.toxml()
-               t = t.replace('<regular-title)', '')
-               t = t.replace('</regular-title)', '')
-               poid = postid[i].attributes["id"].value
-               posts[ poid ] = t
-               n = n + 1
-           if ( postid[i].attributes['type'].value != 'regular' ):
-               pass
-           i = i + 1
-       return posts
+       soap = BeautifulSoup(self.blogread())
+       return soap('post')
思いっきり弄ってますね。。。
minidomの代わりにBeautifulSoupを、urllib2の代わりにurlfetchを使っています。したがってgetblogは自分のregular情報のdictだけを返すのではなくBeautifulSoupを使ってlink、photo、quoteを返す様にしてあります。
実際に動くよって所は以下のサイトで確認して下さい。
pytumblr
このサイトのURLの後ろに
http://mattn.appspot.com/tumblr/mattn
と言った感じにtumblrアカウント名を付けてみて下さい。
今回のデモにはweb.pyというフレームワークを使用してみました。
以下スクリプトソースです。
#!-*- coding:utf-8 -*-
import web
from pytumblr import pytumblr
from BeautifulSoup import BeautifulSoup

def unescape(str):
  return  BeautifulSoup(str, convertEntities=BeautifulSoup.HTML_ENTITIES).contents[0].encode('utf-8', 'replace')

urls = (
  '/tumblr/(.*)', 'tumblr'
)
render = web.template.render('templates/')

class tumblr:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    template_values = { 'name': '', 'posts': [] }
    name = name.replace('/', '')
    if name:
      pt = pytumblr(name, None, None)
      template_values['name'] = name
      for blog in pt.getblog():
        if blog['type'] == 'link':
          template_values['posts'].append({
            'type': 'link',
            'text': unescape(blog('link-text')[0].string),
            'desc': blog('link-description') and unescape(blog('link-description')[0].string) or '',
            'link': unescape(blog('link-url')[0].string),
          })
        if blog['type'] == 'photo':
          template_values['posts'].append({
            'type': 'photo',
            'text': unescape(blog('photo-caption')[0].string),
            'link': unescape(blog('photo-url')[0].string),
          })
        if blog['type'] == 'quote':
          template_values['posts'].append({
            'type': 'quote',
            'text': unescape(blog('quote-text')[0].string),
            'link': unescape(blog('quote-source')[0].string),
          })
    return render.tumblr(template_values)

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
案外短く書けますね。そしてテンプレートHTML
$def with (res)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/static/css/tumblr.css" type="text/css" />
$if res['name']:
    <title>pytumblr - $res['name']</title>
$else:
    <title>pytumblr</title>
</head>
<body>
$if res['name']:
    <h1>pytumblr - $res['name']</h1>
$else:
    <h1>pytumblr</h1>
    <img src="http://b.hatena.ne.jp/entry/image/http://mattn.appspot.com/tumblr/" title="はてなブックマーク" />
    <div id="content">
$if res['name']:
    <h2>$res['name']'s tumblr</h2>
    $for post in res['posts']:
        $if post['type'] == 'photo':
            <b>PHOTO:</b>$:post['text']<br />
            <blockquote><img src="$post['link']" /></blockquote>
        $elif post['type'] == 'link':
            <b>LINK:</b><a href="$post['link']">$:post['text']</a><br />
            <blockquote class="link">$:post['desc']</blockquote>
        $elif post['type'] == 'quote':
            <b>QUOTE:</b><br />
            <blockquote class="quote">
                $:post['text']<br />
                <cite>$:post['link']</cite>
            </blockquote>
    </div>
    <hr clear="all" />
    <p style="text-align: center">provided by <a href="http://mattn.kaoriya.net">mattn</a>, hosted on google app server.</p>
</body>
</html>
このweb.pyのテンプレートって癖があってpythonインデント方式なのですが、これってpreとかcodeで先頭が入っちゃったら不味いんじゃないかと思ったり...
解決方あるのか、調べてみます。pytumblrで面白いもの作ってみて下さい。

みんなのPython みんなのPython
柴田 淳
ソフトバンククリエイティブ / ¥ 2,940 (2006-08-22)
 
発送可能時間:通常24時間以内に発送


2008/04/23

はてな
Google App Engine上でweb.pyとかCherryPyとか動かないという報告がいくらかあったのでまとめてみます。
Google App Engineでは、以下で述べられているように「WSGIに対応したCGI(と呼ばれるもの)であれば、フレームワークとして扱えるよ」と言っています。
Using the webapp Framework - Google App Engine - Google Code

The CGI standard is simple, but it would be cumbersome to write all of the code that uses it by hand. Web application frameworks handle these details for you, so you can focus your development efforts on your application's features. Google App Engine supports any framework written in pure Python that speaks CGI (and any WSGI-compliant framework using a CGI adaptor), including Django, CherryPy, Pylons, and web.py. You can bundle a framework of your choosing with your application code by copying its code into your application directory.

http://code.google.com/appengine/docs/gettingstarted/usingwebapp.html
今日はこの動かないと言われている部分を解決して頂ける(かもしれない)ポイントをご紹介。

web.py

まずweb.pyですが、オフィシャルがtarballで配布している物(web.py-0.23.tar.gz)では動きません。最新はhttp://webpy.org/bzr/webpy.dev/で配布されており、bzrを使って取得する必要があります(必要であればhttp://bazaar-vcs.org/Downloadからbzrを取得して下さい)。
bzr get http://webpy.org/bzr/webpy.dev/
これまでの1スクリプトからソケットサーバまで起動するweb.pyのコーディング習慣ではそのままでは動きません。flupを使ってWSGIモジュールを動かす必要があります。以下の様にWSGIハンドラとしてアプリケーションを作成しwsgiref.handlersに起動させます。mod_pythonの場合と同じですかね。
ソースコードの中を覗いた所、Google App Engineに対応するコードが入っています。以前までは一般的なCGIとして「print "Hello World!"」してしまえばそのまま出力されていましたが方式が変った様で、GET等はreturnで文字列として返す様になっています。
動くコードとしては以下の様になります。
#!-*- coding:utf-8 -*-
import web

urls = (
  '/hello/(.*)', 'hello'
)

class hello:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    return "Hello World!"

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
但し、「web.pyが内部でimportしているopenid.consumerが無いよ!」と怒られるのでpython-openidをいっそ入れてしまうか、以下のパッチを当てる必要があります。
--- web/__init__.py.orig    Wed Apr 23 19:12:33 2008
+++ web/__init__.py Wed Apr 23 19:12:35 2008
@@ -26,7 +26,7 @@
 from httpserver import *
 from debugerror import *
 from application import *
-import webopenid as openid
+#import webopenid as openid
 
 try:
     import cheetah
まだ開発版の様ですから、今後に期待したいです。

CherryPy

次にCherryPyですが少し小細工が必要です。Google App EngineではPure Pythonで無いものは動かないのですがCherryPyに含まれるWSGIServer(実際にはSSL機能)がsocket._fileobjectを使ってしまっていてモジュールのインポートに失敗します。以下の様にしてSSL_fileobjectを殺してやる必要があります。
--- cherrypy/wsgiserver/__init__.py.orig    Sun Jan 13 17:56:50 2008
+++ cherrypy/wsgiserver/__init__.py Wed Apr 23 16:47:34 2008
@@ -57,11 +57,12 @@
 from urllib import unquote
 from urlparse import urlparse
 
-try:
-    from OpenSSL import SSL
-    from OpenSSL import crypto
-except ImportError:
-    SSL = None
+#try:
+#    from OpenSSL import SSL
+#    from OpenSSL import crypto
+#except ImportError:
+#    SSL = None
+SSL = None
 
 import errno
 socket_errors_to_ignore = []
@@ -676,19 +677,19 @@
                 raise socket.timeout("timed out")
     return ssl_method_wrapper
 
-class SSL_fileobject(socket._fileobject):
-    """Faux file object attached to a socket object."""
-    
-    ssl_timeout = 3
-    ssl_retry = .01
-    
-    close = _ssl_wrap_method(socket._fileobject.close)
-    flush = _ssl_wrap_method(socket._fileobject.flush)
-    write = _ssl_wrap_method(socket._fileobject.write)
-    writelines = _ssl_wrap_method(socket._fileobject.writelines)
-    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
-    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
-    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
+#class SSL_fileobject(socket._fileobject):
+#    """Faux file object attached to a socket object."""
+#    
+#    ssl_timeout = 3
+#    ssl_retry = .01
+#    
+#    close = _ssl_wrap_method(socket._fileobject.close)
+#    flush = _ssl_wrap_method(socket._fileobject.flush)
+#    write = _ssl_wrap_method(socket._fileobject.write)
+#    writelines = _ssl_wrap_method(socket._fileobject.writelines)
+#    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
+#    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
+#    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
 
 
 class HTTPConnection(object):
あとはweb.py同様に
import cherrypy
import wsgiref.handlers

class OnePage(object):
  def index(self):
      return "one page!"
  index.exposed = True
 
class HelloWorld(object):
  onepage = OnePage()

  def index(self):
    return "hello world"
  index.exposed = True

def main():
  app = cherrypy.Application(HelloWorld(), "/helloworld")
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()
とすればGoogle App Engine上でも動きます。上の例にあるCherryPy独特の「/helloworld/onepage」も動きますよ!
これで幾らかのフレームワークが動くようになりました。幾らか敷居が低くなるのではないでしょうか。皆さんも色んなアプリケーションを作ってみませんか。
また時間が出来たら、残るPylonsも検証して見たいと思います。

最後に私が好きなCDを...
いけないチェリー・パイ いけないチェリー・パイ

Sony Music Direct / ¥ 1,677 (2004-07-22)
 
発送可能時間:通常24時間以内に発送


2008/04/22

はてな
Google App EngineでwebSimple.pyを使って作りました。

twitterのfollower発言で、漢字が読めなかった貴方。「@mattn_jp それなんて読むの?」とか聞くのが恥ずかしい貴方。そんな貴方にピッタリのサービスです。twitter followerの発言に「読み」を付けて表示します。(まぁ私は難しい単語なんて使いませんが...)

文字の分解にはYahoo! JAPANの「日本語形態素解析Webサービス」を使用しています。

色の変っている部分にマウスを当てると、ツールチップにて読みを教えてくれるようになっています。

よみふったー
で、いきなりですが「よみふったー」のソースです。
ライブラリとしてはwebSimple.pyのほかにPyWrapperに含まれるElementTree、BeautifulSoupを使っています。
#!-*- coding:utf-8 -*-
import os
import re
import base64
import xmllib
import logging
import elementtree.SimpleXMLTreeBuilder as xmlbuilder
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from BeautifulSoup import BeautifulSoup
from webSimple import Simple as webSimple

class MainPage(webapp.RequestHandler):
  def post(self):
    twitter_user = self.request.get('twitter_user').encode('utf-8', 'replace')
    template_valuse = {
        'twitter_user' : twitter_user,
        'statuses' : []
    }
    try:
      twitter_url = "http://twitter.com/statuses/user_timeline/%s.xml" % twitter_user
      twitter = webSimple({ 'base_url': twitter_url })
  
      yahoo = webSimple({
        'base_url': 'http://api.jlp.yahoo.co.jp/MAService/V1/parse',
        'param' : { 'appid' : 'xxxxxxxx', 'results' : 'ma', },
      })
  
      xml = twitter.get().content
      r = re.compile(r'(\&#\d+;)')
      for st in BeautifulSoup(xml)('status'):
        name = r.sub(lambda x: unichr(int(x.group(1)[2:-1])), st.user.screen_name.string).encode('utf-8', 'replace')
        msg = r.sub(lambda x : unichr(int(x.group(1)[2:-1])), st.text.string).encode('utf-8', 'replace')
        xml = yahoo.get({ 'sentence': msg }).content
        words = []
        for word in BeautifulSoup(xml)('word'):
          words.append({
            'reading' : word.reading.string.encode('utf-8', 'replace'),
            'pos' : word.pos.string.encode('utf-8', 'replace'),
            'surface' : word.surface.string.encode('utf-8', 'replace'),
          })
        template_valuse['statuses'].append({
           'screen_name' : name,
           'words' : words,
        })
    except Exception, e:
      template_valuse['error'] = e
      pass
    path = os.path.join(os.path.dirname(__file__), 'yomifutter.html')
    self.response.out.write(template.render(path, template_valuse))

  def get(self):
    path = os.path.join(os.path.dirname(__file__), 'yomifutter.html')
    self.response.out.write(template.render(path, {}))

def main():
  application = webapp.WSGIApplication([('/yomifutter/', MainPage)], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()
適当なコードで申し訳ない...汗

2008/04/21

はてな
なるべくdictぽく扱えるように作ってみました。
WedataオブジェクトのコンストラクタにAPIKEYを掘り込んで操作します。
api = Wedata('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
databases = api.databases()
for database in databases:
  print database
  for key in database.keys():
    print " %s=%s" % (key, database[key])
  print
print
また、AutoPagerizeのデータベースであれば以下の様にdataプロパティからの属性参照の様に書く事も出来ます。
database = api.database('AutoPagerize')
print "%s : %s" % (database.name, database.description)
items = database.items()
for item in database.items():
  print " %s" % item.data.pageElement
  print " %s" % item.data.insertBefore
  print
Database.create_databaseの戻り値にはデータベース名(キー名)、Item.create_itemの戻り値にはアイテムIDが戻ります。このIDを使ってDatabase.delete_datebaseおよびItem.dalete_itemを呼び出す事が出来ます。
dbid = api.create_database('my_example_database', 'my_example_database', ['name', 'description'], ['value', 'xpath'], False)
api.delete_database(dbid)
今のところGoogle App Engineには対応していません。Google App Engine上での用途を見付けたら対応するかもしれません。
いつものようにコードはcodereposに置いてあります。
/lang/python/wedata

2008/04/18

はてな
世界のナベアツにはてなブックマークで挑戦する - ネットランダム

はてなブックマークのコメントで、1から40まで数えて、3の倍数 と3がつく数字の時だけアホになります。

それではどうぞ!

http://d.hatena.ne.jp/fk_2000/20080418/p2
なんと!ソフトウェアでやるべきネタを人力で!!!

むむむ...こうなればこれをまたソフトウェアで表現するべき!
fk_2000氏と愉快な仲間達がお届けする「世界のナベアツ」

ちなみにコードはこんなの
#!-*- coding:utf-8 -*-
import os
from google.appengine.ext.webapp import template
from google.appengine.api import urlfetch
from BeautifulSoup import BeautifulSoup
import logging

try:
  res = urlfetch.fetch('http://b.hatena.ne.jp/entry/rss/http://d.hatena.ne.jp/fk_2000/20080418/p2')
  print "Content-Type: text/html; charset=UTF-8"
  print ""
  items = []
  for item in BeautifulSoup(res.content)('item'):
    logging.info(item.title)
    logging.info(item.title.string)
    items.insert(0, {
      'title' : item.title.string,
      'description' : item.description.string,
    })
  template_values = {
    'items': items
  }
  path = os.path.join(os.path.dirname(__file__), 'nabeatzz_fk_2000.html')
  print template.render(path, template_values)
except Exception, e:
  print "Status: 400"
  print ""
  print e