Ruby+Mechanizeによるファイル送信方法

# はじめに

これは[【その1】ドリコム Advent Calendar 2015 ](http://www.adventar.org/calendars/1043)の4日目です。

3日目は [ogwmtnr](http://qiita.com/ogwmtnr) さんの[バックグラウンド時の位置情報常時取得にジオフェンスを駆使する手法を思いついたのでやってみた](http://qiita.com/ogwmtnr/items/3229a92d534888fa50c8) です。

# 自己紹介

- はじめまして、JunJunHiroi です。社内で呼ばれやすいアダ名を募集しています。
  - 「じゅんじゅん」だったり、「婚活の人」だったりで呼ばれましたが、いずれも浸透が足りません。
  - ドリコムは婚活のアドバイスをしてくれる方が多い、イケイケな会社です。
- 今年の4月に新卒入社したクライアントエンジニアです。


# 社内ツール作成サークルについて

弊社には主に社内業務の改善を目指してハッカソンをする「社内ツール作成サークル」というものがあり、わたしはそれに参加しています。

先輩の方たちはWebサービスを社内サーバーで実現したり、現場の問題点を吸い上げて改善したりしており、技術力・発信力が高くてすごいです。

わたしはこのサークル活動で、趣味で行なっていたスクレイピングを活用することはできないかと考えて学習をしていました。


# Ruby + Mechanize のファイル送信まとめ

今回は自分が使っている Ruby + Mechanize のスクレイピングについて、体系的な資料が少なかったファイル送信方法を書いていきたいと思います。
[Mechanize: https://github.com/sparklemotion/mechanize](https://github.com/sparklemotion/mechanize)

以下の、3パターンで紹介します。

1. 送信フォームを使ったファイル送信
1. 送信フォームを使ったファイル送信(`input type='file'`がない場合)
1. 送信フォームがないURLへのファイル送信

## __1.__ 送信フォームを使ったファイル送信

まずは取得したWebページの __form__ タグの中に、__input type="file"__ があるウェブサイトでのファイル送信方法です。
一般的なWebサイトで、一般的に行われているファイル送信です。

```rb
agent = Mechanize.new
page = agent.get('https://website')
form = page.forms[0] # 対象のformを選んで、
form.file_upload.file_name = 'tmp.jpg' # file_nameを参照するファイル名に変更し、
form.submit # 送信する！
```

対象の**form**を探して、**file_upload**の更新をすればOKです。


## __2.__ 送信フォームを使ったファイル送信(`input type='file'`がない場合)

次に取得したWebページの __form__ タグの中に、__input type="file"__ が__ない__ウェブサイトでのファイル送信方法です。
たとえば、画像ファイルを指定するとき、URLを入力するかローカルファイルを送信するかの2パターンの指定方法があるサイトを見たことありませんか？
この場合、 __javascript__ で動的に __input type="file"__ を作っているため、Mechanizeからはファイルフォームが無いように見えてしまうときがあります。

そんなときはこれ！

```rb
agent = Mechanize.new
page = agent.get('https://website')
form = page.forms[0] # 対象のformを選んで、
file = Mechanize::Form::FileUpload.new(form, 'tmp.jpg') # ファイルアップロードを作成し、参照するファイル名を指定して、
form.file_uploads << file # form の file_uploads に追加して、
form.submit # 送信する！
```

`Mechanize::Form::FileUpload.new` を使って、無理やり __input type="file"__ のようなものを作っています。

### なんかいびつじゃない？

`Mechanize::Form::FileUpload.new` の引数に __form__ を入れて、__file__ を作っているのに、
その後ろで __form__ に __file__ を追加しています。
プログラムとして、いびつに見えます。

ここらへんは資料を読み込んで、この実装が正しいのか確かめる必要がありますが、検証する時間がなかったためこのままにしておきます。
[Mechanize::Form::FileUpload.new(node, file_name)](http://mechanize.rubyforge.org/Mechanize/Form/FileUpload.html)


## __3.__ 送信フォームがないURLへのファイル送信

あれれ〜このサイト、それっぽい __form__ がないのにドラッグアンドドロップでファイル送信ができるよ〜〜？

__javascript__ を活用しているサイトの場合、__form__ を使わずに動的にファイル送信リクエストを送っている場合があります。
__form__ がないんじゃ、__submit__ できない！　どうしよう！？

そんなときはまず、デベロッパーツールのネットワークタブを開きながら、ファイルの送信を試してみます。
そして、ファイルを送信しているらしい通信を見つけたら、copy as cURL！

```
curl 'http://website' 
 -H 'Referer: ~~~'
 --data-binary
 〜〜〜
  Content-Disposition: form-data; name="file"
 〜〜〜
```

ヘッダーやらなんやらと一緒にファイルを送っているのがわかります。
わかってしまえば、それを真似してPOST！　単純でいいですね。

```rb
agent = Mechanize.new
content_data = {
  'file': File.new('tmp.jpg') # file_nameでも、File.readでもなく、File.new なことに注意。
  # サイトによって、'Content-Type', 'Token' なんかも追加する。
}
result = agent.post('http://website', content_data)
```

サイトにもよりますが、これでうまくいくことがあります。
あるサイトでは、通信の中にトークンを入れている場合もあったので、その場合は丁寧にトークンを探してみてください。


## その他つまりどころ など

- `file_upload` は `file_uploads[0]` の省略のように振る舞っていた。
  - `file_upload` をいじってうまくいかないときは `file_uploads[0]` に変えて挑戦するのがいいかも。
- Referer が入っていないと、通信に失敗するサイトがやや多い印象。
- Mechanize デフォルトの User-Agent でも、通信に失敗することがほぼない印象。
  - スクレイピングを趣味にする前は、 User-Agent が不正な場合に通信失敗させるサービスが多いと思っていた。逆に Referer は通信を解析するためだけに使っていると思っていた。
  - スクレイピングの通信に失敗したら、細かなパラメータも実通信に似せることで解決できたことがよくある。
- ファイルデータを無理やり送信するときは __enctype__ や __mime_type__ を設定する。
  - `file_upload` には、_file_data, file_name, mime_type_ を指定できる。通常は _file_name_ を指定するだけでいいが、画像バイナリデータをローカルファイルに保存せずに送りたい場合などは、3つを適切に設定してやる必要がある。
  - `file_upload` がもともとない送信フォームの場合、__enctype__ を適切に設定してやる必要がある。`form.enctype = 'multipart/form-data'`など。


# 結果

Ruby + Mechanizeを使って、ファイル送信ができるようになりました！
これで実現できるスクレイピングツールの幅が広がった気がします。

# おわりに

社内ツール作成サークルとしてこちらを活用できるようなツールを作り、業務の改善を目指したいなと思っています。

# さいごに注意

ファイル送信は重いリクエストなので、くれぐれもやりすぎ _(時間を空けずに連続してアップロードするなど)_ と外部サービスの場合利用規約には注意してください。

# 明日は

kakaricho さんです。

追記：[kakaricho さん ２年目エンジニアがエンジニアリーダーをやってみた話](http://kakaricho.hatenadiary.jp/entry/2015/12/05/172344)

追記：[【その2】ドリコム Advent Calendar 2015 ](http://www.adventar.org/calendars/1044)もあります。