2010年12月29日水曜日

ある重さを量るのに使う錘の組み合わせは何通り?

このブログ記事をはてなブックマークに追加

問題: 1, 5, 10, 50, 100, 500グラムの6種類の錘があります。それぞれの錘はいくつでも使うことができます。ある重さを量るときに使う錘の組み合わせは何通りになりますか。それを求めるプログラムを書いてください。ここで量る重さはグラム単位の整数とし、最大で100キログラムとします。10グラムの重さを量る場合、10グラムの錘が1個、5グラムの錘が2個、5グラムの錘1個と1グラムの錘が5個、1グラムの錘が10個の組み合わせになり、全部で4通りとなります。

以下、解答例。

weights.cpp

// 指定した重さを量るのに使う錘の組み合わせの数を求めるプログラム. // 錘は 1g, 5g, 10g, 50g, 100g, 500g の6種類. // 重さ 700,000g 程度までなら桁あふれせずに計算できる. #include <iostream> #include <map> #include <algorithm> #include <cstdlib> using namespace std; const int weights[] = { 1, 5, 10, 50, 100, 500 }; // 錘(昇順). const int N = sizeof(weights) / sizeof(int); // 錘の種類. // 求めた錘の組み合わせの数のメモ. // 下位3ビットは使用する錘(solveのidx)を表す. // 残りのビットは重さ(solveのv)を表す. map<int, unsigned long long> memo; // 指定した重さに対する錘の組み合わせの数を求める. // v : 重さ // idx : 使用する錘 // N-1の場合はweights[0]からweights[N-1]までの錘を使用する(すべての錘). // 2の場合はweights[0]からweights[2]までの錘を使用する(1, 5, 10の錘). // 戻り値: 組み合わせの数 unsigned long long solve(int v, int idx=N-1) { // 既に計算済みであるならその結果を返す. if (memo.find(v<<3|idx) != memo.end()) return memo[v<<3|idx]; // 重さが0になったのならば新たな組み合わせが1つ見つかった. if (v == 0) return 1; unsigned long long cnt = 0; // 錘の種類だけループを回す. for (int i = idx; i >= 0; i--) // 重さが錘よりも軽い場合は次の錘. if (v >= weights[i]) // 求めた組み合わせの数をカウントに加える. cnt += solve(v - weights[i], i); // 求めた組み合わせの数をメモしておく. memo[v<<3|idx] = cnt; return cnt; } int main(int argc, char* argv[]) { if (argc < 2) { cerr << "Usage: " << argv[0] << " [weight]" << endl; exit(1); } cout << solve(atoi(argv[1])) << endl; return 0; }

この手のアルゴリズムの問題に慣れた人なら簡単だと思う。自分はこれと同類の問題を初めて聞いたときに即座に書けなかったので自戒を込めてここで書いてみた。

ところで、このコードで使っているメモ化だが、これをしないと実用的には使うことはできない。例えば、2,000グラムの組み合わせを求めた場合、メモ化していれば自分の環境では0.002秒で答えが出るが、メモ化していないと85秒も掛かる。因みにこの場合の組み合わせは4,443,355通り。

続きを読む...

2010年11月8日月曜日

Android端末による自動通訳を一行のPythonコードで実装する

このブログ記事をはてなブックマークに追加

最近このブログではAndroidのSL4Aネタが多いのだけど、それだけ遊べるのだから仕方がない。SL4AのPythonを使えば実質一行で自動通訳プログラムを作ることもできる。日本語で喋った文章が翻訳されて英語の音声で返ってくる。

import android,urllib,urllib2,simplejson;droid=android.Android();droid.ttsSpeak(simplejson.loads(urllib2.urlopen('http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=%s&langpair=%s%%7C%s'%(urllib.quote(droid.recognizeSpeech().result.encode('utf-8')),'ja','en')).read())['responseData']['translatedText'])

このプログラムでは日本語を英語に変換するが、コード中の en を変更すれば別の外国語に通訳することもできる。例えば fr に変更すればフランス語に通訳される。ただし、通訳した外国語はGoogleによる翻訳の精度に依存するので、実用的に用いるにはもう少し精度が良くなって欲しいところだ。それでもこれだけ簡単に自動通訳プログラムを作れるのだからAndroidとSL4Aは大したものだと思う。

以下にもう少し分かりやすいコードを示しておく。ついでに翻訳結果をダイアログに表示するように変更してある。

translator.py

# -*- coding: utf-8 -*- import android,urllib,urllib2,simplejson def translator(text,from_lang,to_lang): url='http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=%s&langpair=%s%%7C%s'%(urllib.quote(text.encode('utf-8')),from_lang,to_lang) data=simplejson.loads(urllib2.urlopen(url).read()) return data['responseData']['translatedText'] droid=android.Android() text=droid.recognizeSpeech().result t=translator(text,'ja','en') droid.ttsSpeak(t) droid.dialogCreateAlert(u'翻訳結果',u"%s\n\n%s"%(text,t)) droid.dialogShow()

続きを読む...

2010年11月2日火曜日

SL4AのPythonを使ってAndroid端末に日本語を喋らせてみた

このブログ記事をはてなブックマークに追加

先日、ドコモのGALAXY Sを購入した。丁度1年前に購入したHT-03Aからの機種変更になる。Androidのバージョンは1.6から2.2となり、性能は格段に良くなった。この1年での技術の進歩には驚かされる。GALAXY SでSL4Aが利用できるかは気になるところだが、実は購入する前に既に動作することを確認していたので、その点については心配なかった。もしSL4Aが使えなかったら、多分購入しなかったと思う。

この新しいAndroid機を使って、SL4AのPythonで日本語を喋らせてみることにした。英語であればttsSpeakのAPIを使えば一発なのだが漢字かな混じりの日本語では簡単には行かない。まずは日本語の発音を取得しなくてはならないが、それについてはYahoo! デベロッパーネットワーク日本語形態素解析を使うことにした。これなら助詞の「は」も「わ」という発音であることがわかるし、後々、応用が効くだろう。

作成したソースコードは最後に示してあるが、短いコードで簡単に実装できた。使い方も簡単で、入力ダイアログから喋らせたい文章を入力するだけだ。それを形態素解析のWeb APIでXMLとして取得し、Beautiful Soupで解析している。取得した平仮名をその発音に近い英語に直すだけだ。ローマ字に置き換えるだけでは日本語として正しく発音されないので、適当にいじってある。

実用的なプログラムにしたければ、Galatea Projectなどを利用して、ちゃんとした日本語を喋らせるようにしたほうが良いのだろうけど、形態素解析を使ってみたかったし、英語を無理やり日本語にするところも面白かったので、今回はこれで良しとした。やっぱり自分が楽しめないとね。

例によって、このプログラムを使って子供相手に遊んでみた。元の文章を知っていれば問題なく聞き取れるのだが、知らないと思ったよりも聞き取れないことを逆手に取って、何を喋っているかを当てるゲームをしてみた。こんな単純なことなのだが、子供たちは思いのほか楽しんでいたようだ。インチキ外国人ぽい雰囲気が面白いらしい。

作成したソースコードは以下の通り。

speak_ja.py

# -*- coding: utf-8 -*- import android import sys,os,urllib,urllib2 from BeautifulSoup import BeautifulSoup YAHOO_APP_ID='Yahoo!デベロッパーネットワークのアプリケーションID' KEITAISO_API='http://jlp.yahooapis.jp/MAService/V1/parse?appid=%s&results=ma&sentence=%s' KANA=[u'きゃ',u'きぃ',u'きゅ',u'きぇ',u'きょ', u'くぁ',u'くぃ',u'くぅ',u'くぇ',u'くぉ', u'ぎゃ',u'ぎぃ',u'ぎゅ',u'ぎぇ',u'ぎょ', u'ぐぁ',u'ぐぃ',u'ぐぅ',u'ぐぇ',u'ぐぉ', u'しゃ',u'しぃ',u'しゅ',u'しぇ',u'しょ', u'すぁ',u'すぃ',u'すぅ',u'すぇ',u'すぉ', u'じゃ',u'じぃ',u'じゅ',u'じぇ',u'じょ', u'ちゃ',u'ちぃ',u'ちゅ',u'ちぇ',u'ちょ', u'てゃ',u'てぃ',u'てゅ',u'てぇ',u'てょ', u'とぁ',u'とぃ',u'とぅ',u'とぇ',u'とぉ', u'ぢゃ',u'ぢぃ',u'ぢゅ',u'ぢぇ',u'ぢょ', u'でゃ',u'でぃ',u'でゅ',u'でぇ',u'でょ', u'どぁ',u'どぃ',u'どぅ',u'どぇ',u'どぉ', u'にゃ',u'にぃ',u'にゅ',u'にぇ',u'にょ', u'ひゃ',u'ひぃ',u'ひゅ',u'ひぇ',u'ひょ', u'ふぁ',u'ふぃ',u'ふぅ',u'ふぇ',u'ふぉ', u'びゃ',u'びぃ',u'びゅ',u'びぇ',u'びょ', u'ぴゃ',u'ぴぃ',u'ぴゅ',u'ぴぇ',u'ぴょ', u'みゃ',u'みぃ',u'みゅ',u'みぇ',u'みょ', u'りゃ',u'りぃ',u'りゅ',u'りぇ',u'りょ', u'うぁ',u'うぃ',u'うぇ',u'うぉ'] PRON=['kya','kyi','kyu','kyei','kio', 'qa','qi','qwu','qei','qo', 'gya','gyi','gyu','gyei','gyo', 'gwa','gwi','gwu','gwei','gwo', 'sha','syi','shu','shei','sho', 'swa','swi','swu','swei','swo', 'ja','zee','juu','jei','jo', 'cha','tyi','chu','chei','cho', 'tha','thi','thu','thei','tho', 'twa','twi','two','twei','two', 'dya','dyi','dyu','dyei','dyo', 'dha','dhi','dhu','dhei','dho', 'dwa','dwi','dwu','dwei','dwo', 'nya','nyi','nyu','nyei','nyo', 'hya','hyi','hyu','hyei','hyo', 'fa','fi','foo','fei','fo', 'bya','byi','byu','byei','byo', 'pya','pyi','pyu','pyei','pyo', 'mya','myi','myu','myei','myo', 'rya','ryi','ryu','ryei','ryo', 'wha','wi','wei','wo'] KANA+=u'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽぁぃぅぇぉ' PRON+=['a','ee','oo','ae','o', 'ka','kee','koo','kei','ko', 'sa','she','soo','sei','so', 'ta','chee','tsu','tei','toe', 'na','nee','nuu','neigh','no', 'ha','hee','foo','hey','ho', 'ma','mee','moo','mei','mo', 'yah','you','yor', 'ra','ree','roo','rei','ro', 'wa','ee','ae','wo','nn', 'ga','gee','goo','gei','go', 'za','zee','zoo','zei','zo', 'da','zee','doo','dei','do', 'ba','bee','boo','bei','bo', 'pa','pee','poo','pei','po', 'a','ee','oo','ae','o'] def get_keitaiso(text): xml=urllib2.urlopen(KEITAISO_API % (YAHOO_APP_ID,urllib.quote(text.encode('utf-8')))).read() ret='' for data in BeautifulSoup(xml)('ma_result'): for r,p in zip(data('reading'),data('pos')): d,x=r.renderContents(),p.renderContents() if x=='助詞' and d=='は': ret+='わ' elif x!='特殊': ret+=d ret=ret.decode('utf-8') T={} for i in range(86): T[ord(u'ァ')+i]=unichr(ord(u'ぁ')+i) ret=ret.translate(T) for k,r in zip(KANA,PRON): ret=ret.replace(k,r+' ') rett=ret; for i,c in enumerate(ret): c=c.encode('utf-8') if c=='ー': ret=ret.replace(u'ー',rett[i-2]+' ',1) if c=='っ': if len(rett)>i+1: ret=ret.replace(u'っ',rett[i+1],1) else: ret=ret.replace(u'っ','',1) return ret droid=android.Android() text=droid.dialogGetInput(u'日本語を喋ります',u'何を喋らせますか?').result.strip() droid.ttsSpeak(get_keitaiso(text)) droid.makeToast(text)

