Category Archives: AutoPagerize

AutoPagerize

AutoPagerizeのスクリプト実行順序制約をなくせるようになりました

Tumblrが新しくなって、よく見ていた/show/quotes/by/everyoneがちゃんとページングされなくなって悲しいと思っていたらcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyというスクリプトを書いてくれていました。

しかし21世紀はじめの10年最後の2009年ももう終わろうとしているにも関わらず、未だにTumblr dashboard reblog 4点セットのAutoPagerizeLDRizeMiniBufferreblogCommandの実行される順序をちゃんと覚えておかないといけないなんてローテクすぎる!という怒りにまかせて、順番に関係なく入れておけば動くように細工をしました。

それぞれ上記以降のバージョンであれば、Greasemonkeyで実行される時の順番を気にせず、とにかくインストールしておけばよくなりました。

しくみ

AutoPagerizeが実行されたときに

	var ev = document.createEvent('Events')
	ev.initEvent('GM_AutoPagerizeLoaded', false, true)
	window.dispatchEvent(ev)

というコードが実行されてGM_AutoPagerizeLoadedという名前のイベントが送られてくるようにしました。LDRizeのようにAutoPagerizeに依存してなにかを実行したときに、もしwindow.AutoPagerizeが存在しなかったらGM_AutoPagerizeLoadedイベントが送られてくるまで待ってから実行するようなコードを書けば、スクリプトが実行される順序に関係なく動作させることができるようになります。

以下はAutoPagerizeでページが継ぎ足されたときに、継ぎ足された部分をLDRizeが正しく認識するためのコードです。実行したいコードをaddFilterHandlerという名前の関数にしておいて、window.AutoPagerizeが存在していればそのまま実行、存在していない時はAutoPagerizeからGM_AutoPagerizeLoadedイベントが送られてくるのを待ってから実行することで、AutoPagerizeに依存するスクリプトを順序に関係なく正しく動作させることができます。

      var addFilterHandler = function(){
          window.AutoPagerize.addFilter(
              function(pages){
                  self.removeSpace();
                  setTimeout(function(){
                      self.initParagraph(pages);
                  }, 0);
              });
      }
      if(window.AutoPagerize){
        addFilterHandler();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', addFilterHandler, false);
      }

AutoPagerizeと連動するスクリプトを書く人へのおねがい

AutoPagerizeに依存する部分を

      var f = function () {
        // やりたいこと
      }
      if(window.AutoPagerize){
        f();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', f, false);
      }

と書くとスクリプトが実行される順序を気にしなくてよくなって、使ってくれる人の手間も省けるのでぜひご採用ください。

Webサイト側でのAutoPagerize検出

うれしい副作用として、ユーザスクリプトと同じようにHTMLドキュメント側でもGM_AutoPagerizeLoadedをlistenしておくことでAutoPagerizeが存在するかどうかを検出することができます。

  var isAutoPagerizePresent = false;
  window.addEventListener( 'load', function () {
    alert(isAutoPagerizePresent);
  }, false );
  window.addEventListener( 'GM_AutoPagerizeLoaded', function () {
    isAutoPagerizePresent = true;
  }, false );

Firefox3.1beta2+OSXの場合、GM_AutoPagerizeLoadedイベントが発生するタイミングはjQueryのreadyメソッドよりも後、loadイベントよりも前でした。

オチ

もとのcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyの説明をよく読むと

AutoPagerizeと併用する場合は、「ユーザスクリプトの管理」でAutoPagerizeよりもに置いておく必要があります。

と書かれていたのでした… にしないといけない制約しか考慮していなかったのでにしないといけない場合は、今後も順番を気にし続けるか、AutoPagerizeにそういう細工をして取り込んでくれるようにとswdyhにpull requestを送ってください。

次のページのリンクを推測する上での困難メモ

HTMLから次のページのURLを推測するAPIを作る上での問題点メモ。

http://ido.nu/kuma/az-speculator/?u=http://jp.techcrunch.com/&callback=f&format=jsonp

old/new, next/prev問題

デザイン上の問題として昔から言われているもの。時系列 descendant で並べてると見ていく人的にはNextはolderだけど、レイアウトする時は左側?右側?みたいなやつ。

