#362 Exporting CSV and Excel
- Download:
- source codeProject Files in Zip (56.3 KB)
- mp4Full Size H.264 Video (21 MB)
- m4vSmaller H.264 Video (8.94 MB)
- webmFull Size VP8 Video (9.65 MB)
- ogvFull Size Theora Video (22.1 MB)
WebアプリケーションからCSVやExcel形式でデータをエクスポートしたいというのはよくある話です。今回のエピソードではこれをRailsアプリケーションでおこなう方法を紹介します。下に示したページには、商品リストが表示されています。リンクを追加して、このリストをCSVかExcelファイルとしてダウンロードできるようにしたいと思います。
これをおこなうためのgemはいくつかありますが、今回はそれらは使わないで実現してみます。実際ゼロから作るのはとても簡単です。
CSVにエクスポートする
Ruby 1.9には便利なCSV libraryが付いてきますが、これはRuby 1.8ではFaster CSVとして知れれていたものです。このライブラリを用いてCSVデータを生成することにします。これは標準ライブラリの一部なので、ただrequireするだけで利用できるようになります。これをアプリケーションの設定ファイルで行ないます。
require File.expand_path('../boot', __FILE__) require 'csv' require 'rails/all'
ユーザが/products.csv
のパスにアクセスしたときに、このライブラリを使ってカンマ区切りのデータを生成することができます。現状では、アクションがこの形式に対応しないため、これは動作しません。しかし、ProductsController
のindexアクションにrespond_to
ブロックを追加することで簡単に修正できます。このフォーマット用のビューテンプレートを生成するか、あるいはコントローラでインラインでデータを出力するかの、いずれかの方法から選択します。今回のニーズに対しては、よりシンプルである後者の方法をとることにしますが、いずれの方法でも同じ結果になります。
class ProductsController < ApplicationController def index @products = Product.order(:name) respond_to do |format| format.html format.csv { render text: @products.to_csv } end end end
Productモデル内に新たに作成するto_csv
メソッドでCSVデータを生成することにします。メソッド名は既存のto_json
とto_xml
メソッドに合わせてわかりやすいよう設定しました。
class Product < ActiveRecord::Base attr_accessible :name, :price, :released_on def self.to_csv CSV.generate do |csv| csv << column_names all.each do |product| csv << product.attributes.values_at(*column_names) end end end end
データを作成するために、ブロックに渡されたcsv
オブジェクトに値の配列を追加します。1行目はヘッダ行で、column_names
と設定してモデルのすべての属性を配列で返します。列のサブセットを返したい場合は、ここで列名を列挙する必要があります。次に全商品をループして、ひとつずつにCSVファイルに行を追加して列名に一致する値を設定していきます。これにより、正しいフィールドが正しい順番で返されます。csv
ライブラリを読み込ませるためにサーバを再起動すると、CSVデータを表示できるようになりました。
このデータをブラウザ内に表示するのではなくダウンロードさせたい場合は、コントローラでrender :text
ではなくsend_data
を使います。ページをリロードするとCSVファイルをダウンロードできるようになります。
Excel形式でエクスポートする
ではExcel形式はどうすればいいでしょうか?一番簡単なのはユーザにCSVファイルをExcelで開いてもらう方法ですが、できればデフォルトでExcelが起動するxls
形式のファイルを提供する方がいいでしょう。そのためにはxls
ファイルのための新しいMIMEタイプを追加する必要があり、これをmime_types
初期化ファイルで行います。
Mime::Type.register "application/xls", :xls
このタイプのファイルに対応するようコントローラを修正します。Excelで正しく開くためにはデータがタブ区切りになっている必要があります。幸いcsvライブラリがカラム区切りオプションに対応しているので、これをおこなうためにタブ記号を渡します。
class ProductsController < ApplicationController def index @products = Product.order(:name) respond_to do |format| format.html format.csv { send_data @products.to_csv } format.xls { send_data @products.to_csv(col_sep: "\t") } end end end
Product
モデルのto_csv
メソッドを修正して、CSV.generate
に渡すオプションのハッシュを受け付けるように変更します。
class Product < ActiveRecord::Base attr_accessible :name, :price, :released_on def self.to_csv(options = {}) CSV.generate(options) do |csv| csv << column_names all.each do |product| csv << product.attributes.values_at(*column_names) end end end end
/products.xls
のパスにアクセスすると、ファイルがダウンロードされExcelで開くことができます。ほとんどの部分はこれでうまく行きました。ファイルは正常に開き、列は正しく区切られています。しかし少し問題があります。例えばファイルはエンコードが正しく処理されていないため、温度記号などの文字の表示がおかしくなっています。この問題を回避する方法はありますが、その場合に別の問題が発生します。その一つが、セル内に改行を入れることができないという問題です。
コントローラでデータを出力する代わりに、xls
ファイルを出力するためのテンプレートファイルを作成することにします。最初にコントローラのxls
のフォーマットからブロックを削除します。
class ProductsController < ApplicationController def index @products = Product.order(:name) respond_to do |format| format.html format.csv { send_data @products.to_csv } format.xls end end end
次に新規のテンプレートを作成します。
<table border="1"> <tr> <th>ID</th> <th>Name</th> <th>Release Date</th> <th>Price</th> </tr> <% @products.each do |product| %> <tr> <td><%= product.id %></td> <td><%= product.name %></td> <td><%= product.released_on %></td> <td><%= product.price %></td> </tr> <% end %> </table>
HTMLテーブルにエクスポートしたいデータを入れることで対応可能で、Excelはこれを問題なく表示できます。これをExcelで開くと、文字コードの問題はなくなり、改行記号を含むデータも正しく処理されています。ただし表の見た目が少し違っています。これはHTMLのフォーマットで、セルにborderを付け、ヘッダのセルを中央揃えしているからです。Excelでのフォーマットを制御したい場合は、HTMLドキュメントの中でカスタマイズすることもできます。よりExcel文書らしい見た目のファイルを作成したい場合は、テンプレートを次のように修正することで対応可能です。
<?xml version="1.0" encoding="UTF-8"?> <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40"> <Worksheet ss:Name="Sheet1"> <Table> <Row> <Cell><Data ss:Type="String">ID</Data></Cell> <Cell><Data ss:Type="String">Name</Data></Cell> <Cell><Data ss:Type="String">Release Date</Data></Cell> <Cell><Data ss:Type="String">Price</Data></Cell> </Row> <% @products.each do |product| %> <Row> <Cell><Data ss:Type="Number"><%= product.id %></Data></Cell> <Cell><Data ss:Type="String"><%= product.name %></Data></Cell> <Cell><Data ss:Type="String"><%= product.released_on %></Data></Cell> <Cell><Data ss:Type="Number"><%= product.price %></Data></Cell> </Row> <% end %> </Table> </Worksheet> </Workbook>
今度はデータをExcelが解釈できるXML形式で出力します。この方法によってできることの自由度が高まります。例えば複数のシートを作成したり、各列のフォーマットを指定したりすることが可能です。このファイルをリロードして新規にスプレッドシートを生成すると、ファイルのダウンロード時に警告が表示されます(少なくともOS Xでは)。これはこのフォーマットのファイルが、マクロなどの危険性のある要素を含んでいる可能性があるからです。
今回のファイルはエンコードが正しく処理され、本来のExcelドキュメントらしくなっています。この形式についての最良な情報源はMSDNのWebサイトにある“XML Spreadsheet Reference”です。ここにはExcelドキュメントで使用できるすべてのタグの一覧が含まれています。使いたい機能がスプレッドシートに含まれていたら、それをXMLファイルとして保存してテキストエディタで開くことでリバースエンジニアリングすることが可能です。
リンクを追加する
両方のフォーマットを生成できるようになったので、商品一覧ページ上にユーザが対応するファイルをダウンロードするためのリンクを追加します。これをページの最上部に追加します。
<h1>Products</h1> <p> Download: <%= link_to "CSV", products_path(format: "csv") %> | <%= link_to "Excel", products_path(format: "xls") %> </p><!-- Rest of file omitted -->
これでページをリロードするとダウンロードリンクが表示され、いずれかをクリックすると対応するファイルをダウンロードすることができます。