Tカード個人情報提供先新着bot

2年前にプチ話題になったアレです

![image](https://qiita-image-store.s3.amazonaws.com/0/8059/dbc4bc6a-3cdd-735b-8f8f-ee335c9049c1.png)

* https://twitter.com/ccc_privacy_bot
* ソースコード： https://github.com/sue445/ccc_privacy_crawler

# 過去の記事
* [Tカード個人情報提供先新着botを作りました - くりにっき](http://sue445.hatenablog.com/entry/2014/11/19/000836)
* [ccc_privacy_bot を支える技術 - くりにっき](http://sue445.hatenablog.com/entry/2014/12/09/000000)

2年前に書いた時から若干アップデートがあるのでそれについても書きたいと思います。

# ボットでやっていること
1. 1時間に1回、提供先企業一覧のPDFをダウンロードできるページにいく
  * http://qa.tsite.jp/faq/show/2512
2. PDFをダウンロードして提供先企業一覧を取得
  * スクレイピングだけでいけると思ったらPDFで提供されていたのでparseするのが大変だったｗ
3. 新着があればボットでつぶやく

ってだけです。

ちなみにPDFをダウンロードしてスクレイピングするロジックは2年間で一切手を加えていませんが、だいたい動いているようです。

# 使っている技術
* ruby
* [Padrino Framework](http://jp.padrinorb.com/)
  * 全部入りのRails と シンプルな [Sinatra](http://www.sinatrarb.com/) の中間くらいのフレームワーク
  * リリース当時はスクレイピングとボットのみだったのでpadrinoはおろかsinatraすら不要だったのですが、スキーマのmigrationを自前で実装するのが嫌だったのでpadrinoを採用
  * 後からwebを追加したくなったので（後述）、結果的にはpadrino使ったのは正解だった
* Heroku

# HTMLのスクレイピング
[Mechanize](https://github.com/sparklemotion/mechanize) を使ってます。

* メリット
  * 軽い
  * Ruby単体で動く
* デメリット
  * htmlのbodyのparseしかしないので、jsでDOMが操作されている場合には使えない

Ajaxがふんだんに使われたページをスクレイピングするには [phantomjs](https://github.com/ariya/phantomjs/wiki/Headless-Testing) + [Capybara](https://github.com/jnicklas/capybara) + [poltergeist](https://github.com/jonleighton/poltergeist) の組み合わせが鉄板だと思います。（が、phantomjsはデバッグしづらいのが。。。）

参考： [Rails4 + Capybara + PhantomJS (poltergeist) なテスト環境 - (ﾟ∀ﾟ)o彡 sasata299's blog](http://blog.livedoor.jp/sasata299/archives/51924944.html)

実際のソースコードで解説します

https://github.com/sue445/ccc_privacy_crawler/blob/672bc0dbe8c88f431603b4913c2d1236df6e2d99/lib/workers/pdf_crawl_worker.rb#L28-39

```ruby:lib/workers/pdf_crawl_worker.rb
  def download_ccc_pdf(dest_pdf_file)
    # mechanizeのインスタンスを初期化
    agent = Mechanize.new

    # http://qa.tsite.jp/faq/show/25129 を開く
    agent.get("http://qa.tsite.jp/faq/show/25129")

    # <a href="/attachment_file/〜.pdf"> のようなリンクを探す
    download_link = agent.page.link_with(href: %r(/attachment_file/.+\.pdf))

    # リンクが見つからなければエラー
    raise "Not found download_link" unless download_link

    # リンク先のPDFをダウンロードしてファイルに保存する
    pdf_content = agent.get_file(download_link.href)
    File.open(dest_pdf_file, "wb") do |file|
      file.write(pdf_content)
    end
  end
```

# pdfのスクレイピング
[pdf-reader](https://github.com/yob/pdf-reader) というgemを使ってます。

pdfをrubyから読むためのgemはいくつか使ってみたのですが、今回はこのgemじゃないとうまくテキストで取得出来ませんでした。（cccのpdfはExcelをpdfに変換してるみたいなのですが、他のgemだとセルの中のテキストが列単位でしか取得できない。pdf-readerだとpdfとしてレンダリングされる時の実際の座標もある程度考慮してくれる模様）

ダウンロードしたpdfをテキストで読み込む

https://github.com/sue445/ccc_privacy_crawler/blob/672bc0dbe8c88f431603b4913c2d1236df6e2d99/lib/workers/pdf_crawl_worker.rb#L65-74

```ruby:lib/workers/pdf_crawl_worker.rb
  def read_pdf(pdf_file)
    pdf_content = ""

    reader = PDF::Reader.new(pdf_file)
    reader.pages.each do |page|
      pdf_content << page.text
    end

    pdf_content
  end
```

これ自体は特別なことをしていないんですが、そのままだと下記のようにpdf内の日付がうまく取得できませんでした

```
1 TSUTAYA・蔦屋書店                      2014/10/2提携先：TSUTAYAフランチャイズチェーン加盟企業
2 ＪＸ日鉱日石エネルギー株式会社                   2014/10/2提携先：ENEOS
3 株式会社アプラス                          2014/10/2提携サービス：Tカードプラス， Tカードプラスα ，TSUTAYAWカード
4 株式会社Ｍｉｓｕｍｉ                        2014/10/2提携先：BOOKSmisumi，Misumiグループ（ガス・水）
5 ＪＲ九州ドラッグイレブン株式会社                  2014/10/2提携先：ドラッグイレブン
```

モンキーパッチで文字描画の位置を無理矢理変えて対応してます。

https://github.com/sue445/ccc_privacy_crawler/blob/672bc0dbe8c88f431603b4913c2d1236df6e2d99/lib/pdf-reader.rb

```ruby:lib/pdf-reader.rb
class PDF::Reader::PageLayout
  # fix rate: 1.05 -> 1.5
  def col_count
    @col_count ||= ((@page_width  / @mean_glyph_width) * 1.5).floor
  end
end
```

pdfを文字列で取得でした後は正規表現でparseしてます

https://github.com/sue445/ccc_privacy_crawler/blob/672bc0dbe8c88f431603b4913c2d1236df6e2d99/lib/workers/pdf_crawl_worker.rb#L41-62

```ruby:lib/workers/pdf_crawl_worker.rb
  def parse_ccc_pdf(pdf_file)
    companies = []
    read_pdf(pdf_file).each_line do |line|
      line = line.strip

      matched_data = %r(
        ^(?<no>[0-9]+)\s*
        (?<company_name>.+)\s*
        (?<receipted_date>[0-9]{4}/[0-9]{1,2}/[0-9]{1,2})
        (?<destination_name>.+)$)x.match(line)
      next unless matched_data

      companies << Company.new(
        no:               matched_data[:no].to_i,
        company_name:     matched_data[:company_name].strip,
        receipted_date:   matched_data[:receipted_date].strip,
        destination_name: matched_data[:destination_name].strip,
      )
    end

    companies
  end
```

ソースコード中の正規表現の

```ruby
%r(
^(?<no>[0-9]+)\s*
(?<company_name>.+)\s*
(?<receipted_date>[0-9]{4}/[0-9]{1,2}/[0-9]{1,2})
(?<destination_name>.+)$)x.match(line)
```

は

```ruby
/^([0-9]+)\s*(.+)\s*([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})(.+)$/ =~ line
```

と同等ですが下記のような工夫があります

* `$1` や `$2` だと分かりづらいので `(?<no>[0-9]+)` や `(?<company_name>.+)` のように名前付きキャプチャを使う
  * http://docs.ruby-lang.org/ja/2.3.0/doc/spec=2fregexp.html
* `/〜/` だと正規表現内にスラッシュがあるとエスケープしないといけないので `%r(〜)` を使う
  * http://docs.ruby-lang.org/ja/2.3.0/doc/spec=2fliteral.html#regexp

# クローラ
作った当初は

* Heroku schedulerの無料枠は1日1回実行のみ。（1時間に1回実行は有料）
* インスタンスを24時間フル稼働していても無料

ということもあり [sidekiq-cron](https://github.com/mperham/sidekiq) のプロセスを常駐して30分に1回実行していたのですが、2015年4月頃 [^heroku-plan] に新しい料金体系が発表され

[^heroku-plan]: 自分が対応した日付より推測 https://github.com/sue445/ccc_privacy_crawler/pull/25

* 24時間インスタンスを起動し続けると有料になる
* その代わりHeroku schedulerが無料でも1時間に1回動かせるようになった

ということになり、Heroku Schedulerで1時間に1回動かすようにしています。

![heroku_scheduler.png](https://qiita-image-store.s3.amazonaws.com/0/8059/1e5983ac-0507-1411-4ace-f55bcc364387.png)

元々workerで実行してた頃の名残で、rake task内でworkerのインスタンスを `perform` しています。

https://github.com/sue445/ccc_privacy_crawler/blob/6146b3ac985274b2c686f98143ac6cd84a2c9f5c/lib/tasks/crawl_pdf.rake

```ruby:lib/tasks/crawl_pdf.rake
desc "crawl pdf"
task :crawl_pdf => :environment do
  PdfCrawlWorker.new.perform
end
```

# web画面
特にどこにもアナウンスしていなかったですが、レコードを見るのにローカルから `padrino console` するだけだと不便だったのでwebで見れるようにしています。

https://ccc-privacy-crawler.herokuapp.com/

自分が見れればいい感あったので画面は雑です