Help us understand the problem. What is going on with this article?

PhantomJS で Content-Disposition: attachment なレスポンスを取得する方法

More than 5 years have passed since last update.

はじめに

とあるサイトに定期的にログインして利用履歴のCSVを取ってくるというプログラムを AppleScript + Safariで書いていたがいちいち Safari が起動してめんどくさいので、PhantomJS で書きなおすことにした。

さっそく PhantomJS の最新版(1.9.2) をインストールして、スクリプトを書いた。コードは下記のとおりである(簡易化してある)。

download_csv.coffee
page = (require 'webpage').create()

callbacks = [
  -> # ログイン画面の処理
    page.evaluate ->
      document.getElementById('user').value = 'user'
      document.getElementById('pass').value = 'pass'

      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('submit').dispatchEvent(e)
  -> # ホーム画面の処理
    page.evaluate ->
      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('to_history').dispatchEvent(e)
  -> # 履歴画面の処理 -> CSVをダウンロードする!
    page.evaluate ->
      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('download').dispatchEvent(e)
]

page.onLoadFinished = ->
  callback = callbacks.shift()
  phantom.exit() unless callback
  callback()

page.open "http://localhost:9292"

しかし、肝心のCSVの取得がうまくいかない。というよりも、どうやってCSVのレスポンスを得るのかがわからない。ファイルとしてどこかに保存されているわけでもないようだ。

いろいろ調べた結果、公式#10052: File downloadという issue があり、そのスレッドによると、 Content-Disposition: attachement なレスポンスは取得できない ようだ。

しかし、同スレッドで公式から fork された add_download_capabilities というバージョンなら対応してるらしいということが分かった。なお、ベースとなった PhantomJS のバージョンは 1.6 である。

インストール

さっそく git clone してビルドしてみる(ビルド方法については公式を参照) 。環境は、CentOS 6.3 x86_64。ビルドには2時間くらいかかった。

$ sudo yum install gcc gcc-c++ make git openssl-devel freetype-devel fontconfig-devel
$ git clone https://github.com/woodwardjd/phantomjs.git
$ cd phantomjs
$ git checkout add_download_capabilities
$ ./build.sh

ビルドが終了した後、所定のディレクトリに移動し PATH を通す。今回は、/usr/local/phantomjs/add_download_capabilitiesとした。

$ mv phantomjs add_download_capabilities
$ sudo mkdir -p /usr/local/phantomjs/
$ sudo mv add_download_capabilities /usr/local/phantomjs/
$ sudo vi /etc/profile.d/phantomjs.sh
/etc/profile.d/phantomjs.sh
export PATH=/usr/local/phantomjs/add_download_capabilities/bin:${PATH}

これでインストールは完了のはずだが、実行すると下記のエラーがでる。

$ phantomjs --version
phantomjs: error while loading shared libraries: libQtWebKit.so.4: cannot open shared object file: No such file or directory

{PhantomJSディレクトリ}/src/qt/libに libQtWebKit.so.4 があったので、/etc/ld.so.conf.d/phantomjs.confにパスを追加した後 ldconfig する。

$ sudo vi /etc/ld.so.conf.d/phantomjs.conf
/etc/ld.so.conf.d/phantomjs.conf
/usr/local/phantomjs/add_download_capabilities/src/qt/lib
$ sudo ldconfig

なんとか PhantomJS のインストールができた。

$ phantomjs --version
1.6.1

Content-Disposition: attachement なレスポンスの取得

この fork されたバージョンでは、WebPageオブジェクトに新たに、 WebPage#onUnsupportedContentReceived()コールバックが追加されており、これで Content-Disposition: attachment なレスポンスをフックする。

レスポンスの保存には、WebPage#saveUnsupportedContent()メソッドを用いる。第一引数にファイル名、第二引数にレスポンスIDを指定する。なお、レスポンスを文字列として直接得る方法はないようだ。

download_csv.coffee
page = (require 'webpage').create()

callbacks = [
  -> # ログイン画面の処理
    page.evaluate ->
      document.getElementById('user').value = 'user'
      document.getElementById('pass').value = 'pass'

      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('submit').dispatchEvent(e)
  -> # ホーム画面の処理
    page.evaluate ->
      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('to_history').dispatchEvent(e)
  -> # 履歴画面の処理 -> CSVをダウンロードする!
    page.evaluate ->
      e = document.createEvent('MouseEvents')
      e.initEvent('click',false,true)
      document.getElementById('download').dispatchEvent(e)
]

page.onLoadFinished = ->
  callback = callbacks.shift()
  phantom.exit() unless callback
  callback()

# Content-Disposition: attachment なレスポンスが返された時に呼び出されるコールバック
page.onUnsupportedContentReceived = (response) ->
  page.saveUnsupportedContent('tmp/history.csv', response.id)

page.open "http://localhost:9292"

また、onUnsupportedContentReceived()の引数、responseの内容は下記の通りである。

{
  "contentType": "text/csv", // サーバーから返されたContent-Type
  "headers": [ // サーバーから返されたHTTP-Header
    {
      "name": "Content-Type",
      "value": "text/csv"
    },
    {
      "name": "Content-Disposition",
      "value": "attachment; filename=\"history.csv\""
    },
    {
      "name": "Content-Length",
      "value": "94"
    },
    {
      "name": "Server",
      "value": "WEBrick/1.3.1 (Ruby/1.9.3/2012-12-25)"
    },
    {
      "name": "Date",
      "value": "Fri, 22 Nov 2013 12:28:47 GMT"
    },
    {
      "name": "Connection",
      "value": "Keep-Alive"
    }
  ],
  "id": 1, // レスポンスID。saveUnsupportedContent()メソッドで必要となる。
  "redirectURL": null,
  "status": 200, // サーバーが返したHTTPステータスコード
  "statusText": "OK ", // サーバーが返したHTTPステータス説明句
  "time": "2013-11-22T12:28:47.221Z",
  "url": "http://localhost:9292/download_csv" // リクエストURL 
}

おわりに

この問題、結構基本的なことだと思うが、何故かほとんど情報がなく今回の記事を書くこととなった。
公式もはやくこの問題を解決してほしいものである。

なお、この記事で示したコードは GitHub にアップロードしてある。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away