Ruby on Rails - import danych z pliku CSV

205

Chciałbym zaimportować dane z pliku CSV do istniejącej tabeli bazy danych. Nie chcę zapisywać pliku CSV, po prostu weź dane z niego i umieść w istniejącej tabeli. Używam Ruby 1.9.2 i Rails 3.

To jest mój stół:

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

Czy możesz dać mi kod, który pokaże mi najlepszy sposób, dzięki.

najświeższe
źródło

Odpowiedzi:

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
źródło
2
Możesz umieścić go w zadaniu Rake, akcji kontrolera lub gdziekolwiek chcesz ...
yfeldblum,
1
Działa idealnie. Mam jednak pytanie na poziomie początkującym - kiedy próbowałem przeglądać opisane metody w dokumentacji interfejsu API Ruby i Rails, nie byłem w stanie ich znaleźć na miejscu (szukałem oficjalnych stron Ruby i Rails, dokumentów API). Np. Nie mogłem znaleźć, który obiekt zwraca CSV.parse (), nie znalazłem metod to_hash () i with_indifferent_access () ... Może szukałem w złym miejscu lub brakowało mi podstawowych zasad, jak przechodzić przez interfejs API Ruby & Rails dokumenty Czy ktoś może podzielić się najlepszą praktyką, jak czytać dokumenty API Ruby?
Vladimir Kroz,
2
@daveatflow: tak, patrz moja odpowiedź poniżej, która odczytuje w pliku po jednej linii na raz.
Tom De Leu,
1
@ lokeshjain2008, odnosi się do modelu OP.
Justin D.,
3
Ta metoda jest nieefektywna! Na ogromnych plikach CSV użycie pamięci RAM wzrasta. ten poniżej jest lepszy.
unom
206

Prostsza wersja odpowiedzi yfeldblum, która jest prostsza i działa również z dużymi plikami:

require 'csv'    

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

Nie ma potrzeby korzystania z opcji_indifferent_access lub symbolize_keys i nie trzeba najpierw wczytywać pliku do ciągu.

Nie zachowuje jednocześnie całego pliku w pamięci, ale czyta wiersz po wierszu i tworzy profil na linię.

Tom De Leu
źródło
1
To jest lepsze do zarządzania dużymi rozmiarami plików, prawda? Czy czyta się w jednym wierszu na raz?
NotSimon
1
@ Simon: rzeczywiście. Nie zachowuje jednocześnie całego pliku w pamięci, ale czyta wiersz po wierszu i tworzy profil na linię.
Tom De Leu,
Mam ten błąd, czy wiesz dlaczego ?: ActiveModel :: UnknownAttributeError: syrena nieznanego atrybutu; nom_ent; adresse; dopełnienie_adresu; cp_ville; płaci; region; departament; activite; data; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti ; categorie; tel 'for Transaction
nico_lrx
1
@AlphaNico Stwórz pytanie dotyczące swojego problemu. Ten błąd nie ma związku z tym, obiekty Modelu wydają się niesynchronizowane.
unom
W takim przypadku, jak do tego piszesz TestCases?
Afolabi Olaoluwa Akinwumi
11

smarter_csvKlejnot został stworzony specjalnie dla tej przypadków użycia: aby odczytać dane z pliku CSV i szybko tworzyć wpisy bazy danych.

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

Możesz użyć opcji chunk_sizeodczytu N wierszy csv naraz, a następnie użyć Resque w wewnętrznej pętli, aby wygenerować zadania, które utworzą nowe rekordy, zamiast tworzyć je od razu - w ten sposób możesz rozłożyć obciążenie generowaniem wpisów do wielu pracowników.

Zobacz także: https://github.com/tilo/smarter_csv

