はじめに
これは クローラー/スクレイピング Advent Calendar 2014 - Qiita の9日目です
8日目
id:dkfj さんの クローラー/スクレイピングのWebサービス 「Kimono」のユースケース - プログラマになりたい でした
9日目:ccc_privacy_bot を支える技術
先日書いたエントリがめでたく580はてブいきました。
気づいたらGIGAZINEさんにも取り上げてもらえました。
ファッ!? / “Tカードが個人情報を提供する企業を通知してくれる「Tカード個人情報提供先新着bot」 - GIGAZINE” http://t.co/8j0JNPylod
— sue445 (@sue445) 2014, 11月 20
このボットで使ってるスクレイピングとクローリングのTipについて解説します
ソースコード
クローラでやってること
- 30分に1回、提供先企業一覧のPDFをダウンロードできるページにいく
- ページをスクレイピングしてPDFをダウンロード
- ダウンロードしたPDFをスクレイピングして提供先企業一覧を取得
- 新着があればボットでつぶやく
使っている技術
- ruby
- 現時点での最新バージョン(2.1.5)
- Padrino Framework
- Heroku
HTMLのスクレイピング
Mechanize を使ってます。
- メリット
- 軽い
- Ruby単体で動く
- デメリット
- htmlのbodyのparseしかしないので、jsでDOMが操作されている場合には使えない
Ajaxがふんだんに使われたページをスクレイピングするには phantomjs + Capybara + poltergeist の組み合わせが鉄板だと思います。(が、phantomjsはデバッグしづらいのが。。。)
参考
実際のソースコードで解説します
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 というgemを使ってます。
pdfをrubyから読むためのgemはいくつか使ってみたのですが、今回はこのgemじゃないとうまくテキストで取得出来ませんでした。(cccのpdfはExcelをpdfに変換してるみたいなのですが、他のgemだとセルの中のテキストが列単位でしか取得できない。pdf-readerだとpdfとしてレンダリングされる時の実際の座標もある程度考慮してくれる模様)
ダウンロードしたpdfをテキストで読み込む
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 JX日鉱日石エネルギー株式会社 2014/10/2提携先:ENEOS 3 株式会社アプラス 2014/10/2提携サービス:Tカードプラス, Tカードプラスα ,TSUTAYAWカード 4 株式会社Misumi 2014/10/2提携先:BOOKSmisumi,Misumiグループ(ガス・水) 5 JR九州ドラッグイレブン株式会社 2014/10/2提携先:ドラッグイレブン
モンキーパッチで文字描画の位置を無理矢理変えて対応してます。
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してます
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
%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)
は
/^([0-9]+)\s*(.+)\s*([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})(.+)$/ =~ line
と同等ですが下記のような工夫があります
$1
や$2
だと分かりづらいので(?<no>[0-9]+)
や(?<company_name>.+)
のように名前付きキャプチャを使う/〜/
だと正規表現内にスラッシュがあるとエスケープしないといけないので%r(〜)
を使う
クローラ
Heroku Scheduler だと1日1回以上cronを動かすには 有料になりますが、sidekiq-cron で自前でcronすることで無料枠で30分に1回cronを動かしています
詳しくはこちらを参照
sidekiqでcron処理を行うにはいくつかgemがありますが、starの数で比較するとsidetiqがメジャーみたいですね。
- sidekiq-cron 79 star
- sidekiq-scheduler 78 star
- sidetiq 836 star
今回は会社で使い慣れていたsidekiq-cronを使いました。
Herokuのアドオン一覧
- Deploy Hooks
- デプロイした時にRollbarに通知を送るためのwebhook(が、半日遅れくらいでSlackに通知がくるのであてにならないw)
- Rollbar - Tracking Deploys with Heroku
- Heroku Postgres
- pdfからparseした会社情報を保存する先のデータベース
- New Relic
- アプリケーションのパフォーマンス解析。無料なので入れたけどボットだから意味なかったw
- Papertrail
- アプリのログをwebから見るためのアドオン
- Redis Cloud
- sidekiqを使うためのKVS
- 他に Redis To Go もあったのですが無料枠どうしだとRedis Cloudの方がスペック高かったのでこっちを使ってます。(Redis Cloudだとメモリ25MBでRedis To Goだと5MB)
- Rollbar
- エラー検知
- 無料枠だと他に Bugsnag があったんですが同じ無料枠どうしだと月に受信できるエラーの件数が多いRollbarを選択(Rollbarが3000件でBugsnagが100件)
CircleCI
ビルドやHerokuへのデプロイは全部CircleCIでやってます(トピックブランチがmasterにmergeされたら自動的にherokuにデプロイ)
ローカルからいちいち
git push heroku master
する必要がないのはいいですね
Wercker を使うという手もあったのですが、公開設定していてもログインしないとビルドの一覧が見れないのが不便なのでCircleCIを選択。*1
TravisCIはpush後のビルドが始まるのに時間が掛かるのでマトリクステストが必要になるgem作成以外では使ってないです。。。
Slack
デプロイやRollbarのエラーの通知を自分のチャット部屋に送ってます
10日目
furandon_pig さん
追伸
人間ドックでバリウム飲んていまだに気持ち悪い