2011/01/12

Ruby De Web Scraping!

今回は、経済指標カレンダーのスクレイピングを行いたいと思います。
今回選考させて頂いた業者さんは、『Foreland Forex』です。
選考基準は、以下の通りです。
①過去のデータを開示されていること 。
②ランク付けされていること 。
③日本語であること。

【試行①】
先回紹介した方法でスクレイピングしてみた結果は、以下の通りです。
deta
日付部分のデータが歯抜けになっているし、ランクが消えています。
このままでは、使用しづらいので別の方法を考えてみました。

【試行②】
先回紹介したサイト(勉強用メモ-WEBから情報の抜き出し(スクレイピング、スパイダリング))に記載のある別の方法を選択してみました。
選考基準は、以下の通りです。
①なるべく簡単にスクレイピング処理が可能なこと。
②Windows上で単独で作動可能なこと。
③どうせなら、マルチプラットフォームを意識した言語を学習したい。
④日本人の諸先輩方がたくさんいる(参考になるサイトがたくさんある)言語であること。
上記を考えると、①③④を満たし、EXEに加工できることから②を満たすため、[RUBY]となりました。

【設計概要】
やりたいことをまとめてみました。(以下 sp=スクレイピング)
①サイトから必要な情報をspしたい。
②spした内容を、【日付】【国名】【行事名】【ランク】【前回】【予想】【結果】としてCSVファイルに保存したい。【日付】は、【年】【月】【日】【時】【分】と分割保存したい。
③spした内容に欠落部がある場合に補正したい。
④【時】を0~23時のフォーマットにしたい。
⑤サーバータイムに変更したい。
⑥夏時間に対応させたい。
⑦バックナンバーを一括spしたい。それをまとめたい。

【コード】
上記のやりたいことを元にコードを作成してみると・・
下記は一部コード表示が化けています。コードダウンロード
#! ruby -Ks
require 'rubygems'
require 'hpricot'
require 'open-uri'
require 'csv'

#####################################################################
#ファイルの作成
def GetDate( address ="", direct = "",gmt ="0",booldst = "false")
thday     = []
dayofweek = ["日","月","火","水","木","金","土"]
cnv       = CSV.readlines('Conversion.csv')
begin
#HPを開いてdocに格納
doc = Hpricot( open(address).read )
 #発行日取得
 daydate = (doc/"h1.heading01").inner_html
 /\d+\.+\d+\.+(\d\d|\d)/ =~ daydate
 daydate = $&
 #発行日の分解day[0]:年 dya[1]:月 day[2]:日
 day = daydate.split(/[.]/)
 #桁数の統一
 rescue 
return false
end
 2.times { |i| if day[i+1].size == 1 then day[i+1] = "0" + day[i+1] end }
 #ファイル名の作成
 if direct ==""
  failname = "CurrentDate.csv"
 else
  failname = direct +"/" + day[0] + day[1] + day[2] + ".csv"
 end
 CSV.open(failname , "w") do |csvfail|
 #docからtr要素を1行づつ抽出しtrdに格納
 (doc/:tr).each do |trd|
   #配列をtrdate名で作成
   trdate = []
   #年の格納
   trdate << day[0]
   #trdからth要素かつclass="eventdate"の値を抽出
   trdate << (trd/"th.eventdate").inner_html.strip
     #もし上記条件が否定されたら
     if (trd/"th.eventdate").empty?
      #上記変数から値を代入
      trdate = thday.clone
     else
      #上記変数に値を代入
      thday = trdate.clone
     end
     #日時の書式変更分割
     /\// =~ trdate[1]
     trdate << "$`" << "$'"#注意:この行の””は表示用
     trdate.delete_at 1
     #年の修正
     if day[1] == "12" && trdate[1] == "1" then trdate[0] = trdate[0].to_i + 1 end
   #trdからtd要素をtddに1行づつ抽出
   (trd/:td).each do |tdd|
     #trdからimg要素を抽出し、あれば"src"の値をtrdateに格納する
     trdate << (tdd/:img).attr("src").strip  if not (tdd/:img).empty?
    #trdからtdd要素を抽出し、あればそのテキストをtrdateに格納する
     trdate <= 24 then dtime = 1; dhour -= 24; end
     mktimes   = Time.local(trdate[0].to_i,trdate[1].to_i,trdate[2].to_i,dhour,trdate[4].to_i)
     mktimes  += dtime*24*60*60
     #サーバータイムに変換
     mktimes  -= gmt.to_i*60*60
     #夏時間変換
     if booldst == "true" && GDST(mktimes) == true then  mktimes  += 60*60 end
     trdate[1] = mktimes.mon
     trdate[2] = mktimes.day
     trdate[3] = mktimes.hour
    end
   end
   #ランク覧の空欄に0を挿入
   if trdate[7] == "" then trdate[7] = "0" end
 
  #trdateがからでなければcsvfailに書き込み
  csvfail << trdate if not trdate[0].nil?
 end
 end
