並列処理で「君の名は。」を観に行こう!

こんにちは、またまた久保田です。 最近ブログ書きすぎで久保田ブログみたいになっていると言われましたが、
最近は書きたいことがありすぎて勝手にやってるだけなんですね。。。ご容赦を。

ところでみなさん、「君の名は。」観ましたか??
僕は今どハマりしています。

いやー、面白いですよね。観るたびに1人で泣いています。
あまりにハマってしまい、1回観て本読んでまた観に行ってCDを買ってしまいました。
今週末にまた行って泣いてきます。

そんな中、2回目に行った時の席が前の方から二列目真ん中でちょっと悔しい思いをしました。
僕はトイレがものすごく近いので、1時間20分くらい我慢しながら見てた気がします。
そうなると気が散ってしまい、集中できない時間が続いていました。(まぁ泣きましたが)

なぜそんな悲劇が起きたかというと、僕はめんどくさくて、映画館の席情報を比べなかったんですね。。
もしかしたら同じ時間近くの映画館にもう少しいい席があったかもしれなかったです。。。

もう二度とあんな思いをしながら大切な君の名はを観る機会を使いたくないので、作りました。
「君の名は。」の空き席情報をまとめて教えてくれるシステムを!

これは日時と時間を与えると、jsonで上映時間と席の空き情報をホストしてくれます。
これでもう席選びのことでめんどくさくなる必要はなしです!(新宿のピカデリーと東宝シネマズのみ対応)

yourname_finder(github)

(※後日APIとして動かす予定です。)

これを作る時にそれなりに苦労したので、そのお話をしたいと思います。

まず、映画館のサイトは、jsがゴリゴリ動いています。
日時を選ぶのもそうですし、席を選ぶ画面に遷移するのも基本的にjsで制御しているみたいです。

なので普通のスクレイピングではなく、このブログでもよく使われているPhantomjsを使用します。

流れは、

PolterGeist(RubyからPhantomjsを使うためのドライバー)でページを入力された日時のページまで遷移させる

スクレイピング

入力された時間に当てはまる上映スケジュールを取得

取得した上映スケジュールのページに順次アクセス、空き席情報を取得

jsonに整形してホストする

という流れになります。

(※今回はスクレイピングのお話はしません。ゴリゴリやっているだけなので、興味ある方はgithubを見てください。)

今回はこの全ての処理を行う上で並列処理で速度をあげたお話をします。
僕のこのシステムの大まかな内部的なフローは、

映画を見たい希望の日時を受け取る

東宝シネマズとピカデリーから席情報をスクレイピングするクラスをNewする

ループでそれぞれのインスタンスで、インスタンスメソッドのset_schedulesを使い、上映スケジュールを取得、空き席情報をインスタンスにセットする。

それぞれインスタンスメソッドのto_jsonで整形、ホスティング

という流れです。
これを普通に実行すると。。。

普通に実行

require './base'

date = '2016-10-07'
times = ['18:00', '22:00']

objs = [Toho.new(date, times), Piccadilly.new(date, times)]

begin
  objs.map do |obj|
    obj.set_schedules # このメソッドが上映スケジュールを取得し、空き席情報をインスタンスにセットする。
  end

  objs.each do |obj|
     puts obj.to_json
  end
rescue  => e
  p e  # => "unhandled exception"
end

これが、ご想像通りまーーーー遅いんですねぇ。

f:id:AdwaysEngineerBlog:20161007132307p:plain

話にならない遅さ。

それもそのはず、 まず、東宝シネマズのサイトにアクセス、目的の日付のページになるようにjsを動かして(ここで少し待たないとページが切り替わらないままスクレイピングする)、
入力した時間に当てはまる上映スケジュールを取得し、
取得できた上映スケジュールにそれぞれ順次アクセスし(10回くらい繰り返すことも)(またページが変わるまで少し待つ必要あり)席情報をスクレイピング(あわわ)

という流れをピカデリーと合わせて2回やっています。

このままでは使い物にならない!という訳で、処理を並列にして早くしてみましょう。

Threadで並列にする

まず、やっていることは同じことを別のサイトにやっているだけなので、ここは並列処理にして早くしてみます。
とりあえず東宝シネマズへのアクセスとピカデリーへのアクセスと席情報の取得を同時に行うようにします。

require './base'

objs = [Toho.new('2016-10-07', ['18:00', '22:00']), Piccadilly.new('2016-10-07', ['18:00', '22:00'])]

begin
  ts = objs.map do |obj|
    Thread.start(obj) { |obj| p Thread.current; obj.set_schedules }
  end

  # 全てのThreadの終わりを待つ。
  ThreadsWait.all_waits(*ts) do |th|
    printf("end %s\n", th.inspect)
  end

  objs.each do |obj|
     puts obj.to_json
  end

rescue  => e
  p e  # => "unhandled exception"
end

それぞれのアクセスと席情報を取得する処理をThread化してみました。
Threadにしておけば変数を親のプロセスと共有できるので楽チンですね。

そして実行。。。。

f:id:AdwaysEngineerBlog:20161007132317p:plain

少し改善されましたね。

(Threadはちゃんと並列で動いているのかtop -Hで確認します。)

Process

今度はProcessにしてみましょう。
もしかしたらThreadではRubyのGVLの問題でうまく並列にできていないかもなので。。。(外へのアクセスが発生しているのでそこは並列にできていそうですが)

require './base'

pid = fork do
  toho = Toho.new('2016-10-07', ['18:00', '22:00'])
  toho.set_schedules
  p toho.to_json
end

pid2 = fork do
  piccadilly = Piccadilly.new('2016-10-07', ['18:00', '22:00'])
  piccadilly.set_schedules
  p piccadilly.to_json
end

Process.waitall

f:id:AdwaysEngineerBlog:20161007132313p:plain

またちょっと早くなりました。やはりThreadでのIO以外のところでスレッドセーフにしているんですかね。
ただ、processは変数の共有がめんどくさいので、今回はProcessはやめておきThreadを採用します。(dRubyまでやりたかったけど時間が足りなかった。。)

今回は以上です。
まだ「君の名は。」を観ていない人はこれを機に絶対観てくださいね!!!