風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

ページング(他ページを読み込んで継ぎ足す)スクリプトを作った際のつまずきメモ

まえがき

自分的にはすごく使えるのだけれど、他人の反応は皆無だった

【backstage_pass】サンデーまんが家バックステージをページングするスクリプトを作ってみた: 風柳亭
サンデーまんが家バックステージをページングするスクリプト…を作っていて、つくづく…… - 風柳メモ

ですが、この手の
『他のページを非同期に読み込んで、今のページに継ぎ足す』
系のスクリプトをクロスブラウザ対応を目指して作る際、よくつまずく所に、今回も懲りずにつまずいたので、今度こそメモしておこうと思いたったのが、記事を書いた動機です。
AutoPagerlike: Sleipnir+SeaHorse版 AutoPagerize(もどき): 風柳亭や、Yin and Yang:リンク先の内容をその場で表示するスクリプト(Greasemonkey/SeaHorse/ブックマークレット): 風柳亭なんかを作った時にもつまずいているはずなのに、まったく成長がないというかなんというか。


今回、大きくつまずいたのは、

なので、これらを中心にしてみました。
あくまで自分のための覚え書きなので、内容の正確さなどは疑問符が付きます。悪しからず。

文字化け

今回の場合元のページ(例)も、取得して切り抜き&くっつけるページ(例)も、どちらも同一サイト(ドメイン)で、かつ同一文字コード(charset=Shift-JIS)だから楽勝……かと思ったら、単純にXMLHttpRequestオブジェクトでGETしてくるだけだと、ほとんどのブラウザで文字化けが発生…orz。
やっぱ、UTF-8じゃないとだめなのかよ〜と愚痴の一つも出ようというもの。

対策(Firefox、Safari、Google Chromeの場合)

これらのXMLHttpRequestオブジェクトには、overrideMimeType()という関数が付いてくる。
これで、要求時に読み込み先の文字コードを指定してやれば、正しい文字コードで取得出来る模様。

var xh=new XMLHttpRequest();
// :
var mimeType='text/html; charset='+(document.characterSet||document.charset);
xh.overrideMimeType(mimeType);

なお、Greasemonkeyの場合には、overrideMimeTypeをGM_xmlhttpRequestの引数のキーで指定してやればよい。

GM_xmlhttpRequest({
    url: url,
    method: 'GET',
    overrideMimeType: mimeType,
    onload: onload,
    onerror: onerror,
})

  • document.characterSet||document.charsetにしているのはmimeTypeを他にも使いまわすかもしれないので、IE用に(IEにはcharacterSetの方がない)。
  • document.〜にしているのは、今回は読込元も読み込み先も同じ文字コードだったから。違う場合にはなんらかの方法で読み込み先のコードを知る必要が有る。

対策(IE、Operaの場合)

OperaのXMLHttpRequestオブジェクトには、overrideMimeType()が付いているにも関らず、これを指定してもやっぱり文字化けが発生してしまった……Opera、ダメな子?*1
→Operaではopen()後にoverrideMimeType()を記述しないと無視されてしまうらしい(os0xさん、ご指摘有り難うございました)。


また、IEのそれ(new ActiveXObject('Microsoft.XMLHTTP')とか)には、そもそもoverrideMimeType()は付いてこない。


IE…というか、Sleipnirならば、

var adstrm=sleipnir.CreateObject('ADODB.Stream');
adstrm.Type=1;  // binary
adstrm.Open();
adstrm.Write(xh.responseBody); // xh: new ActiveXObject('Microsoft.XMLHTTP')オブジェクト
adstrm.Position=0;
adstrm.Type=2;  //  text
adstrm.Charset=mimeType; // document.charset
var html=adstrm.ReadText();
adstrm.Close();

のように、sleipnir.CreateObject('ADODB.Stream')を使って変換する手が使えるが*2、Sleipnirスクリプトと他のユーザサイドスクリプトの判別処理や、bookmarkletだと動かないなど、繁雑な手順が煩わしくなり、今回は見送った。