これはクライアントが必ず1ページ目(連続するなにかの端)から見ていると仮定できるのなら、既に読み込んだページを記録して参照することで解決できる。

階層構造問題

Next articleとNext pageが同時に存在するとどっちを優先すればいいかはコンピュータにはわからない。単語の意味を知らないから。単語の意味を統計的に与えることは理論的には可能だけど、サイトの構造がこういう階層を持っているかどうかを判別して、統計データとして役に立つだけの量を集めるのは困難。

これはパスを見て、上方向の変化にペナルティ、下方向の変化にインセンティブをつけるとわりと解決する。

ナビゲーション»問題

こういう
navigation
ナビゲーションで»がどのページへのリンクになっているかはサイトによって不定。googleのように2ページ目になっていることが大半だが、わりと13ページ目になっているサイトも多い。

これは解決が困難。ナビゲーション部分の»にペナルティをつける?

ページオリジン問題

ユーザが見はじめたページを1ページ目と仮定することで次のページのリンクを予測することは格段に容易になるが、全てのサイトが1ページ目から始まるわけでなく、古い画像掲示板等で0ページ目から始まるものがある。
絶対数は少なく、現代的なサイトでは見られないので無視していいかも。

誤判定問題

次のページを間違って推測していても間違っているかがわからない。訂正する方法が必要。

おまけ

WedataのデータをもとにXML::libXMLでテストするときの問題。

cakePHPのscaffoldテンプレートをAutoPagerizeする

cakeのscaffoldでフォルトテンプレートを自分のアプリケーションのview/scaffoldsにコピーする。

cp cake/libs/view/scaffolds/index.ctp app/views/scaffolds/

そしてAutoPagerizeのためにautopagerize_page_elementrel="next"をマークアップする。できあがり。


--- index.ctp   (revision 2644)
+++ index.ctp   (working copy)
@@ -31,7 +31,7 @@
 'format' => 'Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%'
 ));
 ?></p>
-<table cellpadding="0" cellspacing="0">
+<table cellpadding="0" cellspacing="0" class="autopagerize_page_element">
 <tr>
 <?php foreach ($scaffoldFields as $_field):?>
        <th><?php echo $paginator->sort($_field);?></th>
@@ -78,7 +78,7 @@
 <div class="paging">
 <?php echo "\t" . $paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled')) . "\n";?>
  | <?php echo $paginator->numbers() . "\n"?>
-<?php echo "\t ". $paginator->next(__('next', true) .' >>', array(), null, array('class'=>'disabled')) . "\n";?>
+<?php echo "\t ". $paginator->next(__('next', true) .' >>', array('rel' => 'next'), null, array('class'=>'disabled')) . "\n";?>
 </div>
 <div class="actions">
        <ul>

SITEINFOのないページをAutoPagerizeするSITEINFO speculator for AutoPagerize version 0.0.2

Pagerization version 0.2.2 – ?D of Kに触発されてちょっと自分でも書いてみた。

AutoPagerizeよりもあとに実行されるようにインストールしておくと、AutoPagerizeでSITEINFOにマッチしなかったとき自動的にnextLinkpageElementを推測してAutoPagerizeを再実行します。

インストール

siteinfospeculator.user.js

つかいどころ

全部のページで使うとちょっと重たいので、Greasemonkeyの設定でincludeを*.tumblr.comで使ったりするといいとおもいます。tumblrのAutoPagerizeされてないテーマも、だいたい正しくAutoPagerizeできるかんじです。

フィードバック

SITEINFO speculatorでAutopagerizeが起動されたときは、右上の緑色がほんもののSITEINFOで起動されたときよりも暗くなります。マウスを持っていくと、自動生成されたSITEINFOの質がどうだったかのフィードバックを送るためのリンクが出てくるので、今後の開発の参考になるので、ひどいSITEINFOができてむかついたというときはmessyを、完璧で感動したというときはperfect, 一応動いてるけどみためがおかしくなるとかのときはokを押してもらえると助かります。

rating UI

SITEINFOのレイティングと一緒に、ページのURLと使っているブラウザのUserAgentと生成されたXPathが送信されます。送られたデータはRatingsからみることができます。もし間違っておくって消したい、というときにはここで削除してください。

