Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2010-01-27

Greasemonkeyスクリプトの開発で役に立ったサイトや本

Firefox にこんな機能があればいいなあ → ん、Greasemonkey というのでできるらしいぞ → ユーザースクリプトとやらを書けばいいのか → どうやって書くんだ?

というところからスタートして、最終的に自作のユーザースクリプトを公開するに至るまでの間、参考にしたサイトや本をできるだけ自分の学習順に時系列に沿って列挙してみました。

JavaScript を少々かじったことのある人が Greasemonkeyスクリプトを書いてみようと思い立ったときに、その学習の指針というか、道標のようなものとして役立つリンク集になればいいなと思ってます。

Greasemonkey

まずは Greasemonkey ってなんだとか、ユーザースクリプトってどう書くんだというのを調べるところからスタート。(以下小見出しがリンクになっています)

Greasemonkeyの開発をまとめてみる

screenshot

ここで Greasemonkey とは何をするアドオンで、そのユーザースクリプトはどう書くのか、その体裁の大まかなところを掴みます。「// ==UserScript==」で始まるスクリプト冒頭のメタデータブロックの書き方と、GM_log() で Firefox のエラーコンソールにデバッグのためのログを出力できることぐらいを押さえておけば、後はページの DOM操作、ということになります。

ただ、この先さらに調べていくとわかりますが、

無名関数でラップかけて、コードを書かないと名前の競合がおこってしまう。

の部分はかなり古いバージョンの Greasemonkey での話なので、今となってはやる必要はないと思います。

追記:ユーザースクリプトFirefox + Greasemonkey という環境で実行されるとは限らないため(Opera とか)、無名関数でコードを囲っておくというテクニックは今でも有効で意味があります。以下のエントリと、そこから参照されているエントリが大変参考になりますので、一読をお勧めします。

Dive Into Greasemonkey

screenshot

で、実際に何か書いてみようとなる訳ですが、その前に目を通しておいた方がいいのが「Dive Into Greasemonkey」です。これは Greasemonkeyスクリプト開発の定本と言ってもいいドキュメントで、

など、開発の基本がコンパクトにまとまっています。

中でも「4. Common Patterns」は DOM の扱いに慣れていなかった自分には非常に有用で、スクリプトを書きながら何度も参照しました。

mozdev.org - greasemonkey: authoring

screenshot

「Dive Into Greasemonkey」を補足するドキュメントです。unsafeWindow ってなんじゃらほい? と疑問に思ったらこのドキュメントの該当箇所を熟読です。安易な unsafeWindow の利用がなぜ、どのように危険かといったことが説明されています。これで unsafe の意味がわかれば、知らず知らずとっても危ないスクリプトを書いていた、なんてことはなくなると思います。

Greasemonkey Manual:API - GreaseSpot

screenshot

「Dive Into Greasemonkey」が入門編・チュートリアルであるとすると、こちらは Greasemonkey の仕様/API を網羅的に解説したリファレンスです。

「Dive Into Greasemonkey」の APIリファレンスでは一部のメソッドが紹介されているだけですが、こちらには完全な APIリファレンスがあります。また、メタデータブロックの仕様も詳細に解説されています。

Greasemonkeyの共通な落とし穴を避ける - minghaiの日記

screenshot

スクリプトが思った通りに動かない。そんな落とし穴にハマったらここを見てみます。簡単にまとめると、

あなたが Element と思っているそれ、それは実は Element ではなくて、Element をラップした XPCNativeWrapper なんだYO!!

ということに尽きると思います。

JavaScript

JavaScript そのものについても、コアライブラリや DOMリファレンスをいつでも参照できるようにしておいた方がいいでしょう。

JavaScript Reference - MDC

JavaScriptリファレンス」という名前のサイトは Web上にたくさんあるのですが、Greasemonkeyスクリプトの実行環境はあくまで Firefox なので、mozilla.org のそれが公式といえば公式でしょう。

