アイマスで始めるWebスクレイピング

※この記事は[クローラー／Webスクレイピング Advent Calendar 2015](http://qiita.com/advent-calendar/2015/crawler)の8日目の記事です。昨日は[プリキュアで学ぶワンライナーWebスクレイピング](http://qiita.com/greymd/items/368faf898c161165fee7)でした。

こんにちは、trebyと申します。
プロデューサーの皆さん、今日も元気にアイドルたちとお仕事していらっしゃいますでしょうか。
私自身は基本的には[ミリオン](http://jp.apps.gree.net/ja/58737)から入ったクチなのですが、[デレステ](http://cinderella.idolmaster.jp/sl-stage/)がリリースされてからシンデレラの魅力にも気づきまして、ますますお仕事が捗る今日この頃です。[舞踏会](http://idolmaster.jp/event/cinderella3rd.php)最高でした。

今日はそんなプロデューサーの皆さんが幸せにお仕事できるようにするためのスクレイピングの技を紹介します。

__※お約束ですが、プログラムを使って人様のサーバにリクエストする以上、節度のある行動を心がけましょう。__

# プロデューサー予定表をGoogle Calendarに取り込みたい
さて、お仕事を効率よくこなすために大事なことといえばスケジュール管理です。
この業界は超絶ホワイト(ただし、お財布的にはブラック)なので、各プロデューサーがアイドルを適切にマネジメントできるよう、なんと会社の方でスケジュール帳を準備してくれています。

- [プロデューサー予定表](http://idolmaster.jp/schedule/index.php)

これを参照すれば抜け漏れなくアイドルをプロデュースできる、やったぜ、Power of Smileだぜ！といきたいところなのですが、問題は予定表の形式です。HTML形式とPDF形式でしか配布されていないのです。

おそらく多くのプロデューサーさんがGoogle Calendarなど専用のアプリケーションでスケジュールを管理されていることと思います。そこで今回は、[準備されているスケジュール](http://idolmaster.jp/schedule/index.php)をいい感じにこれらのアプリケーションに取り込むことを考えます。

多くのカレンダーアプリケーションがサポートしているでスケジュールの標準フォーマットに[iCalendar形式](https://ja.wikipedia.org/wiki/ICalendar)があります。提供されている予定表をこの形式に変換できれば、ひとまず目的を達成できそうです。

そこでRubyで[組んだプログラムがこちら](https://github.com/imas/pd-schedule/blob/adf4b34c4d2d6b6248ca2d6a4179e7f320b1abe2/crawl.rb)です。実行すると、以下のような[`.ics`ファイル](https://github.com/imas/pd-schedule-ics/blob/master/201512.ics)が生成されます。


```
BEGIN:VCALENDAR
VERSION:2.0
PRODID:icalendar-ruby
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Asia/Tokyo
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20151207T145511Z
UID:662dee44-0a08-4bd3-8928-960e93294798
DTSTART;VALUE=DATE:20151202
DTEND;VALUE=DATE:20151203
DESCRIPTION:http://www.lantis.jp/release-item/LACA-15523.html
SUMMARY:【ミリオン】THE IDOLM@STER LIVE THE@TER DREAMERS 03(CD)
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20151207T145511Z
UID:e1816b4e-ff50-4d58-a601-c6ffc977abc8
DTSTART:20151202T220000
DTEND:20151202T230000
DESCRIPTION:http://ch.nicovideo.jp/cinderellaparty
SUMMARY:【シンデレラ】CINDERELLA PARTY!(webラジオ)
:
:
```

これをカレンダーツールに読み込ませれば、いい感じに予定表を取り込むことができます。

![Googleカレンダーに取り込んだ様子](https://qiita-image-store.s3.amazonaws.com/0/35353/5e8b4f15-3d27-dde7-2cd5-bae7aabc07c5.png)

さて、コード中で処理は大きく以下の部分に分かれています。

- HTMLのソースコードの読み込み(入力)
- スケジュール情報の解析(スクレイピング)
- iCalendarファイルの書き出し(出力)

実装について、私はたまたま、Rubyで[Mechanize](http://docs.seattlerb.org/mechanize/)と[iCalendar](https://github.com/icalendar/icalendar)というgemを使って実装しましたが、もちろんやりようはいくらでもいけると思います(HTMLの取得にopen-uri + Nokogiriくらいで十分なのにMechanize使っているオーバースペックな感じはご愛嬌ということで……)。

以上、対象のマークアップ構造に即した解析器が必要という点で、もし皆様の興味分野で似たようなことがありましたらスクレイピングの練習をしてみると楽しいかと思います。

# 開発Tips
ここからは前述のプログラムを作成する上で詰まった点や工夫した点を紹介してみます。

## 取得したファイルをローカルに保存しておき、無駄なアクセスを減らす

プログラムの作成段階においてはトライ＆エラーを繰り返して完成系に近づけていくものです。それ自体は悪いことではないのですが、クローリングやスクレイピングなど、プログラム中で自分のものではないリソースを使っている場合は注意が必要です。

今回のような場合、対象となるスケジュール情報は頻繁に更新されるものではなく、プログラムの実行のたびに最新のページを取得する必要はありません。むしろ考えられるのは、とってきたファイルに対してどうやって情報を抽出するかのスクレイピング部分であり、ファイルそのものは一度取得してきたらあとは使い回せば事足ります。

そこで今回のプログラム中では、[取得してきたページを保存しておき後から使いまわせるようにしています](https://github.com/imas/pd-schedule/blob/adf4b34c4d2d6b6248ca2d6a4179e7f320b1abe2/crawl.rb#L14-L36)。

```rb
    def crawl(datetime)
      agent = Mechanize.new
      agent.tap do |mec|
        mec.follow_meta_refresh = true
        page_uri = if datetime
                     "http://idolmaster.jp/schedule/#{datetime.year}#{datetime.strftime('%B').downcase}.php"
                   else
                     'http://idolmaster.jp/schedule/index.php'
                   end
        mec.get(page_uri)
      end
      agent.page
    end

    def cache(datetime)
      src_path = if datetime
                   html_path(datetime.year, datetime.month)
                 else
                   Dir.glob('html/[0-9]*.html').sort{|a, b| b <=> a}.first
                 end
      Nokogiri.HTML(open(src_path))
    end
```

そして、二回目以降の実行時に保存したページを使用することを指定することで、サーバ側に余計なリクエストを行わないようにしています。

## スケジュールのメタ情報の切り出し

アイドルマスターのコンテンツは10年続いているだけあって、いくつものシリーズを展開しています。プロデューサー予定表では複数のシリーズをサポートしている他、予定のジャンルのデータを網羅しています。

![スケジュールに紐づくメタ情報](https://qiita-image-store.s3.amazonaws.com/0/35353/3f88f058-2b4e-98a0-30e2-50db908e287b.png)

これらの情報をいかに抽出していくかがスクレイピングの醍醐味となります。[プログラム中ではこのあたりでゴリゴリやっています](https://github.com/imas/pd-schedule/blob/adf4b34c4d2d6b6248ca2d6a4179e7f320b1abe2/crawl.rb#L53)。

この段階で、後で取り扱いやすい形に変換しておけば、「765」「シンデレラ」「ミリオン」「SideM」とあるシリーズごとにカレンダーを分けることも容易となります。

## 時間の曖昧なスケジュールへの対処

プロデューサー予定表は人が見ることを想定して作られているので、スクレイピングするには易しくない部分も幾つかあります。

![時間が指定されていないスケジュール](https://qiita-image-store.s3.amazonaws.com/0/35353/a1397000-5fb9-2c47-3962-4233e5c1aa4b.png)

予定の始まりと終わりの時間をどうするか、というのもそのうちの一つです。全ての予定に始まりの時間と終わりの時間が指定されていればいいのですが、大抵は終わりの時間が書いてなかったり、あまつさえ時間情報がないものも少なくありません。

そこでexperimentalではありますが、プログラム中では[愚直に時間らしきものを探してみて、あったらそれを採用するような実装](https://github.com/imas/pd-schedule/blob/adf4b34c4d2d6b6248ca2d6a4179e7f320b1abe2/crawl.rb#L110-L127)としました。

なお、終わりの時間が指定されていない予定の終了時間については開始時間の1時間後を指定するようにしました。

## 同イベントのUIDを一意とするための対処
前に予定表の内容は頻繁に更新されないことに触れましたが、それでも予定であるが故、公開時点から内容が更新されることがあります(というか、だいたいしれっと更新されています)。

予定が更新された場合、新たにiCalendarファイルを生成しなおすことになるわけですが、ただ作り直すだけだとスケジュールのUIDが一意になりません。これではカレンダーアプリケーションで読み込んだ際に、本来同じイベントであるのに別物と判断されかねません。

そこでプログラムでは、以前に作成したiCalendarファイルがあったら読み込んだ上で、[変更のないスケジュールについてはUIDを変えないようにする工夫](https://github.com/imas/pd-schedule/blob/adf4b34c4d2d6b6248ca2d6a4179e7f320b1abe2/crawl.rb#L93-L100)を入れています。


# まとめ
以上、プロデューサーの皆さんが幸せにお仕事するためのスクレイピングの技を紹介しました。

最初にコードを組んでからだいぶ時間が経っていたので、本記事を執筆する際に取り上げるプログラム自体にも「今ならこう書く」という変更を入れてみたのですが、やっぱりモノを作るのって面白いです。時間を見つけながらにはなりますが、これからも趣味なモノづくりをやっていければと思います。

さて、[本Advent Calendar](http://qiita.com/advent-calendar/2015/crawler)は執筆時点で明日の担当の方がいらっしゃいません。ぜひどなたか素敵な<del>ステージ</del><ins>記事</ins>をお待ちしております！

それでは、2016年もアイマスですよ、アイマス！