アルゴリズム

nextLinkの検出は適当です。単純なOperaのFast Forwardアプローチで、”»”, “次”, “Older”, “older”, “Next”, “next”, “→”, “←”, “>”, “<”, “«”, “前”, “Prev”, “prev” をキーワードにして引っかかったテキストリンクだけを探しています。なのでnextLinkが画像だと機能しません。(version 0.0.2で画像もいちおう対応しました)

前にSITEINFOで調べたかんじでは、nextLinkは時系列の向きに関わらず右向きの矢印で示されることが多かったので、そういう並び方に変えたほうがよさそう。今はマッチしたキーワードのリンクを、テキストの長さに反比例するコスト関数で評価して、コストの小さいものをnextLinkにしてます。のページのほうが10 年の今日、Netscape Communications 社が mozilla.org を設立よりも優先されるように、という小細工。

pageElementXML::Diffのアルゴリズムをベースにしてます。Diffは正確に差分を出す必要があるけど、今回は差異があることだけわかればいいので、かなり簡略化して差異がある部分だけをみつけて、ツリーを下から上に差異を足しこんでいって、終わったところで、今度は上から差異の変化を見ていって差異のほとんどが含まれている要素を見つけてそれをpageElementにしています。0.5~1.0秒くらいブロックしちゃうかんじだけど、そこはyieldで書き直せば解決。

精度のほうはあんまり試してないけど、nextLinkが見つかったものに関しては5~6割くらい正しくpageElementを出せそう。

Pagerization version 0.2.2 – ?D of Kのリストを参考にいくつか試した結果は

http://1470.net/list/memo/recent
ok
http://2ch.ru/g/
nextLinkが見つけられない
http://www.adiumxtras.com/index.php?a=search&cat_id=3
ok
http://allabout.co.jp/relationship/secondmarriage/closeup/CU20071006A/index.htm
ok
http://www.altphotos.com/Gallery.aspx?browseby=toprated
ok(でも2ページしか出ない。AutoPagerizeのSITEINFOがあっても2ページしか出ないけど偶然?)
http://jp.techcrunch.com/
ok(アルゴリズム的には正しいけどレイアウトが乱れる)
http://mozillazine.jp/
ok
http://coderepos.org/share/changeset/
ok(ただしレイアウトが大きく異なっているChangeset 8044 – CodeRepos::Share – Tracで止まる)

かんそう

nextLinkを見つけるようなアルゴリズムがない。たくさんデータを集めて統計的に解決するしかなさそう。

Greasemonkeyが実行されてから実際にAutoPagerizeで次のページがロードされるまで時間があるので、Greasemonkeyでページの解析をしなくても、リモートのサーバにリクエストを送って差分を解析してもらうこともできる。これは基本的にブラウザで新しいページをロードするたびにリクエストが送られることになる。サーバ側はpathtraqと同じデータを得ることができる。だから今のpathtraqに欠けているインストールすることのインセンティブとして、nextLinkpageElementを返してくれるAPIとかどうかなーと思ったり。pageElementの検出はWebページの本文抽出にも似ているところがあって、ますますpathtraqと相性いいなと感じました。

検出精度を高めていくには自動でテストをまわしてカバレッジを出してくれる何かがないとつらいです。perlのWWW::Mechanize::AutoPagerHTML::TreeBuilder::XPathでSITEINFOをパースしてみたらXPathの評価で例外が出まくってだめでした。PHPのXMLまわりはクソでwell-formedにするのも一苦労なので、ruby, pythonをまともに使えない自分には手詰まり。

Firefox3beta4とAutoPagerize0.0.24で起きる問題の修正patch

よくわかんないけどp.innerHTMLに代入したときに中にbodyinnerHTMLが全部入るみたいなへんなことになってた。Twitter / みずも: AutoPagerizeのせいというよりはグリースモンキーのせいだな とも書かれてるけど、原因は調べてない。なんとなくinnerHTML使わないでDOMで書き直したら正しく動いた。

追記

id:flagmeisterさんにrange.createContextualFragmentinnerHTMLが怪しいって教えていただきました。

