この記事はクローラー/Webスクレイピング Advent Calendar 2017 - Qiitaの25日目の記事です。
最近仕事でもスクレイピングをすることが多くなってきました
今回はCSVダウンロードをフロント側でポーリングしてるページにスクレイピングでダウンロードさせてみた話を書きます
実際に本番では使いませんでした(そもそもスクレイピングしない方向になった)が何とかCSV取得するところまで持って行けたので書いておきます
下記のようなCSVダウンロードをスクレイピングでやってみようというお話です
動作環境は capybara, selenium webdriver, headless chromeです
1. (ユーザー)DLボタンクリック 2. (javascript)csv生成のリクエストが送られる(おそらくjob的な感じ) 3. (javascript)一定間隔でcsv生成完了かどうかのステータスを見るリクエストを送る 4. (javascript)完了した段階でレスポンスにCSVのurlが返ってくる 5. (javascript)urlをたたいてcsv取得 6. (ユーザー)ファイル保存のダイアログが出てくるので保存する
こんな感じのフローでダウンロードが行われるページに対してスクレイピングでダウンロードまでやってみます
手順
よくあるのはDLボタンをクリックしたらレスポンスとして'text/csv'のコンテンツが返ってくるパターンですね
そもそもcapybara(selenium webdriver)でダウンロードができるようであれば特に問題はないのですが、色々試したけどうまくいきませんでした
なのでjavascriptが気を利かせて色々やっていくれている部分をなんとかしてCSVを取得します
具体的には下記手順になるかと思います
- evaluate_scriptでxhrのメソッドを書き換える
- 通信内容を記録する
- 記録した通信内容をreturnする関数を仕込む
- capybara経由でDLボタンをクリック
- javascript側で色々やってくれたであろうタイミングで仕込んだ関数を呼び出し通信内容を取り出す
- 通信内容からCSVのURLをゲット!
- Curlで叩いてCSVゲット!
といった感じで取ってきます
まずはxhrの中身を書き換えてみます
xhrのurl履歴を取る
参考
ajax - How to view the last GET HTTP request in JavaScript - Stack Overflow
- コードを差し込む
ブラウザのconsoleから下記コードを実行します(実際には1行にまとめて突っ込みます)
var ajaxCalls = []; XMLHttpRequest.prototype._originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { ajaxCalls.push(url); this._originalOpen(method, url, async, user, password); }
DLボタンを押す
- xhrを発生させる
consoleからajaxCallsの中身を見る
url履歴が入ってる
うまー!!!!!
ということで本題のCSVのURLを取得してみます
xhrのレスポンスを取得
window.responses = []; window.XMLHttpRequest.prototype._originOpen = XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function(method, uri, async, user, pass) { this.addEventListener("readystatechange", function(event) { if(this.readyState == 4){ var self = this; var response = { method: method, uri: uri, responseText: self.responseText }; window.responses.push(response); } }, false); this._originOpen.call(this, method, uri, async, user, pass); }; window.getResponses = function(){ return window.responses; }
上記のコードを差し込んで先ほどと同様の手順でhxrで送っている通信のレスポンスを取得することができました
ここで仕込んだgetResponses
関数をevaluate_script
で実行、中身を取得しruby側でごにょごにょできるようにします
capybara経由でDLできるようにする
cookieをcapybaraのsessionから取得して突っ込んで認証などを通せるようにします
実際のコードの一部です(対象ページに合わせて修正が必要です)
def click_dl_button script = <<-EOS window.responses = []; window.XMLHttpRequest.prototype._originOpen = XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function(method, uri, async, user, pass) { this.addEventListener("readystatechange", function(event) { if(this.readyState == 4){ var self = this; var response = { method: method, uri: uri, responseText: self.responseText }; window.responses.push(response); console.log(response); } }, false); this._originOpen.call(this, method, uri, async, user, pass); }; window.getResponses = function(){ return window.responses; } EOS session.execute_script(script) session.find(:xpath, '//dl-button[.//i[text()="file_download"]]').click sleep 30 json = session.evaluate_script('getResponses()') res = json.select{|row| row['responseText'].match('csv.s3.url') }.first['responseText'] parsed = JSON.parse(res) # {"a": "b": "https//csv.s3.url....."}といった感じのレスポンスが返ってくる # 取得したURLにunicode文字列が入っていたためunescape処理する parsed["a"]["b"].gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*') } end def csv2data(csv_url,filename) Curl::Easy.download(csv_url, filename){|curl| curl.headers["Cookie"] = session.driver.browser.manage.all_cookies.each_with_object([]) {|row, array| array.push("#{row[:name]}=#{row[:value]}") }.join('; ') } sleep 1 raw = "" open(filename, "rb:BOM|UTF-16:UTF-8"){|f| raw = f.read } CSV.parse(raw, headers: :first_row) end url = click_dl_button csv = csv2data(url,'sample.csv')
まとめ
案件の途中で方針が変わったのでこの方法はいらなくなったのですがせっかく結構調べたので形になるところまで持っていきました
xhrのメソッドを書き換えて色々やるっていうのは考えたことがなかったので面白いなと思いました