Ruby on Rails - Importar datos desde un archivo CSV

205

Me gustaría importar datos de un archivo CSV a una tabla de base de datos existente. No quiero guardar el archivo CSV, solo tomar los datos y ponerlo en la tabla existente. Estoy usando Ruby 1.9.2 y Rails 3.

Esta es mi mesa:

create_table "mouldings", :force => true do |t|
  t.string   "suppliers_code"
  t.datetime "created_at"
  t.datetime "updated_at"
  t.string   "name"
  t.integer  "supplier_id"
  t.decimal  "length",         :precision => 3, :scale => 2
  t.decimal  "cost",           :precision => 4, :scale => 2
  t.integer  "width"
  t.integer  "depth"
end

¿Me puede dar un código para mostrarme la mejor manera de hacer esto, gracias.

más fresco
fuente

Respuestas:

381
require 'csv'    

csv_text = File.read('...')
csv = CSV.parse(csv_text, :headers => true)
csv.each do |row|
  Moulding.create!(row.to_hash)
end
Yfeldblum
fuente
2
Usted puede ponerlo en una tarea Rake, o en una acción del controlador, o cualquier lugar que desee ....
yfeldblum
1
Funcionó perfectamente. Sin embargo, tengo una pregunta de nivel principiante: cuando intenté examinar los métodos descritos en la documentación de la API de Ruby and Rails, no pude encontrarlos en el lugar (busqué en los sitios oficiales de Ruby and Rails, documentos de la API). Por ejemplo, no pude encontrar qué objeto devuelve CSV.parse (), no encontré los métodos to_hash () y with_indifferent_access () ... Tal vez busqué en el lugar equivocado o perdí algún principio básico sobre cómo atravesar la API de Ruby & Rails docs. ¿Alguien puede compartir las mejores prácticas sobre cómo leer documentos de Ruby API?
Vladimir Kroz
2
@daveatflow: sí, vea mi respuesta a continuación, que se lee en el archivo línea por línea.
Tom De Leu
1
@ lokeshjain2008, se refiere al modelo del OP.
Justin D.
3
¡Este método es ineficiente! En grandes archivos CSV, el uso de RAM se dispara. el de abajo es mejor.
Unom
206

Versión más simple de la respuesta de yfeldblum, que es más simple y funciona bien también con archivos grandes:

require 'csv'    

CSV.foreach(filename, :headers => true) do |row|
  Moulding.create!(row.to_hash)
end

No hay necesidad de with_indifferent_access o symbolize_keys, y no es necesario leer primero el archivo en una cadena.

No mantiene todo el archivo en la memoria a la vez, sino que se lee línea por línea y crea un moldeado por línea.

Tom De Leu
fuente
1
Esto es mejor para administrar archivos de gran tamaño, ¿verdad? ¿Se lee en una línea a la vez?
NotSimon
1
@ Simon: de hecho. No mantiene todo el archivo en la memoria a la vez, sino que se lee línea por línea y crea un moldeado por línea.
Tom De Leu
Tengo este error, ¿sabe por qué ?: ActiveModel :: UnknownAttributeError: atributo desconocido 'sirena; nom_ent; adresse; complementa_adresse; cp_ville; paga; región; departamento; activite; fecha; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti ; categoría; tel 'para Transacción
nico_lrx
1
@AlphaNico Cree una pregunta con su problema. Ese error no está relacionado con esto, sus objetos Modelo parecen no estar sincronizados.
Unom
En este caso, ¿cómo escribir TestCases para esto?
Afolabi Olaoluwa Akinwumi
11

La smarter_csvgema se creó específicamente para este caso de uso: para leer datos del archivo CSV y crear rápidamente entradas en la base de datos.

  require 'smarter_csv'
  options = {}
  SmarterCSV.process('input_file.csv', options) do |chunk|
    chunk.each do |data_hash|
      Moulding.create!( data_hash )
    end
  end

Puedes usar la opción chunk_size para leer N csv-rows a la vez, y luego usar Resque en el bucle interno para generar trabajos que crearán los nuevos registros, en lugar de crearlos de inmediato, de esta manera puede distribuir la carga de generar entradas a múltiples trabajadores.

Ver también: https://github.com/tilo/smarter_csv

Tilo
fuente
3
Como se incluye la clase CSV, creo que es mejor usarla en lugar de agregar o instalar una gema adicional. Por supuesto, no propusiste que se agregue una nueva gema a la aplicación. Es muy fácil agregar una serie de gemas individuales, cada una para un propósito específico y antes de que te des cuenta, tu aplicación tiene dependencias excesivas. (Me encuentro evitando conscientemente la adición de gemas. En mi tienda necesitamos justificar la adición a nuestros compañeros de equipo.)
Tass
1
@Tass también es bastante fácil agregar una serie de métodos individuales, cada uno para un propósito específico y antes de que te des cuenta, tu aplicación tiene una lógica excesiva que debes mantener. Si una gema funciona, está bien mantenida y usa pocos recursos o puede ser puesta en cuarentena en los entornos relevantes (es decir, puesta en escena para tareas de producción), siempre me parece una mejor opción para usar la gema. Ruby y Rails tienen que ver con escribir menos código.
zrisher
Tengo el siguiente error, ¿sabes por qué? ActiveModel :: UnknownAttributeError: atributo desconocido 'sirena; nom_ent; adresse; complementa_adresse; cp_ville; paga; región; departamento; activita; fecha; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel' for Transaction
nico_lrx
Intenté esto en una tarea de rastrillo, la consola regresa: ¡rastrillo abortado! NoMethodError: método indefinido 'close' para nil: NilClass stackoverflow.com/questions/42515043/…
Marcos R. Guevara
1
@Tass fragmentando el procesamiento CSV, mejorando la velocidad y ahorrando memoria podría ser una buena justificación para agregar una nueva gema;)
Tilo
5