インチキ外国人なら喋る内容もインチキっぽくしようと思い付いたので、入力した文章をGoogleの翻訳APIで英訳し、それをまた和訳するという処理を挟んでみた。例えば「一番いいのを頼む。」と入力したら「最高のお問い合わせください。」と喋る。まあ、これはジョークプログラムにしかならないけど、形態素解析を利用した人工無脳に仕立て上げたりするのも楽しいかもしれない。

上記のspeak_ja.pyに以下の赤で示したコードを追加すればいい。

fake_japanese.py

# -*- coding: utf-8 -*- import android import sys,os,urllib,urllib2,simplejson from BeautifulSoup import BeautifulSoup ### 途中省略 def translator(text,from_lang,to_lang): url='http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=%s&langpair=%s%%7C%s' % (urllib.quote(text.encode('utf-8')),from_lang,to_lang) data=simplejson.loads(urllib2.urlopen(url).read()) return data['responseData']['translatedText'] droid=android.Android() text=droid.dialogGetInput(u'日本語を喋ります',u'何を喋らせますか?').result.strip() text=translator(translator(text,'ja','en'),'en','ja') droid.ttsSpeak(get_keitaiso(text)) droid.makeToast(text)

続きを読む...

2010年10月9日土曜日

Android端末上に電子掲示板システムを構築する方法

このブログ記事をはてなブックマークに追加

最近になって日本においてもAndroid端末が続々と発表されて巷で賑わいを見せている。ドコモからはGALAXY S、auからはIS03、ソフトバンクからはDesire HDと立て続けに発売される。このほかにも年末までにタブレット端末を含めた複数のAndroid端末が発売されるようで結構なことだ。


Androidも盛り上がってきたし、Android端末を使って手軽で役に立つことができないか考えてみた。そこで今回、Android端末をウェブサーバにして、そこに電子掲示板システム(BBS)を構築してみた。これさえあれば不意にプライベートなBBSを使いたくなったときにいつでも利用することができる。たとえばライトニングトークやセミナーなどのようなイベントの参加者からその場でちょっとしたアンケートや感想を貰いたいときに便利ではないかと思う。ちょっとした話題作りにもなるかも。

電子掲示板システムの作成に必要なものはAndroid端末とSL4A/Pythonだけ。あとは今回作成した android_bbs.py を使えばいい。そして、SL4Aを使ってサーバを立ち上げてから、android_bbs.pyを起動すれば完了だ。SL4Aによるサーバの起動方法とアドレス取得についてはPythonを使ってAndroid端末を5分でリモートカメラにする方法で書いたのでそちらを参考にして欲しい。

今回の電子掲示板システムはPythonを使って即席で作ったこともあってエラー処理などを入れずに30行ちょいのシンプルな作りになっている。sqlite3モジュールによるSQLiteデータベースで記事を管理し、wsgiref.simple_serverモジュールを使ってウェブサーバ構築した。簡単だ。ただ、Android独自の機能は使っていないのでAndroid端末専用というわけではないけど。Android独自の機能を入れるとしたら、たとえばTwitterなどの投稿者の位置情報表示に対抗して、投稿者ではなくサーバの位置、さらに向きや速度も一緒に表示されるようにするとか。誰得な機能だけど。

以下にソースコードを示す。

android_bbs.py

# -*- coding: utf-8 -*- import cgi,sqlite3,datetime from wsgiref.simple_server import make_server LIMIT=50 # 最大表示記事数. DB_FILE='/sdcard/bbs.sqlite' con=sqlite3.connect(DB_FILE) cur=con.cursor() cur.execute('CREATE TABLE IF NOT EXISTS bbs (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, user TEXT, datetime TEXT, data TEXT)') INSERT_DB='INSERT INTO bbs VALUES(NULL,?,?,?)' def post(user,data): if data=='': return if user=='': user='匿名' dt=datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') cur.execute(INSERT_DB,(cgi.escape(user.decode('utf-8')),dt,cgi.escape(data.decode('utf-8')).replace('\n','<br />'))) con.commit() def bbs(environ,start_response): if environ['PATH_INFO']=='/': if environ['REQUEST_METHOD']=='POST': fs=cgi.FieldStorage(fp=environ['wsgi.input'],environ=environ,keep_blank_values=1) post(fs.getfirst('user','').strip(),fs.getfirst('data','').strip()) data=u'<html><head><title>BBS by Android</title></head><body><form action="/" method="post"><div><textarea name="data" cols="40" rows="5"></textarea></div><span>名前:<input type="text" name="user" size="20" maxlength="30" /></span> <span><input type="submit" name="submit" value="送信" /></span> <span><input type="button" value="更新" onclick="location.reload(true);" /></span></form>' cur.execute('SELECT * FROM bbs ORDER BY id DESC') for i, row in enumerate(cur): if i>=LIMIT: break data+=('<div><p>%d <b>%s</b> %s</p><p>%s</p></div>' % row) data+='</body></html>' start_response('200 OK',[('Content-type','text/html;charset=utf-8')]) return [data.encode('utf-8')] httpd=make_server('',8080,bbs) httpd.serve_forever()

続きを読む...

2010年9月14日火曜日

Pythonを使ってAndroid端末を5分でリモートカメラにする方法

このブログ記事をはてなブックマークに追加

Android端末をリモートカメラにしてしまう方法「Spycam」という記事経由でTurn your Android Phone Into a Remote Spy Camera with Ruby in 15 Minutesを知った。SL4AのJRubyを使ってAndroid端末を15分で遠隔操作のスパイカメラにしてしまえるらしい。これは面白い。そこで、Rubyが15分ならPythonを使って5分でリモートカメラにしてしまおうと思い立った。

まず、Pythonでは標準モジュールのSimpleHTTPServerやwsgiref.simple_serverを使って簡単にWebサーバを構築することができる。そしてSL4Aを使えばAndroid端末をそのままサーバとして起動させることができる。これを組み合わせればできたも同然だ。

それで書いたのが以下のコードだ。10行そこそこでAndroid端末をWebサーバに仕立て上げ、Webブラウザ経由で写真を撮ることができる。こんなちっこい端末がWebサーバになるなんて世の中進歩したものだ。

remote_camera.py

import android from wsgiref.simple_server import make_server droid=android.Android() pic='/sdcard/snapshot.jpg' def camera(env,res): if env['PATH_INFO']=='/': droid.cameraCapturePicture(pic) res('200 OK',[('Content-type','image/jpeg')]) return [file(pic).read()] httpd=make_server('',9998,camera) httpd.serve_forever()

使い方は次の通り。まず、SL4Aを起動し、メニューボタンから"View"を選ぶとダイアログが出てくるので"Interpreters"を選択する。


再度、メニューボタンから今度は"Start Server"、"Public"を選択する。これでAndroid端末がサーバとして機能する。


次に、Android画面上部のインジケータ部分を開き、"SL4A Service"をタップする。そうするとScript Monitorが立ち上がるので、Serverのアドレスを確認する。それから先ほど作成したPythonスクリプト(remote_camera.py)を起動する。


サーバのアドレスの最後にソースコードで指定したポート(上記のコードでは9998)を加えたURLをWebブラウザに入力する。例えばサーバのアドレスが your.address.net だとすれば、 http://your.address.net:9998/ がWebブラウザに入力するURLとなる。これで遠隔操作による撮影が可能になった。ページをリロードすればその度に写真が撮影され表示される。試しに最近購入した書籍などを撮影。


これを使って次のような遊びをしてみた。Android端末を見つかりにくいところに隠し、あとでWebブラウザから写真を撮る。その画像とかすかに聞こえるシャッター音を頼りに隠したAndroid端末を探すというゲームだ。子供たち相手にやってみたが皆大喜びだった。もっと広い場所でやれば本当に楽しそうだ。必要なものはAndroid端末とWebブラウザの使えるデバイスだけだ。デバイスもPCではなくAndroid端末のようなスマートフォンで良いのだから手軽だ。

これは使い方の一例だが、Android端末とSL4Aを使えば可能性が本当に広がってくる。

続きを読む...

2010年9月13日月曜日

Android端末で青空プログラミング! SL4Aの導入からアプリケーションの作成・公開まで

このブログ記事をはてなブックマークに追加

以前、Android上でPython、Lua、JavaScriptなどを実行するスクリプティング環境ASE (Android Script Environment)について記事にした。現在ASEはSL4A (Scripting Layer for Android)に変更され、機能がさらに進化している。APIの充実やインターフェイスの改良、スクリプト環境は独立になり、HTMLインタプリタ導入やAndroidパッケージ(APK)の作成などもできるようになった。そこで改めてSL4Aのインストール方法、使い方、Pythonによるアプリケーションの作成、QRコードによるソースコードの公開方法などを紹介する。

導入

まず、SL4Aの公式サイトで最新版のSL4Aをダウンロードする。現時点ではsl4a-r2.apkだが、頻繁に更新されるので注意すること。さらに必要なスクリプト環境をダウンロードする。BeanShell, JRuby, Lua, Perl, Python, Rhino (JavaScript)などがあるが、ここではPythonを選んだ。現時点での最新版はpython_for_android_r1.apkになる。SL4Aとスクリプト環境は独立しているのでそれぞれをインストールする。

これらのAndroidパッケージは公式のものではないので、ダウンロード後インストールするためには、Android端末の設定で「アプリケーション」の「提供元不明のアプリ」にチェックを入れておく必要がある。


これでSL4AでPythonを使用するための準備は整った。

スクリプトの作成

SL4Aをインストールすると左のようなアイコンが出てくるのでそれをタップして起動する。Pythonスクリプトファイルの作成するには、メニューボタンから"Add"を選択し(スクリーンショット左側)、表示されるダイアログから"Python"を選ぶ(スクリーショット左から2番目)。そうすると、Pythonスクリプトの雛形を伴った編集画面になるので(スクリーンショット中央)、それを更新して作成する(スクリーンショット右から2番目)。ファイル名は自由に付けることができ、作成したスクリプトファイルは"Save & Run"ですぐに起動できる(スクリーンショット右側)。編集画面時のメニューボタンから"API Browser"を選択するとAPI一覧が表示される。これはコードを書く上で非常に役に立つ。


さて、ここで実際にアプリを作ってみる。しばらく前にPythonワンライナーで数独ソルバを作成したのでそれをAndroidアプリとして作り替えてみた。ワンライナーではプログラム中に問題を埋め込んでいたが、Androidではそれダイアログから入力するように変えた。入力時に電話のテンキーにすれば素早く片手で入力できるようになるが、HT-03Aではなぜか全角文字になってしまったのでそれをASCII文字に変換するテーブルも追加した。さらに、ワンライナーのようなコンソールへの出力ではバックグラウンドでの動作ができないので、ダイアログで表示させるように変更した。作成したのが以下のスクリプトだ。

sudoku_solver.py

