中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

続・javascriptの循環参照オブジェクト

以前書いた記事の続き

実際に使って行くと、色々至らぬ点が見えてきたので
かなり本格的にリファクタリングする事にした。

Object crawler

使い方

string の値を全て拾って来る方法

var callback_object = function(){
}
callback_object.prototype = {
  ret : [],
  cb_string    : function (path,value,cyclic,in_array,objid){
    this.ret.push(value);
  }
}
var data = get_object(); // 何かのオブジェクト
var callback = new callback_object;
crawl_object(data,callback);
callback.ret; // data内に含まれる文字列の配列
I/F
function crawl_object( data , cbobj ) {
  function cb_nop(path,value,cyclic,in_array,objid){
    return ! cyclic;
  }
  if ( cbobj.cb_undefined === undefined )  cbobj.cb_undefined  = cb_nop;
  if ( cbobj.cb_null      === undefined )  cbobj.cb_null       = cb_nop;
  if ( cbobj.cb_string    === undefined )  cbobj.cb_string     = cb_nop;
  if ( cbobj.cb_function  === undefined )  cbobj.cb_function   = cb_nop;
  if ( cbobj.cb_other     === undefined )  cbobj.cb_other      = cb_nop;
  if ( cbobj.cb_date      === undefined )  cbobj.cb_date       = cb_nop;
  if ( cbobj.cb_regexp    === undefined )  cbobj.cb_regexp     = cb_nop;
  if ( cbobj.cb_array     === undefined )  cbobj.cb_array      = cb_nop;
  if ( cbobj.cb_hash      === undefined )  cbobj.cb_hash       = cb_nop;
  if ( cbobj.cb_object    === undefined )  cbobj.cb_object     = cb_nop;
  if ( cbobj.cb_leave_array === undefined )  cbobj.cb_leave_array = cb_nop;
  if ( cbobj.cb_leave_hash  === undefined )  cbobj.cb_leave_hash  = cb_nop;
  if ( cbobj.cb_leave_object=== undefined )  cbobj.cb_leave_object= cb_nop;

  crawl_object_impl(data,cbobj,[],[],[],undefined);
}
ロジック
function crawl_object_impl( data , cbobj , path , done , parent ) {

  var in_array = false;
  if ( parent && parent.constructor === Array ){
    in_array = true;
  }

  if ( data === undefined ) {
    cbobj.cb_undefined(path,undefined,false,in_array,undefined)
  } else if (data === null ) {
    cbobj.cb_null(path,null,false,in_array,undefined)
  } else if ( typeof(data) === 'object') {
    var ref    = false;
    var cyclic = false;
    // Check reference objects
    var objid = undefined;
    for ( var no in done ) {
      if ( done[no] === data ) {
        ref   = true;
        objid = no;
        break;
      }
    }
    if ( ! ref ) {
      done.push(data);
      objid = done.length;
    }
    // Check cyclic objects
    for ( var no in path ) {
      if ( objid === path[no][1] ){
        cyclic = true;
      }
    }
    if ( data.constructor === RegExp ) {
      cbobj.cb_regexp(path,data,cyclic,in_array,objid);
    }else if ( data.constructor === Date ) {
      cbobj.cb_date(path,data,cyclic,in_array,objid);
    }else if ( data.constructor === Array ) {
      if ( cbobj.cb_array(path,data,cyclic,in_array,objid) ) {
        for ( var i in data ){
          path.push([i,objid]);
          crawl_object_impl ( data[i],cbobj,path , done , data );
          path.pop();
        }
        cbobj.cb_leave_array(path,data,cyclic,in_array,objid);
      }
    }else if ( data.constructor === Object ) {
      if ( cbobj.cb_hash(path,data,cyclic,in_array,objid) ) {
        for ( var i in data ){
          path.push([i,objid]);
          crawl_object_impl( data[i],cbobj,path , done , data );
          path.pop();
        }
        cbobj.cb_leave_hash(path,data,cyclic,in_array,objid);
      }
    }else{
      if ( cbobj.cb_object(path,data,cyclic,in_array,objid) ) {
        for ( var i in data ){
          path.push([i,objid]);
          crawl_object_impl ( data[i],cbobj,path , done , data );
          path.pop();
        }
        cbobj.cb_leave_object(path,data,cyclic,in_array,objid);
      }
    }
  }else{
    if ( typeof(data) === 'string' ) {
      cbobj.cb_string(path,data,false,in_array,undefined)
    }else if( typeof(data) === 'function' ) {
      cbobj.cb_function(path,data,false,in_array,undefined)
    }else{
      cbobj.cb_other(path,data,false,in_array,undefined)
    }
  }
}
解説
crawl_object(data,callback);

この関数に、解析したいデータ(data)と、解析オブジェクト(callback)を渡す。

解析オブジェクトには値の型に応じてコールバックが設定できる。

全てのコールバック関数の型は同じ。
設定されていない場合はデフォルトコールバックが採用される。

  function cb_nop(path,value,cyclic,in_array,objid){
    return ! cyclic;
  }
path
[ [key名,objedtID] ] : 再起的に追って来たキーとオブジェクトのパス。つまりpath[path.length-1] がvalueのキーということ
value
cyclic
valueがobjectの場合、循環参照になっている場合はtrue
in_array
valueの親オブジェクトがArrayの場合はtrue
objid
valueのobjectをユニークに識別できるID
戻り値
valueがobjectの場合、更に中を追う場合はtrueを戻す。falseなら追わない。

例:

{ a : [ {
   b : "1" //※
}] }