結局、同一ドメインだし、ということで、不可視のIFRAMEをページに埋めこんで、これを使って他のページを読み込み(これなら、IEでもOperaでも、その他のブラウザの場合でも文字化けしない)、onloadイベントをとらえて*3、加工することにした。

クロスドメイン時について

今回は不要だったが、他ドメイン上のページの取得が必要になった場合のやり方は……正直よくわからない。


まぁ、取得自体は、FirefoxとSleipnirは比較的容易*4だが、取得元ページと文字コードが違ったりした場合はなんらかの判別手段が必要だろうし*5


それ以外のブラウザでは、そもそもクロスドメインの取得方法からして調べる必要が有る*6ので、ここでは深く言及することはしない*7

HTMLDocumentの取り扱い

別ページを読み込んだ後、必要な部分の切出しを行なう方法には

  1. テキストとして扱い、正規表現で切出す。
  2. DOM化し、getElementsByTagName()等を使って頑張る。
  3. DOM化し、XPathで切出す。
  4. DOM化し、jQueryやquerySelectorなどを使って、CSS指定で切出す。

など、いくつか考えられるが、今回は、最初は正規表現とXPathを平行して使用し、ver.0.01c 辺りからXPathに一本化した。
なお、IEでのXPathの使用は、例によってid:amachangさんのJavaScript XPathを使わせて頂いた。


ちなみに、欲しい部分を切り出すためのXPathは

var XPATH_ELEMENT = 'id("message")/*[following-sibling::div[@id="profile" or @class="k"]]';

で、正規表現だと

var REG_ELEMENT = /<div id="message">([\s\S]*?)<div (?:id="profile"|class="k")/;

こんな感じだった。
CSSセレクタだとどんな感じで書けるんだろう?

IEとOperaの場合

これらは、そもそもがIFRAMEで読み込んでいるために既にHTMLDocumentとなっており、XPathを使って目的部分を抜き出すのは、比較的容易であった。


ただし、IE7以前では、このようにして抜き出したIFRAME下のDOM要素は、そのままでは親HTMLDocument下には貼り付けられない。
つまり、

for (var ci=0,len=childElms.length; ci<len; ci++) parentContainer.insertBefore(childElms[ci],insertPoint);

のようにするのではなく(IE8やOperaならこれでも動く)、

for (var ci=0,len=childElms.length; ci<len; ci++) insertPoint.insertAdjacentHTML('BeforeBegin',childElms[ci].outerHTML);

のように、いったんouterHTMLでHTML化した物を、insertAdjacentHTML()を使って再度DOM要素に変換して挿入してやる必要が有った。
これでIE6/IE7でも動くように*8

Firefox、Safari、Google Chromeの場合

実は、これらはXMLHttpRequestを使ってHTMLをテキスト(responseText)で取得出来ていたので、正規表現で必要な部分を切り出してからDOM化して挿入する方が楽だったため、実際、最初の頃(ver.0.01bまで)はそうしていた。


…だがしかし、ブラウザによって、必要箇所の指定方法が異なる、というのも気持ち悪いため、IEやOperaと同様にHTMLDocument化して必要箇所を取り出すことにした……のだが、これが意外と苦労した。


まず、IFRAMEを使った場合。
普通にcreateElement()した不可視のIFRAMEをdocumentに挿入し、open()/write()/close()してアクセスしようとした……が、何故かうまく行かない。
その後、SCRIPT要素をあらかじめ消しておいてからやるとうまくいった…ように思えたのだが、そもそも、Google Chromeの場合には、IFRAME下のdocumentにはアクセス出来なかったことを思い出し、中止。


いろいろと試行錯誤した結果、

  1. documnet.implementation.createHTMLDocumentを持っているブラウザならば、これを使うのがもっとも楽。
  2. ↑を持っていない場合、createDocumentメソッドを使うなどの方法はあるが、これは完全に独立したHTMLDocumentとしては扱いにくい*9上に、名前空間の問題などがあってややこしいため、XSLT の HTML 出力を使うのが一番楽そう。