# -*- coding: utf-8 -*- import sys,android droid=android.Android() L=[] def S(D): if 0 in D: L.append(D.index(0)) A=D[L[-1]//9*9:L[-1]//9*9+9] B=D[L[-1]%9:81:9] C=[d for n in(0,1,2)for d in D[L[-1]//27*27+L[-1]%9//3*3+n*9:L[-1]//27*27+L[-1]%9//3*3+n*9+3]] for i in set(range(1,10))-set(A+B+C): D[L[-1]]=i S(D) D[L.pop()]=0 else: M=''.join(['%d'%d+('\n' if i%9==8 else ' ')for i,d in enumerate(D)]).rstrip() droid.dialogCreateAlert(u'解答',M) droid.dialogShow() sys.exit() T={ord(u'\n'):None,ord(u' '):None,ord(u'*'):u'0',ord(u'*'):u'0',ord(u'.'):u'0',ord(u'.'):u'0'} for i in range(10): T[ord(u'0')+i]=u'%d'%i P=droid.dialogGetInput(u'数独ソルバ',u'問題を入力してください:').result.strip().translate(T) S(map(int,P))

sudoku_solver.pyを起動するにはファイル一覧からsudoku_solver.pyをタップする。すると以下のような5つのアイコン(左から順に、コンソール上での起動、バックグラウンドでの起動、編集、ファイル名の変更、削除)が現れるのでそのうち左の2つのアイコンのどちらかを選ぶことで起動できる。今回はバックグラウンドで起動した。


起動後、以下のようなダイアログが出てくるので問題となる数独を入力する。空白部分は * もしくは . とする。


以下が入力が完了した状態。ここでOKボタンを押すと解析が開始される。


解析が完了すると以下のような解答が表示される。


スクリプトの共有

これでスクリプトによるAndroidアプリを作成することができた。作成したアプリを別の人にも使ってもらうために公開したい場合はどうすればよいのだろうか。ソースコードをそのまま公開することもできるが、それだと一々Android端末にコピーしなくてはならず、場合によっては打ち込み直すはめになり、面倒だ。そこで、QRコードを使ったスクリプトの共有がある。4,296文字という制限があるが、今回のような短いコードでは十分だ。

QRコードを作成するためにQR Code Generatorを利用する。ここのサイトで"Contents"を"Text"とし、"Text content"に、先頭行をファイル名(上述のスクリプトの場合 sudoku_solver.py )としたスクリプトのソースコードを入れる。"Barcode size"は"L"とする。これで"Generate"ボタンを押せばQRコードが生成される。

次にQRコードからソースファイルに戻す方法だが、Android端末でSL4Aを起動し、メニューボタンから"Add"を選ぶ。表示されるリストから"Scan Barcode"でQRコードを読み込むだけだ。ただし、Android 1.6以前ではバーコードスキャナーが入っていないので、Android MarketからZXingのQRコードスキャナー(無料)をインストールしておくこと。


これで簡単にコードを共有することができた。因みにこの記事の先頭に表示しているQRコードは今回作成した数独ソルバのソースコードになっている。

***

まだドラフト段階だがAndroidパッケージ(APK)の作成も可能だし、前回ではPCからadbによるコマンドラインでの操作を取り上げたが、いずれもPCとAndroid端末を繋いでの操作になり青空プログラミングという今回の趣旨から外れてしまうし、一回で説明するには記事が長くなってしまうので今回は割愛することにした。それらについては別の機会に書くかもしれない。

続きを読む...

2010年9月7日火曜日

アルゴリズムの素晴らしさに気付かせてくれたのはエラトステネスの篩だった

このブログ記事をはてなブックマークに追加

Haskellによるエラトステネスの篩(sieve of Eratosthenes)の美しい実装を見て、初めてアルゴリズムの素晴らしさに気付かせてくれたのがエラトステネスの篩だったことを思い出した。たしか中学生の頃だ。そこで、当時を懐かしみながら簡単な実装を書き留めておくことにした。とりあえず、Haskell, C++, Pythonの実装を以下に示す。コードは比較的短いが、実行効率を優先させているわけではない。

Haskell

これはHaskellのサンプルコードでよく出てくる無限リストと遅延評価による実装だが、とても分かりやすいし、美しいコードだと思う。

primes = sieve [2..] sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]

以下のように関数takeを使って必要な分だけ素数を取り出すことができる。ただし、実行効率はあまり良くない。

take 10 primes [2,3,5,7,11,13,17,19,23,29]

C++

次にC++を使って実装してみた。ここではC++0xを使っている。そのほうが簡潔で分かりやすく書けるし、これからはC++0xがより使われ、普及して欲しいという意味もある。因みにgccではバージョン4.5以降、Intelコンパイラではバージョン11.0以降、Visual C++では2010(16.0)以降でコンパイルできる。

std::vector<int> primes; for (int i = 3; i < 100; i += 2) primes.push_back(i); auto end = primes.end(); for (auto x = primes.begin(); *x * *x <= *(end-1); ++x) end = std::remove_if(x + 1, end, [&x](int p){ return p % *x == 0; }); primes.erase(end, primes.end()); for (auto p = primes.begin(); p != primes.end(); ++p) std::cout << *p << " "; std::cout << std::endl;

上記のコードでは3から100までの素数を表示する。ノートPC (Core 2 Duo T9800 2.93 GHz / Visual Studio 2010)を使って1000万までの素数を生成してファイルに書き出すのにかかった時間は約2秒だった。

そういえば以前にC++のテンプレートで素数を求めたことがあったな。エラトステネスの篩ではなかったけど。

Python

最後にPythonによる実装を示す。効率はあまり良くないかも。

primes = range(3, 100, 2) for i in range(len(primes)): if primes[i]**2 > primes[-1]: break primes[i+1:] = filter(lambda p: p % primes[i] != 0, primes[i+1:]) print primes

これをそのままワンライナーに。

python -c "import sys;globals().__setitem__('P',range(3,100,2));[P.__setslice__(i+1,len(P),filter(lambda p:p%P[i]!=0,P[i+1:]))for i in range(len(P))if i<len(P)and P[i]**2<=P[-1]];[sys.stdout.write('%d '%p)for p in P]"

上記のコードではそれぞれ、赤字の数値が求める素数の上限値となっている。

続きを読む...

2010年8月23日月曜日

Pythonワンライナーで数独を解いてみた

このブログ記事をはてなブックマークに追加

GIGAZINE数学のエキスパートが3ヶ月かけて作成した「世界一難しい数独」なるものがあったので、即席でPythonワンライナー(一行プログラム)を作ったら1秒かからず解けた。問題を作るのは大変でも解析プログラムで一瞬とは儚い。

python -c "import sys;L=[];S=lambda D:(0in D)and[L.append(D.index(0)),[(D.__setitem__(L[-1],i),S(D),D.__setitem__(L.pop(),0))for i in set(range(1,10))-set(D[L[-1]/9*9:L[-1]/9*9+9]+D[L[-1]%9:81:9]+[d for n in(0,1,2)for d in D[L[-1]/27*27+L[-1]%9/3*3+n*9:L[-1]/27*27+L[-1]%9/3*3+n*9+3]])]]or([sys.stdout.write('%d'%d+('\n'if i%9==8 else' '))for i,d in enumerate(D)],sys.exit());S([int(c)if c!='.'else 0for c in'..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97..'])"

コードの最後のリスト内の文字列が9行9列の数独の問題となっていて、上の行の数字から順に入っている。未知の部分はドット(.)で表している。上記のコードではGIGAZINEの問題となっている。

因みにPythonによるもっと効率の良いアルゴリズムについてはSolving Every Sudoku Puzzleが参考になるかも。

以下、ワンライナーによる出力結果。

1 4 5 3 2 7 6 9 8 8 3 9 6 5 4 1 2 7 6 7 2 9 1 8 5 4 3 4 9 6 1 8 5 3 7 2 2 1 8 4 7 3 9 5 6 7 5 3 2 9 6 4 8 1 3 6 7 5 4 2 8 1 9 9 8 4 7 6 1 2 3 5 5 2 1 8 3 9 7 6 4

続きを読む...

2010年7月21日水曜日

日本全国ハンバーガーショップ分布地図

このブログ記事をはてなブックマークに追加

以前、Processingを使って日本全国のコンビニ店舗の分布地図を作成したが、今回はハンバーガーショップの分布地図を作成してみた。

個人的にハンバーガーショップには小さい頃から思い入れがあって、小学生の頃、初めてマクドナルドのハンバーガーを食べたときに「なんて美味しいんだろう」と感動した覚えがある。しかし、今でこそ安価な食べ物という認識だが、当時は他の食べ物に比べて割高であまり頻繁に食べられなかった。

その後はだんだんとハンバーガーから離れていったが、モスバーガーと出会ったときに二度目の感動を覚えた。冷たくてジューシーな野菜と熱々のハンバーグが絶妙にマッチしてそれまで食べたことのないハンバーガーだった。少々食べにくくはあったが、逆にそれが溢れる美味しさを表現していたようにも思う。

最近気に入っているハンバーガーは、アトレヴィ 秋葉原2階にあるChelsea Marketのアボガドバーガーだ。少々値は張るが、普通のチェーン店に比べて味は飛び抜けていると思う。チェーン店ではないハンバーガー専門店なら他にも美味しいハンバーガーがいろいろとありそう。

閑話休題。以下に作成したプログラムを示す。


左上の店名をクリック: 分布の表示・非表示を切り替える。起動時はマクドナルドのみ表示。 右クリックしたままマウス移動: 地図を平行移動する。 左クリックしたままマウス上下移動: 地図を拡大・縮小する。

プログラムの作成方法は前回のコンビニ店舗分布地図を作成した方法とほとんど変わらない。まず、住所をネットから取得する。それをGoogle Maps APIで緯度経度変換し、さらにPythonスクリプトでUTM図法に合わせてXY座標変換する。そのデータをtar/gzipでアーカイブし、Processingを使って読み込み、日本地図に表示する。ブログへの貼り付けはProcessingでJavaアプレットにエクスポートできるのでそれを使う。

今回の分布地図を見て、マクドナルドよりもかなり後に出てきたモスバーガーの店舗が思ったより多く、全国的に展開していて驚いた。また、子供の頃に食べた森永LOVEは既に存在せずロッテリアに買収されたことも今回作成する過程で知った。他には東京に最近出現してきたバーガーキングや沖縄のみに存在するA&Wがやや気になった。沖縄のA&Wへは簡単には行けないが、バーガーキングは今度行ってみよう。

住所データさえあればコンビニ店舗やハンバーガーショップに限らずどんな分布地図でも作れることだし、何か面白い題材ないかな。

以下、ソースコード。

fastfood.pde