ネームスペース付きで操作するの、やりかたしらないのでまちがってたらすいません。
はじめsetAttributeNS(HTML_NAMESPACE, ...)にしたらなんかリンクになってくれなかったのでsetAttributeに変えました。

--- autopagerize.user.orig.js   2008-03-11 20:04:34.000000000 +0900
+++ autopagerize.user.js        2008-03-11 20:03:47.000000000 +0900
@@ -273,8 +273,14 @@
     var self = this
     this.insertPoint.parentNode.insertBefore(hr, this.insertPoint)
     this.insertPoint.parentNode.insertBefore(p, this.insertPoint)
-    p.innerHTML = 'page: <a class="autopagerize_link" href="' +
-        this.requestURL + '">' + (++this.pageNum) + '</a>'
+    this.insertPoint.parentNode.insertBefore(document.createTextNode("page: "), this.insertPoint)
+
+    var a = document.createElementNS(HTML_NAMESPACE, 'a')
+       a.appendChild( document.createTextNode((++this.pageNum)) );
+       a.setAttribute('class', 'autopagerize_link');
+       a.setAttribute('href', this.requestURL);
+    this.insertPoint.parentNode.insertBefore(a, this.insertPoint)
+
     return page.map(function(i) {
         var pe = document.importNode(i, true)
         self.insertPoint.parentNode.insertBefore(pe, self.insertPoint)
Greasemonkey以外のふつうのスクリプトでもこのinnerHTMLがらみで問題が出てるようなので、けっきょくbeta3にもどしました。beta4ははずれってことで。5に期待。

json2infogami siteinfo format converter

jsonで書いてあるSITEINFOを、infogamiのフォーマットにあわせるの、いつも手作業でやっててアホだったのに気がついた。

function json2infogami(firstarg) {
   var infogamiEscape = function (v) { return v.replace(/([_\*])/g, "\\$1" ) };
    var textfilter = infogamiEscape ;
var a =  (firstarg.constructor.name == "Array") ? firstarg : arguments;
    var r = [];
       for(var i = 0; i < a.length; i++) {
                 var siteinfo = "";
         for ( var n in a[i] ) {
            var v = a[i][n];
            siteinfo += n + ":    " + textfilter(v) + "\n";
      }
      r.push('<textarea class="autopagerize_data" readonly="readonly">\n' + siteinfo + '</textarea>\n');
    }
    return r.join("\n\n");
}

配列を渡してもいいし、引数として複数書いてもいいです。argumentsがArrayのインスタンスだったらいいのにね。これをFirebug consoleで実行する。


copy( json2infogami( 
{
url:            'http://blogs.yahoo.co.jp/(.+)/GALLERY/.+',
nextLink:       '((//p[@class="forwardNext"]/a))[2]',
pageElement:    '(//div[@id="newestImages2"])[last()]',
insertBefore:   '(//div[@class="clearFix pagingNavi2"])[last()]',
exampleUrl:   'http://blogs.yahoo.co.jp/kyo_he_piece/GALLERY/gallery.html?fid=0&p=12',
},

{
url:            'http://blogs.yahoo.co.jp/(.+)/MYBLOG/yblog.html.+',
nextLink:       '((//p[@class="forwardNext"]/a))[2]',
pageElement:    '//div[@class="entry"]',
insertBefore:   '(//div[@class="clearFix pagingNavi2"])[last()]',
exampleUrl:   'http://blogs.yahoo.co.jp/boooy2005/MYBLOG/yblog.html?fid=0&p=12',
}
) );

copyはさっき知ったので入れただけ。クリップボードに文字列をコピーする。Firebugの出力コンソールは改行とかスペースとか変になったりするので、そういうときにcopyは便利だ(とおもう)。

こういうのはどういう風に使うといいんだろう。userchrome.jsなんかで window.FirebugCommandLineAPI.prototype.json2infogami に追加してFirebugのconsoleAPIにしておくのが便利かなあ。でも追加しても使おうと思ったときには絶対名前とか忘れてると思う。囲んだ部分をjsonとして渡して起動するブックマークレット?元のデータがvim上でみてるやつのときには不便。

The art of AutoPagerize nextLink XPath writing

SITEINFOのnextLinkケーススタディ – 0x集積蔵を読んで自分が書いたのを思い出したら、これを公開したつもりで公開してませんでした…

os0xさんの書かれた記事のほうが網羅的なので、あちらを読んでもらえばよいですが、兄弟要素から取得するケースnumberaの中の数字を利用することができるときもある、というのだけいいたいので公開しておきます。

AutoPagerizeのSITEINFOを書いていると、ナビゲージョン部分のHTMLがださくてnextLinkをXPathで表現できなさそうなことがよくあります。

が、ふたつほどコツを見つけたのでご紹介。

following-sibling, descendant-or-selfを使う

ありがちなのがナビゲーションに次のページへのリンクがなくて、数字だけが並んでいて今いるページだけハイライトされてるタイプのやつです。
Behance :: Make Ideas Happenのナビゲーションがそれでした。みたいめがこんなやつ。

Behance navigation

Behanceの場合HTMLはきれいでこんなふうになっています。

behance navigation HTML

この場合は楽勝。spanの次にあるaを表すXPathを作ればokです。

//span[@class="pages"]/following-sibling::a[1]

HTMLによってはspanaが同じ親を持っていなくてaがさらにfont(!)なんかで囲まれていることもありますが、そのときはdescendant-or-selfを組み合わせて使えば大丈夫。

numberを使う

nextLink固有の特徴なので他のときには使えませんが、ページのナビゲーションなのでたいていは末端の要素に数字が入っています。それを利用します。さっきのBehanceを例に。今見ているページの数字に1足したものを取り出すというアプローチができます。

//a[text() = number( //span[@class='pages']/text() ) + 1 ]

Userscripts.orgの検索結果ページはナビゲーション部分のHTMLが

みたいに、ひどいことになっていて、現在のページを表す要素を取り出すことが困難です。でもnumberを使うとわりとかんたん。

id("content")/p[last()]/node()[not(self::a) and ( number(self::text()) > 0  ) ]/following-sibling::a[1]

pの子要素でaじゃないやつで数値として評価したときに0より大きくなるやつの次にあるaというふうに表現できます。かんたん。ただ1ページ目のときはPages: とページ番号のところがくっついてひとつのテキストノードになって

number("Pages: 1")が0を返すので、Pages:の部分はあらかじめ取り除いておく必要があります。それを加えて

id("content")/p[last()]/node()[not(self::a) and ( number(self::text()) > 0 or number( substring-after(self::text(), "Pages:") ) > 0 ) ]/following-sibling::a[1]

にすればかんぺき。

ちなみにnumberの仕様は、文字列の先頭にある空白文字は全部無視、数値列のあとにある空白も全部無視するという仕様。atoiは数値列のあとに数値じゃない非空白文字があっても、それ以前の部分を値として評価してくれますがXPathのnumberはそういうときはNaNになるという仕様で、Firefoxでの実装もそうなっています。

paragraphのdt/dd問題

AutoPagerizeのSITEINFOについての考察の複数の要素でひとつのparagraphが構成されている場合の補足。

いくつかのサイトの構造を見ているうちにMozilla Developer Centerの検索結果ページのように、ひとつのparagraphがdtとddで構成されている場合があるのに気がついた。

この書き方はひとつのエレメントがひとつのparagraphであってほしい立場からすると美しくないけれど、この書き方は仕様的にLists in HTML documents (ja)正しい書き方だ。ひとつのエレメントがひとつのparagraphであることが望ましいという主張は、DL要素の仕様と相容れないことになる。

親要素が何かをチェックして、dlの場合はdtから次のdtまでがひとつの繰り返しだと判断するのが妥当ということになる。

いまさらながらAutoPagerizeでmixi日記のコメント欄が逃げる件

いまさらながら、10/1にmixiのデザインが新しくなったときに、前よりぜんぜんXPathフレンドリーなHTMLになっていたのがうれしくてためしにSITEINFOを書いたけど、コメント欄がpageElementに入っていなかったのでコメントが書けなくなってAutoPagerizeを使われているひとに迷惑をかけた件について。まず迷惑をおかけしてしまってごめんなさい。そして直していただいたotusneさんありがとうございます。

このことについてまとめられているAutoPagerizeでmixi日記のコメント欄が逃げる件 – cameraLadyこの件を通じて思ったことを読んで思ったことを。

この件を通じて思ったこと

個人的にはSITEINFOは気軽に書くことができて、気軽に使えるほうがよいと思う。

もし気軽にSITEINFOを書くことができないなら、たとえば一度書いてSITEINFOコミッタに提出したあとレビューをうけて反映される、ような手順が必要だったら、いまのようにはwiki上でSITEINFOが共有されずに各ユーザのautopagerize.user.jsの中に死蔵されてしまって今のようにはハッピーでなかってのではないかと思う。コミッタのレビューが必要な場合、コミッタのレビューを経て反映されるものと、誰のチェックもなく反映されるものと、どちらが参加しやすいかといったら後者だから。

Wikipediaとの比較

でも誰のレビューも通らないで自由に書けるのなら間違っていたときに困るという問題はある(実際今回困ったわけだし)。Wikipediaの場合は、間違っていたら気がついた誰かが直す、という仕組みを採用していて、たしかにときどきネタが仕込まれて正しくない状態になったりもしているが、全体で見れば極めてよく機能している。

SITEINFOも基本的にはWikipediaと同じやり方でうまくいくのではないかと思う。ただSITEINFOはWikipediaと決定的に違うところがある。Wikipediaは誰でも使いこなせる自然言語で書かれているのに対して、SITEINFOはあんまり使える人のいないXPathで書かれているので、おかしいと気がついても誰でもは修正できない。その点でWikipediaとまったく同じ方法をとることはできない。

XPathは誰もが書けるようなものでない問題を解決する方法のひとつとしてAutopagerize IDEのようなスクリプトでXPathの生成を助けることが考えられるが、実際にはHTMLの美しくなさに起因する問題があるので容易ではない。

スクリプト側での対策

SITEINFO自体の正しさを保つことは参加のしやすさとのトレードオフになるので、ある程度の誤りは許容する必要があるだろう。
AutoPagerize自体はそういった機能は持っていないが、それをAutoPagerizeとは別のひとつのGreasemonkeyスクリプトとして実装してどういう実装が好ましてか、どの程度有用かを検証することは難しくない。

AutoPagerizeはGreasemonkeyのwindowオブジェクトにwindow.AutoPagerize.addFilterという変数を設定している。自分のGreasemonkeyスクリプトからFirefox、evalの第二引数、プライベートメンバ/クロージャーの実行コンテキストへのアクセス – 実用を使って

var autopagerize = eval("AutoPagerize", window.AutoPagerize.addFilter);

というコードでAutoPagerize全体にアクセスすることができるようになり、AutoPagerizeのコードを直接書き換えることなく他のGreasemonkeyスクリプトからAutoPagerizeの動作を変更することができるので、他の人にもスリプトをインストールしてもらうだけでかんたんに試してもらうことができる。

各SITEINFOの記述のMD5を取って、変化があったときには旧バージョンのものを保存しておき、あたらしいものを適用して問題があったら前に戻せるようにする、ことでSITEINFOの誰かが書いたものが自動的に反映されるという利点を損なわずに、問題があるSITEINFOに対処することができる。と思う。

最後に

今回自分がmixiのSITEINFOを追加してからotsuneさんによって修正されるまでに5日が必要だった。infogamiのアカウントが新規で発行されていないことで限られたひとしか修正できず長くなったところもあるかもしれないが、問題に気がついたひとがXPathが書けなくても修正できるようになれば、問題のある状態の期間は短くなるだろうし、さらにAutoPagerizeが対応するサイトも増やせるかもしれない。

クライアント側での問題回避は比較的容易に実装できるが、SITEINFOの正しさを保つのは集合知の利用とのトレードオフにもなるのでどういう方法でそれを実現するかはよく考える必要がある。

AutoPagerizeのSITEINFOについての考察

AutoPagerizeSITEINFOpageElementについて気がついたことを書いておく。

LDRize paragraph構造とplagger EntryFullText構造

AutoPagerizeで複数のページをひとつに繋ごうとするとき、各ページの構成には大きくわけて二種類ある。

ひとつめはGoogleの検索結果のように、ひとつのページに10回程度の繰り返し部分があるもの。この場合pageElementにはこの繰り返されている部分にマッチするXPathが書かれている。これはLDRizeのSITEINFOにおけるparagraphで表現されるものと一致するので、ひとつのページ内に繰り返される部分が存在し、その部分ひとつひとつにマッチするXPathをpageElementとして記述するものをLDRize paragraph構造と呼ぶことにする。

もうひとつはCNETのインタービュー記事のように、多くの場合ひとつのページの中にはひとつの長い文章が記述されていて繰り返し部分が存在しないもの。この場合pageElementにはそのページにある本文を構成する文章全体にマッチするXPathが記述される。plaggerのFilter::EntryFullTextで取り出そうとする部分に近いので、ページの本文を構成する文章全体にマッチするXPathをpageElementとして記述するものをplagger EntryFullText構造と呼ぶことにする。

美しくない現実世界の問題

基本的には前述の通りページの構造は二つに分類することが可能で、それぞれにあった方法でpageElementを記述することになる。しかし、実際のウェブページには論理構造とマークアップが一致してないことに起因する美しくなさが多数存在するため、結果としてpageElementのXPathの記述も美しくなくなる。

複数の要素でひとつのparagraphが構成されている

XPathの記述的にも論理的にも、ひとつのparagraphがHTML上でひとつの要素として記述されていることが望まれるが、現実に行われているマークアップは繰り返しの単位がひとつの要素であるように書かれていないことがある。HTML上の複数の要素でひとつのparagraphが構成されているとXPath単体ではどこからどこまでがひとつのparagraphなのかを表現することはできない。

実例としてTechCrunch Japaneseがあげられる。TechCrunch Japaenseは、ひとつのページに5つのエントリ全文が一覧表示されるようになっている。各エントリは日付を表すh2タグと本文を持つdivの二つの要素で構成されている。

この場合XPathではどの部分がひとつのparagraphかを表現することができない。

id('content')/*[ self::h2 or self::div[@class='post']]

と書けば、全てのparagraphに含まれている要素集合と同じものにマッチさせることができるが、各パラグラフがどの要素で構成されているのかという情報は失われている。(XPathで得られた要素集合の並びを調べて、ひとつのparagraphを推定するのは簡単かもしれない)

本文のテキスト要素とその他の部分が同一要素のsiblingになっている

この問題はCSSが一般的に使われるようになる前の古い時代に作られたサイトでよく見られる。文章のタイトルと本文とがひとつのIMG要素やHR要素で区切られていると、XPathで本文だけを切り出すことは非常に困難である。

実例として東京ふーどページの新店情報があげられる。
新店情報はdiv.eventtextの中に全て入っており、各繰り返し部分のタイトルはdiv.eventtitleの中に入っているが、本文は全てdiv.eventtextの子要素のテキストノードとして入っている。HR要素が各繰り返しの区切りとして使われている。

一見following-siblingを用いて記述することだけはできそうだが、よく見るとdiv.smallphotonewが存在するときと存在しないときがあるため

//div[@class="smallphotonew"]/following-sibling::text()

という記述では漏れが出てしまう。努力すればすべてを網羅する記述はできそうだが、XPathに習熟していなければ書くことはできないだろう(自分にはできないです)。

この問題は、もともとの文章がplain textでそれをHTMLに流し込んだものでも発生する。こちら第2編集部隊から本文だけをXPathで切り出すことはできない。(そもそも全文がひとつの要素の子要素のテキストノードになっているから)

まとめ

AutoPagerizeのpageElementには、LDRize paragraph構造を表現するものとplagger EntryFullText構造を表現するものとがあると思ったけれど、実際に調べてみるとHTMLの構造が原因でparagraphをXPathで正確に表現できないことも多く、LDRize paragraph構造を持っているページでもplagger EntryFullText構造でpageElementが記述されているものも多い。

繰り返し部分を繰り返しとして記述すればよりAutoPagerizeのpageElementとLDRizeのparagraphの互換性が高くなるが、実際に書かれているHTMLの美しくなさがそれを阻んでいる。