IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

WebKit サーバーというものを作ってみた

みなさん

お久しぶりですヽ(´ー`)ノ夏休みの宿題終わりました?
毎日が夏休みの最終日みたいな生活してるあまちゃんです!

さてさて

今日は WebKit サーバーというものを作ってみたので、紹介してみます。

WebKit って何?

WebKit っていうのは ChromeSafari の中に入ってるブラウザのエンジンのことです!
実はブラウザっていうのは、エンジン部分と見た目の部分(タブとかボタンとかね)に別れていて、意外と違うブラウザでもエンジン部分は同じものを使ってるってことも多いんですよ(*´ー`)

ブラウザのサーバーってどういうこと?

要は、サーバーサイドでブラウザを起動して 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

今回作った WebKit サーバーの特徴
  • 外部マシンや、外部プロセスとソケットで通信する
  • ライブラリとしても使える
  • サーバーなので使う度の起動や停止のコストがない
  • メモリを食うけど、メモリ豊富なマシンが一台あれば良い
  • Xvfb をインストールするマシンが一台で良い
  • 重い JavaScriptWebKit の割り込みが発生した場合、ちゃんと教えてくれる
  • WebKit を操作するときに、タイムアウト時間を設定できる
  • 一つのコネクションに対して一つのウェブページオブジェクト
  • 行ベースの JSON プロトコル
  • シングルプロセス、シングルスレッド、イベントループによる非同期処理

起動のしかた

とりあえず、デフォルトの設定でインストールしたての 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 と答えます。ちょっと時間がかかります><

2. コードを持ってくる

git で WebKit サーバーのコードを持ってきます

$ git clone git://github.com/amachang/webkitd.git
(略)
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"}}
他にも

以下のようなことが出来ます

  • スクロールの操作
  • 要素が出現するまで待つ
  • 任意の JavaScript を実行
  • 値の入力

ライブラリとしても使えます

この 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:nishiohirokazuJython 本を買えば完璧ですね!

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 で動くとか!すごい!
さっそくやって試してみるべ!ってことで試してみた!

ちゃんと動いてます!

http://dankogai.appdrop.com/
おおおお

Google App Engine のアカウントがない人は

とりあえず appdrop で試してみてはいかがでしょうか?

俺も俺も! Google App Engine!

これを見て

Google App Engine SDKを使ってみた | 秋元@サイボウズラボ・プログラマー・ブログ
俺も俺もやりたいよ><
ってことで骨髄反射的に

今からインストールするよ!


ダウンロード完了

インストーラ起動

ひたすら「continue
インストール先選んで

パスワード入れて

はい、完了

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 

脱線し過ぎだろ常識的に考えて><

というわけで、もう一個くらい 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 温泉に行ってみることにしました。

Pythonista の皆様

まだまだ Python 初心者ですが、よろしこです><
いじめないでね><
いや、むしろ色々教わるためにはいじめられたほうがいいのか!
ライフハック!ライフファッk(自重しました

Google App Engine で Tropy っぽいやつ作ってみた

Google App EngineSDK

何か作ってみよう!
というわけで、 Tropy みたいなやつを作ってみる
python で 20 行以上のプログラムを書くのはたぶん初めてだ

Tropy とは

以下が詳しいです。
Tropyとは - はてなキーワード
ちなみに、以下のスクリーンショットid:naoya さんが作った Tropy のクローンの Haropy です。

で、僕もそんな感じのものを作ってみた

ソースを晒しておきます。

ファイル構成
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

動かしてみる


わーいわーい動いたー
あれ、複数行入れるとエラーになるなあ。たぶん、モデル定義のといに型の指定を間違えたっぽい><
でも、まあ、そこは本質的な作業じゃないので間違えたままにしておきます。
あれ、タイムスタンプもおかしいな。まあいいや。