String shopData = "convert_xy.tsv.gz"; String japanMap = "japan.svg"; PShape mapShape; int totalCount; Place[] places; int placeCount = 0; final float minX = -0.19834; final float maxX = 0.2425; final float minY = -0.1875; final float maxY = 0.1845; PImage mapImage; PFont font; float offset_x = 0.0; float offset_y = 0.0; int center_x = 0; int center_y = 0; float zoom = 1.0; int mx_start, my_start, my_pos; String[] shopName = { "McDonald", "MOS", "Lotteria", "Freshness", "First Kitchen", "DOMDOM", "Burger King", "A&W", "Becker's" }; color[] shopColor = { #ff0000, #007fff, #00ff00, #ffff00, #ff00ff, #00ffff, #7f0000, #0000ff, #007f00 }; boolean[] shopIds = { true, false, false, false, false, false, false, false, false }; int numShops = 9; public void setup() { mapShape = loadShape(japanMap); smooth(); loop(); size(int(mapShape.width), int(mapShape.height), JAVA2D); // 539, 563 shapeMode(CORNER); mapShape.disableStyle(); font = loadFont("CourierNewPSMT-14.vlw"); textMode(SCREEN); textFont(font); readData(); } public void draw() { background(0); noStroke(); fill(32); shape(mapShape, int(offset_x * zoom) + center_x, int(offset_y * zoom) + center_y); for (int i = 0; i < placeCount; i++) places[i].draw(); for (int i = 0; i < numShops; i++) { fill(shopColor[i]); text(shopName[i], 15, 15 * (i + 1)); if (shopIds[i]) text("*", 5, 15 * (i + 1)); } } float TX(float x) { float offset = width * (1.0 - zoom) * 0.5; return map(x, minX, maxX, offset, width - offset); } float TY(float y) { float offset = height * (1.0 - zoom) * 0.5; return map(y, minY, maxY, height - offset, offset); } void mousePressed() { mx_start = int(mouseX - offset_x * zoom); my_start = int(mouseY - offset_y * zoom); loop(); for (int i = 0; i < numShops; i++) if (mouseX < shopName[i].length() * 8 + 15 && mouseY > 15 * i && mouseY < 15 * (i + 1)) shopIds[i] = !shopIds[i]; my_pos = mouseY; } void mouseReleased() { noLoop(); } void mouseDragged() { if (mouseButton == LEFT) { offset_x = (mouseX - mx_start) / zoom; offset_y = (mouseY - my_start) / zoom; } else if (mouseButton == RIGHT) { if (mouseY - my_pos > 4) { float zoom_out = 9.8 / 10.0; mapShape.scale(zoom_out); zoom *= zoom_out; center_x = int(width * (1.0 - zoom) * 0.5); center_y = int(height * (1.0 - zoom) * 0.5); my_pos = mouseY; } else if (mouseY - my_pos < -4) { float zoom_in = 10.0 / 9.8; mapShape.scale(zoom_in); zoom *= zoom_in; center_x = int(width * (1.0 - zoom) * 0.5); center_y = int(height * (1.0 - zoom) * 0.5); my_pos = mouseY; } } } void readData() { new Slurper(); } Place parsePlace(String line) { String pieces[] = split(line, '\t'); int id = int(pieces[0]); String name = pieces[1]; float x = float(pieces[2]); float y = float(pieces[3]); return new Place(id, name, x, y); }

Place.pde

class Place { int id; String name; float x, y; public Place(int id, String name, float x, float y) { this.id = id; this.name = name; this.x = x; this.y = y; } public void draw() { if (shopIds[this.id]) { int xx = (int)TX(this.x) + int(offset_x * zoom); int yy = (int)TY(this.y) + int(offset_y * zoom); set(xx, yy, shopColor[this.id]); } } }

Slurper.pde

class Slurper implements Runnable { Slurper() { Thread thread = new Thread(this); thread.start(); } public void run() { try { BufferedReader reader = createReader(shopData); String line = reader.readLine(); totalCount = int(line); places = new Place[totalCount]; while ((line = reader.readLine()) != null) { places[placeCount] = parsePlace(line); placeCount++; } } catch (IOException e) { e.printStackTrace(); } noLoop(); } }

続きを読む...

2010年7月13日火曜日

2次元セルオートマトンとエントロピー

このブログ記事をはてなブックマークに追加

2状態の2次元セルオートマトン(Cellular Automaton)をActionScript 3.0 (AS3)で作ってみた。2次元セルオートマトンで最も有名なのはライフゲーム(Conway's Game of Life)だろう。プログラムでは初期ルールをライフゲームとしている。初めにC++で書こうと思ったのだけど、Web上でグラフィカルに操作できないとつまらないと思い、AS3でFlashとして作成しwonderflに登録することにした。ついでに時間的なエントロピー変化を視覚的に認識できたら面白そうだと思ったので、それについても実装した。

今回、セルオートマトンのコードを書こうと思ったのはTCO Marathon Round 2の問題として出されたからだ。2次元セルオートマトンのサイズと初期配置、それにルールが与えられ、指定した世代で生存セルが最大になるように初期配置を変更しろという問題だ。そして、問題を解くために色々と調べているうちにセルオートマトンが面白く感じられたのでブログ記事にしてみた。とは言っても今回のコードはコンテストのような特別なチューンなどはしていない。ビット配列や剰余テーブルなどは使っているがどちらかというと読みやすさを優先させている。

作成したプログラムを以下に示す。



1-9 : ルール設定 - セルの周りの生存セル数(0-8)に対応 : '.' 死亡, '&' 変化, '+' 誕生, '=' 維持 SPACE : 一時停止 ENTER : リセット V : メッセージ表示/非表示 Z : 初期生存セルの割合を減少 X : 初期生存セルの割合を増加

最初に実行されるルールはライフゲームだ。つまり、セルの回りに2つの生存セルがあった場合、そのセルは現状「維持」となり、3つの生存セルがあればそこに生存セルが「誕生」する。生存セルが1つ以下か4つ以上の場合は死亡セルとなる。また、上端と下端、右端と左端は繋がっていて周期境界条件となっている。現在のルール設定はFlash画面の左下に[..=+.....]と表示される。[]内の9つの文字はそれぞれ左から順に、セルの回りに生存セルがない場合、1つある場合、2つある場合、…周りがすべて生存セル(8つ)の場合を表していて、. は「死亡」(死亡セルとなる)、+ は「誕生」(生存セルとなる)、= は「維持」(現在のセルのまま)、& は「変化」(死亡セルであれば生存セルとなり、生存セルであれば死亡セルとなる)となっている。ルールの変更はキーボードの1~9キーに対応しており、キーを押す毎に、.&+= と変化していく。

ルールの右隣にある数値は初期状態の生存セルの割合で、その右隣の数値は現在の生存セルの割合を示している。初期状態の生存セルの割合はXキーで増加、Zキーで減少させることができる。また、一番上に書かれているFPSは1秒間に表示されるフレーム数を示し、Nは現在の生存セル数、Hはエントロピーを示す。

ここでエントロピーについての説明をしておく。時間的なエントロピーHについては下記の情報エントロピーの式を用いた。


1世代におけるセルの変化は0→0、0→1、1→0、1→1の4通りあり、それらの確率Pkからエントロピーを求めている。またlogの底を4とすることでHを0~1にスケールしている(参考: 2次元セルオートマトンの相転移と結晶化)。

表示されるセルの色は生存セル数が多いほど赤く、少ないほど青く表示される。また、背景色はエントロピーに比例しており、エントロピーが高いほど明るい緑となり、低いほど暗くなる。

以下にいくつかのルールによる変化を例示する。

ルール [..=+.....] / 初期生存セルの割合 0.3

これはライフゲームのルールであり、初期生存セル数の割合は0.3としている。最初は生存セル数が多くエントロピーが高いので背景は比較的明るい緑となっているが、徐々に生存セル数およびエントロピーが減ってきて暗くなってきている。5万ほどいた生存セルも1万を切るぐらいに減ってしまい、活動しているセルも縮小していることが分かる。


ルール [..=++....] / 初期生存セルの割合 0.9

これはライフゲームのルールに周りが4つのセルで「誕生」するルールを加えたものだ。初期生存セル数の割合を0.9と高くして最初にほとんどのセルを死滅させている(周りの生存セル数が高いと次の世代で「死亡」するため)。これにより非常に少ない生存セルから始めることができる。このルールではほんの僅かに残った生存セルからでも驚異的な再生力を示し、最終的にはすべてに生存セルが生息し、エントロピーは0.98以上と非常に高い値を示す。


ルール [..=+====.] / 初期生存セルの割合 0.1

これは現状維持の性質を非常に強く持ったルールで、初期生存セル数の割合を0.1から始めるとスピードは遅いが侵食するように生存セルが繁殖していき、最終的には全体の75%ほどを生存セルで占める。動きはほとんど見られないのでエントロピーは0.4ほどであるが、生存セル数が多いので画面は赤くなる。


この他にも[..+=.....] 0.9[..=+=....] 0.9[..=+&....] 0.9なども面白いので試してみて欲しい。セルの繁殖する様は、実際の細胞増殖を顕微鏡で眺めているかのようであり、とても興味深い。

続きを読む...

2010年6月14日月曜日

小学3年生の授業参観でアルゴリズムに出会った

このブログ記事をはてなブックマークに追加

小学3年生の娘の日曜授業参観に行ってきた。算数の授業だ。授業の後半、以下のような問題が出された。

問題: ゴマダラチョウとトノサマバッタがあるゲームをしている。0から9までの数字の書かれたカードがそれぞれ一枚ずつ全部で10枚あって、それを使って3桁の数を2つ作り、その差をできるだけ小さくした方が勝ちとなるゲームだ。ただし百の位は0にできない。できるだけ小さくするにはどのようにカードを選べばよいだろうか。

これを約30人の生徒に考えさせていた。解き方を先に教えるということはしない。生徒が問題を考えている間、先生は生徒たちを見回り、質問などに答える。しばらくするといろいろと答えが挙がってきた。自分の娘は以下のように考えたようだ。

百の位は差が1であればどの数字でも良いので、まず十の位を最小にする数字を考えてみると、最小の数字0から最大の数字9を引いた場合が最も小さくなる。同様に一の位では0と9以外の最小・最大の数字を選ぶ。つまり1から8を引いた場合が最小になる。百の位は残りの数字カードから差が1となるものを選ぶ。解答例は「501-498」や「701-698」となり、差は3になる。

これはまさにアルゴリズムだ。Pythonであれば以下のコードと同じだろう。

digits = range(10) ten = (digits.pop(), digits.pop(0)) one = (digits.pop(), digits.pop(0)) idx = random.randrange(len(digits) - 1) hund = (digits.pop(idx), digits.pop(idx)) a = reduce(lambda x, y: x * 10 + y, map(array, (hund, ten, one))) print "%d - %d = %d" % (a[1], a[0], a[1] - a[0])

出力例:

601 - 598 = 3

小学3年生の算数だと思ってそれほど期待せずに観に行ったのだが、生徒たちはみな楽しそうに問題に取り組んでいたし、なかなか面白い授業をしているようで何だか安心した。因みに、今回のようなクラス全員で考える問題は普段からよく行っているとのことだった。

続きを読む...

2010年6月3日木曜日

スーパーコンピュータとプログラミング言語

このブログ記事をはてなブックマークに追加

世の中には星の数ほどのプログラミング言語があるかもしれないが、スーパーコンピュータで利用するとなるとかなり限られてくる。ライフサイエンス分野のデータの処理にはPython、Ruby、Perlなどのいわゆるスクリプト言語はよく使われるし、データベース関連だとJavaなどもあるだろう。しかし科学技術計算に限れば、FortranやC/C++が多数を占めると思う。やはり実行効率と既存資産の存在は大きい。

数値計算プログラムでは、とにかく速さが求められる。どれだけの速さがあれば十分かだって? この質問はナンセンスだと思う。たとえ現在の最高速度を誇るスーパーコンピュータの1億倍の速度があったとしても十分ではない。使える資源でできる範囲の計算をするだけなのだから。

自分の携わる分野では一回の計算に半年かかることもざらなので、計算速度は死活問題だ。1分で終わる処理なら倍に高速化したとしても差は30秒でしかない。しかし、半年となるとその差は3ヶ月にもなる。だから、Fortranを使うにしても、よりモダンなFortran90/95だけではなく、古めかしいが実行速度の速いFORTRAN77もよく使われる。まあ、最近は速度差も縮まってきているのでそこまでFORTRAN77にこだわらなくなってきているけど。本当にクリティカルな部分にはアセンブリ言語なども使われている。

また、汎用コンピュータのコンパイラに比べてスーパーコンピュータに最適化されたコンパイラではバグの入っていることが多く、C++でもBoostなどの外部ライブラリは避けた方が無難だ。使うにしてもGoogleのC++コーディング規約のように一部に限定して使う方が良い。実際にBoost絡みのコンパイラバグによる混乱を見るに、STLなどの標準ライブラリのみをシンプルに利用していた方がまだ安全だ(それでもバグるけど)。そうは言ってもBoostは便利なのでできるだけ早くC++0xの標準化がされてほしい。標準化されていればコンパイラベンダも言い訳できないしね。

そういえばFortressなんて言語もあるね。面白い試みではあるけれど現状で利用するのは難しいような気がするなぁ。特に冒頭でも述べた、Fortranと同程度の実行効率が出せるかという点と既存資産の代替が存在するのかという点で。因みにFortressでの"Hello, World!"は以下のように書くみたい。

hello.fss

component hello export Executable run() = println("Hello, World!") end

続きを読む...

2010年5月16日日曜日

OAuthを使ってAndroidからPythonでTwitterに投稿する

このブログ記事をはてなブックマークに追加

以前、ASE (Android Scripting Environment)を使って「AndroidからPythonでTwitterに投稿する」という記事を書いたが、来月末にTwitterのBASIC認証が廃止されるので使えなくなる。そこで、OAuthを利用するコードに書き直してみた。最近はAndroidのtwiccaがとても使いやすいのでPythonスクリプトによるTwitterへの投稿もあまりないかもしれないが、Android端末単体でOAuthを利用したTwitterの認証ができることを示す意味でも公開することにした。

まず、「コマンドラインで動作するOAuth対応TwitterクライアントをPythonで作ってみた」という記事で用意したoauth.pyとoauthtwitter.pyをAndroid機の/sdcard/ase/scripts/ディレクトリにコピーする。これはadb pushでコピーしてもいいし、SDカードに直接コピーしてもいい。oauthtwitter.pyは一部修正してあるので上述記事の該当箇所を参照して書き直して欲しい。

次に、それらのライブラリを利用して以下のようなコードを書いた。赤字で示した認証コードの部分はTwitterのサイトのOAuthクライアント登録で取得する必要がある。

tw_oauth_ase.py

#!/usr/bin/env python # -*- coding: utf-8 -*- import os, pickle, time, android from oauthtwitter import * CONSUMER_KEY = "CONSUMER_KEY" CONSUMER_SECRET = "CONSUMER_SECRET" KEY_FILE = "/sdcard/ase/scripts/twitter_key.dat" droid = android.Android() def twitter(): if os.path.isfile(KEY_FILE): access_token = pickle.load(file(KEY_FILE)) else: tw = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET) request_token = tw.getRequestToken() authorization_url = tw.getAuthorizationURL(request_token) droid.view(authorization_url) tw = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, request_token) time.sleep(60) oauth_verifier = droid.getInput(u"What is the PIN?", u"暗証番号を入力してください")["result"].strip() access_token = tw.getAccessTokenWithPin(oauth_verifier) pickle.dump(access_token, file(KEY_FILE, "w")) return OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, access_token) def main(): tw = twitter() post = droid.getInput(u"Tweet", u"いまどうしてる?")["result"].strip() if post: tw.PostUpdate(post.encode("utf-8")) droid.makeToast(post) if __name__ == "__main__": main()

使い方は以下の通り。一番最初の起動時だけOAuthによる認証をする必要がある。

まずはASEを起動して以下の画面で先に示したtw_oauth_ase.pyを実行する。


実行させると以下のようにブラウザが起動してTwitterのOAuth認証画面が表示される。


「許可する」を選ぶと以下の画面が表示される。


表示された暗証番号をメニューボタンから「テキストを選択してコピー」を選び、番号をコピーする。ここまでの作業を一分以内に完了させる必要がある。ただし、tw_oauth_ase.pyのtime.sleep(60)の数値(秒)を設定することで完了させるまでの時間を変更することができる。


一分後に以下のダイアログボックスが表示されるので、クリップボードにコピーした暗証番号を入力する。クリップボードから入力するにはテキストエリアを長押しすれば良い。


認証作業が完了すると以下のメッセージ入力ダイアログボックスが表示される。


入力したメッセージがAndroid画面に表示され、Twitterに投稿される。次以降の起動時には最初から上記のダイアログボックスが表示され、認証作業の必要はない。

追記(2010/5/16):

twitter.pyは日本語だと140文字の半分ほどでツイートできなくなってしまうので、文字コードをutf-8、len(status)len(status.decode("utf-8"))に変更してバイトコンパイル、Android端末の/sdcard/ase/extras/にコピーして使っている。

続きを読む...

2010年5月12日水曜日

C++0xでYコンビネータ

このブログ記事をはてなブックマークに追加

stackoverflow.comFixed point combinators in C++ (C++による不動点結合子)に載っていたC++とboostで書かれたYコンビネータ(Y combinator)のサンプルコードをC++0xを使って書き直してみた。C++0xだと標準ライブラリだけでこれだけ簡潔に書ける。

#include <iostream> #include <functional> using namespace std; // Y-combinator for the int type function<int(int)> y(function<int(function<int(int)>, int)> f) { return bind(f, bind(&y, f), placeholders::_1); } int main() { // Y-combinator compatible factorial auto fact = [](function<int(int)> f, int v){ return v == 0 ? 1 : v * f(v - 1); }; auto factorial = y(fact); cout << factorial(5) << endl; return 0; }

続きを読む...

2010年5月6日木曜日

一行でテキストに含まれるURLをすべて短縮URLに変換するPythonスクリプト

このブログ記事をはてなブックマークに追加

テキストに含まれるURLをすべてbit.lyの短縮URLに変換するPythonワンライナーを書いた。使用しているモジュールはre、urllib、urllib2、simplejson。コードに一行追加するだけだし、Twitterなどのメッセージを処理するのに便利だと思う。

for link in sorted(re.findall(r"(http(?:s?)\:\/\/(?!bit\.ly[\/\ ])[^\/\ ]+\/?.*?)(?:[\ <>\"\{\}\|\\\^\[\]\`]|$)", msg), reverse=True): msg = (lambda x: re.sub(re.escape(x), (lambda y: str(simplejson.loads(urllib2.urlopen("http://api.bit.ly/shorten?version=2.0.1&longUrl=%s&login=BITLY_ID&apiKey=BITLY_API_KEY" % urllib.quote(y)).read())["results"][y]["shortUrl"]))(x), msg))(link)

bit.lyのAPIキーは別途取得しておく必要がある。

msg = u"ここのブログのURLはhttp://handasse.blogspot.com/ です。記事のURLはhttp://handasse.blogspot.com/2010/05/urlurlpython.html です。"

としてURLを含むメッセージを処理すると以下のように変換される。

print msg ここのブログのURLはhttp://bit.ly/PQa9F です。記事のURLはhttp://bit.ly/cVRVu6 です。

上記のコードを展開したものが以下のコード。簡単なエラー処理を追加してある。

def bitly(x): try: data = urllib2.urlopen("http://api.bit.ly/shorten?version=2.0.1&longUrl=%s&login=BITLY_ID&apiKey=BITLY_API_KEY" % urllib.quote(x)).read() return str(simplejson.loads(data)["results"][x]["shortUrl"]) except: return x replace_url = lambda x: re.sub(re.escape(x), bitly(x), msg) links = sorted(re.findall(r"(http(?:s?)\:\/\/(?!bit\.ly[\/\ ])[^\/\ ]+\/?.*?)(?:[\ <>\"\{\}\|\\\^\[\]\`]|$)", msg), reverse=True) for link in links: msg = replace_url(link)

続きを読む...

2010年5月2日日曜日

コマンドラインで動作するOAuth対応TwitterクライアントをPythonで作ってみた

このブログ記事をはてなブックマークに追加

6月末にTwitter APIのBASIC認証が終了してしまうので、OAuth対応のTwitterクライアントを作ってみることにした。とりあえず最もシンプルだと思われるコマンドラインで動作するクライアントをPythonで作成してみた。

まず、Twitterクライアントを作成する前に、TwitterのサイトでOAuthクライアントの登録を行わなくてはならない。ここで、「アプリケーーション名」、「アプリケーションの説明」、「アプリケーションのウェブサイトURL」を記入する必要がある。また、今回はPC上で実行するクライアントで読み書きを行いたかったので、「あなたの招待状」には「送信」、"Default Accdess type"には"Read & Write"を選択した。

登録を済ますと、"Application Details"のページで"Consumer key"と"Consumer secret"が与えられるので、これを作成するアプリケーションで利用する(CONSUMER_KEY, CONSUMER_SECRET)。

次にコーディングだが、できるだけ短くシンプルに作りたかったので、Python外部モジュールのtwitteroauthoauthtwitterを利用させてもらうことにした。ただ、作者のページのコードでは途中でエラーになってしまうので、 oauthtwitter.pyのOAuthApiクラスのgetAccessTokenメソッドの下に以下のgetAccessTokenWithPinメソッドを追加した。

def getAccessTokenWithPin(self, pin, url=ACCESS_TOKEN_URL): token = self._FetchUrl(url, parameters={"oauth_verifier": pin}, no_cache=True) return oauth.OAuthToken.from_string(token)

今回作成したTwitterクライアントの使い方だが、

tw.py

で最新のタイムラインを表示する。取得件数はここでは20件にしているが、ソースコード中の引数で変更することができる。Twitterへの投稿は以下のようにすれば良い。

tw.py "つぶやき"

最初の起動時に、アクセストークンを取得するための認証URLが表示される。それをブラウザで開いて表示された認証番号(PIN)を入力する必要がある。取得したアクセストークンは指定されたファイル(KEY_FILE)に保存され、次回以降はそのファイルからアクセストークンを取得する。

ソースコードを以下に示す。

tw.py

#!/usr/bin/env python # -*- coding: utf-8 -*- import sys, os, pickle from oauthtwitter import * CONSUMER_KEY = "CONSUMER_KEY" CONSUMER_SECRET = "CONSUMER_SECRET" KEY_FILE = "twitter_key.dat" def twitter(): if os.path.isfile(KEY_FILE): access_token = pickle.load(file(KEY_FILE)) else: tw = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET) request_token = tw.getRequestToken() authorization_url = tw.getAuthorizationURL(request_token) print authorization_url tw = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, request_token) oauth_verifier = raw_input("What is the PIN? ") access_token = tw.getAccessTokenWithPin(oauth_verifier) pickle.dump(access_token, file(KEY_FILE, "w")) return OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, access_token) def main(args): tw = twitter() if len(args) < 2: for status in tw.GetFriendsTimeline(count=20): print status.GetUser().GetScreenName() + ":", status.GetText().encode("cp932", "replace") else: post = " ".join(args[1:]) tw.PostUpdate(post.decode("cp932").encode("utf-8")) if __name__ == "__main__": main(sys.argv)

続きを読む...

2010年4月29日木曜日

Google App EngineとSafe Browsing APIを利用して短縮URLチェッカーを作ってみた

このブログ記事をはてなブックマークに追加

最近、Twitterやはてなブックマークなどを使うことが多くなり、字数制限付きのコメントを書くことが増えた。コメント中にURLを入れたいこともままあるのだが、長いURLだと字数をオーバーしてしまう。そのため短縮URLを使っているが、コメント内に直接短縮URLを入れるのがやっかいだった。そこで、Google App Engine (GAE)を使ってシンプルで高速な短縮URL変換ツールを作成してみた。

しかし、短縮URLは悪意のあるサイトの判別が難しいという欠点がある。見た目で判断できないので開くのを躊躇してしまう。そこで、今回作成したツールでは、短縮URLに変換するだけではなく、それを元のURLに戻す機能も追加した。さらに、Google Safe Browsing APIを利用してフィッシングサイトなどの悪意あるサイトの判別も試みている。

短縮URLチェッカー

使い方はシンプルで、テキストボックスに変換したいURLを入力すればbit.lyの短縮URLがテキストボックス表示され、bit.lyの短縮URLを入力すればもとのURLが表示される。テキストボックスをクリックすれば表示されているURLが選択されるのでクリップボードへのコピーも簡単だ。テキストボックスの下には展開されたURLがリンク付きで表示されるが、もしフィッシングサイトだと疑われる場合は、以下のように警告してくれる。


さて、今回のコード作成だが、bit.lyによる短縮URLの変換は簡単だった。というのも以前にPythonスクリプトで作ったことがあるからだ。それをそのままGAEに載せればいい。しかし、Safe Browsing APIによる悪意あるサイトの検出は思ったよりも面倒だった。まず、APIの使い方がよく分からない。使う前は任意のURLをAPIで確認するのかと思っていたのだが、そうではなく、MD5でハッシュ化したリストをダウンロードして、調べたいURLをハッシュ化してそれと照らし合わせなくてはならない。今回問題になったのはそのダウンロード容量で、フィッシングサイト用データが700KB程、マルウェア用データが10MBを超えていた。GAEでは一回のダウンロード容量の上限が1MBと決まっており、フィッシングサイト用データは何とかダウンロードできても、マルウェア用データは途中で切れてしまった。仕方がないので今回はフィッシングサイト用のみを使うことにした。ただし現在、Safe Browsing APIの新しいバージョンが開発中であり、それを使えば何とかなるかもしれない。自分がちょっと試したときには何故か新しいAPIが使えなかったので、今回は見送ることにした。気が向いたら試してみるかも。

APIによりダウンロードされるリストはMD5でハッシュ化されているが、ハッシュ化される前のURLは標準化された参照URLだ。そこで、調べるURLについても標準化して参照URLを作成しなくてはならない。あまり時間を掛けたくなかったので、今回は標準化については実装しなかった。参照URLの作成についてはThejaswi Puthraya氏が作成したコードを利用させてもらった。URLの標準化についてはGoogleのサイトにも参考例が載っているので、時間ができたら作ってみたい。

ダウンロードされたリストはデータストアに格納している。ハッシュ化されたURLを個別にデータストアに入れるとデータ数が非常に多くなってしまうので、ハッシュ化リストの先頭の2桁(16進数)をインデックスにし、256分割のデータとして格納した。また、リスト更新中にデータストア上のURLの参照が行われても問題が発生しないように、データモデルを2つ用意して更新は交互に行うことにした。加えて、データストアへのアクセスを減らすためMemcacheを利用している。

フィッシングサイト用リストはcronを使って30分毎に更新している。しかし、リスト更新の際に処理が30秒を超えることがあり、そのため更新に失敗してしまうことがある。これはGAEの制限なのだが、前述のデータ取得の容量制限と合わせて、このあたりが改善されるとより素晴らしくなるのだけどなぁ。無料で使わせてもらっているのに贅沢かもしれないけど。

追記(2010/5/2): リスト更新の半分以上が30秒を超えてエラーになったので、TaskQueueによる処理に変更した。

最後に今回作成したソースコード(実装したクラスのみ)を示しておく。

class Bitly(): def __init__(self): self.apiurl = "http://api.bit.ly/%s?version=2.0.1&%s=%s&login=BITLY_ID&apiKey=BITLY_API_KEY" self.api_index = { "shorten": "longUrl", "expand": "shortUrl" } self.info_index = { "shorten": "shortUrl", "expand": "longUrl" } def is_error(self, url, url_info): return url_info["statusCode"] != "OK" or (url_info["results"][url].has_key("statusCode") and url_info["results"][url]["statusCode"] != "OK") def bitly(self, url, api): url_data = urllib2.urlopen(self.apiurl % (api, self.api_index[api], urllib.quote(url))).read() url_info = simplejson.loads(url_data) if api == "expand": url = url.replace("http://bit.ly/", "") if self.is_error(url, url_info): return "" return url_info["results"][url][self.info_index[api]] def get(self, url): if url.lower().split(":")[0] not in ("http", "https", "ftp"): url = "http://" + url bitly_url = "" if re.match(r"http://bit.ly/", url) and not re.search("[^a-z^A-Z^0-9]", url.replace("http://bit.ly/", "")): self.shorten_url = url bitly_url = self.bitly(url, "expand") self.long_url = bitly_url if not bitly_url: self.long_url = url bitly_url = self.bitly(url, "shorten") self.shorten_url = bitly_url return bitly_url class DataProperty(db.Model): phishing = db.IntegerProperty() malware = db.IntegerProperty() class PhishingDataA(db.Model): index = db.IntegerProperty() hash_urls = db.TextProperty() class PhishingDataB(db.Model): index = db.IntegerProperty() hash_urls = db.TextProperty() class MalwareDataA(db.Model): index = db.IntegerProperty() hash_urls = db.TextProperty() class MalwareDataB(db.Model): index = db.IntegerProperty() hash_urls = db.TextProperty() class ShortUrl(webapp.RequestHandler): def __init__(self): self.html = u"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-script-type" content="text/javascript" /> <meta http-equiv="content-style-type" content="text/css" /> <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" /> <title>短縮URLチェッカー</title> </head> <body> <form action="/short_url" method="post"> <div>URL: <input type="text" name="content" size="31" maxlength="1024" value="%s" onclick="this.select()" /> <input type="submit" value="変換" /></div> </form> %s <div style="font-size:xx-small;"><p>短縮URLの変換およびフィッシングサイトの判別を行います。[ver.20100502]<br /><a href="http://code.google.com/apis/safebrowsing/safebrowsing_faq.html#whyAdvisory">Advisory provided by Google.</a></p></div> </body> </html>""" def lookup_by_url(self, url): """from http://github.com/theju/safebrowsing-python""" url_re = re.compile("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") url = url.lower() url_components = url_re.match(url).groups() lookup_list = set() hostname = url_components[3] hostname_comp = hostname.split(".") if not hostname_comp: raise AttributeError("Invalid URL.") for i in xrange(len(hostname_comp) - 1): filtered_hostname_comp = ".".join(hostname_comp[i:]) lookup_list.add(filtered_hostname_comp + "/") if url_components[4]: path = url_components[4].split('/') for j in xrange(len(path) + 1): filtered_paths = '/'.join(path[:j]) if not '.' in filtered_paths: lookup_list.add(filtered_hostname_comp + "%s/" % filtered_paths) lookup_list.add(filtered_hostname_comp + url_components[4]) if url_components[5]: lookup_list.add(filtered_hostname_comp + ''.join(url_components[4:6])) if url_components[7]: lookup_list.add(filtered_hostname_comp + ''.join(url_components[4:6]) + url_components[7]) return lookup_list def get(self): self.response.out.write(self.html % ("", "")) def post(self): content = self.request.get('content') bitly = Bitly() bitly_url = bitly.get(content) dp = memcache.get("dp") if not dp: dp = DataProperty().all().get() memcache.add("dp", dp) phishing, malware = dp.phishing, dp.malware P = PhishingDataA if phishing == 0 else PhishingDataB M = MalwareDataA if malware == 0 else MalwareDataB urls = self.lookup_by_url(bitly.long_url) hash_urls = [hashlib.md5(x).hexdigest() for x in urls] is_phishing, is_malware = False, False for hash_url in hash_urls: idx = int(hash_url[:2], 16) phishing_list = memcache.get("phishing_%d" % idx) if not phishing_list: phishing_list = P().all().filter("index =", idx).get().hash_urls memcache.add("phishing_%d" % idx, phishing_list) if re.search(hash_url, phishing_list): is_phishing = True """ malware_list = memcache.get("malware_%d" % idx) if not malware_list: malware_list = M().all().filter("index =", idx).get().hash_urls memcache.add("malware_%d" % idx, malware_list) if re.search(hash_url, malware_list): is_malware = True """ link = '<a href="%s">%s</a>' % (bitly.long_url, bitly.long_url) if is_phishing: link += u'<br />フィッシングサイトの疑いがあります。<span style="font-size:xx-small;color:gray;">詳しくは<a href="http://www.antiphishing.org">AntiPhishing.org</a>を参照してください。</span>' if bitly_url: self.response.out.write(self.html % (bitly_url, "<div><p>%s</p></div>" % link)) else: self.response.out.write(self.html % ("", u"<div><p>変換できませんでした。</p></div>")) class SafeBrowsing(webapp.RequestHandler): def __init__(self): self.urls = { "phishing": "http://sb.google.com/safebrowsing/update?client=api&apikey=SAFE_BROWSING_API_KEY&version=goog-black-hash:1:-1", "malware": "http://sb.google.com/safebrowsing/update?client=api&apikey=SAFE_BROWSING_API_KEY&version=goog-malware-hash:1:-1" } def get_data(self, target, selection): idx = 0 txt = "" all_data = urllib2.urlopen(self.urls[target]).read().split() for l in all_data: l = l.strip() if len(l) == 33 and l[0] == '+': current_idx = int(l[1:3], 16) if idx != current_idx: taskqueue.add(url="/short_url/safe_browsing/process", params={"target": target, "selection": selection, "idx": idx, "txt": txt}) txt = "" idx = current_idx txt += l taskqueue.add(url="/short_url/safe_browsing/process", params={"target": target, "selection": selection, "idx": idx, "txt": txt}) def get(self): target = self.request.get("target") if target in ("phishing", "malware"): dp = DataProperty().all().get() if not dp: dp = DataProperty(phishing=0, malware=0) dp.put() dp = dp.all().get() if target == "phishing": selection = abs(dp.phishing - 1) self.get_data(target, selection) dp.phishing = selection else: selection = abs(dp.malware - 1) self.get_data(target, selection) dp.malware = selection self.response.out.write("Done: %s, %d" % (target, selection)) dp.put() memcache.delete("dp") else: self.response.out.write("Incorrect: %s" % target) class SafeBrowsingProcess(webapp.RequestHandler): def __init__(self): self.data = { "phishing": lambda (idx): PhishingDataA if idx == 0 else PhishingDataB, "malware": lambda (idx): MalwareDataA if idx == 0 else MalwareDataB } def post(self): target = self.request.get("target") selection = int(self.request.get("selection")) idx = int(self.request.get("idx")) txt = self.request.get("txt") M = self.data[target](selection) model = M().all().filter("index =", idx).get() if not model: model = M(index=idx, hash_urls="") model.hash_urls = txt model.put() memcache.delete("phishing_%d" % idx)

続きを読む...

2010年4月14日水曜日

一つのGoogle App Engineアプリケーションで複数のWaveボットを作成する

このブログ記事をはてなブックマークに追加

Google App Engine (GAE)では現在10個までのアプリケーションを登録することができる。そして、GAEを利用してGoogle Waveのボットを作成することができる。しかし、多種多様な利用方法が存在するWaveボットをGAEアプリケーションとして登録していてはすぐに上限に達してしまうだろう。そこで、一つのGAEアプリケーションで複数のWaveボットを作成するためのいくつかの方法を利用することになる。サブドメインを利用する方法が一般的だろうか。

しばらく前からサブドメインを利用して複数のWaveボットを作成しようと思っていたのだが、ちょっと面倒に思えて手を付けていなかった。そうしたら、technohippy氏によるappengine_multi_robot_runnerというライブラリが公開され、それがとても便利そうだったので簡単なWaveボットを作成してみた。ただし、Google Wave Robots API v2が3月30日にアップデートされて実装の一部が変更されたことにより、appengine_multi_robot_runner.pyがそのままでは動作しなかったので該当箇所を修正した。それについては最後に記述しておく。

追記(2010/4/15): 現在、appengine_multi_robot_runnerは最新版に更新されているので、後述の修正は必要無くなっている。

作成したのはインチキ日本語ボット(fake-japanese.robotic-wave@appspot.com)とインチキ英語ボット(fake-english.robotic-wave@appspot.com)の二つのWaveボットだ。インチキ日本語ボットでは、書き込んだ日本語を一度Googleにより英語に翻訳し、それを再度日本語に翻訳し直して表示する。インチキ英語ボットは、日本語を英語に翻訳したものを表示する。インチキと書いたが、特にでたらめにするために細工しているわけではない。現在の翻訳技術ではインチキっぽく見えてしまうというだけである。因みに、これらのボットのアイコン画像はAtnet Japan!を利用させてもらった。

どのように動作するか、以下の例文を実際に入力して動作を見てみよう。

「花便り」
「ここ数日は暖かな日が続いていますが、その後いかがお過ごしでしょうか。」
「待ちに待った桜の花もそろそろ咲き始めております。近いうちにお花見にお誘いしようと思っておりますが、ご都合はいかがでしょうか。」

インチキ日本語ボット(fake-japanese.robotic-wave@appspot.com)では以下の通り。


インチキ英語ボット(fake-english.robotic-wave@appspot.com)では以下の通り。


後述のソースコードを見てもらっても分かるように、この程度の内容であれば非常に短いコードで済んでしまう。Waveボットは一般的なネットボットよりもインタラクティブに利用でき、効果も大きいので、このように簡単に作成できることは非常に重要だと思う。簡単に作成できると言えば、technohippy氏によるas-a-robot@appspot.comを利用することで、GAEを意識せずにGoogle Wave上で簡単にWaveボットを作成することができる。

以下にWaveボットのソースコードを示す。

robotic-wave.py

#!/usr/bin/env python # -*- coding: utf-8 -*- """robotic-wave.py by nox, 2010.4.14""" import urllib, urllib2 from waveapi import robot from waveapi import events from waveapi import simplejson import appengine_multi_robot_runner def Translate(text, from_lang, to_lang): url = "http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=%s&langpair=%s%%7C%s" % (urllib.quote(text.encode("utf-8")), from_lang, to_lang) data = simplejson.loads(urllib2.urlopen(url).read()) return data["responseData"]["translatedText"] class FakeJapaneseRobot(robot.Robot): def __init__(self): robot.Robot.__init__(self, "FakeJapanese", image_url="http://robotic-wave.appspot.com/assets/edodaikan03i.gif", profile_url="http://robotic-wave.appspot.com/") self.register_handler(events.BlipSubmitted, self.OnBlipSubmitted) def OnBlipSubmitted(self, event, wavelet): blp = event.blip text = blp.text.strip() text = Translate(Translate(text, "ja", "en"), "en", "ja") blp.range(1, len(blp.text)).replace(text.encode("utf-8")) class FakeEnglishRobot(robot.Robot): def __init__(self): robot.Robot.__init__(self, "FakeEnglish", image_url="http://robotic-wave.appspot.com/assets/202aschwarzenegger.gif", profile_url="http://robotic-wave.appspot.com/") self.register_handler(events.BlipSubmitted, self.OnBlipSubmitted) def OnBlipSubmitted(self, event, wavelet): blp = event.blip text = blp.text.strip() text = Translate(text, "ja", "en") blp.range(1, len(blp.text)).replace(text.encode("utf-8")) def main(): appengine_multi_robot_runner.compound_and_run([ ("fake-japanese", FakeJapaneseRobot()), ("fake-english", FakeEnglishRobot()) ]) if __name__ == "__main__": main()

app.yaml

application: robotic-wave version: 1 runtime: python api_version: 1 handlers: - url: /_wave/(.*) script: robotic-wave.py - url: /assets static_dir: assets - url: /(.*) script: index.py

Google Wave Robots API v2が3月30日にアップデートされ実装が一部変更された。そのため、appengine_multi_robot_runner.pyが動作しなくなったのでそれを修正するために加えた変更を以下に示しておく。

変更箇所その1。

class GetHandler(appengine_robot_runner.GetHandler): def __init__(self, method, contenttype): appengine_robot_runner.GetHandler.__init__(self, method, contenttype) def get(self): self.response.headers['Content-Type'] = self._contenttype self.response.out.write(self._method(host=self.request.host))

上記のコードを以下のように変更する。

class CapabilitiesHandler(appengine_robot_runner.CapabilitiesHandler): def __init__(self, method, contenttype): appengine_robot_runner.CapabilitiesHandler.__init__(self, method, contenttype) def get(self): self.response.headers['Content-Type'] = self._contenttype self.response.out.write(self._method(host=self.request.host)) class ProfileHandler(appengine_robot_runner.ProfileHandler): def __init__(self, method, contenttype): appengine_robot_runner.ProfileHandler.__init__(self, method, contenttype) def get(self): self.response.headers['Content-Type'] = self._contenttype if self.request.get('name'): self.response.out.write(self._method(self.request.get('name'), host=self.request.host)) else: self.response.out.write(self._method(host=self.request.host))

変更箇所その2。

([('/_wave/capabilities.xml', lambda: GetHandler(robot.capabilities_xml, 'application/xml')), ('/_wave/robot/profile', lambda: GetHandler(robot.profile_json, 'application/json')),

上記のコードを以下のように変更する。

([('/_wave/capabilities.xml', lambda: CapabilitiesHandler(robot.capabilities_xml, 'application/xml')), ('/_wave/robot/profile', lambda: ProfileHandler(robot.profile_json, 'application/json')),

続きを読む...

2010年4月6日火曜日

Android Scripting Environment (ASE) Python API簡易リファレンス

このブログ記事をはてなブックマークに追加

Android Scripting Environment (ASE)について、「Android上でPython、Lua、JavaScriptなどを実行するスクリプティング環境が凄い」で紹介した。今回はAndroid端末の機能を利用するためのPython APIの使い方をリファレンスとしてまとめてみた。Python 2.6の標準モジュールは最初から利用できる。また、twitterモジュールなど、いくつかの標準外のモジュールがデフォルトでインストールされている。詳細については利用する環境で確認して欲しい。

ASE上のPythonスクリプトの編集画面でメニューボタンを押し、そこから"API Browser"で簡単なリファレンスを読むことができる。また、ASEのWikiとしてWiki pages - android-scripting、サンプルプログラムとしてtest.pyが参考になる。

以下にASE Python APIの簡易リファレンスを示すが、APIのすべてを記しているわけではない。足りない部分については随時追加していく予定だ。

Androidモジュールを使用する

import android droid = android.Android()

現在のクリップボード取得と貼り付け

clip = droid.getClipboard()["result"] droid.setClipboard("Hello, world!")

GDataの使用

import gdata.docs.service client = gdata.docs.service.DocsService() # クライアント. client.ClientLogin(username, password) # 接続. feed = client.GetDocumentListFeed() # ドキュメントリストのAtomをフィード.

GPSで現在の位置情報を取得

droid.startLocating() # 開始. location = droid.readLocation()["result"] lat = location["network"]["latitude"] # 緯度. lng = location["network"]["longitude"] # 経度. droid.stopLocating() # 停止.

位置情報取得などのイベントを受け取るタイプのAPIでは、開始直後にイベントを取得できない場合がある。そこで以下のように数回リトライできるようにする。

for i in range(10): location = droid.readLocation()["result"] if location: break time.sleep(1)

Android端末のセンサー情報を取得

droid.startSensing() # 開始. sensors = droid.readSensors()["result"] droid.stopSensing() # 停止.

センサー情報はディクショナリとして保存されている。

sensors accuracy 精度. azimuth 方位(0-360) pitch 縦の傾き. roll 横の傾き. xforce X方向の加速度. yforce Y方向の加速度. zforce Z方向の加速度. xmag X方向の磁場. ymag Y方向の磁場. zmag Z方向の磁場.

Android端末で音声出力

droid.speak("Hello, world!")

Android端末で音声認識

result = droid.recognizeSpeech()["result"] droid.makeToast(result) # 認識した語句を表示.

指定したURLでウェブブラウザを開く

droid.view("http://www.google.com/")

指定した語句をネット検索

result = droid.recognizeSpeech()["result"] # 音声認識で語句を指定. droid.webSearch(result)

通話状況の取得

droid.startTrackingPhoneState() # 開始. result = droid.readPhoneState()["result"] droid.stopTrackingPhoneState() # 停止.

サイレントモードのトグル

droid.toggleRingerSilentMode()

電話呼び出し音の音量情の取得と変更

vol = droid.getRingerVolume() droid.setRingerVolume(0) # 音量を0にする. droid.setRingerVolume(vol["result"]) # 元に戻す.

最後に取得した位置情報の取得

result = droid.getLastKnownLocation()

ジオコード情報を取得

result = droid.geocode(35.698, 139.774) # 緯度, 経度.

Wi-Fiのトグル

droid.toggleWifiState()

Android端末に文字を表示

droid.makeToast("Hello, world!")

Android端末を振動させる

droid.vibrate()

Android端末にメッセージを送る

droid.notify("Hello, world!")

実行中のAndroidパッケージの取得(com.android.phoneなど)

result = droid.getRunningPackages()

テキスト入力ダイアログの表示

result = droid.getInput("title", "message")["result"]

ボタン付きダイアログの表示

droid.dialogCreateAlert("title", "message") droid.dialogSetPositiveButtonText("Yes") droid.dialogSetNegativeButtonText("No") droid.dialogSetNeutralButtonText("Cancel") droid.dialogShow() response = droid.dialogGetResponse()["result"]

response["which"]には以下の値が入る: Yesボタン: "positive" Noボタン: "negative" Cancelボタン: "neutral"

指定したボタンが1つ(例えばdroid.dialogSetPositiveButtonText("Yes"))なら1つのボタンのみを表示する。

スピナープログレス(回転型進捗アイコン)付きダイアログ

droid.dialogCreateSpinnerProgress("title", "message") droid.dialogShow() time.sleep(2) droid.dialogDismiss()

プログレスバー付きダイアログ

droid.dialogCreateHorizontalProgress("title", "message", 50) # 50分割. droid.dialogShow() for i in range(50): time.sleep(0.1) droid.dialogSetCurrentProgress(i) droid.dialogDismiss()

リスト付きダイアログの表示

droid.dialogCreateAlert("title") droid.dialogSetItems(["foo", "bar", "baz"]) droid.dialogShow() response = droid.dialogGetResponse()["result"]

response["item"]にはリストの順に0からの数値が入る。

追記(2010/6/27):

ASE r22からreadLocation()のデータ構造が変更されたようなので、それに合わせて上述のコードを修正した。

続きを読む...

2010年3月16日火曜日

Google Waveを使わないと人生損する

このブログ記事をはてなブックマークに追加

「Google Waveを使わないと人生損する」…これはちょっと大げさかもしれないが、1年半前に書いたブログ記事「Evernoteを使わないと人生損する」で言及したEvernoteと同じぐらい、Google Waveが重要な技術になると感じたのは本当だ。当初、Google Waveは自分にとって、Google App Engine (GAE)などでプログラミングできる単なるおもちゃだった。しかしながら、今になってやっと、オンラインコラボレーションプラットフォームであるGoogle Waveの潜在能力の高さを知ることになった。

大量のメールに埋もれる情報と予定の立たないミーティング

最近、仕事上のメールのやりとりが頻繁ですぐに流されてしまうことが多く、大事な知らせを失念してしまったり、大量のメールから目的の情報を探し出さなくてはならなくなったりと、メールの扱いに手を焼いていた。さらに、直接顔を合わせるミーティングにおいても、この時期は皆忙しいようで、集まりたいときにすぐに集まれない状況が続き、何かと仕事が滞りがちになっていた。

そこで、昨年にアカウントを取得していたGoogle Waveに目を付けたのだ。今までGoogle Waveを仕事で利用しなかったのは、もともとGAEと連動させてWaveボットなどを開発するのが目的であり、コラボレーションツールとしてはあまり興味を持っていなかったからだ。それに、知人や同僚にもアカウントを持っている人がいなかった。

しかし、メールでのやりとりがひどく非効率に感じられ、必要なときにミーティングもできないというこの状況を打破すべく、Google Waveのアカウントを職場で配ってみることにした。そこでまずは、IT関連の最新技術に明るい同僚に利用してもらい、しばらく一緒に使ってみたのだが、特に問題となるような点もなく、使い勝手もそれほど悪くはなかったので、他のメンバーにもアカウントを配ることにした。

本当に新しいことは理解され難い

Google Waveアカウントを配ったメンバーに早速使ってみてもらったところ、おおむね好評を得られて…というほど甘くはなく、やはり第一印象は「難しい」、「使いづらい」、「メールでいいんじゃないの」という意見だった。誰もGoogle Waveの名前すら聞いたことがないというのだからそれも仕方が無いのだろう。これらの意見については、Google WaveコンプリートガイドによるGoogle Waveの紹介でも全く同じように述べられている。

“Google Wave の最大の欠点は、新しいユーザーが試しに使ってみた場合にあまりに理解しづらくて困惑してしまうことにあるといえます。EasierToUnderstandThanWave.comのようなパロディーサイトでは、炭素年代測定法や新古典派経済学、ポリモーダルな半音階理論といった頭脳を要求する話題でさえ、Wave に比べれば理解しやすいとされています。この冗談が真に迫っているのは、初めて Wave に触れたときにほとんどの人が困惑を経験するといえるからです。あなたがはじめて友人や同僚からうけとる wave は得てして、「なんだこれは」や「これは変なツールだね」といったものになることでしょう。” (Google Waveコンプリートガイドより引用)

そして、難しいと思わせる理由として以下の4つが挙げられている。

  • 「ドキュメントが会話」という前例のない新しいパラダイム
  • 無秩序な木構造の会話、ノンリニアなメッセージスレッド
  • ドキュメントのバージョン管理
  • Waveはまだ未完成、足りない機能の存在

確かにもっともな理由ではあるが、それでも使ってもらわないことには何も始まらないので、自分の関わっているプロジェクトのいくつかについてGoogle Waveを使ってもらうことにした。新しいパラダイムは常に受け入れられがたいものだ。

利用して初めて分かること

まず最初にしたことは、メンバーと共同で進めている自分の仕事の連絡をWaveで管理することだった。

今までは自分の担当分の結果が出たら、その報告と結果データの場所(通常はコンピュータ上のディレクトリ)などをメールで知らせていたのだが、そのやりとりが頻繁になると以前の報告やデータを把握しづらくなってくるし、ある仕事の成果を引き継いで別の仕事を行う場合、その対応付けも大変だった。また、別のメンバーが新たに参加する場合、これまでの進捗を説明するのも一苦労だ。

しかし、Waveを利用することによって、それぞれのメンバーの進捗状況は一目でわかるし、仕事のデータも簡単に取り出せる。画像やPDFなどのファイルもドラッグ&ドロップで簡単に共有できる。さらに、ToDoリストとして使うことで、既にメンバーの誰かが完了していた仕事を別のメンバーが繰り返したり、既にあるユーティリティスクリプトを作成してしまうことがなくなった。車輪の再発明の防止だ。加えて、他のメンバーの状況と照らし合わせて、ここまでは急ぎで行わなければならないとか、これはまだ急がなくても良いだろうとかの判断が、簡単にできるようになった。

次に、ある商品の名称を決める打ち合わせを例に挙げてみよう。

まずは、Waveを使わない通常のミーティングの場合、メンバー全員を同じ時間に拘束する必要があり、さらに、その時間内で名称候補を考えつかなくてはならない。しかしそれでは考えつくすべての候補が出る前に時間切れとなってしまうかもしれない。では、時間を決めずに各自に考えてもらってそれを持ち寄るのではどうだろうか。これだと、各個人がバラバラに考えているので、別メンバーのアイデアから触発されることはない。実際に自分の場合、ミーティングやメールでアイデアを募ってみたが、ほとんど反応はなかった。ミーティングでは時間が短すぎたし、メールでは良いアイデアを思いつかないまま別の仕事に追いやられてしまうわけだ。

ではWaveを使った場合はどうだろうか。まずはいくつのかの候補の議論がなされ、それに触発されて他のメンバーからも新たな候補が挙がるようになった。それによりさらにアイデアが浮かんでくる。まさにブレインストーミングだ。しかも、Waveであれば、全員が同じ情報を共有し漏れが無く、時間にも拘束されない。進行のまとめもWave上で編集していけばよいし、何かを決めるときには投票などのガジェットも使える。実際に上記で挙がった候補から最終的な名称を決めるために投票ガジェットは大いに役立った。

どれだけWaveが有用に使えるか、使用する前にすべてを見積もっていた訳ではない。車輪の再発明の防止やアイデアの触発などは、実際に導入して初めて実感できたことだった。頭では分かったつもりでも使ってみるまでは本当に分かったことにはならない。

TwitterとEvernoteで相乗効果

iPhoneとツイッターで会社は儲かる」(*1)の著者であるEC Studioの山本氏によると、会社のコミュニケーションは電話、チャット、メール、Twitterで成り立っているそうだ。電話はSkypeでもGoogle Voiceもいいし、Twitterの替りにGoogle Buzzを使ってもいいだろう。しかし、チャットとメールはGoogle Waveに置き換えられていくのではないかと感じている。もちろん、すべてのメールとチャットが置き換わるとは思っていないが、多くの部分でWaveが使われるのではないだろうか。

そして、ここで重要になってくるのが、WaveとTwitterとの共存だ。Twitterは今までにないゆるいつながりのコミュニケーションツールとしての立場を確立してる。実際に自分も使っているが、コミュニケーションツールの他に情報収集の手段としてとても有用だ。しかし、Twitterの情報はノイズが多く発散しやすい。最新の情報を肌で感じることができても、それを保存して加工する目的には向いていない。しかし、Waveを使えばそれを補うことができる。例えば、重要だと思われるTwitterの発言があったらそのポインタだけ示しておけば、その情報を共有できる。そして、それが古くなったりただのノイズだと判断したのならそれを捨てればいいだけだ。編集可能なWaveでは、チャットやメールのスパムのように邪魔になったりしない。リアルタイムの今の情報を共有できるのだ。因みに、Wave上でTwitterを動作させるボットもあるし、特定のキーワードでつぶやきを表示させるガジェットもある。

さらに、Google WaveはEvernoteとも非常に相性が良い。というのもGoogle Wave自体が複数人で共有できるEvernoteのようなものだからだ。もともと個人のWebスクラップノートとして利用されているものだからEvernoteを複数人で共有して利用するのは難しいが、Waveを利用することでその弱点を克服できる。

自分の場合、仕事ごとにEvernoteのノートを作って管理している。そして、ある仕事が共同作業であった場合、Waveを作ってそのURLをEvernoteに貼り付けておく。個人的な細々したメモや形になっていないアイデアなどはEvernoteに書いておき、皆で共有する情報はWaveに書く。Waveには他のメンバーも書き込んだり、編集したりするので情報は常に遷移していくのだが、その遷移する情報のすべてを一つのURLだけで保存できる。そして、EvernoteからWaveの情報にアクセスするのはクリック一つだ。これは画期的だ。因みに、Wave上でEvernoteを動作させるガジェットも存在する。

ここで示したように、Google WaveとTwitterとEvernoteを活用することで、とても大きな効果を期待できるだろう。

使用するにあたって

Google Waveの導入について、セキュリティを心配する声を聞く。外部にデータを持つのは不安だという。この点についても問題ない。と言うのも、Waveサーバプロトコルが公開されているからだ。自社サーバでメールサーバを立ち上げるように、Waveサーバを立ち上げることができる。現在はプレビュー版ということもありGoogle以外での実装は見かけないが、将来的には普通に立ち上げることができるだろう。

もっとも、自社でサーバを持つことと、セキュリティが確保できることとは同義ではない。セキュリティを保つためにはそれ相応の技術が必要だ。その点で自社でサーバを管理するよりも、Googleなどのような高い技術を持つ企業に任せた方が安全であるとする見方もある。もし、Gmailなどを仕事で使っているのであれば、Google Waveもそれと同程度にはセキュリティが確保されていると考えられる。

Waveを使用するにあたり、次に問題になるのは、各ユーザが感じる使い勝手だろう。いくら便利だからといって、使い勝手が悪ければ使われない。例えば、SkypeやGmailであればチャットやメールが届いたときにその旨が画面にポップアップして知らせてくれるユーティリティがある。Google WaveにもFirefoxやGoogle Chrome用にブラウザ上で知らせてくれるAdd-onがあるが、それだと常時ブラウザを立ち上げている必要がある。また、新しいWaveが来たときにメールで知らせてくれるオプションもあるが、メールの代わりにWaveを使うという目的からして本末転倒だ。そこで、Google Wave Notifierの使用をお勧めしたい。これを使えば新しいWaveが届いたときにポップアップして通知してくれる。また、Mac OS XやiPhoneならWaveboardというWaveクライアントが利用でき、通知機能もあるようだ。

どちらにしろ現在はまだプレビュー段階であり、リリースまでにはもうしばらくかかるだろう。それまでにはもっと便利でより良い環境が揃ってくることになると思う。

最後に

Google Waveはまだ正式にリリースされたわけではない。まだまだ発展途上のプレビュー版だ。機能も不完全であり、開発環境も安定していない。なので、今すぐにWaveアカウントを手に入れられなくても気にすることはないと思う。将来のGoogle Waveがどのように使えて、どのように便利なのかを知ってもらい、Google Waveが利用されることなく廃れてしまうことが私たちにとってどれだけ「損する」ことなのかを理解してもらうことが、これを記した理由だ。なので、今すぐ使い始めなければ「損する」というわけではない。しかし、それでも今すぐに使いたい方もいると思う。残念ながら自分の持っている招待枠は使ってしまったので、「Waveプレビュー版の招待を得る」などを参考にアカウントを手に入れて欲しい。

参考サイト

Google Waveの最新情報や便利な使い方を知りたい場合には、以下のサイトが参考になる。


紹介ビデオ

Google Waveの使い方が簡潔にまとめられているビデオ(約5分半)。



(*1) 「iPhoneとツイッターで会社は儲かる

ブログクラブより献本して頂いた。Twitterの利点がわかりやすくまとめられており、今回のテーマとも一部共通するので紹介させてもらった。Twitterの利点や利用方法を知りたい方は読んでおいて損はないと思う。個人的には最後の社員アンケートが面白かった。

続きを読む...