return true
end
##########################################################################
#既存ファイルの確認(有:true 無:false)
def Selectfail( failname = "20070917.csv" ,direct = "./tmp" )
 #カレントディレクトリの移動
 Dir.mkdir(direct) unless FileTest.exist?(direct)
 Dir.chdir(direct) 
  #ファイルの確認(bool型)
  getf = FileTest.exist?(failname)
 Dir.chdir("../")
 return getf
end
###########################################################################
#既存ファイル名の成形
def namefail(no = 0)
 #No3の日付作成
 fday = Time.local(2007, 8, 27)
 fday += 7 * no * 60 * 60 * 24
  p "%04d"%fday.year + "%02d"%fday.month + "%02d"%fday.day + ".csv"
  return "%04d"%fday.year + "%02d"%fday.month + "%02d"%fday.day + ".csv"
  
end
############################################################################
#過去のデータの取得
def HistDate(direct = "./tmp",gmt ="0",booldst = "false")
 count = 3
 #無限ループ
 loop do
  failname = namefail(count)
  break if Selectfail(failname) == false 
  count +=1
 end
 
 if count >170 then count +=6 end
 
 loop do
  address = "http://www.foreland.co.jp/marketreport/calendar_detail_id_marketcalendar_" + count.to_s + ".html"
  p address
  break if GetDate(address,direct,gmt,booldst) == false && count >=180
  count +=1
  #sleep(3)
 end
end
###########################################################################
#既存ファイルの削除
def Inzfaile(failname = "HistoricalDate.csv")
  #ファイルの確認(bool型)
  if FileTest.exist?(failname) then
   begin
    File.delete(failname)
   rescue
    p "古いファイルが削除できません"
    return false
   end
  end

 return true
end
##########################################################################
#ファイルの結合
def Joinfail(direct = "./tmp")
 #ファイルの初期化
 failname = "HistoricalDate.csv"
 if Inzfaile(failname) == false then return end
 #書き込み用ファイルを開く
 sumdate = CSV.open(failname,"w") 
 #ディレクトリの移動
 Dir.chdir(direct)
 count =3
 loop do
  begin
   getdate= 0
   fname   = namefail(count)
   CSV.open(fname,"r") do |gdate|
    sumdate << gdate if not gdate[0].nil?
   end
   count += 1
  rescue
  p "完了"
  break
  end
 end
 Dir.chdir("../")
 sumdate.close
end
##########################################################################
#過去のデータの取得
def GetHistDate(direct = "./tmp",gmt ="0",booldst = "false")
 HistDate(direct,gmt,booldst)
 Joinfail(direct)
end
##########################################################################
#最新のデータを取得
def CurrentDate(gmt ="0",booldst = "false")
 add ="http://www.foreland.co.jp/marketreport/calendar_detail.html"
 GetDate(add,"",gmt,booldst)
end
##########################################################################
#分義
def Hab(gmt ="0",booldst = "false")
 CurrentDate(gmt,booldst)
 if not getf = FileTest.exist?("HistoricalDate.csv") then GetHistDate("./tmp",gmt,booldst) ; return ; end
