LDR のクロール頻度が低い?件について

以前から指摘されていることだと思うんですが、LDR ではフィードの更新が反映されるのが結構遅い場合がありますよね?

例えば、久しぶりに更新があったと思ったら一週間も前の記事だったり、といったことが割とよくある気がするんです。

そこで、「更新日時の古いもの」など任意の条件でフィードを抽出し、バックグラウンドで ping を打つ、ということを考えてみました。


まず、条件に合致するフィードを抽出する関数を作ります (LDR の Subscribe.Controller#update をベースにしています):

function _filter_all_subs(params) {
  State.subs_loader && State.subs_loader.cancel();

  var list = [];
  var limit = 200;
  var count = 0;
  var canceled = false;
  var ticker = [$("total_unread_count"), "progress"];

  State.subs_loader = {
    cancel: function() {
      canceled = true;
      oncomplete();
      message("Canceled obtaining subs.");
    }
  };

  var load = function(from_id) {
    new API([
      "/api/subs?unread=0&from_id=",from_id,"&limit=",limit
    ].join("")).post({}, onload);
  };
  var onload = function(json) {
    if (canceled) return;
    var part = (params.filter && json.filter(params.filter) || json);
    Array.prototype.splice.apply(list, [list.length,0].concat(part));

    params.onload && params.onload(list, part);
    if (json.length == limit) {
      load(json.last().subscribe_id + 1);
      count+=limit;
      message([ "Fetching", count+1, "-", count+limit ].join(" "));
    }
    else
      oncomplete();
  };
  var oncomplete = function() {
    params.oncomplete && params.oncomplete(list);
    State.subs_loader = null;
    removeClass.apply(null, ticker);
  };
  addClass.apply(null, ticker);
  load(0);
}

ちなみに splice で配列を合成する手法については id:brazil さんがLDR重複チェックスクリプトで使ってらっしゃるのを見て覚えました。

格好良いので prototype 拡張にしてみました:

Array.prototype.replace = function(ary) {
  Array.prototype.splice.apply(this, [0,this.length].concat(ary));
  return this;
};
Array.prototype.append = function(ary) {
  Array.prototype.splice.apply(this, [this.length,0].concat(ary));
  return this;
};
Array.prototype.prepend = function(ary) {
  Array.prototype.splice.apply(this, [0,0].concat(ary));
  return this;
};

"list1 = list1.concat(list2)" などとすると list1 の参照が切り替わってしまうんですが、上の方法だと元の参照を保つことができます。参照さえあれば、他所の関数内の配列を外から操作することだって可能なので、何かと便利です。


閑話休題

アプリケーション・コードはこんな感じです:

// conditional ping
register_command("cping", function() {
  var expr = Array.from(arguments).join(" ");

  _filter_all_subs({
    filter: new Function("v","with(v){return("+expr+")}"),
    oncomplete: function(list) {
      message("Pinging "+list.length+" items...");

      (function() {
        var item = list.pop();
        if (!item) return;
        ping(item.link);
        setTimeout(arguments.callee, 5000);
      })();

      /* 以前 LDR ソースの初学者だった頃は、こういう逐次処理には Queue クラスを
       * 使っていたんですが、最近は上のようにクロージャを setTimeout で再帰する
       * 方法が best practice! という結論に至りました
       */
    }
  });

  function ping(link, callback) {
    GM_xmlhttpRequest({
      method: "POST",
      url: "http://rpc.reader.livedoor.com/ping",
      headers: {
        "Content-Type": "text/xml;"
      },
      data: [
        // E4X とか扱ったことが無いので…
        '<?xml version="1.0"?>',
        '<methodCall>',
        '  <methodName>weblogUpdates.ping</methodName>',
        '  <params>',
        '    <param>',
        '      <value></value>', // title
        '    </param>',
        '    <param>',
        '      <value>'+link+'</value>', // escapeHTMLされたURL (item.linkはされてます)
        '    </param>',
        '  </params>',
        '</methodCall>'
      ].join("\n"),
      onload: function(res) {
        (callback||Function.empty)(res.responseText.replace(/<[^>]+>/g," "));
      }
    });
  }
});

これで、例えば

:cping modified_on < new Date(2006,9,1)/1000 [Enter]

とすると (少々面倒ですが) 2006/10/01 よりも更新時刻が古いフィードに対して ping を掛けることができます。

条件は、"rate" とか "link" など、フィードが持っている情報に対して自在に設定することができます (詳しくは id:reinyannyan:20061013:p1 を参照してください)。

条件によってはマッチする件数が多くなり過ぎる可能性がありますので、スクリプト側で何か工夫を加えた方が良いかもしれません。


なお、余談ですが、以前作った取得に失敗しているフィードを検出するスクリプトの filter_all_subs 関数は以下のように書き換えることが出来ます (呼び出し方法は変わりません):

function filter_all_subs(params) {
  var mode = Config.view_mode;
  if (mode != "flat")
    Config.view_mode = "flat";

  var over_limit = function(list) {
    return Config.use_limit_subs && list.length > Config.limit_subs;
  };
  var flush = function(list, final) {
    var exceeded = flushed && over_limit(list);
    if ((final && exceeded) || !(final || exceeded)) {
      subs.model.load_data(list);
      subs.sort();
      subs.show();
      flushed++;
    }
  };
  var flushed = 0;

  message(params.message);
  subs.model.load_start();

  _filter_all_subs({
    filter: params.filter,
    onload: function(list, part) {
      subs.model.load_partial_data(part);
      flush(list, false);
      update("total_unread_count");
    },
    oncomplete: function(list) {
      flush(list, true);
      $("subs_container").scrollLeft = 0;
      State.now_reading && set_focus(State.now_reading);

      if (mode != "flat")
        Config.view_mode = mode;
      message("Load completed.");
    }
  });
}

以前のものに比べ、全フィードの配列を保持しなくて済む点でメモリー効率が良くなっていると思います。

flush の中身が少し分かりにくいかも知れませんが、無駄に flush し過ぎないための工夫がしてあります。


追記 [20061019]:

h2u さんより、更新があるかどうか分からないサイトに対して ping を掛けるのは ping 本来の目的を逸脱している、という主旨の御指摘 (マングローブ - 勝手にping) を頂き、愕然としてしまいました。

確かに、フィードの提供側が能動的に更新を知らせるための機能、というのが本来でしたよね…。

クローラのスケジューリング・アルゴリズムがあまりに控え目に設計されているのでは?という意識が大きく、事の悪質性の認識が薄かったと認めざるを得ません。

また、全購読からフィードを軽快に検索するための汎用的な仕組みができた、ということが個人的には意義が大きかったんですが、そのアプリケーション例が (今回は) マズい内容であった、ということだと思います。

コードをご覧になった方には賢明な御判断をお願いするばかりです。申し訳ありませんでした。