全体的にレスポンスがもっさりしています。後、日本語版ページからだとリンクがうまく辿れないことがあって、そんな場合は検索結果から英語ページに飛ぶか、最初から英語版を参照した方が無難です。

DOMリファレンス

英語版のホーム

JavaScript style guide - MDC

Greasemonkey とは直接関係ないですが、コーディングスタイルで迷ったらここを見てみます。あくまで参考程度に。*1

JavaScript 第5版

JavaScript 第5版

サイ本。オブジェクト指向風味で書いてみるか! とか思ったりした時に手元にあると安心です。やみくもに prototype とかいじる前に読むことを推奨。(もちろん、その辺を解説した他の本でもサイトでもいいです*2

XPath

DOM要素の切り出し(選択)に XPath は必須の知識と思われます。Greasemonkeyスクリプトでも、DOM要素の切り出し用に document.evaluate() をラップした getElementsByXPath() なんて名前の関数がよく定義されて使われています。

そんな訳で、XPath の書き方について調べます。(知ってる人は飛ばして下さい><)

XPath 教程

screenshot

「Dive Into Greasemonkey」よりのご紹介。サイト名に漢字が使われていますが海外のサイトです。Example を順番に見ていけば XPath の書き方が大体わかります。

XPathXML -TECHSCORE-

screenshot

XPath はそれなりに使えるようになったけど、なんだかもやもや、軸とかノードテストとか言われてもピンとこない…… という場合はここでみっちり勉強しましょう。基礎からきちんと解説してくれる上に、図解が多用されていて非常にわかりやすいです。ここを読み終わる頃には、

id("PAGEBODY")//table[@class="pager"]/preceding-sibling::*[not(./descendant-or-self::form[@name="sort"])]

こんな XPath を理解し、また書けるようになるでしょう。

特定のclass属性を持った任意の要素にマッチするXPath | 3.14

screenshot

//div[@class="something"]

なんてうっかり書いていませんか? class属性は複数の値を持てる上、その区切りには改行やタブが使われることもあるよ、という内容。自分で class属性を設定した要素を選択するだけならともかく、そうでない場合は XPath の書き方に配慮が必要です。

特に、実行時に動的に要素のスタイルを切り替えるようなページの場合、ソースの見かけでは「class="something"」となっていても、ロード後にページの JavaScript によって class属性が追加されて実質「class="something even"」のようなことになっているかも知れません。このような場合、上記の XPath ではマッチに失敗してしまいます。

class属性に "something" を含む、という意味の XPath にするには、

//div[contains(concat(" ", normalize-space(@class), " "), " something ")]

とする必要があります。

GreasemonkeyスクリプトのGM_xmlhttpRequestとかで取得したhtmlとかxmlをいじる場合はxpathだと遅いことが多いかも - さらさら宇宙忍法帖

screenshot

GM_xmlhttpRequest() で取得した HTMLXML をパースする場合、XPath を使うより正規表現を使った方がかなり高速です。というのも、GM_xmlhttpRequest() はレスポンスから DOMツリーを構築してくれないからで、XPath を適用するためには自分でレスポンステキストから DOMツリーを構築しなければならず、そのコストが高くつくからです。

実際に速度比較を行ってみたところ、正規表現の方が概ね 10倍以上高速でした。(もっとも、10倍といってもミリ秒の世界のことなので、その違いを体感できるかは微妙ですが)

XPath を使うためにやたらゴニョゴニョやってるなーと思ったら、そんなものはバサッと削って正規表現を使う、という選択肢もあっていいと思います。

設定画面

スクリプトを作り込んでいくと、設定によって動作をあれこれ変更できるようなものへと進化していくことが多いと思います。そんな時、「設定を変更するにはスクリプトを編集して下さい」というのはちょっと格好が悪いです。*3そこで、Greasemonkeyスクリプトに設定画面を追加してくれるライブラリを探すことになります。

GM_config - a configuration GUI for your scripts – Userscripts.org

screenshot

メニューから呼び出せる設定用GUI を比較的簡単に Greasemonkeyスクリプトに追加できるライブラリとして GM_config があります。

使い方はそんなに難しくなく、上記の API reference や GM_config を使っている既存のスクリプトなどを参考にすれば大体のところはわかると思います。ただ、

  • 設定画面のレイアウトを思った通りにやりにくい
  • 設定画面がたまに出ない*4

といった問題があって、そういう点に不満がある場合は自分でコードをいじってどうにかするぐらいの意気込みで使うべきものだと思います。*5

なお、こういったライブラリを利用したスクリプトを配布する場合、レポジトリの trunk から @require するのは、よほど信頼のおけるプロジェクトでない限りやめた方がいいと思います。安定動作を確認したバージョンを自分の管理下にあるサーバーに配置し、そっちを @require するようにした方がなにかと安全です。*6

Greasemonkeyスクリプトに設定画面を追加するライブラリ USConfig

手前味噌になりますが…… GM_config に対する不満点を解消すべく私が開発しているライブラリです。一応紹介(;^ω^)開発終了しました。

自動更新機能

スクリプトをリリースしてからも機能を追加したりバグを直したりと、スクリプトのさらなる改善は続きます。そんな時に問題となるのが「自分がスクリプトアップデートしたことをどうやってユーザーに告知するか」です。

スクリプトの自動更新機能を提供するものとしては以下のものがあります。

Wescript

ユーザースクリプトの自動更新機能を提供するアドオンです。スクリプトの配布元ページでこのアドオンのインストールをお願いするのが一番楽な方法です。

screenshot

We Ain’t Seen Nothin’ Yet. : Greasemonkeyスクリプトに自動更新チェック機能をつける方法 改

screenshot

こちらはスクリプト自身に自動更新機能を持たせるというアプローチです。エントリ内で配布されているサンプルスクリプトからコードをコピペして使うというスタイルです。

しかし、肝心のコピペ部分にミスってる部分があったりするので使う場合は修正が必要です。

--- update-notification-fu.user.js  2010-01-26 23:04:52.000000000 +0900
+++ update-notification-fu-fix.user.js  2010-01-26 23:04:48.000000000 +0900
@@ -50,7 +50,7 @@
                 '\u73FE\u5728\u304A\u4F7F\u3044\u306E',
                 'Greasemonkey\u30B9\u30AF\u30EA\u30D7\u30C8 \u0027',
                 this.script_name,
-                '(var. ', this.current_version, ')',
+                '(ver. ', this.current_version, ')',
                 '\u0027 \u306F\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3 ',
                 this.remote_version,
                 ' \u304C\u516C\u958B\u3055\u308C\u3066\u3044\u307E\u3059\uFF0E',
@@ -129,7 +129,7 @@

         // Check script update remote
         check_update: function() {
-            if(!this.has_need_for_check) return;
+            if(!this.has_need_for_check()) return;
             var user_script = this;
             GM_xmlhttpRequest({
                 method: 'GET',

AutoPagerize

AutoPagerize はおそらく最も利用されているユーザースクリプトです。Greasemonkey のあるところ AutoPagerize あり、と言っても過言ではないと思います。なので、自分の書いたスクリプトAutoPagerize と併用される可能性は(どこを @include するかにもよりますが)必然的にかなり高くなります。AutoPagerize が利用可能なページで動作するスクリプトAutoPagerize と連動できないのは致命的で、AutoPagerize を愛用しているユーザーには使ってもらえない可能性が大です。

というわけで、AutoPagerize に対応したスクリプトにしようということになります。

AutoPagerize Wiki: APIs (ja)

screenshot

ほとんどの場合、このページにある Sample をそのまま流用して、pop_alt() を自分のスクリプト関数で置き換えることで対応できると思います。*7

  if (window.AutoPagerize) {
    boot();
  } else {
    window.addEventListener('GM_AutoPagerizeLoaded', boot, false);
  }
  function boot() {
    window.AutoPagerize.addFilter(function(pageElems){
      pageElems.forEach(yourFunc);
    });
  }

↑大体こういう形になります。この例の場合だと、AutoPagerize が次のページをロードする度に、AutoPagerize が切り出した次のページの pageElement を引数として yourFunc() が呼び出されることになります。

ここで注意すべきは、AutoPagerize によってロードされたページが単独の pageElement であるか、それとも複数の pageElement からなるかは pageElement の切り出し用 XPathの 定義(SITEINFO)による、ということです。

ユーザースクリプトでは DOM を操作する訳ですから、pageElement として何が渡されてくるかは把握しておく必要があります↓

アイテム - データベース: AutoPagerize - wedata

screenshot

AutoPagerize は次のページをロードするための nextLink や pageElement の切り出し用 XPath の定義(SITEINFO)をこの外部 DB に持っています。ここであなたのスクリプトが @include しているサイトの pageElement 切り出し用 XPath の定義を調べ、何が pageElement として渡されることになるかを確認しましょう。

例えば、Google の検索結果ページであれば、pageElement は以下の XPath で切り出された要素になります。

id("res")/div[ol or div]|id("ofr")

最後に

自分自身まだ Greasemonkeyスクリプトを書き始めて日が浅いので、手持ちのリソース(というかブックマーク)はとりあえずこれぐらいです。今後も開発に役立つサイトやページを見つけ次第、随時追加していく予定です。

*1:ちなみに自分のスクリプトは逸脱しまくりですw

*2:と言いながら自分が持ってるのは今となっては古い第3版だったりするのであまり偉そうなことは言えません(汗)

*3:自分が最初にスクリプトを公開した時はまさにこれで、その後 GM_config の存在を知り大急ぎで設定画面を追加したという恥ずかしい経緯があります。

*4:これは GM_config の問題というか Firefoxバグではないかと睨んでいるんですが詳細はわかりません。現在調査中。

*5:かくいう自分もコードをいじって機能追加などを行っているうちに完全に既存のものと非互換のものになってしまい、パッチを送るに送れなくなったという経緯があったりします。

*6:実際、GM_config に対し、設定のデフォルト値を指定するキーである default を _def に変更するコミットがされたことがあり、それが原因で GM_config を利用するスクリプトが動かなくなったことがあります。

*7:ただ、個々のスクリプトにこういった対応を強いている現状は正直どうかなと思う。できれば AutoPagerize にはもうひとつメタなレベルで動いてもらって、ページのロードに際して Greasemonkey 自体を起動する側にまわってもらえると有難いかも。多分アドオンになる必要があるけど。

h1mesukeh1mesuke 2010/06/05 21:02 エントリを加筆/修正しました。

JavaScript
*mozilla.org の英語版のホームを追加

XPath
*「XPathXML -TECHSCORE-」を追加
*「特定のclass属性を持った任意の要素にマッチするXPath | 3.14」の紹介文を加筆/修正

AutoPagerize
*説明文を加筆/修正
*「AutoPagerize Wiki: APIs (ja)」のサンプル修正/適用に関する説明文を加筆/修正
*「アイテム - データベース: AutoPagerize - wedata」を追加

h1mesukeh1mesuke 2010/06/07 04:48 やっぱり無名関数でコードをラップした方がいいよ的なことを追記しました。

h1mesukeh1mesuke 2010/07/04 15:03 Greasemonkey
*「Greasemonkey Manual:API - GreaseSpot」を追加

というか、なんでこれを外してたんだろう(;^ω^)

h1mesukeh1mesuke 2010/07/14 13:26 設定画面
*「Greasemonkeyスクリプトに設定画面を追加するライブラリ USConfig」を追加

手前味噌ですいません(;^ω^)