2011-08-08

PrintWhatYouLike に動的パッチ


PrintWhatYouLike というサービスをご存じでしょうか?

ブックマークレットとして実行すると、ページ上のコンテンツ(HTML 要素)を
自由に削除したりできるサービスです。

Evernote の Web Clipper と組み合わせると最強。

ブログ記事の本文部分だけを抜き出してクリップできるので、
あとで Evernote 上で検索したときのノイズが劇的に減らせます。

参考)
Evernote と PrintWhatYouLike の組み合わせが便利すぎる! -- マインドマップ1年生 plus ライフハック!
さて、とっても便利な PrintWhatYouLike ですが、 先日(7月のどこか)からうまく動かなくなってしまいました。 ブックマークレットを実行するとツールバー(サイドバー)は 表示されるのですが、コンテンツ編集用の赤色ボーダーも 要素クリック時のポップアップも表示されません。 あと、ツールバー上の "close" ボタン「X」も。 いろいろ調べた結果、とりあえず動くようにする代替ブックマークレットが 作れたので、ここで公開します。 ⇒ PrintWhatYouLike(Seaoak版)   ※ ブックマークバーに Drag&Drop すれば使えます 公式ブックマークレットの代わりに使えます。当然ながら非公式です。 この非公式ブックマークレットの中では、公式のと同じ JavaScript プログラムを PrintWhatYouLike.com から読み込んで、問題のある箇所だけ修正して、再実行しています。 そのうち公式に修正版がリリースされる(またはこっそり JavaScript プログラムが 差し替えられる)ハズなので、それまでの「つなぎ」としてお使いください。 問題になりそうな処理は仕込んでいないので大丈夫だと思いますが、 例によって無保証ということでお願いします。 以下、技術的な詳細です。 ===== ■ 1. 公式ブックマークレットが動かなくなった原因 ブラウザのコンソールを見ると、null に設定されてる変数のプロパティを 読もうとしてエラーになっていることがわかります。エラーになった時点で プログラムの実行が中断されて、その結果として従来の動作ができなく なっているものと推測されます。 公式ブックマークレットの中で読み込んでいる JavaScript ファイルを 見てみると、次のようなコードが含まれています。
if(nextPage.nextLink.finalScore>17000){
この nextPage.nextLink には、直前である関数の返値を代入しているのですが、 その値が null になっているようです。 とりあえず、次のように修正してみたところ、動くようになりました。
if(nextPage.nextLink&&nextPage.nextLink.finalScore>17000){
変数名から推測するに、新しく組み込まれた機能の "PrintWhatYouZip" で 「次のページ」をたどろうとしたとき、その「次のページ」が見つからないと この変数に null が設定されてしまうようです。 ===== ■ 2. ブックマークレットで動的にパッチを当てる ブックマークレットで読み込んでいる JavaScript プログラムを 「その場で」書き換えて実行する方法を考えます。 そんなことをしなくても、公式の JavaScript プログラムをローカル保存して、 修正して、修正版を適当な Web サーバにアップロードしておいて、 そのファイルを参照するブックマークレットを作る方が簡単です。 しかし、   ・公式の JavaScript プログラムのライセンスが不明   ・他の人にも安心して使ってもらうのが難しい   ・公式版の更新に追従できない   ・自分で用意できる Web サーバだと遅い(読み込みに時間がかかる) などなどの問題があります。 さて、問題の JavaScript プログラムは、動的に生成した script 要素で 読み込まれています。したがって、読み込まれた時点でソースコードの字面を 書き換えられれば一番簡単です。しかしながら、src 属性を指定した script 要素の ソースコードを参照する方法がありません(text 属性を読んでみても「空」)。 一方で、問題の JavaScript プログラムを眺めてみたところ、 問題があるのは _pgzpInitPwyl() という関数です。 この関数はグローバルな名前空間で定義されています。 つまり、ブックマークレットからも可視。 すなわち、ブックマークレットからも書き換え可能! というわけで、次のような方法を考えました。   1. 関数 _pgzpInitPwyl() のソースコードを得る   2. 得られたソースコードを書き換える   3. ソースコードを eval() して関数オブジェクトに変換   4. 得られた関数オブジェクトをシンボル _pgzpInitPwyl に設定 関数のソースコードは .toString() で得られます。 関数オブジェクトへの変換およびシンボルへの設定は、 ひとつの eval() でまとめてできます。 具体的には、以下のようなコードになります:
var code = _pgzpInitPwyl.toString(); code = code.replace('hogehoge', 'foobar'); code = '_pgzpInitPwyl = (' + code + ')'; eval(code);
===== ■ 3. 修正したコードを実行させる 上記のように関数の定義を動的に書き換えることが可能なのですが、 問題は「書き換えられるのは最初に実行された後」ということです。 src 属性を指定した script 要素の場合、 読み込み直後(実行される前)に処理を割り込ませることができません。 script 要素の onload イベントハンドラが呼ばれるのは、 残念ながら読み込んだコードが実行された後です。 また、onbeforescriptexecute イベントという便利そうなモノがあるのですが、 Firefox でしか実装されていないみたいです(未確認)。 さらに、IE8 では script 要素の onload イベントハンドラは 実装されていないようです(IE9 では実装されているようです)。 幸いにして、問題の JavaScript プログラムは、エラーで死んだ後でも、 上記パッチを当てて再実行すれば動作してくれます。ただし、最初の (エラーになった)実行でツールバー(サイドバー)が開いた状態に なっているので、再実行は2回繰り返す必要があります。 「一度閉じて、開き直す」というわけですね。 最終的に、次のような方法を採用しました:   1. 「コード書き換え+再実行2回」を1回だけ実行する、という独自関数を定義。   2. script 要素の onload イベントハンドラとして独自関数を設定する。   3. IE8 対策として setInterval() で定期的に DOM を監視して、    問題の JavaScript が実行されたのを検知したら clearInterval() して    独自関数を実行する。 ちょっと hacky ですが、まぁ、動きます。 ===== ■ 4. 完成したブックマークレット
javascript : (function () { var timer; var handler = function () { if (window['ppw'] && ppw['bookmarklet'] && timer) { window.clearInterval(timer); timer = null; if (! _pgzpInitPwyl) { throw 'can not modified (by Seaoak)'; } var regexp = /\bif\s*\(\s*nextPage.nextLink.finalScore\s*>\s*17000\s*\)\s*\{/; var code = _pgzpInitPwyl.toString(); if (! regexp.test(code)) { return; } code = code.replace('_pgzpInitPwyl', ''); code = code.replace(regexp, 'if(nextPage.nextLink&&nextPage.nextLink.finalScore>17000){'); code = '_pgzpInitPwyl = (' + code + ')'; eval(code); var toggle = function () { ppw.bookmarklet.toggle(); }; window.setTimeout(toggle, 100); window.setTimeout(toggle, 100); } }; if (window['ppw'] && ppw['bookmarklet']) { ppw.bookmarklet.toggle(); } else { window._pwyl_home = 'http://www.printwhatyoulike.com/'; window._pwyl_pro_id = null; window._pwyl_bmkl = document.createElement('script'); window._pwyl_bmkl.setAttribute('type', 'text/javascript'); window._pwyl_bmkl.setAttribute('src', window._pwyl_home + 'static/compressed/pwyl_bookmarklet_10.js'); window._pwyl_bmkl.setAttribute('pwyl', 'true'); window._pwyl_bmkl.onload = handler; timer = window.setInterval(handler, 100); document.getElementsByTagName('head')[0].appendChild(window._pwyl_bmkl); } })();
ソースコードの整形&色づけは、こちらを使わせていただきました。 ===== ■ 5. まとめ 先日から PrintWhatYouLike が使えなくて非常に困っていたのですが、 ソースコードを眺めてみたらすぐに原因がわかって、 そこで「動的パッチ」というアイディアを思いついたので、 ひと晩で実装してみました。 JSON 以外で eval() を使ったのは初めてだったので、 その挙動に悩みました。特に、IE8 だけ挙動が違ったのが罠でした。 いちおう動作確認したのは以下のブラウザです:   ・Google Chrome 13.0.782.109 beta-m   ・Firefox 5.0   ・Opera 11.50   ・Internet Explorer 8 OS は Windows 7 Professional 32bit です。 ちょっと長い記事になってしまいましたが、今回のコードは PrintWhatYouLike の開発元が対応してくれたら無用の長物と 化してしまうので、ブログネタにしてみました。 ツッコミ歓迎です~

| | コメント (0) | トラックバック (0)

2011-07-13

bk1_to_amazon をアップデート


自作グリモン(ユーザスクリプト)の bk1_to_amazon をアップデートしました!

    ⇒ userscripts.org の bk1_to_amazon のページ


このスクリプトはオンライン書店ビーケーワン (bk1) の書籍のページに
その本の Amazon のページへのリンクを自動的に埋め込むモノです。

あらすじや書評(レビュー)などを参考にしたいときに便利かもです。

このスクリプト自体は2009年9月にリリースしていて、
しばらく更新していなかったのですが(とくに追加したい機能もなかったし)、
先月くらいに bk1 が全面的にリニューアルされて動作しなくなっていたのでした。

で、本日、やっと改造に着手して、無事に完成しました♪

ひさしぶりに自分のコードを見てみたら、いろいろ気に入らない部分があって、
結局、全面的に書き直しています。

Sleipnir と Firefox に加え、Google Chrome と Opera でも動くことを確認済み
(いずれも Windows 版)。

簡単なスクリプトですが、快適な読書生活のお役に立てれば幸いです!

| | コメント (0) | トラックバック (0)

2011-07-08

RESTful のお勉強


最近、REST (RESTful) というアーキテクチャについて勉強してます。

  ⇒ Wikipedia のページ

REST (Representational State Transfer) というのは、
HTTP でアクセスできる Web サービスの設計方針のひとつで、
よく SOAP と対比されてるみたいですが、SOAP よりだいぶシンプル。

REST の思想をベースに設計されたシステムは RESTful とか呼ばれることも。

個人的には Twitter の API とかは RESTful なんじゃないかなぁ、と思ってます。




オライリーから本も出てます:


なかなか読み応えのある本で、かなり勉強になりました。

 「RESTful にするとなにがうれしいのか?」

 「SOAP だとなぜダメなのか?」

 「RPC スタイルとの違いは?」

 「URI のデザインで考慮すべきことは?」

 「エラー処理の基本方針は?」

などなどが理解できました。

RESTful なアーキテクチャの例として ROA (Resource Oriented Architecture) を
とりあげて、実際に ROA な Web サービスの設計の進め方を具体的に説明してます。

また、より実用的な話として、ユーザ認証の話や、エラー処理の話も
なかなか詳しいです。




あと、Web でも参考になる記事を見つけました:

The Good, the Bad, and the Ugly of REST APIs - O'Reilly Broadcast http://broadcast.oreilly.com/2011/06/the-good-the-bad-the-ugly-of-rest-apis.html
この記事によると、   「JSON と XML を両方サポートすべし」   「レスポンスのエラーコードは正確にし、可能な限りヒントも添える」   「OAuth は使うべきでなく、クエリへの署名による認証がよい」   「API リクエスト回数を制限するときは十分に気をつけるべし」 などといった経験則が得られます。 ところで、突然 RESTful のお勉強を始めたのは、 ちょっとしたツテで Web サービスを作ってほしいと 頼まれているからでした。 RESTful にしてみたい理由は、   ・サーバ側をステートレスにできる ←個人的に「セッション」がキライ!   ・"View" (Web ページの表示)と内部処理を分離できる   ・もしかしたらデスクトップガジェットも作れちゃうかも?? などなど。 まぁ、まだ考えが固まっていないんですけどね(熟成中)。

| | コメント (0) | トラックバック (0)

2011-05-14

微妙な配列リテラル


ちょっと気になる JavaScript ネタを発見。

    Javascriptが難しくてわからないので誰か教えてください
      --  Aduca

これは JavaScript の配列リテラルの仕様から説明ができます。

JavaScript の仕様書(というかベースとなっている ECMAScript の仕様書)を
ちょっと見てみましょう:

    ECMA-262
    ECMAScript Language Specification
    5th Edition / December 2009
    http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf


p.63 の "11.1.4  Array Initialiser" の第2段落にそれらしきことが書いてあります。

ざっくり訳してみます。
配列要素は、要素リストの先頭でも、中間でも、末尾においても、省略されうる。 有意な式がコンマの前に置かれていない場合、つまり「いきなりコンマで始まる」 とか「別のコンマのすぐ後に続けてコンマが置かれた」というケースでは、 省略された要素は配列の長さに含まれ(配列の長さが増加し)、それに続く要素に 割り当てられる添字がそのぶんだけ大きくなる。省略された要素は定義されない (undefined となる)。もし、要素リストの最後の要素が省略された場合、 その要素は配列の長さに含まれない。
つまり、次のようになると考えられます。
> [] ⇒ 最終要素しか無くて、その要素が省略されているので、長さ 0 の空配列。 > [,] ⇒ カンマの前にある先頭要素が省略されているので、その値は undefined で、 最終要素は省略されているので無視。結果として長さ 1 の配列。 > [,,] ⇒ カンマの前にある先頭要素が省略されており、次のカンマも連続しているので、 先頭の2個の値はいずれも undefined である。 最終要素は省略されているので無視。結果として長さ 2 の配列。
処理系の表示によってはまぎらわしいのですが・・・
> [] [] > [,] [] > [,,] []
空の要素に見えても要素数はちゃんと保持されています。
> [].length 0 > [,].length 1 > [,,].length 2
次に、toString() の結果についてですが、 上記仕様書の "15.4.4.2 Array.prototype.toString()" (p.123) を 見てみると、内部的には join を呼んでいるようです。 つまり、[].toString()[].join() と同じ。 仕様書の "15.4.4.5 Array.prototype.join(separator)" (p.125) を 見てみると、次のようなルールがわかります。   ・空の配列には空の文字列が返る   ・1要素の配列ではその要素そのものが返る   ・複数の要素があればカンマで連結したものが返る   ・undefined な要素は空文字列になる。   ・区切り文字 (separator) が省略された場合はコンマが使われる。 というわけで、次のような挙動となります。
> [].toString() ←空の配列なので空文字列 "" > [,].toString() ←長さ1の配列なのでカンマで区切られない "" > [,,].toString() ←長さ2の配列なのでカンマで区切られる ","
これで仕様通りなのです。 結局のところ、undefined な要素しか含まない配列の 簡易表示がイマイチ、という話なのかもしれませんね。
> [,,,,] [] > [,,,1,] [undefined, undefined, undefined, 1] > [,,,1,,,] [undefined, undefined, undefined, 1] > [,,,1,,2,] [undefined, undefined, undefined, 1, undefined, 2]
どうも、配列の末尾の undefined な要素は省略されるお約束っぽい。 まぁ、new Array(50) とか書いたときに "undefined" と 50個も表示されるのも困るので、まぁ、妥当といえば妥当かもですが・・・ p.s. 処理系の実行例は Google Chrome のコンソールのものです。 Firefox (Firebug) では undefined な要素がきちんと全部表示されました。

| | コメント (0) | トラックバック (0)

2011-05-05

iframe 要素の onload ハンドラ


開発中のブックマークレット「PruneBeforeClip」に
AutoPagerize みたいな機能を追加しようとしています。


ニュースサイトの記事で複数のページに分割されているものがあります。
最初のページだけクリップすると結論が書かれていないし、
途中のページだけクリップすると話の流れが把握できないし、
とにかくクリップして保存するには不便な状態になってしまっています。
ロイターには「記事を1ページに表示する」というリンクがあって
とても便利なのですが、他のサイトには残念ながらありません。

ところで、AutoPagerize という有名なユーザスクリプト (GreaseMonkey) があります。
Google の検索結果のページとかでページの末尾にスクロールすると、自動的に
次のページを読み込んで、ページ末尾に「継ぎ足して」くれるモノです。
Seaoak は使っていませんが、かなり便利だという評判。

で、考えてみると、ニュース記事の単一ページ化も、AutoPagerize と
同じ仕組みで実現可能なハズです。違いは、スクロールに関係なく
すべてのページを一気に展開してしまうところだけ。


とりあえず、実験ということで、あるニュースサイトで記事の単一ページ化が
できるようにしてみました。

処理はそんなに複雑ではなくて、次のような流れ:

  ・ページ内に「次のページ」や「前のページ」へのリンクがないかチェック。
  ・あったら、リンク先のページをバックグラウンドで読み込む。
  ・読み込んだページの本文部分を抜き出して、元のページのリンクの代わりに埋め込む。
  ・以上の処理をリンクがなくなるまで繰り返す。

「本文部分の抜き出し」はすでに PruneBeforeClip の基本機能として実装しているので、
問題になるのはリンク先のページの動的な読み込みです。

最初は XMLHttpRequest を使おうかと思ったのですが、読み込みたいのはテキストデータとか
XML ドキュメントではなく、ふつーの HTML ドキュメントなので、iframe を使うことにしました。

  ・iframe 要素を動的に生成して、元ページの DOM ツリーに追加する。
  ・iframe 要素の src にはリンク先の URL を指定する。
  ・iframe 要素の onload イベントハンドラで処理を行う。
  ・読み込んだドキュメントは iframe.contentDocument で参照できる
   (これをふつうの window.document と同じように扱えば OK)。

というわけで、出来たコードが以下のモノ:
function loadUrl(url, onload, onerror) { var iframe = document.createElement('iframe'); iframe.style.visible = 'hidden'; iframe.setAttribute('height', '0'); iframe.setAttribute('width', '0'); iframe.setAttribute('src', url); if (onload) { iframe.onload = function(event) { try { onload(iframe); } catch(e) { if (isDebug) alert('FATAL: ' + e); } }; } if (onerror) { iframe.onerror = function(event) { try { onerror(iframe); } catch(e) { if (isDebug) alert('FATAL: ' + e); } }; } document.body.appendChild(iframe); } (function() { var ctrlDom = document.getElementsByClassName('ctrl')[0]; var url = ctrlDom.getElementsByTagName('a')[0].href; loadUrl(url, function(iframe) { var target = iframe.contentDocument.getElementById('tmplBody'); if (! target) throw 'unexpected content'; removeGarbage(target); arrayEach(function(elem) { ctrlDom.parentNode.insertBefore(elem.parentNode.removeChild(elem), ctrlDom); }, toArray(target.childNodes)); iframe.parentNode.removeChild(iframe); removeAll(ctrlDom); myYield(_self); // もう一度最初から処理をやる }, function(iframe) { throw 'can not load next page: ' + url; }); })();
実際のコードは、もっとゴチャゴチャしてます。 先頭ページや最終ページに到達したときは処理を変えないと行けないし、 できれば「次ページへ」みたいなリンクは削除しておきたいわけです。 ちなみに、IE のどれかのバージョンだと、iframe の onload ハンドラがうまく呼ばれない ことがあるみたいです。そのときは onreadstatechange ハンドラを使えばよいらしい。 まぁ、今回は IE には対応できないと思うので、無視ですが・・・

| | コメント (0) | トラックバック (0)

«CSS ルールの動的な追加