#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)
Es muy normal tener que exportar datos de una aplicación web en formato CSV o Excel, y en este episodio veremos como hacerlo en una aplicación Rails. A continuación se muestra una página con un listado de productos al que querríamos añadir unos enlaces que permitan la descarga en CSV o Excel.
Hay muchas gemas que nos pueden servir de ayuda, pero no usaremos ninguna porque hacerlo desde cero es realmente sencillo.
Exportación a CSV
Ruby 1.9 incluye una excelente librería CSV anteriormente llamada Faster CSV en Ruby 1.8. Como es parte de la biblioteca estándar lo único que tenemos que hacer es requerirla, lo que haremos en el fichero de configuración de nuestra aplicación.
require File.expand_path('../boot', __FILE__) require 'csv' require 'rails/all'
Ya podemos usar esta librería para generar datos separados por comas cuando el usuario visite la ruta /products.csv
. Por supuesto esto todavía no funciona porque la acción no responde a este formato todavía, lo cual es fácil de corregir añadiendo un bloque respond_to
a la acción index
en ProductsController
. Ahora tenemos una decisión que tomar: podemos generar una plantilla de vista o bien podemos mostrar los datos en línea desde el controlador. Haremos esto último porque es bastante más sencillo para lo que necesitamos, si bien ambas técnicas funcionan igual de bien.
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
Vamos a generar los datos CSV en el modelo de producto, con un nuevo método llamado to_csv
cuyo nombre está estratégicamente escogido para que no desentone con to_json
y 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
Para crear estos datos le añadimos el array de valores al objeto csv
que se le pasa al bloque. La primera fila es el encabezado, y hemos hecho que su valor sea column_names
, que devuelve una lista con los nombres de los atributos del modelo. Si quisíeramos devolver un subconjunto de estos atributos tendríamos que enumerarlos aquí a mano. Luego iteramos sobre los productos y añadimos una fila al fichero CSV para cada producto, y luego le añadios los valores que se corresponden con el nombre de la columna. Con esto estamos seguros de que se devuelven los campos correctos en el orden adecuado. Tras reiniciar el servidor para que coja la librería deberíamos poder ver nuestro fichero CSV.
Si queremos descargar estos datos en lugar de mostrarlos en el navegador podemos utilizar la opción send_data
en lugar de render :text
del controlador. Si recargamos la página ahora veremos que al mostrar el fichero CSV se inicia su descarga.
Exportación a Excel
¿Y qué pasa con Excel? La solución más sencilla sería que el usuario abriese el fichero CSV en Excel, pero es mejor proporcionar un fichero xls
que se abriría por defecto en Excel. Para esto tendremos que añadir un nuevo tipo MIME para los ficheros xls
, lo que haremos en el fichero de inicialización mime_types
.
Mime::Type.register "application/xls", :xls
Tenemos que modificar el controlador para que responda a este tipo MIME. Los datos tienen que estar separados por tabuladores para que se muestren en Excel; por suerte la librería CSV permite indicar un separador de columnas, por lo que usaremos el carácter de tabulación.
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
Tenemos que cambiar el método to_csv
de la clase Product
para aceptar un hash con las opciones que puede recibir 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
Si ahora visitamos la ruta /products.xls
veremos que se descarga un archivo que se puede abrir con Excel. En la mayoría de casos con esto nos bastará: el fichero se abre y las columnas se encuentran separadas correctamente, pero hay algunos problemas. Por ejemplo el fichero no tiene la codificación correcta, por lo que algunos caracteres como el símbolo de grado centígrado se ve mal. Hay varias soluciones a este problema, pero el enfoque que estamos usando tiene uno aún más grave, y es que las celdas no pueden contener un carácter de salto de línea.
En lugar de mostrar los datos en el controlador vamos a crear un fichero de plantilla para mostrar el fichero xls
. Primero tenemos que quitar el bloque del formato xls
en el controlador.
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
Empezaremos con nuestra nueva plantilla.
<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>
Podemos usar una tabla HTML con los datos que queremos exportar porque Excel no tiene ningún problema cargando este tipo de tablas. Cuando lo abramos con Excel veremos que el problema de la codificación ha desaparecido y que se muestran bien las celdas con saltos de línea. Pero la hoja de cálculo tiene un aspecto un poco diferente porque el formato en HTML añade un borde a las celdas, y además justifica al centro las celdas del encabezado. Esto se puede ajustar un poco en el documento HTML. Si queremos generar un fichero que se parece más a un documento Excel nativo podemos cambiar la plantilla:
<?xml version="1.0"?> <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>
Con esto nuestros datos se muestran como XML en un formato comprensible por Excel, lo que nos otorga mucha más flexibilidad en lo que podemos hacer. Por ejemplo podemos generar múltiples hojas, y especificar el formato de cada celda. Si generamos otra hoja de cálculo veremos un aviso (al menos en OS X) porque un fichero con este formato puede incluir código potencialmente peligroso como macros.
Ahora veremos que el archivo tiene la codificación correcta y tiene el aspecto de un documento Excel de verdad. La mejor documentación acerca de este formato es la Referencia de XML para Hojas de Cálculo en el sitio web de MSDN, que contiene un listado con todas las etiquetas que podremos usar en nuestros documentos Excel. Si además una hoja de cálculo incluye una funcionalidad que queramos usar, siempre podemos intentar hacer ingeniería inversa guardándola como XML y abriéndolo en un editor d etexto.
Enlaces
Ahora que podemos generar ambos formatos vamos a añadir los enlaces que permitirán al usuario descargar los ficheros relevantes. Los añadiremos en la parte superior de la página.
<h1>Products</h1> <p> Download: <%= link_to "CSV", products_path(format: "csv") %> | <%= link_to "Excel", products_path(format: "xls") %> </p> <!-- Se omite el resto del archivo -->
Al recargar la página veremos los enlaces de descarga, que al hacer clic en ellos devolverán el fichero solicitado.