時刻表データをうまく抜き出してくる方法

おとといからスクレイピングだとか言ってるけど、WebサイトのHTMLを解析して必要なデータを取ってくることのことである。

行儀が悪い方法だが、HTMLを解析することによって得られるデータは多い。


僕が実際に近鉄のNextTrainのデータを作るために作ったスクリプトはまぁなにかとひどいので、

これを例示するのもひどい気がしたので、比較的わかりやすい大阪の市バスの時刻表を例に取って話をしようと思う。

おまけとして市バス62系統の時刻表も作っておこうと思ったときにやったやりかたを示す。

大阪駅前 バス時刻表 (62系統・103系統)

ここから62系統の発時刻を抜き出したい。

ソースコードを見てみる。するとclassにtimetableが指定されたtable要素がある。a要素に隣接している。

そのtable要素の下にtr要素があって1時間ごとに出発するバスがまとめられている。

その下にはth要素とtd要素が3つならんでいて、thは発時、tdそれぞれは平日・土曜・休日の発車するバスを示している。

そのtd要素の中にはバスごとにdiv要素が並んでいて、注釈を付けるdiv要素と、分を示すdiv要素がある。


さて、それをPerlのWeb::Scraperを使って抜き出してNextTrainのTBLデータに加工するプログラムがこれだ。

use strict;
use warnings;
use Encode;
use Web::Scraper;
use URI;
use utf8;
my $getdetailurl=scraper{
  process "//table[\@class='timetable'][1]/colgroup/tr[position()>1]","lines[]"=>scraper{
    process "//th","hour"=>'TEXT';
    process "//td",'section[]'=>scraper{
      process "//div[\@class='item'][div[2]/text()!=' ']",'bus[]'=>scraper{
        process "/div/div[1]",'attr'=>'TEXT';
        process "/div/div[2]",'min'=>'TEXT';
      }
    };
  };
};
my $res=$getdetailurl->scrape(URI->new("http://kensaku.kotsu.city.osaka.jp/bus/dia/jikoku/jikoku034484.html"));
open my $ofh,'>:encoding(shiftjis)','out.tbl';
sub attr{
  #if($_[0]=~m/★/){ return ('NS'); }
  #else            { return (); }
}
my %attr=();
sub todata{
  my @res;
  for(@{$_[0]}){
    my @attr=attr($_->{'attr'});
    $attr{$_}++ for @attr;
    push @res,[$_->{'min'}+0,[@attr]];
  }
  return @res;
}
my @data=([],[],[]);
for my $l(@{$res->{'lines'}}){
  push @{$data[$_]} ,[$l->{'hour'}+0 , [todata($l->{'section'}->[$_]->{'bus'})] ] for 0..2;
}
my $chr=ord('A');
for(keys(%attr)){
  print $ofh chr($chr),':',$_."\n";
  $attr{$_}=chr($chr);
}
print $ofh "\n";
for(0..2){
  print $ofh ($_==0 ? "[MON][TUE][WED][THU][FRI]\n#(平日)\n" : $_==1 ? "[SAT]\n#(土曜)\n" : "[SUN][HOL]\n#(休日)\n");
  for(@{$data[$_]}){
    print $ofh $_->[0].': ';
    for(@{$_->[1]}){
      print $ofh $attr{$_} for @{$_->[1]};
      print $ofh $_->[0],' ';
    }
    print $ofh "\n";
  }
  print $ofh "\n"
}

この中でWeb::Scraperで処理をしているのは最初の方である。

scraper{ … } という風に書けば処理のためのオブジェクトが得られる。

{ … } には処理の内容を書き込むが、processではXPathまたはCSSセレクタで指定した要素についてとやかくすることがかける。

今回はあえてXPath式で書いてみた。

//table[@class=’timetable’][1]/colgroup/tr[position()>1] というXPath式で、classがtimetableのtable要素の最初のものの直下のcolgroup直下のtr要素のうち2番目からという条件で抜き出している。

かなり複雑だが、これで62系統の毎時の出発する時刻の書かれた行だけ抜き出せた。

103系統のときは2番目の表にあるから、最初の[1]を[2]に書き換えればいい。

抜き出したデータは”lines[]”と書いたから、$res->{“lines”}に条件を満たす要素について=>の後ろに示した条件で処理されたものが出てくる。

=>以降にはscraper{…}で作られたオブジェクト、サブルーチンリファレンス、そして”TEXT”でテキスト、”@href”で属性だとかが取り出せる。

いろいろ細かい細工をXPathで書いてあるが、おおざっぱに抜き出して細かく選び出すのはPerlでやるというのもよいかもしれない。

XPathは確かにいろいろ表現できるけど、細かい事をするのはめんどくさい。おおざっぱにCSSセレクタで抜き出してしまえば楽なのも確かな話だ。


こんな風にしてHTMLから必要なデータを抜き出して処理することができるわけだ。

目で見て必要なデータを見つけるのに比べると、どこにあるか探して指定するのには手間がかかる。

しかし一度指定すればあとは自動で抜き出してくれるのは便利だし間違えがない。

実にスクライピングの意義はそこなんじゃないかなと思っている。

本当は処理に便利なようにXMLとかで配布してくれればうれしいけど、なかなかそんな気の利いたサービスはない。

けど、人が目で見るためには便利なものがたくさんある。それをプログラムで上手く処理できれば得られるデータの幅は広がる。

そういうことですよ。