Puedes probar Upsert:

require 'upsert' # add this to your Gemfile
require 'csv'    

u = Upsert.new Moulding.connection, Moulding.table_name
CSV.foreach(file, headers: true) do |row|
  selector = { name: row['name'] } # this treats "name" as the primary key and prevents the creation of duplicates by name
  setter = row.to_hash
  u.row selector, setter
end

Si esto es lo que desea, también puede considerar deshacerse de la clave primaria de incremento automático de la tabla y establecer la clave primaria en name. Alternativamente, si hay alguna combinación de atributos que forman una clave primaria, úsela como selector. No es necesario un índice, solo lo hará más rápido.

Seamus Abshere
fuente
2

Es mejor envolver el proceso relacionado con la base de datos dentro de un transactionbloque. El fragmento de código es un proceso completo de envío de un conjunto de idiomas al modelo de idioma,

require 'csv'

namespace :lan do
  desc 'Seed initial languages data with language & code'
  task init_data: :environment do
    puts '>>> Initializing Languages Data Table'
    ActiveRecord::Base.transaction do
      csv_path = File.expand_path('languages.csv', File.dirname(__FILE__))
      csv_str = File.read(csv_path)
      csv = CSV.new(csv_str).to_a
      csv.each do |lan_set|
        lan_code = lan_set[0]
        lan_str = lan_set[1]
        Language.create!(language: lan_str, code: lan_code)
        print '.'
      end
    end
    puts ''
    puts '>>> Languages Database Table Initialization Completed'
  end
end

El fragmento a continuación es parcial del languages.csvarchivo,

aa,Afar
ab,Abkhazian
af,Afrikaans
ak,Akan
am,Amharic
ar,Arabic
as,Assamese
ay,Aymara
az,Azerbaijani
ba,Bashkir
...
Lorem Ipsum Dolor
fuente
0

Use esta gema: https://rubygems.org/gems/active_record_importer

class Moulding < ActiveRecord::Base
  acts_as_importable
end

Entonces ahora puede usar:

Moulding.import!(file: File.open(PATH_TO_FILE))

Solo asegúrese de que sus encabezados coincidan con los nombres de columna de su tabla

Michael Nera
fuente
0

La mejor manera es incluirlo en una tarea de rastrillo. Cree el archivo import.rake dentro de / lib / task / y coloque este código en ese archivo.

desc "Imports a CSV file into an ActiveRecord table"
task :csv_model_import, [:filename, :model] => [:environment] do |task,args|
  lines = File.new(args[:filename], "r:ISO-8859-1").readlines
  header = lines.shift.strip
  keys = header.split(',')
  lines.each do |line|
    values = line.strip.split(',')
    attributes = Hash[keys.zip values]
    Module.const_get(args[:model]).create(attributes)
  end
end

Después de eso ejecuta este comando en tu terminal rake csv_model_import[file.csv,Name_of_the_Model]

Ipsagel
fuente
0

Sé que es una pregunta antigua, pero todavía está en los primeros 10 enlaces en Google.

No es muy eficiente guardar filas una por una porque causa una llamada a la base de datos en el bucle y es mejor evitarlo, especialmente cuando necesita insertar grandes porciones de datos.

Es mejor (y significativamente más rápido) usar inserción por lotes.

INSERT INTO `mouldings` (suppliers_code, name, cost)
VALUES
    ('s1', 'supplier1', 1.111), 
    ('s2', 'supplier2', '2.222')

Puede compilar dicha consulta manualmente y luego hacerlo Model.connection.execute(RAW SQL STRING)(no recomendado) o usar gem activerecord-import(se lanzó por primera vez el 11 de agosto de 2010) en este caso, simplemente coloque los datos en la matriz rowsy llameModel.import rows

consulte los documentos de gemas para más detalles

Yaroslav
fuente
-2

Es mejor usar CSV :: Table y usar String.encode(universal_newline: true). Convierte CRLF y CR a LF

ysk
fuente
1
¿Cuál es su solución propuesta?
Tass
-3

Si quieres usar SmartCSV

all_data = SmarterCSV.process(
             params[:file].tempfile, 
             { 
               :col_sep => "\t", 
               :row_sep => "\n" 
             }
           )

Esto representa datos delimitados por tabuladores en cada fila "\t"con filas separadas por nuevas líneas"\n"

Makled magentado
fuente