Ruby on Rails - Importieren Sie Daten aus einer CSV-Datei

205

Ich möchte Daten aus einer CSV-Datei in eine vorhandene Datenbanktabelle importieren. Ich möchte die CSV-Datei nicht speichern, sondern nur die Daten daraus entnehmen und in die vorhandene Tabelle einfügen. Ich benutze Ruby 1.9.2 und Rails 3.

Das ist mein Tisch:

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

Können Sie mir einen Code geben, der mir den besten Weg zeigt, danke?

am frischesten
quelle

Antworten:

380
require 'csv'    

csv_text = File.read('...')
csv = CSV.parse(csv_text, :headers => true)
csv.each do |row|
  Moulding.create!(row.to_hash)
end
Yfeldblum
quelle
2
Sie können es in eine Rake-Aufgabe oder in eine Controller-Aktion oder an einen beliebigen Ort stellen ...
Yfeldblum
1
Es hat perfekt funktioniert. Ich habe jedoch eine Frage für Anfänger: Als ich versuchte, die beschriebenen Methoden in der Ruby and Rails-API-Dokumentation zu durchsuchen, konnte ich sie nicht vor Ort finden (ich habe auf offiziellen Ruby and Rails-Websites nach API-Dokumenten gesucht). ZB konnte ich nicht finden, welches Objekt CSV.parse () zurückgibt, ich habe die Methoden to_hash () und with_indifferent_access () nicht gefunden ... Vielleicht habe ich an der falschen Stelle gesucht oder ein Grundprinzip zum Durchlaufen der Ruby & Rails-API übersehen docs. Kann jemand die Best Practice zum Lesen von Ruby API-Dokumenten teilen?
Vladimir Kroz
2
@daveatflow: Ja, siehe meine Antwort unten, die die Datei zeilenweise einliest.
Tom De Leu
1
@ lokeshjain2008, bezieht sich auf das Modell des OP.
Justin D.
3
Diese Methode ist ineffizient! Bei riesigen CSV-Dateien steigt die RAM-Nutzung sprunghaft an. der unten ist besser.
Unom
206

Einfachere Version von yfeldblums Antwort, die einfacher ist und auch bei großen Dateien gut funktioniert:

require 'csv'    

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

Keine Notwendigkeit für with_indifferent_access oder symbolize_keys und keine Notwendigkeit, die Datei zuerst in eine Zeichenfolge einzulesen.

Es wird nicht die gesamte Datei auf einmal gespeichert, sondern zeilenweise eingelesen und ein Moulding pro Zeile erstellt.

Tom De Leu
quelle
1
Dies ist besser für die Verwaltung großer Dateien, oder? Liest es jeweils in einer Zeile?
NotSimon
1
@ Simon: in der Tat. Es wird nicht die gesamte Datei auf einmal gespeichert, sondern zeilenweise eingelesen und ein Moulding pro Zeile erstellt.
Tom De Leu
Ich habe diesen Fehler, wissen Sie warum?: ActiveModel :: UnknownAttributeError: unbekanntes Attribut 'siren; nom_ent; adresse; komplement_adresse; cp_ville; bezahlt; Region; Abfahrt; Aktivit; Datum; nb_salaries; nom; prenom; zivil; adr_mail; libele_acti ; kategorie; tel 'für Transaktion
nico_lrx
1
@AlphaNico Erstellen Sie eine Frage mit Ihrem Problem. Dieser Fehler hat nichts damit zu tun. Ihre Modellobjekte scheinen nicht synchron zu sein.
Unom
Wie schreibt man in diesem Fall Testfälle dafür?
Afolabi Olaoluwa Akinwumi
11

Das smarter_csvJuwel wurde speziell für diesen Anwendungsfall erstellt: um Daten aus einer CSV-Datei zu lesen und schnell Datenbankeinträge zu erstellen.

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

Sie können die Option verwenden chunk_size , um N CSV-Zeilen gleichzeitig zu lesen, und dann Resque in der inneren Schleife verwenden, um Jobs zu generieren, die die neuen Datensätze erstellen, anstatt sie sofort zu erstellen. Auf diese Weise können Sie die Last der Generierung von Einträgen verteilen an mehrere Arbeiter.

Siehe auch: https://github.com/tilo/smarter_csv

