GreaseMonkeyとDOM/prototype.js入門

目標

  • GreaseMonkeyで実際のサイトを書き換えてみよう
  • DOM理解してprototype.jsを使ってみよう

intro

GMとは

直訳すると「整備士」。特定のページを見たときに自分で用意したJavaScriptを実行することが出来る。おもな使い道としては、ページのデザインなどの改善(デザインの改善ならStylishでも出来る)と、ちょっとした機能追加。

GMの導入

firefox のアドオン : GreaseMonkey - Firefox addon

※safariやIEにも同様のものがある (参考:GreaseMonkeyとは@hatena

便利なスクリプト紹介

  • AutoPagerize
  • JavaAPI検索
  • サイト内検索

実際には、個人の利用に応じた機能追加・改造を行うことが多いので、これといったものはあまり多くない。

情報源

まとめ系

入門

リファレンス的

TIPS

その他

関連ツール

GMを始める

基本的なGM開発の流れを確認しましょう。

GMでの新規スクリプト

GMのアイコンからメニューを出して新規スクリプトを作ってみましょう。

初回の場合、ここでエディタ設定を行う。後で変更するには、 about:config から greasemonkey.editor の値を変更する。

名前空間はGM_setValueやスクリプトの入れ替えのときに使われるもの。個人でひとつ持っておいて、hatenaや自分管理のURLにしておくことが多い。

特定のサイトで以下のようなコードを書いて hello world を出す。

document.body.innerHTML = "<h1>hello world</h1>";

議論

抑えなければいけないポイント:エラーはエラーコンソールを見る。メニューの「Tool」→「Error Console」。

デバッグのポイントとしてはログを一番最初に押さえておく GM_log, console.log。

スクリプトはUTF8で書く。昔は書けなかったのでエスケープしていたことがある。(このあたり、いろいろ変わってきた歴史があるので、人のコードを参考にする場合は気を付ける。変わってきた原因としては機能追加とセキュリティー対応)

コツとしては自分コードを集めておいてコピペする。ライブラリとかでまとめていくよりはコピペが早いかも。

書き換え、サイトの再構築実習

実際にサイトを書き換える練習問題をやってみて、DOMとprototype.jsについて理解しましょう。

GoogleのロゴをYahooにしてみる

GoogleのロゴをGMでYahooのロゴに入れ替えてみましょう。

ポイント:

  • firebugのinspectで調査
  • consoleでコードの確認

某新聞社のニュースサイトをシンプルにしてみる

記事のみを一覧表示させて、素早くニュースを確認できるように見やすく整形してみる。

1:右側の記事でないエリアを削除

邪魔だと思う箇所を消す方法で、整形をやってみましょう。

  • firebugのinspectで、全体のHTMLの構成を確認してください。
  • 記事でないエリアの要素のIDを調べてください。
  • console上で、そのIDで document.getElementById して、 style.display = "none" して消してみましょう。
  • GMでやってみましょう。
議論
  • 消す前に画面に一瞬出てしまう → DOMcontentloadedのイベントでGMが動くから。
  • display: "none" するだけなら stylish の方が楽。

2:記事一覧の内容だけ表示する

上とは逆に必要なものを取ってくる方法を考えてみましょう。

  • firebugで記事の一覧を格納している要素を探してみてください。
  • firebugでクリックした後、$0.innerHTML で中身を確認してみてください。($0でクリックした要素にアクセスできる)
  • document.body.innerHTML に、その中身を入れてみて表示を確認してみましょう。
  • GMでやってみましょう。

3:a要素を集める

上ではまだ元のレイアウトが残っていました。今度はもっと積極的に、必要なタグのみ集めてみましょう。

  • document.getElementsByTagName でa要素を列挙して、それぞれの中身を確認してみてください。
    • 欲しい記事のリンク以外もたくさん含まれています。
    • 良く見ると、liの直下のa要素が記事へのリンクになっているようです。
  • DOM APIを使って、aの親がliであるものだけを集めるプログラムをconsole上で書いてみましょう。
  • 集めたaを使って、ulのリストを作ってみましょう。
  • GMでやってみましょう。

DOMとDOM APIの簡単な説明

  • タグの入れ子とツリー構造
  • 良く使うプロパティとメソッド(tagName, length, childNodes, parentNodeなど)
  • 今回は tagName と parentNode
  • MDCやその他ドキュメントでAPIを調べる
  • Firebugで調べる方法の確認

プログラム例:

var tags = document.getElementsByTagName("a");

// liが親のaを集める
var list = [];
for(var i=0;i<tags.length;i++) {
    if (tags[i].parentNode.tagName == "LI") {
        list.push(tags[i]);
    }
}

// 集めたaからリストを作る
var html = ["<ul>"];
for(var i=0;i<list.length;i++) {
    var a = list[i];
    html.push("<li>");
    html.push("<a href='"+a.href+"'>"+a.textContent+"</a>");
}
html.push("</ul>");

document.body.innerHTML = html.join("\n");
議論

4:prototype.js の Array.select, mapを使ってみる

ちょっと上のプログラムは洗練されていないため、prototype.jsを使ってもう少しモダンな書き方をしてみましょう。

  • プログラム上の問題点の整理:

    • 似たようなループが続く
    • →ループは頻出するためもう少し短く書きたい。
  • prototype.jsについて

    • prototype.jsについての簡単な説明
    • Arrayのいくつかのメソッド(each, select, map)について説明
    • 対象サイトには既にprototype.jsが組み込まれている。
  • 解決方針:

    • document.getElementsByTagNameで取ってきた要素の配列を$Aでprototype.jsのArrayにする
    • if文でa要素を選択している部分をselectにする
    • a要素の配列を文字列の配列に変換している所をmapにする

プログラム例:

// サイトのライブラリを使いたい
var $A = unsafeWindow.$A;
var tags = document.getElementsByTagName("a");
tags = $A(tags);

// liが親のaを集める
var list = tags.select(function(elm){ 
    return elm.parentNode.tagName == "LI";
});

// 集めたaからリストを作る
var html = list.map(function(a) {
    return "<li><a href='"+a.href+"'>"+a.innerHTML+"</a>";
});
html.unshift("<ol>");
html.push("</ol>");

document.body.innerHTML = html.join("\n");

もう少し短くまとめられる:

$A(document.getElementsByTagName("a")).select(function(elm){ 
    return elm.parentNode.tagName == "LI";
}).map(function(a) {
    return "<li><a href='"+a.href+"'>"+a.innerHTML+"</a>";
});
html.unshift("<ol>"); html.push("</ol>");

ここで行ったような「集めて→フィルター→加工→結果」というパターンはWebの整形だけでなく、いろいろなプログラムに応用できるパターン。関数的。

議論
  • unsafeWindow とは?
    • 対象サイトのグローバルコンテキストであるwindowオブジェクト。対象サイトで定義してある関数やライブラリにアクセスするために必要。
    • セキュリティ上問題がある。このオブジェクトを通してWeb側からGMの実行環境で任意のコードを実行することが出来る。GMの環境はセキュリティ的に緩いので問題がある。
    • 関数を使うだけではなくて、触れるだけでもだめ(getter setterで実装が関数だったりするので)
    • 特にどのサイトでも有効になるスクリプトは危ない。逆に特定のサイトだけ有効になるスクリプトは、そのサイトの信頼性と同程度のリスク。
    • セキュリティの問題はGM自身の設計・実装の問題のように思われる。グローバルに関数がいろいろあるし、もっといいフレームワークが設計できるのではないか?
  • まだプログラムに納得がいかないんですが・・・ → まだ続きがあります。もう少し待ちなさい。

5:$$でもっとスマートに集めてくる

上で作ったリストを良く見ると、まだ必要ないa要素があります。prototype.jsの$$関数を使って、もっと複雑な条件で要素を集めてくる方法を試してみましょう。また、文字列では無くDOMオブジェクトで結果を組み立ててみましょう。

  • $$関数、CSSセレクタについて確認してください。
    • prototype.jsの便利関数のひとつ。クラス名で集めてくる時に良く使う。
    • CSSで要素を特定する書き方とほぼ同じ
  • console上で $$('ul.list li a')とやって、結果の中身を確認してみましょう。
  • $$を使って、GMのプログラムを書き変えてみましょう。
  • 文字列では無く、DOMを使ってHTMLを組み立ててみましょう。
    • 文字列はわかりやすいが、拡張しづらく複雑な組み立てが難しい。
    • DOM API の確認: createElement, appendChild

プログラム例:

var $$ = unsafeWindow.$$;

var tags = $$("ul.list li a");

var ul = document.createElement("ul");
tags.each(function(a) {
    var li = document.createElement("li");
    li.appendChild(a);
    ul.appendChild(li);
});

document.body.innerHTML = "";
document.body.appendChild(ul);
議論
  • document.body.innerHTML = "";
    • 手っ取り早くbodyの中身をクリアする。たぶん間違いでは無いらしい。
  • xpathも良く使う。

6:MashUpしてみる

別のサイトから情報を取ってきて機能を追加してみましょう。

プログラム例:

var $$ = unsafeWindow.$$;

var tags = $$("ul.list li a");

var ul = document.createElement("ul");
tags.each(function(a) {
    var li = document.createElement("li");
    li.appendChild(a);
    var img = document.createElement("img");
    img.src = "http://b.hatena.ne.jp/entry/image/normal/"+a.href;
    li.appendChild(img);
    ul.appendChild(li);
});

document.body.innerHTML = "";
document.body.appendChild(ul);
議論
  • 記事の数が多いとリクエストアクセスが集中して凶悪かもしれない。
    • callLaterなどを使って一度にたくさんアクセスが集中しないような仕組みを入れるべき
  • 特定の文字列を含む記事を選んだり、記事の数でソートなどを行うと楽しそう。
    • →少しずつでもやってみると、いろいろな発展が見えてくる。

7: XMLHttpRequestを使ってScraping

XHRを使ったページの切り取り手法を使ってさらに機能追加してみましょう。

  • やりたいこと:クリックしなくても記事のサマリーを確認したい
    • →マウスでリンクを触れるとポップアップで記事の第1パラグラフを表示する
  • 実装方針:
    • 上のリンク一覧で、a要素にmouseover, mouseoutイベントを付ける
    • イベントの中でポップアップ表示、削除を行う
    • ポップアップ表示のタイミングでXHRでリンク先のページの内容を取得して、必要な箇所を切り取って表示する

実際にやってみる

プログラム例:

var $$ = unsafeWindow.$$;

var tags = $$("ul.list li a");

var ul = document.createElement("ul");
tags.each(function(a) {
    var li = document.createElement("li");
    li.appendChild(a);
    var img = document.createElement("img");
    img.src = "http://b.hatena.ne.jp/entry/image/normal/"+a.href;
    li.appendChild(img);
    ul.appendChild(li);

    //ポップアップ表示、消滅
    li.addEventListener("mouseover",function(event){ showDigest(event,li,a.href); },true);
    li.addEventListener("mouseout",function(){hideDigest();},true);
});

document.body.innerHTML = "";http://diaspar.jp/node/61
document.body.appendChild(ul);

//ポップアップ表示
function showDigest(event,elm,href) {
    hideDigest();
    var div = document.createElement("div");
    with(div.style) {
        //classでやろう!
        position = "absolute";
        border = "3px dashed gray";
        left = event.clientX+"px";
        top = (event.clientY+unsafeWindow.scrollY+10)+"px";
        background = "cornsilk";
        padding = "5px";
        zIndex = "1000";
    }
    div.id = "gm_digest";
    retrieveDigestText(href,function(text){
        div.innerHTML = text;
    });
    elm.appendChild(div);
}

function hideDigest() {
    var elm = document.getElementById("gm_digest");
    if (elm) {
        elm.parentNode.removeChild(elm);
    }
}

function retrieveDigestText(href,setter) {
    GM_xmlhttpRequest({
        method: 'get',
        url: href,
        overrideMimeType: document.contentType+"; charset="+document.characterSet,
        onload: function(details){
            setter(scrapingKiji(details.responseText));
        }
    });
}

function scrapingKiji(wholeText) {
    var m = wholeText.match(/<div class="kiji"><p>([^<]*)/);
    return (m) ? trim(m[1]) : "Not available...";
}

function trim(t) {
    return t.replace(/^\s*/,"").replace(/\s*$/,"");
}
議論
  • 「scrapingKiji」のKijiって何よ? → 抜き出すdivのclassが「kiji」なので・・・。
  • CSSスタイル直書きなの? → とりあえずわかり易さ優先のサンプルなので・・・。
  • 正規表現なの? → DOMを解析するより速い。複雑な場合はDOMを解析するほうが良い。
    • 「.」は改行を含まない。改行を含む正規表現について議論。
  • 相変わらずアクセスが凶悪そう。 → サンプル(ry。実際には一定時間待った方がいいでしょうね。
  • show, hide とかではなくて、ポップアップのクラスを作って、ステートパターンで実装するべき。 → いや、だからサンプル(ry。
  • 意外に速い。 → ページ読み込みの時間の8割以上はHTMLのロード以外に消費されている。前回復習事項。

まとめ

GreaseMonkeyでDOM操作とprototype.jsの簡単な使いかた講座をしようとしました。

→来場者のレベルが高すぎて、講演者のサンプルプログラムのツッコミ大会になってしまいました。

changed October 9, 2009