end
##########################################################################
#夏時間
def GDST(dtime)
 check = false
 mon_s  = 3
 week_s = 2
 mon_e  = 11
 week_e = 1
 
 lastmonth  = Time.local(dtime.year ,mon_s ,1)
 lastmonth -= 24*60*60
 day_s      = week_s * 7 - lastmonth.wday
 stime      = Time.local(dtime.year , mon_s ,day_s)
 
 lastmonth  = Time.local(dtime.year ,mon_e ,1)
 lastmonth -= 24*60*60
 day_e      = week_e * 7 - lastmonth.wday
 etime      = Time.local(dtime.year , mon_e , day_e)
 
 if stime <= dtime && dtime <= etime then check = true end
  return check
end
########################################################################
if ARGV[0].nil? then ARGV[0] ="0" end
if ARGV[1].nil? then ARGV[1] ="false" end
Hab(ARGV[0],ARGV[1])

【結果】
デフォルト設定での結果は、以下の様になります。 
yyy  
簡単に説明すると
①《A列~E列》
日付および時間を示します。ただし、《D列(時)》の(-1)は、無指定を意味します。
つまり、時間指定がないイベントを意味しています。

②《E列》
国名を示します。※下部添付内の『Conversion.csv』にて変更可能です。
③《G列》
イベント名
④《H列》
ランクを示します。※下部添付内の『Conversion.csv』にて変更可能です。
⑤《I列~K列》
【前回】【予想】【結果】を示します。

【使い方】
GetEventDate.zipをダウンロードします。
②解凍すると、以下のファイルがあります。
【Coversion.csv】文字変換に使用するファイルです。お好みで変更してください。
【GetEventDate.bat】タイムゾーンの変更および夏時間の有無を設定できます。
※デフォルトは、変換しないように設定してあります。
【GetEventDate.exe】起動ファイルです。

【初期設定】
【GetEventDate.bat】を右クリック→編集で開き下記の設定を行います。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
REM 引数の設定を行います。
REM ①日本標準時とサーバータイムの差を記入する。
REM サーバータイムがGMT 0 の場合は、”9”を記入してください。
REM デフォルト”0”の場合は、日本時間を示す。(タイムゾーン変換を行いません)
REM ②サマータイムの設定を行います。"true"の場合は、サマータイム変換を実施します。
REM "false"の場合は変換を行いません。
REM ③記載例: EventDate.exe 【"タイム差"】 【"サマータイムの有無"】
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
【Coversion.csv】を開きB列をお好みで変更してください。
[B1~B5]ランクの表示[B6~B17]国名表示です。

【初作動】
【GetEventDate.bat】をクリックして作動させます。exeファイルをクリックしても作動しますがその場合は、パラメーターが、デフォルト状態になります。(日本時間で夏時間なし)

【結果】
【tmp】フォルダーが成形されてその中にバックナンバーがまとめられます。
【HistoricalDate.csv】バックナンバーの結合ファイルです。
【CrrentDate.csv】今週のカレンダーです。

【次回からの作動】
初作動以外は、【HistoricalDate.csv】がある場合、【CrrentDate.csv】のみ書き換えます。
【HistoricalDate.csv】を更新したい場合は、【HistoricalDate.csv】を削除後作動させてください。

【まとめ】
本来は、夏時間の有無やGMT変換が、できるようですが、OSの壁(Windows特有のタイムゾーン表示)が原因でうまく作動しませんでした。(対応策はあるようですが・・・)そのため、地道な方法を採用しました。また、調べているときこんなフレーズを目にしました。『C++ユーザーは、rubyを覚えないほうがいいよ!・・・戻れなくなるから・・・』C++ユーザーではありませんが納得です。MQL5のAPI移植は、C++を使用しましたがもう少し早くrubyと出会えていたら、もっと簡単に加工できたと思う今日この頃です。ONZ