WebKit サーバーというものを作ってみた
さてさて
今日は WebKit サーバーというものを作ってみたので、紹介してみます。
WebKit って何?
WebKit っていうのは Chrome や Safari の中に入ってるブラウザのエンジンのことです!
実はブラウザっていうのは、エンジン部分と見た目の部分(タブとかボタンとかね)に別れていて、意外と違うブラウザでもエンジン部分は同じものを使ってるってことも多いんですよ(*´ー`)
ブラウザのサーバーってどういうこと?
要は、サーバーサイドでブラウザを起動して JavaScript を実行したり、 JavaScript が実行されないと読めないページから値を持ってくるのに使ったりしようという魂胆です。
今まではそういうのなかったの?
実は、今までは JavaScript を使わないと読めないようなページから値を持ってくるのに Phantom.js という便利なソフトウェアがあったんです。
http://www.phantomjs.org/
でも、 Phantom.js にはちょっとした問題点があって、今仕事でやってることに Phantom.js が合わなくなってしまったのです><
Phantom.js の問題点
- 他のプログラムと連携しようと思ったときに、プロセス間で通信する手段やライブラリとして呼び出す手段がない
- 度々起動したり終了したりするには重すぎる
- メモリを食う
- Xvfb を入れないといけない(これもメモリ食う)
- 重い JavaScript を実行すると WebKit の割り込みが発生するが、それを何も通知してくれないので何が起こってるのか分からなくなる。(この状態になると、プロセスを殺すしかなくなる)
- 実行をタイムアウトする手段がない
- confirm や prompt で止まってしまう(この状態になると、プロセスを殺すしかなくなる)
というわけで
これらの問題点をぜーんぶ解決しちゃうぜ!っていうのが今回作ったサーバーです。
コード
とりあえず、以下のところに置いておきました
webkitd/webkitd.py at master · amachang/webkitd · GitHub
起動のしかた
とりあえず、デフォルトの設定でインストールしたての Ubuntu 11.04 サーバーがあったとして解説しますね。
これを順にやっていけば、たぶん使えると思います。
1. 必要なものをインストールする
apt-get で必要なものをインストールします
$ sudo apt-get install git-core python-qt4 python-daemon xvfb daemon rlwrap (略) Do you want to continue [Y/n]? Y
Y と答えます。ちょっと時間がかかります><
3. Xvfb を起動する
$ sudo daemon -X "/usr/bin/Xvfb :1 -screen 0 1024x768x24" --name Xvfb $ export DISPLAY=:1.0
4. WebKit サーバーを起動する!
$ python webkitd/webkitd.py start
エラーになりませんね?おおおお!これで起動しましたね!
ちなみに、詳細なオプションは、以下のようにすると見ることが出来ます
$ python webkitd/webkitd.py -h
使ってみる!
では、さっそく使ってみましょう
本当は、ソケットを使ってプログラムを書かなければいけないのですが、 telnet というコマンドを使うとコマンドラインからソケット通信を試すことが出来るのでそれを使います
1. 接続する
以下のように打って接続します
$ rlwrap telnet 127.0.0.1 1982
以下のような状態になったら、接続できています!
Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'.
2. Yahoo にアクセスしてみる
接続できたら。
以下のように一行で書いてみてください
{ "type": "load-url", "data": { "url": "http://www.yahoo.co.jp/", "timeout": 10 } }
これは、「http://www.yahoo.co.jp/ にアクセスして 10 秒以内に完了しなかったらタイムアウトしてくれ」という意味です
成功すると以下のような結果が返されます
{"data": {"status": 200, "ok": true, "title": "Yahoo! JAPAN", "url": "http://www.yahoo.co.jp/", "finalUrl": "http://www.yahoo.co.jp/", "timeouted": false}}
おおおお!アクセスできてる
3. ページから値を取得してみる
次に値を取得してみましょう。
以下のように一行で書いてみてください
{ "type": "get-element-value", "data": { "xpath": "/descendant::a[1]", "attr": "href", "timeout": 10 } }
これは、「最初に出現したリンクの href を持ってきて」という意味です
成功すると以下のような結果が返されます
{"data": {"count": 1.0, "propValue": "http://www.yahoo.co.jp/_ylh=X3oDMTB0NWxnaGxsBF9TAzIwNzcyOTYyNjUEdGlkAzEyBHRtcGwDZ2Ex/r/mphp", "hasProp": true, "attrValue": "r/mphp", "found": true, "error": false, "hasAttr": true, "timeouted": false}}
ちゃんと、リンクを持ってこれましたね!
4. クリックしてみる
以下のようにすると最初のリンクに対してクリックをしたことにできます。
{ "type": "click-element", "data": { "xpath": "/descendant::a[1]", "timeout": 10 } }
5. 現在の状態を確認してみる
以下のようにするとブラウザの状態を確認できます。
{ "type": "server", "data": { "command": "status" } }
ちゃんとページが遷移していますね。
{"data": {"objectCount": 17, "selectedText": "", "objectCountMap": {"QUndoStack": 1, "QWindowsStyle": 1, "QSessionManager": 1, "QNetworkAccessManager": 1, "QGuiEventDispatcherGlib": 1, "WebKitPage": 1, "QWebFrame": 1, "QNetworkCookieJar": 1, "QApplication": 1, "QNativeSocketEngine": 2, "WebKitWorker": 1, "WebKitServer": 1, "QSocketNotifier": 3, "QTcpSocket": 1}, "title": "Yahoo! JAPANトップページをホームページに設定しよう", "url": "http://www.yahoo.co.jp/promotion/startpage/", "totalBytes": 23748, "height": 2083, "width": 950, "requestedUrl": "http://www.yahoo.co.jp/_ylh=X3oDMTB0NWxnaGxsBF9TAzIwNzcyOTYyNjUEdGlkAzEyBHRtcGwDZ2Ex/r/mphp"}}
ライブラリとしても使えます
この webkitd.py を今回直接実行しましたが、外部の python スクリプトから import して使うことも出来ます。
独自のコマンドや、 Java を有効にしたり、 Plugin を有効にしたり、セキュリティの設定を変えたりといろいろ拡張しやすいように作ったつもりです。
だいたいこんな感じです!
まだまだ、未完成な WebKit サーバーですが仕事で使いつつ、コードやテストをアップデートしていこうと思っていますので、生ぬるくウォッチしていただけたらなーと思います。
ではではーヽ(´ー`)ノ
Jython がおもしろい
仕事で Jython を使う機会があって
ほぼ、初めて Jython を触ったんですけど、めっちゃおもしろい。
Java のクラスが何も考えずに使えちゃう。
たとえば、 HTML (not XHTML) をパースして XPath で取得するコードとかを nekohtml と xalan で以下のように書ける
from java.io import FileInputStream from org.xml.sax import InputSource from org.cyberneko.html.parsers import DOMParser from org.apache.xpath import XPathAPI # input source = InputSource(FileInputStream('test.html')) source.setEncoding('UTF-8') # parse parser = DOMParser() parser.parse(source) doc = parser.getDocument() # xpath evaluate print XPathAPI.selectNodeList(doc, '/HTML/BODY/*').item(0)
ほんと楽に Java のクラスが使えて感動した。
てか、このサンプル java のクラス以外使ってないっていう。
後は id:nishiohirokazu の Jython 本を買えば完璧ですね!
urllib2.HTTPSHandler がないと言われる時の対処
py25-socket-ssl をインストールする
$ sudo port install py25-socket-ssl ---> Fetching py25-socket-ssl ---> Verifying checksum(s) for py25-socket-ssl ---> Extracting py25-socket-ssl ---> Configuring py25-socket-ssl ---> Building py25-socket-ssl with target build ---> Staging py25-socket-ssl into destroot ---> Installing py25-socket-ssl 2.5.2_0 ---> Activating py25-socket-ssl 2.5.2_0 ---> Cleaning py25-socket-ssl
この前作った Google App Engine 用のしょぼい Tropy クローンを AppDrop に置いてみた
ここを見て
AppDrop – Google App EngineをAmazon EC2に移植 | 秋元@サイボウズラボ・プログラマー・ブログ
うお! Google App Engine 用のコードが Amazon EC2 で動くとか!すごい!
さっそくやって試してみるべ!ってことで試してみた!
ちゃんと動いてます!
Google App Engine のアカウントがない人は
とりあえず appdrop で試してみてはいかがでしょうか?
俺も俺も! Google App Engine!
これを見て
Google App Engine SDKを使ってみた | 秋元@サイボウズラボ・プログラマー・ブログ
俺も俺もやりたいよ><
ってことで骨髄反射的に
Getting Started でも読むか
Google App Engine Documentation | App Engine Documentation | Google Cloud
Hello world できた
リアルにドキュメント読みながらやったので低速です。
早送りしながら見てね><
Hello world がこんなに簡単でした。
ちょっと partty.org を使いやすくするため jabanner をインストールします。
http://www.coins.tsukuba.ac.jp/~i0611238/pub/jabanner/
libgd をインストールしないとインストールできないようです。
libgd をインストール中。時間かかる
時間かかるー(libgd のメイクが)
jabanner のインストールにはまった
Mac だと
gettext のヘッダがないとか、色々怒られた><
以下のようにすれば入りました。
まず、 src/jabanner.cc に以下の include を書き足す
#include <libintrl.h>
で、以下のように configure && make する
$ ./configure -v --prefix=/opt/local --with-libintl-prefix=/opt/local --with-included-gettext $ LDADD="-L/opt/local/lib/ -lintl" make
jabanner と partty.org - このウェブサイトは販売用です! - partty リソースおよび情報 の相性は異常
脱線し過ぎだろ常識的に考えて><
というわけで、もう一個くらい Google App Engine のサンプルを作ってみる
以下のコードだけで Google のユーザ認証を使えるんだー。
from google.appengine.api import users user = users.get_current_user() if user: print 'Content-Type: text/plain' print '' print 'Hello, ' + user.nickname() + '!' else: # users.create_login_url('http://localhost:8080/welcome') # でログイン画面の URL を取得して、リダイレクトさせる
これは、楽だ
なんか作ってみよう
次ぎのエントリに続く
Google App Engine が楽しかったので Python 温泉にいくことを決意しました!
Google App Engine 楽しい!
ということで、勇気を出して Python 温泉に行ってみることにしました。
Google App Engine で Tropy っぽいやつ作ってみた
Google App Engine の SDK で
何か作ってみよう!
というわけで、 Tropy みたいなやつを作ってみる
python で 20 行以上のプログラムを書くのはたぶん初めてだ
で、僕もそんな感じのものを作ってみた
ソースを晒しておきます。
ファイル構成
PyGropy |-- app.yaml |-- edit.html |-- entry.html `-- pygropy.py
app.yaml
設定ファイル
application: pygropy version: 1 runtime: python api_version: 1 handlers: - url: /.* script: pygropy.py
pygropy.py
コントローラとモデルのプログラム
import os from random import random import wsgiref.handlers from google.appengine.ext import webapp from google.appengine.ext.webapp import template from google.appengine.ext import db from pprint import pprint # models class Entry(db.Model): title = db.StringProperty (required=True) body = db.StringProperty (required=True) timestamp = db.DateTimeProperty(required=True, auto_now=True) def to_hash(self): return { 'id': self.key().id(), 'title': self.title, 'body': self.body, 'timestamp': self.timestamp, } # controllers class MainPage(webapp.RequestHandler): def get(self): redirect_random_id(self) class EntryPage(webapp.RequestHandler): def get(self): id = self.request.get('id'); if id: entry = Entry.get_by_id(int(id)) path = os.path.join(os.path.dirname(__file__), 'entry.html') self.response.out.write(template.render(path, entry.to_hash())) else: redirect_random_id(self) class EditPage(webapp.RequestHandler): def post(self): id = self.request.get('id'); title = self.request.get('title'); body = self.request.get('body'); entry = False if id: entry = Entry.get_by_id(int(id)) if not(entry): entry = Entry(title=title, body=body) else: entry.title = title; entry.body = body; id = entry.put().id() self.redirect('/entry?id=' + str(id)) def get(self): id = self.request.get('id'); entry = False if id: entry = Entry.get_by_id(int(id)) if entry: vars = entry.to_hash() else: vars = {} path = os.path.join(os.path.dirname(__file__), 'edit.html') self.response.out.write(template.render(path, vars)) def redirect_random_id(handler): count = db.GqlQuery('SELECT * FROM Entry').count(); if count == 0: handler.redirect('/edit') else: id = int(random() * count) + 1 handler.redirect('/entry?id=' + str(id)) def main(): application = webapp.WSGIApplication([('/', MainPage), ('/edit', EditPage), ('/entry', EntryPage), ], debug=True) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
edit.html
編集用の html のテンプレート
<html> <head> <title>PyGropy ver.0721</title> </head> <body> <form action="/edit" method="post"> <ul> <li><input type="text" name="id" value="{{id|escape}}" /></li> <li><input type="text" name="title" value="{{title|escape}}" /></li> <li><textarea name="body">{{body|escape}}</textarea></li> <li>{{timestamp|escape}}</li> <li><input type="submit" name="_submit" value="submit" /></li> <li><a href="/">random</a></li> </ul> </form> </body> </html>
entry.html
表示用の html のテンプレート
<html> <head> <title>PyGropy ver.0721</title> </head> <body> <ul> <li>{{id|escape}}</li> <li>{{title|escape}}</li> <li>{{body|escape}}</li> <li>{{timestamp|escape}}</li> <li><a href="/edit?id={{id|escape}}">edit</a></li> <li><a href="/">random</a></li> </ul> </body> </html>
とりあえず、以下のコマンドでテストサーバが動きます。
$ dev_appserver.py ディレクトリ名 INFO 2008-04-09 00:48:48,665 appcfg.py] Checking for updates to the SDK. INFO 2008-04-09 00:48:49,392 appcfg.py] The SDK is up to date. INFO 2008-04-09 00:48:49,401 dev_appserver_main.py] Running application pygropy on port 8080: http://localhost:8080