pywebkitgtkとjswebkitでJavaScriptを実行してJavaScript実行済みのドキュメントを得る

GoogleはJavaScriptで隠匿したメールアドレスを読み取り、検索結果に表示している? | スラド ITを見て、こんなのはWebKitを使えば余裕だろ、ということでやってみました。

Cで記述してもよいのですが、ビルド設定が面倒なのでpythonでやってみます(追記: C言語でも実装しました)。
ubuntuにはpython-webkitがあるのですが、しかしこれにはまだWebKitJavaScriptCoreへアクセスするコードが入っていません。

探したところ、pywebkitgtk(python-webit)のサイトのissue trackerにアップされてたjswebkitというJavaScriptCoreアクセスライブラリを利用することで、JavaScriptを呼ぶことができました。

jswebkitビルド

ダウンロード後

tar zxf jswebkit.tar.gz
cd jswebkit
python setup.py build
cp build/lib.linux-i686-2.6/jswebkit.so ..

ビルドに必要なパッケージはいくつかありそうですが不明。libwebkit-devやpython-webkit-devは入れてありました。

コード例: getbodytext.py

#
# Print entire HTML text after processed JavaScript
#
# usage:
#   /usr/bin/xvfb-run python getbodytext.py test.html
#
# libs:
# - pygtk: http://www.pygtk.org/
# - pywebkitgtk(python-webkit): http://code.google.com/p/pywebkitgtk/
# - jswebkit: http://code.google.com/p/pywebkitgtk/issues/detail?id=28#c9

#import pygtk
#pygtk.require("2.0")

import gtk
import jswebkit
import webkit

def loaded(view, frame):
    try:
        #print frame.get_title()
        gc = frame.get_global_context() # JSGlobalContextRef
        ctx = jswebkit.JSContext(gc)
        #print ctx.EvaluateScript("""document.body.innerHTML""")
        print ctx.EvaluateScript("""
        (function () {
          var scripts = document.getElementsByTagName("script");
          for (var i = 0; i < scripts.length; i += 1) {
            scripts[i].innerHTML = "";
          }
          var range = document.createRange();
          range.selectNodeContents(document.body);
          return range.toString();
        })()
        """)
        pass
    except:
        import traceback
        traceback.print_exc()
        pass
    gtk.main_quit()
    pass

def print_body(url):
    gtk.gdk.threads_init()
    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    webview = webkit.WebView()
    window.add(webview)
    webview.connect("load-finished", loaded)
    webview.open(url)
    webview.show_all()
    window.show_all()
    gtk.main()
    pass

if __name__ == "__main__":
    import sys
    url = sys.argv[1]
    if not url.startswith("http"): url = "file://" + url
    print_body(url)
    pass

getbodytext.c

C言語版も作ってみました。メモリ管理はいい加減かも。

実行例

test.html

<?xml version="1.0"?><!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Test</title>
</head>
<body>&nbsp;<script>document.write("Hello");document.write("@example.com");</script></body>
</html>


コマンドライン(jswebkit.soはgetbodytext.pyと同じディレクトリにおいてある)

$ xvfb-run python getbodytext.py test.html
Xlib:  extension "RANDR" missing on display ":99.0".
 Hello@example.com

xvfbの設定のためか余計な1行が"標準出力側に"出てしまうのですが、おおむねうまくできています。

How to Avoid Spambots | Project Honey Potも余裕です。

付録: aタグ収集の例

       ret = ctx.EvaluateScript("""
        (function () {
          var ret = [];
          var as = document.getElementsByTagName("a");
          for (var i = 0; i < as.length; i += 1) {
            ret.push(as[i].href);
          }
          return ret;
        })()
        """)
        for i in range(len(ret)):
            print ret[i]
            pass

aタグのhrefでjavascript:でlocation書き換えするタイプ( javascript:location.href="foo@example.com" を複雑にしたような物)はもう少し工夫が必要そうです。

付録2: ロード直後のDOMをXML形式で取り出す例

        print ctx.EvaluateScript("""
        new XMLSerializer().serializeToString(document)
        """)

補足: Xlib〜メッセージをstderrに出すxvfb-run

引数のコマンドのstderrメッセージをstdout側に出すのは、ubuntuのxvfb-runのバグっぽいです。bashスクリプトなので、コピーし編集します。

cp /usr/bin/xvfb-run .
emacs xvfb-run

で、このxvfb-runファイルの終わりのほうの

# Start the command and save its exit status.
set +e
DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
RETVAL=$?
set -e

と、stderrをstdoutにつなげている部分があるので、

# Start the command and save its exit status.
set +e
DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" # 2>&1
RETVAL=$?
set -e

コメントアウトし、これを使うといいです。