Tilo
źródło
3
Ponieważ klasa CSV jest uwzględniona, myślę, że lepiej jej używać zamiast dodawać lub instalować dodatkowy klejnot. To prawda, że ​​nie zaproponowałeś dodania nowego klejnotu do aplikacji. Tak łatwo jest dodać serię pojedynczych klejnotów, każdy do określonego celu, a zanim się zorientujesz, Twoja aplikacja ma nadmierne zależności. (Uważam, że świadomie unikam dodawania jakichkolwiek klejnotów. W moim sklepie musimy uzasadnić dodanie do naszych kolegów z drużyny.)
Tass
1
@Tass można również dość łatwo dodać szereg indywidualnych metod, z których każda służy do określonego celu. Zanim się zorientujesz, aplikacja ma nadmierną logikę, którą musisz utrzymywać. Jeśli klejnot działa, jest dobrze utrzymany i zużywa niewiele zasobów lub może zostać poddany kwarantannie w odpowiednich środowiskach (tj. Inscenizacja zadań produkcyjnych), zawsze uważam, że lepszym rozwiązaniem jest użycie klejnotu. Ruby i Rails starają się pisać mniej kodu.
zrisher
Mam następujący błąd, czy wiesz dlaczego? ActiveModel :: UnknownAttributeError: syrena nieznanego atrybutu; nom_ent; adres; dopełnienie; cp_ville; pays; region; departement; activite; data; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel 'for Transaction
nico_lrx
Próbowałem tego na zadaniu rake, konsola zwraca: rake przerwany! NoMethodError: niezdefiniowana metoda „close” dla zera: NilClass stackoverflow.com/questions/42515043/…
Marcos R. Guevara
1
@Tass dzielenie przetwarzania CSV, poprawa prędkości i oszczędność pamięci może być dobrym uzasadnieniem dodania nowego klejnotu;)
Tilo
5

Możesz spróbować 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

Jeśli tego właśnie chcesz, możesz również rozważyć pozbycie się klucza podstawowego z automatycznym przyrostem z tabeli i ustawienie klucza podstawowego na name. Alternatywnie, jeśli istnieje kombinacja atrybutów, które tworzą klucz podstawowy, użyj tego jako selektora. Nie jest wymagany indeks, po prostu przyspieszy.

Seamus Abshere
źródło
2

Lepiej jest owinąć proces związany z bazą danych wewnątrz transactionbloku. Uderzenie fragmentu kodu to pełny proces zapełniania zestawu języków do modelu językowego,

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

Poniższy fragment jest fragmentem languages.csvpliku,

aa,Afar
ab,Abkhazian
af,Afrikaans
ak,Akan
am,Amharic
ar,Arabic
as,Assamese
ay,Aymara
az,Azerbaijani
ba,Bashkir
...
Lorem Ipsum Dolor
źródło
0

Użyj tego klejnotu: https://rubygems.org/gems/active_record_importer

class Moulding < ActiveRecord::Base
  acts_as_importable
end

Następnie możesz teraz użyć:

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

Upewnij się tylko, że nagłówki pasują do nazw kolumn tabeli

Michael Nera
źródło
0

Lepszym sposobem jest włączenie go do zadania prowizji. Utwórz plik import.rake w / lib / task / i umieść ten kod w tym pliku.

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

Następnie uruchom to polecenie w swoim terminalu rake csv_model_import[file.csv,Name_of_the_Model]

Ipsagel
źródło
0

Wiem, że to stare pytanie, ale wciąż w pierwszych 10 linkach w Google.

Zapisywanie wierszy jeden po drugim nie jest zbyt wydajne, ponieważ powoduje wywołanie bazy danych w pętli i lepiej tego unikać, szczególnie gdy trzeba wstawić duże porcje danych.

Lepiej (i znacznie szybciej) jest używać wstawki wsadowej.

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

Możesz zbudować takie zapytanie ręcznie, a potem Model.connection.execute(RAW SQL STRING)nie (nie zalecane) lub użyć gem activerecord-import(został wydany po raz pierwszy 11 sierpnia 2010 r.) W tym przypadku po prostu umieść dane w tablicy rowsi wywołajModel.import rows

szczegółowe informacje można znaleźć w dokumentacji klejnotów

Jarosław
źródło
-2

Lepiej jest używać CSV :: Tabela i używać String.encode(universal_newline: true). Konwertuje CRLF i CR na LF

ysk
źródło
1
Jakie jest twoje proponowane rozwiązanie?
Tass
-3

Jeśli chcesz użyć SmartCSV

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

To reprezentuje dane rozdzielane tabulatorami w każdym wierszu "\t"z wierszami oddzielonymi nowymi wierszami"\n"

Maged Makled
źródło