それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

WSHでCSSセレクタのWEBのスクレーピング

ここまでのまとめ。

WEBスクレーピングをCSSセレクタで遣るためにPerl入れますか?Ruby入れますか?PerlWeb::ScraperRubyでScrapiと同じ事をWSHでやればいいじゃん。わざわざWSHで?うん、わざわざ、してみた。

ActivePerlでCPANモジュール使うの面倒じゃん*1

WindowsRubyいれてパッケージ入れるのすら面倒(笑
WSHならJSだけで動くじゃん
JScriptでWeb.Scraperを作ればいいじゃん。


休日を返上してうんうん唸って捻って考えた結果。どうしてもWSHからIEを使いたかった。
スクレーピングのテストとしてYahooから株価を取得することを考えた。

株価取得コード

これを動作させるのが目標

//株価取得
var stocks = {};
stocks.NIKKEI   = Utena.Scraper.YahooStock("998407.o");//日経平均
stocks.TOPIX    = Utena.Scraper.YahooStock("998405.t");//TOPIX
for ( i in stocks ){
  str = i+"\n\n";
  for( j in stocks[i] ){
    str += j + "=" + stocks[i][j] + "\n";
  }
  WScript.Echo(str);
}

WSHでこういう事が出来ればいいなと。

Yahoo株価の取得関数を作る

実現するために、CSSセレクタで動けばいいな。
JavaScriptXPATHが良いとかなんとか。id:amachangがどっかで書いてたのを読んだ。
でもオレはCSSセレクタが使いたいんだ!!!


CSS セレクタ
http://labnotes.org/svn/public/ruby/scrapi/cheat/scrapi.html
が使えるといいな。


CSSセレクタでスクレーピングするとrubyでもPerlでもセレクタ部分を再利用できる。
コレは何となく嬉しい。

//ScraperをにYahooStock関数を追加
Utena.Scraper.YahooStock = function( stock_code ){
  //株価URL
  var url = "http://quote.yahoo.co.jp/q?s="+stock_code;
  //CSSセレクタ
  var cssSelector = "div.invest table[border='1'] tr[align='right'] td";
  this.moveTo(url);
  e = this.query( cssSelector );
      var stock = {};
      stock.st_code   = e[0].innerText;
      stock.market    = e[1].innerText;
      stock.company   = e[2].innerText;
      stock.update_at = e[3].innerText;
      stock.price     = e[4].innerText;
      stock.diff      = e[5].innerText;
      stock.d_ratio   = e[6].innerText;
      stock.volume    = e[7].innerText;
  return stock;
};

株価のページのCSSセレクタ

  //Yahoo株価をParseする設定 CSSセレクタ
  var cssSelector = "div.invest table[border='1'] tr[align='right'] td";

この部分。


ScraperはURLとCSSセレクタを変更するだけで使い回せる実装が良いな。

じゃぁUtena.Scraperを実装してみる。

スクレーピング本体をつくる

CSSセレクタは、ライブラリがいっぱいあるのでそれを流用する。
ゼロから書き直すなんて面倒だ。


WSHでeval出来、CSS実装の良いのライブラリを探した。cssQuery.jsが良かった。*2

///////////////////////////////////////
//スクレーピングクラスを作る
//////////////////////////////////////
Utena.Scraper = Utena.IEScriptEngine;
Utena.Scraper._libs=[
    "cssQuery.js",
    "cssQuery-level2.js",
    "cssQuery-level3.js",
    "cssQuery-standard.js"
  ];//本体と同一フォルダにある前提

Utena.Scraper.query=function(selector){
    if(!this._window){
      throw "no url or document load error";
    }
    if(!this._window.document.cssQuery){
      this.loadLib(this._libs);
    }
    return this._window.cssQuery(selector);
    
}

WSHからIEスクリプト実装を呼び出す

cssQuery.jsはブラウザが前提なのでIEスクリプトを埋め込む。ここが大事。

動作の流れは、

  1. WSHからIEを実行<--いまここ
  2. 該当URLへ移動
  3. JSライブラリをbookmarklet的に埋め込む
  4. CSSセレクタを実行
  5. WSHに結果を返す。

IEWSHが共にJScript実装。なのでデータの共有やソース、メソッド呼び出しが容易。
ここではIEを呼び出してWSH側からソースをEVALさせる。

///////////////////////////////////////////
//IEのjavascriptをWSH側から操作する
///////////////////////////////////////////
Utena.IEScriptEngine = {
  _doc:null,
  _ie:null,
  _window:null,
  _libs_src:[],
  _ieObject:function(){
      return new ActiveXObject("InternetExplorer.Application");
  },
  loadLib:function(){
    if(!this._window){
      return;
    }
    this.libs_src = Utena.lib.loadSource(this._libs);
    this._ie_js_eval(this.libs_src.join('\n\n'));
  },
  moveTo:function(url){
    if(!this._ie){
      this._ie = this._ieObject();
      this._ie.Visible=false;
    }
    this._ie.Navigate(url);
    while( this._ie.Busy == true ){
      WScript.Sleep(1);
    }
    //IEからWindowオブジェクトを取得する
    this._doc = this._ie.Document;
    this._window = this._doc.parentWindow;
  },
  _ie_js_eval:function(src){
    this._window.eval(src);
  }
}

"InternetExplorer.Application"より"htmlfile"の方が安定しそうだがIEを操作したいのでこの書き方になった。

JavaScriptライブラリをWSHに取り込む

WSHのFSO(FileSystemObject)でソースを読み込みIEでEvalする。ここ大事。

/**
 *JScript ファイルを再利用する関数
 */
//{{
///ファイル読み込み
Utena.lib.read = function ( name ){
  try{
    if( !name ||  name.length < 1 ){
      throw "ファイル名がない"
    }
    var fs = new ActiveXObject("Scripting.FileSystemObject");
    var a  = fs.OpenTextFile( name, 1 , fs.TristateUseDefault );
    var src = a.ReadAll();
    return src
 }catch(e){
    WScript.Echo( e.message );
    return false;
  }
  return true;
}

//perlのuseの代替
Utena.lib.include = function (name){
  try{
    src = Utena.lib.read(name);
    return eval(src);
  }catch(e){
    WScript.Echo( e.message );

    return false;

  }
}

/**
 * JavaScriptの外部ソースを配列に取り込む
 * @param array ファイル名 相対パスか絶対パス
 * @return array ソースファイルの中身の配列
 */
Utena.lib.loadSource = function (files){
  var srcs = new Array();
  for ( i=0;i<files.length;i++ ){
    srcs[srcs.length] = Utena.lib.read(files[i]);
  }
  return srcs;
}

おまけでMHTMLもライブラリに含めてみた。

MHTMLが使えると、RSSを読み込んで、URLをMHTMLにしてオフラインで閲覧できるようになる。これは便利じゃないかと。最近メインでつかってLivedoorReaderにオフライン閲覧greasemonkeyが作れないかなと。

/**
 * URL をMHTML形式でローカルに保存する関数
 */

//{{
Utena.lib.mhtml = function (url, filename){
  if(!url || !filename){
    throw "Usage mhtml(url, ファイル)";
  }
  var  msg = WScript.CreateObject("CDO.Message");
  msg.MimeFormatted = true;
  msg.CreateMHTMLBody(url, 0, "", "");
  msg.GetStream.SaveToFile(filename, 1);
}
//}}

今後この辺を改良しようかな。

ソース全体

/**
* written by takuya morio
* last modified 2007/10/12
* if you have anything to ask, contact me!.
* id:takuya_1st
* License: http://creativecommons.org/licenses/LGPL/2.1/
*/
/**
 * Utena は名前空間
 * WSHからIEを呼び出し、IEのソースにライブラリを突っ込む
 */
var Utena = {IEScriptEngine:function(){},Scraper:function(){},lib:function(){}};//基底名前空間

/**
 *JScript ファイルを再利用する関数
 */
//{{
///ファイル読み込み
Utena.lib.read = function ( name ){
  try{
    if( !name ||  name.length < 1 ){
      throw "ファイル名がない"
    }
    var fs = new ActiveXObject("Scripting.FileSystemObject");
    var a  = fs.OpenTextFile( name, 1 , fs.TristateUseDefault );
    var src = a.ReadAll();
    return src
 }catch(e){
    WScript.Echo( e.message );
    return false;
  }
  return true;
}

//perlのuseの代替
Utena.lib.include = function (name){
  try{
    src = Utena.lib.read(name);
    return eval(src);
  }catch(e){
    WScript.Echo( e.message );

    return false;

  }
}
//}}
/**
 * URL をMHTML形式でローカルに保存する関数
 */

//{{
Utena.lib.mhtml = function (url, filename){
  if(!url || !filename){
    throw "Usage mhtml(url, ファイル)";
  }
  var  msg = WScript.CreateObject("CDO.Message");
  var ie = new ActiveXObject( "InternetExplorer.Application" );
  ie.Navigate(url);
  while(ie.Busy){
    WScript.Sleep(1);
  }
    msg.MimeFormatted = true;
  msg.CreateMHTMLBody(url, 0, "", "");
  msg.GetStream.SaveToFile(filename, 1);
}
//}}
/**
 * JavaScriptの外部ソースを配列に取り込む
 * @param array ファイル名 相対パスか絶対パス
 * @return array ソースファイルの中身の配列
 */
Utena.lib.loadSource = function (files){
  var srcs = new Array();
  for ( i=0;i<files.length;i++ ){
    srcs[srcs.length] = Utena.lib.read(files[i]);
  }
  return srcs;
}

///////////////////////////////////////////
//IEのjavascriptをWSH側から操作する
///////////////////////////////////////////
Utena.IEScriptEngine = {
  _doc:null,
  _ie:null,
  _window:null,
  _libs_src:[],
  _ieObject:function(){
      return new ActiveXObject("InternetExplorer.Application");
  },
  loadLib:function(){
    if(!this._window){
      return;
    }
    this.libs_src = Utena.lib.loadSource(this._libs);
    this._ie_js_eval(this.libs_src.join('\n\n'));
  },
  moveTo:function(url){
    if(!this._ie){
      this._ie = this._ieObject();
      this._ie.Visible=false;
    }
    this._ie.Navigate(url);
    while( this._ie.Busy == true ){
      WScript.Sleep(1);
    }
    //IEからWindowオブジェクトを取得する
    this._doc = this._ie.Document;
    this._window = this._doc.parentWindow;
  },
  _ie_js_eval:function(src){
    this._window.eval(src);
  }
}

///////////////////////////////////////
//スクレーピングクラスを作る
//////////////////////////////////////
Utena.Scraper = Utena.IEScriptEngine;
Utena.Scraper._libs=[
    "cssQuery.js",
    "cssQuery-level2.js",
    "cssQuery-level3.js",
    "cssQuery-standard.js"
  ];//本体と同一フォルダにある前提

Utena.Scraper.query=function(selector){
    if(!this._window){
      throw "no url or document load error";
    }
    if(!this._window.document.cssQuery){
      this.loadLib(this._libs);
    }
    return this._window.cssQuery(selector);
    
}
/**
Utena.Scraper利用例
var scraper = new Utena.Scraper;
scraper.moveTo(url);
var TDs = scraper.query( "body > div.class > table > tr.odd > td" );
var DIVs = scraper.query( "div.title");
var ELEMENT = scraper.query( "span#id")[0];

CSS セレクタの例
http://labnotes.org/svn/public/ruby/scrapi/cheat/scrapi.html
*/

//ScraperをベースにYahoo株価関数を追加する
Utena.Scraper.YahooStock = function( stock_code ){
  //株価コードURL
  var url = "http://quote.yahoo.co.jp/q?s="+stock_code;
  //YahooをParseする設定 CSSセレクタ
  var cssSelector = "div.invest table[border='1'] tr[align='right'] td";
  //
  this.moveTo(url);
  e = this.query( cssSelector );
      var stock = {};
      //if( e.length==9 ){//Scraping妥当性チェック
      stock.st_code   = e[0].innerText;
      stock.market    = e[1].innerText;
      stock.company   = e[2].innerText;
      stock.update_at = e[3].innerText;
      stock.price     = e[4].innerText;
      stock.diff      = e[5].innerText;
      stock.d_ratio   = e[6].innerText;
      stock.volume    = e[7].innerText;
      //}
  return stock;
};


//テストコード
//株価取得
var stocks = {};
stocks.NIKKEI   = Utena.Scraper.YahooStock("998407.o");//日経平均
stocks.TOPIX    = Utena.Scraper.YahooStock("998405.t");//TOPIX
for ( i in stocks ){
  str = i+"\n\n";
  for( j in stocks[i] ){
    str += j + "=" + stocks[i][j] + "\n";
  }
  WScript.Echo(str);
}

CSSセレクタでスクレーピング

JavaScriptでスクレーピングすると何が嬉しいか。Windowsだとインストール実行が楽。やっぱコレが一番。あと、最近流行のJavaScriptを作ってる気分になれる。XMLHTTPで結果をPOSTするとAjaxっぽい。TrimPath.TemplateでHTMLやRSSにすると、Spidering Hacksっぽい。Yahooウィジェットに結果を放り込めたら嬉しい。CDO使えばメールで配信できる。JScript(ECMAScript)なのでJSON化も簡単かも???取得結果をデータベースに放り込むにはJSDBなどライブラリやODBCやADOを利用すれば可能っぽい。



できないこと、WindowsなのでDaemon化が面倒。というかBackGroundで定期実行するのはNTサービス作らなくちゃ無理。ただバックグラウンド実行はWScript.Shell.Run( cmd, 0 );で出来る。


たぶんVBでも出来る。
アプリに組みこめばWindowsプログラムでごにょごにょできる。

よく考えたらxpathCSSセレクタも同じもの???


相互変換できるようだし、殆ど同じものだと考えて良いのかも。個人的にはCSSセレクタの方が手軽いいなぁ。。。ブラウザ互換性とかあるし。

相互変換の記事とか
http://d.hatena.ne.jp/nazoking/20070205/1170673230
http://d.hatena.ne.jp/tociyuki/20070726/1185466930

XpathCSS Selector混ぜるのは良いね。XPATHの互換性が担保されたら。JavaScriptもいいね。

で、Defer属性はどこへ

もともと、Defer属性でスクリプトを読み込むテストするためだった。でも結局Evalした。
Deferだと動作早くなるかな。。。。

*1:coLinuxとか言うのは無しで

*2:Ext.jsは何故かEval()出来ない。