Ruby on Rails - Importa dati da un file CSV


205

Vorrei importare i dati da un file CSV in una tabella di database esistente. Non voglio salvare il file CSV, basta prendere i dati da esso e metterli nella tabella esistente. Sto usando Ruby 1.9.2 e Rails 3.

Questo è il mio tavolo:

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

Puoi darmi un codice per mostrarmi il modo migliore per farlo, grazie.

Risposte:


381
require 'csv'    

csv_text = File.read('...')
csv = CSV.parse(csv_text, :headers => true)
csv.each do |row|
  Moulding.create!(row.to_hash)
end

2
Puoi metterlo in un'attività Rake, o in un'azione del controller, o ovunque tu voglia ....
yfeldblum,

1
Ha funzionato perfettamente. Tuttavia, ho una domanda a livello di principiante: quando ho cercato di sfogliare i metodi descritti nella documentazione dell'API di Ruby and Rails, non sono riuscito a trovarli sul posto (ho cercato sui siti ufficiali di Ruby and Rails, documenti API). Ad esempio, non sono riuscito a trovare l'oggetto che restituisce CSV.parse (), non ho trovato i metodi to_hash () e with_indifferent_access () ... Forse ho guardato nel posto sbagliato o ho perso alcuni principi di base su come attraversare l'API Ruby & Rails Documenti. Qualcuno può condividere le migliori pratiche su come leggere i documenti dell'API Ruby?
Vladimir Kroz,

2
@daveatflow: sì, vedi la mia risposta di seguito, che legge nel file una riga alla volta.
Tom De Leu,

1
@ lokeshjain2008, si riferisce al modello del PO.
Justin D.

3
Questo metodo è inefficiente! Su enormi file CSV i razzi di utilizzo dell'ariete. quello sotto è migliore.
unom

206

Versione più semplice della risposta di yfeldblum, che è più semplice e funziona bene anche con file di grandi dimensioni:

require 'csv'    

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

Non è necessario with_indifferent_access o symbolize_keys e non è necessario leggere prima il file in una stringa.

Non mantiene l'intero file in memoria contemporaneamente, ma legge riga per riga e crea uno Stampaggio per riga.


1
Questo è meglio per gestire file di grandi dimensioni, giusto? Legge in una riga alla volta?
NotSimon

1
@Simon: davvero. Non mantiene l'intero file in memoria contemporaneamente, ma legge riga per riga e crea uno Stampaggio per riga.
Tom De Leu,

Ho questo errore, sai perché ?: ActiveModel :: UnknownAttributeError: attributo sconosciuto 'siren; nom_ent; indirizzo; complement_adresse; cp_ville; pays; region; departement; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti ; categorie; tel 'for Transaction
nico_lrx,

1
@AlphaNico Crea una domanda con il tuo problema. Questo errore non è correlato a questo, gli oggetti Model sembrano non sincronizzati.
unom

In questo caso, come scrivi TestCases per questo?
Afolabi Olaoluwa Akinwumi,

11

La smarter_csvgemma è stata creata appositamente per questo caso d'uso: leggere i dati dal file CSV e creare rapidamente voci di database.

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

È possibile utilizzare l'opzione chunk_sizeper leggere N righe CSV alla volta, quindi utilizzare Resque nel ciclo interno per generare lavori che creeranno i nuovi record, anziché crearli immediatamente - in questo modo è possibile distribuire il carico di generare voci a più lavoratori.

Vedi anche: https://github.com/tilo/smarter_csv


3
Dato che la classe CSV è inclusa, ritengo sia meglio usarla invece di aggiungere o installare una gemma aggiuntiva. Certo, non hai proposto di aggiungere un nuovo gioiello all'applicazione. È così facile aggiungere una serie di gemme individuali, ognuna per uno scopo specifico e prima che tu lo sappia la tua applicazione ha dipendenze eccessive. (Mi ritrovo a evitare consapevolmente l'aggiunta di eventuali gemme. Nel mio negozio dobbiamo giustificare l'aggiunta ai nostri compagni di squadra.)
Tass

1
@Tass è anche abbastanza facile aggiungere una serie di metodi individuali, ciascuno per uno scopo specifico e prima che tu lo sappia la tua applicazione ha una logica eccessiva che devi mantenere. Se una gemma funziona, è ben mantenuta e utilizza poche risorse o può essere messa in quarantena negli ambienti pertinenti (ad esempio la stadiazione per le attività di produzione) mi sembra sempre un'opzione migliore per utilizzare la gemma. Ruby e Rails si occupano solo di scrivere meno codice.
zrisher

Ho il seguente errore, sai perché? ActiveModel :: UnknownAttributeError: attributo sconosciuto 'siren; nom_ent; adresse; complement_adresse; cp_ville; pays; region; departement; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel' for Transaction
nico_lrx

Ho provato questo su un'attività di rake, la console ritorna: rake interrotto! NoMethodError: undefined metodo `stretta' per nil: NilClass stackoverflow.com/questions/42515043/...
Marcos R. Guevara

1
@Tass smantellare l'elaborazione CSV, migliorare la velocità e risparmiare memoria potrebbe essere una buona giustificazione per aggiungere una nuova gemma;)
Tilo

5

Potresti provare 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

Se questo è ciò che desideri, potresti anche considerare di eliminare la chiave primaria con incremento automatico dalla tabella e impostare la chiave primaria su name. In alternativa, se esiste una combinazione di attributi che formano una chiave primaria, usala come selettore. Non è necessario alcun indice, lo renderà più veloce.



2

È meglio avvolgere il processo relativo al database all'interno di un transactionblocco. Il frammento di frammento di codice è un processo completo di seeding di un set di lingue sul modello di lingua,

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

Lo snippet di seguito è un parziale del languages.csvfile,

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


0

Il modo migliore è includerlo in un'attività di rake. Crea il file import.rake all'interno di / lib / task / e inserisci questo codice in quel file.

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

Dopodiché esegui questo comando nel tuo terminale rake csv_model_import[file.csv,Name_of_the_Model]


0

So che è una vecchia domanda, ma è ancora nei primi 10 collegamenti in Google.

Non è molto efficiente salvare le righe una ad una perché causa la chiamata al database nel loop e è meglio evitarlo, soprattutto quando è necessario inserire enormi porzioni di dati.

È meglio (e significativamente più veloce) utilizzare l'inserimento batch.

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

È possibile creare una query del genere manualmente e quindi Model.connection.execute(RAW SQL STRING)(non consigliata) o utilizzare gem activerecord-import(è stato rilasciato la prima volta l'11 agosto 2010) in questo caso basta inserire i dati nell'array rowse chiamareModel.import rows

fare riferimento ai documenti gemma per i dettagli


-2

È meglio usare CSV :: Tabella e usare String.encode(universal_newline: true). Converte CRLF e CR in LF


1
Qual è la tua soluzione proposta?
Tass

-3

Se si desidera utilizzare SmartCSV

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

Ciò rappresenta i dati delimitati da tabulazioni in ogni riga "\t"con righe separate da nuove righe"\n"

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.