やりたいこと
Railsアプリで、あるデータをCSVファイルでダウンロードする機能を実装しました。
あまり体系的にまとまったドキュメントに出会えなかったので、自分でまとめます!
作業環境
Rails 6.1.3.1
手を動かす前に概論 〜CSV生成の基本的な考え方〜
CSVを作るというのはビューを生成するということに似ています。
むしろビューの一種と考えていいかもしれません?
いつものビュー、index.html.erbを生成するような要領で、xxx.csv.ruby
という拡張子を使ってindex.csv.rubyというファイルを作ります。
そして、そのファイルにGETでアクセスすればよいです。ブラウザがダウンロードを処理してくれます。
で、ルーティング的にどこにアクセスすればいいのかというと、例えばusers/index.csv.rubyというディレクトリであるならば、/users.csv
にアクセスすればよいです。もしindex.html.erbも存在していてそれと同じデータをCSV化して落とすのであれば、コントローラの同じindexメソッドを使えばよく、新たなメソッドを作る必要がないので便利です。もっともCSVをダウンロードするという行為は、大体ブラウザのビューで今見えているものを落とすわけなので、直感的ですよね。大概はこのような実装になるのではないでしょうか。
では、概論がわかったところで、まずはそのリソースを作ります。
実装!
index.csv.rubyファイルを作る
CSVといえば、あのカンマ(,
)区切りでデータテーブル形式で縦横に敷き詰められたファイル。
てか、"CSV"って、"Comma-Separated Values"の略なんですってね。
あのCSVファイルをExcelで加工、活用する術は知っていても、それ自体を生成するなんて考えたことがありませんでした...。
生成の仕方は、結論から言うと、**RubyのCSVクラスを使って、配列で値を渡してあげればいい!**です。
まずは、CSVクラスを読み込みます。
require 'csv'
CSV.generate do |csv|
# ここでcsvに配列を渡していく
end
CSV.generateのブロックの中で、引数csv
に対して配列を渡してあげればよいです。
例えば、下記のようなCSVファイルを出力したい時。
日付,名前,ニックネーム
0424,山田,山ちゃん
0425,田中,サトシ
0425,内田,うっちー
下記のようにデータを配列として一行ずつ渡してあげればいいです。
require 'csv'
CSV.generate do |csv|
csv << ['日付', '名前', 'ニックネーム']
csv << ['0424', '山田', '山ちゃん']
csv << ['0425', '田中', 'サトシ']
csv << ['0425', '内田', 'うっちー']
end
ちなみに、これ↓じゃダメなの?と思いませんでしたか?僕は思いました(笑)
require 'csv'
CSV.generate do |csv|
csv = [
['日付', '名前', 'ニックネーム'],
['0424', '山田', '山ちゃん'],
['0425', '田中', 'サトシ'],
['0425', '内田', 'うっちー']
]
end
これなら一回の代入で済むじゃん、と。
しかしながら、これを実際にやってみるとCSVファイルは空になってしまいます。
どうやらCSVには1行につき1配列を渡すというのが原則のようです。それらを配列で囲ってしまうと思ったようなCSVファイルになりません。
アクセスしダウンロードする
今、準備したindex.html.rubyファイルを使って、CSVファイルをダウンロードできるようにします。
まずは試しに、/モデル名.rubyといった具合で、URLにアクセスしてみましょう。
usersモデルのデータをCSVでダウンロードさせるなら、/users.csv
ですね。。
するとダウンロードができて、そのファイルを開けるはずです!
この動きはブラウザでも確認できます。
(Google Chromeだと開発者ツール
- Network
- Nameでcsvファイルを選択
- Headers
)
下図ご参照。
でも実際にはまさかユーザーにURLを毎回打ってもらうわけにはいかないので、ボタンを設置し、ダウンロードできるようにします。
ボタンとリンクの設置
Railsのlink_to ヘルパーメソッドとBootstrap(class="btn")で簡単に生成できます。
下記が一例です。
<%= link_to('CSVをダウンロードする', users_path(format: :csv), class: "btn") %>
ポイントは、ルーテイングヘルパーである_pathヘルパーのパラメータに(format: :csv)
という感じでformatオプションを渡してあげることです。これがindex.csv
というURLを生成してくれます。
これで基本的な実装は完了だが...
簡単な実装だけならこれでOKですが、普通はもっと気遣わなくてはならないところが出てきます。
例えば、下記4点です。
- CSVのファイル名を指定する
- HTTPリクエストでコンテントタイプを指定する
- 1.2などのため、htmlリクエストの時とcsvリクエストの時でコントローラの出力処理を分ける
- Windows対応(BOM付きUTF-8対応)をする
1-3はコントローラで一気に実装できます。見ていきましょう。
ファイル名を指定する
何もしないとリソース名のついたファイル名になってしまいます。users.csvみたいな感じです。
でもこれでは不便です。2回目ダウンロードすると、users(2).csvという名前になってしまうからです。例えば、ダウンロードした時間をファイル名に入れておくと便利そうです。
ファイル名を指定するには、コントローラでsend_dataメソッドを使ってあげる必要があります。
今回は、users_[ダウンロードした時間].csv
という名前を指定する場合を例にとって、コーディングしてみます↓
def index
# 普通はこの辺にindex.html.erbを表示するための処理があって...
respond_to do |format|
# htmlにアクセスする場合
format.html
# csvにアクセスする場合
time_now = Time.zone.now.strftime('%Y%m%d%H%M%S')
format.csv { send_data render_to_string, filename: "users_#{time_now}.csv", type: :csv }
end
end
-
respond_toメソッドは、クライアイントからのリクエストで指定されたフォーマットに応じて出力を変えるというメソッドです。上での例では、1つのコントローラのメソッドで、htmlがリクエストされた時とcsvがリクエストされた時で、出力を分けています。ちなみに、respond_toなしに後述のsend_dataメソッドを定義すると、例えば同じ名前のhtmlファイルがある時、それにビューにアクセスしただけでCSVのダウンロード処理が同時に走ってしまいます。今回の例では、htmlの後は処理を何も書いていないので、そのままindexの処理(index.html.erb)を呼ぶような処理が走ります。csvの場合は、
{}
ブロック内の処理が実行されます。 - send_dataメソッドは、バイナリデータを送るメソッドです。このオプショナルな引数でファイル名やコンテントタイプを指定できます。バイナリデータとは、コンピュータが理解できる2進数のデータで、CSV,画像、動画などなどを指し、テキストデータの対義語として使われることもあるそうです。
コンテントタイプの指定について補足
コンテントタイプはHTTPの話になります。HTTP通信の際、サーバーからクライアントに「CSVレスポンスするねー」と返します。これはHTTPレスポンスのヘッダーのContent-type
という項目で設定されています。
これを設定しないと、環境によってはブラウザがそのレスポンスを無視して従わないケースもあるそうです。MDN docs, "Content-Type"。
そのため、万全には万全を期すためにも、レスポンスでコンテントタイプを指定してあげます。これが、send_dataの引数type: :csv
オプションです。これもChromeの開発者ツールで確認できます。ちなみに自分の環境では、このオプションを外しても結果はあまり変わらなくて、text/csvが指定されていました。
最後に
ここまで実装終えたのですが、Windowsで文字化けするよーと指摘され、まだマージできませんでした(涙)次回、BOM付きUTF-8についてまとめたいと思います。
(追記)BOMまとめました↓
何かご指摘などあればバシバシよろしくお願いします。