LoginSignup
14
14

More than 3 years have passed since last update.

RailsでCSVダウンロード機能を実装する

Last updated at Posted at 2020-10-31

背景

RailsでCSVダウンロード機能を実装することになりました。すでに簡単で優れた事例はQiita上にたくさん上がっていて、このあたりの記事を見ればさらっと実装できたのですが、

仕事先で見つけたCSV出力機能はこれらよりもずっと複雑な作りをしていたので、「この仕組みはどうなっているんだ!」と思ったことを調べつつ、実装してみることにしました。基本的には、上記で紹介した2つ目の記事に近いです。

なお、Railsのバージョンは5.2.4.2です。

controller

controllerは上記で紹介した記事とほとんど変わらないです。htmlとcsvでformatを分けて、csvのフォーマットでリクエストがあったときに、csvを作成するメソッドが呼び出されます。

controllers/somethings_controller.rb
class SomethingsController < ApplicationController
  include SomeCsvModule # これから定義します

  def index
    @somethings = Something.all
    respond_to do |format|
      format.html
      format.csv do
        generate_csv(@somethings) # これから定義します
      end
    end
  end
end

view

ビューは至ってシンプルで、format: :csvを指定したボタンを設置するのみです^^。

views/somethings/index.html.haml
= link_to "CSVダウンロード", somethings_path(format: :csv)

module

ビューとコントローラーがこれだけシンプルな秘密は、moduleにありました。様々なcontrollerでの共通メソッドなので、controllers/concerns/内に共通のモジュールを定義しました。

/controllers/concerns/some_csv_module.rb
module SomeCsvModule
  extend ActiveSupport::Concern

  def generate_csv(somethings)
    filename = "情報一覧_#{Date.today}.csv"
    set_csv_request_headers(filename)

    bom = "\uFEFF" # 解説します(1)
    self.response_body = Enumerator.new do |csv_data| # 解説します(2・3)
      csv_data << bom

      header = %i(id 名前 内容)
      csv_data << header.to_csv # 解説します(4)

      somethings.each do |some|
        body = [
          some.id,
          some.name,
          some.content
        ]
        csv_data << body.to_csv
      end
    end
  end

  def set_csv_request_headers(filename, charset: 'UTF-8') # 解説します(5)
    # ↓解説します(6)
    self.response.headers['Content-Type'] ||= "text/csv; charset=#{charset}"
    self.response.headers['Content-Disposition'] = "attachment;filename=#{ERB::Util.url_encode(filename)}"
    self.response.headers['Content-Transfer-Encoding'] = 'binary'
  end
end

解説1: bom

BOMとは、Byte Order Mark(バイト・オーダー・マーク)の略で、Unicodeで記載された文書の冒頭につく短い文字列です。この文字列には、文書のエンコーディング方式等が記載されています。

Excelの標準の文字コードはShift-JISで、WEBの世界はUTF-8なので、WEBアプリでCSV出力したデータをExcelで開こうとすると、確実に文字化けしてしまいます。

文書の冒頭のbomでこの文書の文字コードはUTF-8であると伝えれば、文字化けを防げます。

このモジュールでは、bom = "\uFEFF"と定義し、続くcsvデータ作成処理の冒頭で、データの一番初めにbomを追加しています。

なお、この部分の解説は、こちらの2記事を参考に作成しています。

解説2: self.response_body

self.response_bodyの部分については、調べたけれども、よくわかりませんでした・・・。ごめんなさい。ただ、この辺りの記事から推測すると...

Railsがレンダリングするビューを定義する部分がself.response_bodyのようです。デフォルトではnilで、self.response_bodyに値を代入することで、レンダリングするビューを作成することができます。

解説3: Enumerator.new

Enumerator.newは、下記の記事を参考に調べた限りでは「配列的な要素を作成する」メソッドのようです。

▼参考
- Ruby の Enumerator とたわむれる

上記のコードのこの部分は

self.response_body = Enumerator.new do |csv_data|
  csv_data << bom

  header = %i(id 名前 内容)
  csv_data << header.to_csv

  somethings.each do |some|
    body = [
      # 中略
    ]
    csv_data << body.to_csv
  end
end

これと同じでした。

csv_data = []
csv_data << bom

header = %i(id 名前 内容)
csv_data << header.to_csv

somethings.each do |some|
  body = [
    # 中略
  ]
  csv_data << body.to_csv
end

self.response_body = csv_data

解説4: to_csv

to_csvは、csvライブラリのメソッドで、配列をcsv形式に変換してくれるメソッドのようです。以下、こちらのドキュメントからの引用コードです。

require 'csv'

csv_string = ["CSV", "data"].to_csv   # => "CSV,data"
csv_array  = "CSV,String".parse_csv   # => ["CSV", "String"]

解説5: キーワード引数

こちらのメソッドのcharset: 'UTF-8'の部分はキーワード引数と言って、ハッシュ のように引数にキーを指定する書き方です。

def set_csv_request_headers(filename, charset: 'UTF-8') 

▼詳しくはこちら
- Rubyのキーワード引数の使い方を現役エンジニアが解説【初心者向け】

このように指定することで、メソッド内ではcharsetのキーワードでUTF-8という値を呼び出すことができます。

解説6: self.response.headers

最後に、こちらの部分ですが、

self.response.headers['Content-Type'] ||= "text/csv; charset=#{charset}"
self.response.headers['Content-Disposition'] = "attachment;filename=#{ERB::Util.url_encode(filename)}"
self.response.headers['Content-Transfer-Encoding'] = 'binary'

レスポンスが返ってくる時の、レスポンスのヘッダーに追加する一連のオプションを指定しています。
オプション一覧は、こちらです。

それぞれ、簡単に解説すると、

  • Content-Type...コンテンツのMIMEタイプを指定する。
  • Content-Disposition...コンテンツがHTML(inline)なのか、添付ファイル(attachment)なのかを指定する。filename="ファイル名"オプションで、名前を付けて保存するウィンドウを出すことができる(*)
  • Content-Transfer-Encoding...は、リクエストの文字列が、どんな仕組みでエンコーディングされているかを記載している(詳細はこちら)

...だそうです。
(*)の部分は、公式ドキュメントにはそう書いてあるのですが、今のところ自分の環境では成功していません。ごめんなさい。。。

まとめ

...以上となります!仕組みを理解するまでに色々な学習が必要でしたが、なんとか無事CSV出力機能を実装することができました^^

▼作成したCSV出力ボタン:sunny:
Image from Gyazo

仕事で見かけたコードは、まだまだレスポンスヘッダーに色々なオプションがついていたり、共通化できる処理をまとめて汎用化できる形に整理したり、職人的な技が目白押しだったのですが、自分もまずはこの形を習得して、様々なオプションを使いこなせるようになろうと思います。

(まずは、これのテストが書けるようになりたいな...)
引き続き、精進あるのみです♪

14
14
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14