Tilo
quelle
3
Da die CSV-Klasse enthalten ist, ist es meiner Meinung nach besser, sie zu verwenden, als ein zusätzliches Juwel hinzuzufügen oder zu installieren. Zugegeben, Sie haben nicht vorgeschlagen, der Anwendung ein neues Juwel hinzuzufügen. Es ist so einfach, eine Reihe einzelner Edelsteine ​​für einen bestimmten Zweck hinzuzufügen, und bevor Sie es wissen, weist Ihre Anwendung übermäßige Abhängigkeiten auf. (Ich vermeide bewusst das Hinzufügen von Edelsteinen. In meinem Shop müssen wir das Hinzufügen zu unseren Teamkollegen rechtfertigen.)
Tass
1
@Tass Es ist auch ziemlich einfach, eine Reihe einzelner Methoden hinzuzufügen, jede für einen bestimmten Zweck, und bevor Sie es wissen, verfügt Ihre Anwendung über eine übermäßige Logik, die Sie warten müssen. Wenn ein Edelstein funktioniert, gut gepflegt ist und wenig Ressourcen verbraucht oder unter Quarantäne gestellt werden kann (dh Bereitstellung für Produktionsaufgaben), scheint es mir immer eine bessere Option zu sein, den Edelstein zu verwenden. Bei Ruby und Rails geht es darum, weniger Code zu schreiben.
Zrisher
Ich habe den folgenden Fehler, wissen Sie warum? ActiveModel :: UnknownAttributeError: unbekanntes Attribut 'Sirene; nom_ent; adresse; complement_adresse; cp_ville; zahlt; Region; Departement; activite; Datum; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; Kategorie; tel' für Transaktion
nico_lrx
Ich habe dies bei einer Rake-Aufgabe versucht. Die Konsole kehrt zurück: Rake abgebrochen! NoMethodError: undefinierte Methode `close 'für nil: NilClass stackoverflow.com/questions/42515043/…
Marcos R. Guevara
1
@Tass Chunking der CSV-Verarbeitung, Verbesserung der Geschwindigkeit und Speicherplatzersparnis könnte eine gute Rechtfertigung für das Hinzufügen eines neuen Edelsteins sein;)
Tilo
5

Sie könnten versuchen 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

Wenn Sie dies wünschen, können Sie auch den Primärschlüssel mit automatischer Inkrementierung aus der Tabelle entfernen und den Primärschlüssel auf setzen name. Wenn es alternativ eine Kombination von Attributen gibt, die einen Primärschlüssel bilden, verwenden Sie diese als Selektor. Es ist kein Index erforderlich, es wird nur schneller.

Seamus Abshere
quelle
2

Es ist besser, den datenbankbezogenen Prozess in einen transactionBlock zu packen . Code-Snippet-Blow ist ein vollständiger Prozess, bei dem eine Reihe von Sprachen in das Sprachmodell übernommen werden.

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

Das folgende Snippet ist ein Teil der languages.csvDatei.

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

Verwenden Sie dieses Juwel: https://rubygems.org/gems/active_record_importer

class Moulding < ActiveRecord::Base
  acts_as_importable
end

Dann können Sie jetzt verwenden:

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

Stellen Sie nur sicher, dass Ihre Überschriften mit den Spaltennamen Ihrer Tabelle übereinstimmen

Michael Nera
quelle
0

Der bessere Weg ist, es in eine Rechenaufgabe aufzunehmen. Erstellen Sie die Datei import.rake in / lib / task / und fügen Sie diesen Code in diese Datei ein.

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

Führen Sie danach diesen Befehl in Ihrem Terminal aus rake csv_model_import[file.csv,Name_of_the_Model]

Ipsagel
quelle
0

Ich weiß, es ist eine alte Frage, aber es ist immer noch in den ersten 10 Links in Google.

Es ist nicht sehr effizient, Zeilen einzeln zu speichern, da dies zu Datenbankaufrufen in der Schleife führt und Sie dies besser vermeiden sollten, insbesondere wenn Sie große Datenmengen einfügen müssen.

Es ist besser (und wesentlich schneller), Batch-Insert zu verwenden.

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

Sie können eine solche Abfrage manuell erstellen und dann Model.connection.execute(RAW SQL STRING)(nicht empfohlen) oder gem verwenden activerecord-import(sie wurde erstmals am 11. August 2010 veröffentlicht). In diesem Fall legen Sie einfach Daten in ein Array rowsund rufen sie aufModel.import rows

Weitere Informationen finden Sie in den Gem-Dokumenten

Jaroslaw
quelle
-2

Es ist besser, CSV :: Table zu verwenden und zu verwenden String.encode(universal_newline: true). Es konvertiert CRLF und CR in LF

ysk
quelle
1
Was ist Ihre vorgeschlagene Lösung?
Tass
-3

Wenn Sie SmartCSV verwenden möchten

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

Dies stellt tabulatorgetrennte Daten in jeder Zeile dar, "\t"wobei die Zeilen durch neue Zeilen getrennt sind"\n"

Maged Makled
quelle