※印の部分を追った時のコールバックの引数
function cb_nop(path,value,cyclic,in_array,objid){
  path  == [ ['a',1],[0,2],['b',3] ]
  value == "1"
  cyclic == false
  in_array == false
  objid == 3
}
コールバックの種類
cb_undefined
undefined 型
cb_null
null 型
cb_string
string 型
cb_function
関数型
cb_other
他の非object型(boolen , int など)
cb_date
Date型(object)
cb_regexp
RexExp型(object)
cb_array
Array型(object)
cb_hash
Object型(object)
cb_object
他のobject型(未対応とも言う・・・)
cb_leave_array
Array型の最後
cb_leave_hash
Object型の最後
cb_leave_object
他のobject型の最後
応用例

関数型 , Regex型 , Date型 などに対応したシリアライザが書ける

var callback_object = function(){
}
callback_object.prototype = {
  indent    : '',
  suffix    : '\n',
  buffer    : '',
  prefix      : function (path,value,cyclic,in_array,objid){
    if ( cyclic ) {
      throw 'Cannot serialize (cyclic object) ! ';
    }
    var key = path[path.length-1];
    var prefix = this.indent;
    if ( key && ! in_array ){
      prefix += '\'' + key[0] + '\' : ';
    }
    return prefix;
  },
  cb_undefined  : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + 'undefined,' + this.suffix;
  },
  cb_null       : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + 'null,' + this.suffix;
  },
  cb_string    : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + '"' + value + '",' + this.suffix;
  },
  cb_function  : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    var lines = value.toString().split('\n');
    for ( var i in lines ) {
      if ( i == 0 ){
        this.buffer += prefix + lines[i];
      }else{
        this.buffer += this.indent + lines[i];
      }
      this.buffer += '\n';
    }
    this.buffer = this.buffer.replace(/\n$/,'');
    this.buffer += ',' + this.suffix;
  },
  cb_other  : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + value + ',' + this.suffix;
  },
  cb_date    : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + 'new Date(\''+value.toString()+'\'),' + this.suffix;
  },
  cb_regexp  : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    var flg = ((value.ignoreCase)?'i':'')+((value.multiline)?'m':'')+((value.global)?'g':'');
    this.buffer += prefix + 'new RegExp(\''+value.source+'\',\''+flg+'\'),' + this.suffix;
  },
  cb_array  : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    if ( value.length ) {
      this.buffer += prefix + '['  + this.suffix;
      return true;
    }else{
      this.buffer += prefix + '[],' + this.suffix;
      return false;
    }
  },
  cb_hash   : function (path,value,cyclic,in_array,objid){
    var prefix = this.prefix(path,value,cyclic,in_array,objid);
    this.buffer += prefix + '{'  + this.suffix;
    this.indent += '  ';
    return true;
  },
  cb_object  : function (path,value,cyclic,in_array,objid){
    throw "Unknown type : " + typeof(value);
  },
  cb_leave_array  : function (path,value,cyclic,in_array,objid){
    this.buffer = this.buffer.replace(/,\n$/,'\n');
    this.buffer += this.indent + '],' + this.suffix;
  },
  cb_leave_hash  : function (path,value,cyclic,in_array,objid){
    this.indent = this.indent.replace(/  $/,'');
    this.buffer = this.buffer.replace(/,\n$/,'\n');
    this.buffer += this.indent + '},' + this.suffix;
  },
}
function serialize(data){
  var callback = new callback_object;
  common.crawl_object(data,callback);
  callback.buffer = callback.buffer.replace(/,\n$/,'\n');
  return callback.buffer;
}

使い方

  var data = {
    foo : function(){
      return 'foo';
    },
    bar : 'bar',
    baz : [ {
    }],
  };
  var str = serialize(data);
利用例

オブジェクトシリアライザ

nodejs + jQuery で汎用クローラを書いたのだけど、nodeのメモリーリーク対策でresume機能が必須だった。
なので現在持っている『キュー配列』をそのままファイルにダンプする事にした。

キューの中身は、取得するURLと、取得したコンテンツの解析方法(jQueryセレクタ形式)が記述されているのだけど
もっと細やかな制御をしたい時もあるので『正規表現フィルタ』や『ユーザ定義関数』を書ける様にしたかった

ところが
単純にJSON.stringfyで保存すると関数や正規表現が抜け落ちてしまう。

正規表現はまだ何とでもなるけど『ユーザ定義関数』はどうしても欲しかったので、こんな事をした訳です。

将来像

現在は未対応なのだけど、今後、クローラ設定を再帰的な構造にしたいので『循環参照』もスコープに入れている。

こんな感じのオブジェクトがシリアライズ出来ると凄くいい感じ!

var setting = {
  SELECTOR : 'div.list > a',
  ACTION : [ {
    SELECTOR : 'div.main > a',
    ACTION : []  
   },{
    SELECTOR : 'div.sub > a',
    ACTION : []  
  }]
};
setting['ACTION'][0]['ACTION'] = setting;
出来ている所まで・・・

https://github.com/cockatoo-org/Cockatoo/tree/master/src/operation/html

# -V を付けると取得したファイルを保存する。
# 単純起動
node htmlmon.js -u http://cockatoo.jp     -l 20
# 標準モード(a,link,script,img)
node htmlmon.js -u http://cockatoo.jp -S  -l 15
# 標準クロール(同一ドメイン)
node htmlmon.js -u http://cockatoo.jp -C  -l 8
# 設定ファイル利用1
node htmlmon.js -c ./wiki_config.js       -l 10
# 設定ファイル利用2
node htmlmon.js -c ./wiki_crawl_config.js -l 10