ということで、

// === createHTMLDocument()を持っているかどうかで場合分け
if (document.implementation&&document.implementation.createHTMLDocument) {
    var htmlDoc=document.implementation.createHTMLDocument('');
}
else {
    var proc=new XSLTProcessor();
    var xsltStyleSheet=new DOMParser().parseFromString([
        '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'
    ,       '<xsl:output method="html" />'
    ,       '<xsl:template match="/">'
    ,           '<html><head><title></title></head><body></body></html>'
    ,       '</xsl:template>'
    ,   '</xsl:stylesheet>'
    ].join(''),'application/xml');
    proc.importStylesheet(xsltStyleSheet);
    var htmlDoc=proc.transformToDocument(xsltStyleSheet);
}
// === ここからは共通化
var range=htmlDoc.createRange(); // rangeまでは予め用意しておけば、GETの度に作成する必要はない

// :
// : XMLHttpRequestオブジェクト(xh)によるGET処理
// :
var	html=xh.responseText;
html=html.match(/<html[^>]*>([\s\S]*)<\/html/i)[1];
range.selectNodeContents(htmlDoc.documentElement);
range.deleteContents();
htmlDoc.documentElement.appendChild(range.createContextualFragment(html));

のように作るのが、今のところ一番すっきりとするかな、と思ってそうしてある。
これなら、名前空間なしのXPathも使えることだし。
なお、この部分に関してはHTMLDocument の動的な作成: Days on the Moonが多いに参考になりました。nanto_vi (なんと)氏にはお礼申し上げます。


あ、IEとOperaは(見出しも分けてあるように)別ということで。
Operaはなんとなく(文字化けの問題さえなければ)上記方法でいけそうな気もするのだけれど。


IEの場合は……まぁ、

var htmlDoc=new ActiveXObject('htmlfile');
htmlDoc.designMode='on';
htmlDoc.open('text/html');
htmlDoc.write(xh.responseText);
htmlDoc.close();

のようにするのがてっとり早いとは思うが、ただ確か、PATHの問題がなにか有ったような。

これかな?

ActiveXObject('htmlfile')でHTMLドキュメント作ると、リンクの相対パスが勝手に(現在のページのドメインをベースとした)絶対パスに書換えられてしまう

AutoPagerize/AutoPagerlike:Google イメージ検索への対応を試みる: 風柳亭

HEAD要素にむりやりBASE要素を埋めこんで、基準となるPATHを指定しなおしてやればなんとかなった、かも知れない。

*1:こういう場合往々にして自分の単純ミスの可能性が高いのだが。

*2:以前AutoPagerLikeを作製した時に使った手。

*3:以前、IEだとonloadじゃだめでonreadystatechangeを使う必要が有る、とどこかで読んだ記憶があるのだが、onloadをattachEvent()することにより、問題無く使えた(IE6/7/8)。

*4:GM_xmlhttpRequest()はもともとクロスドメイン対応だし、Sleipnirはsleipnir.CreateObject('Msxml2.XMLHTTP')オブジェクトを使ってやれば良かったは

*5:Greasemonkeyはとにかく一度取得してcharsetを特定してから、overrideMimeTypeを指定しなおして再読み込みする処理を書いたことがあった。sleipnir.CreateObject('ADODB.Stream')の場合は割と優秀な自動判別機能が付いていたのでそれに頼っていた覚えが…。

*6:Google Chromeの場合はbackgroundタスクで取得する仕組みを作り、表タスクからの要求を受けたら結果を返す、というような通信をすることでどうにかするのだろうと見当は付くが

*7:出来ない、ともいう(哀)。

*8:ちなみにOperaでもこのまま動く。場合分けする必要が無くて助かったけれど、正直意外だった。

*9:rangeは元HTMLDocumentのものを使う必要が有る