Ruby on Rails - Importar dados de um arquivo CSV

205

Gostaria de importar dados de um arquivo CSV para uma tabela de banco de dados existente. Não quero salvar o arquivo CSV, basta pegar os dados e colocá-los na tabela existente. Estou usando o Ruby 1.9.2 e o Rails 3.

Esta é a minha 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

Você pode me dar algum código para me mostrar a melhor maneira de fazer isso, obrigado.

mais fresco
fonte

Respostas:

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
fonte
2
Você pode colocá-lo em uma tarefa Rake, ou em uma ação do controlador, ou em qualquer lugar você gosta ....
yfeldblum
1
Funcionou perfeitamente. No entanto, tenho uma pergunta no nível iniciante - quando tentei procurar os métodos descritos na documentação da API do Ruby e Rails, não consegui encontrá-los no local (procurei nos sites oficiais do Ruby e Rails, documentos da API). Por exemplo, não consegui encontrar qual objeto retorna CSV.parse (), não encontrei os métodos to_hash () e with_indifferent_access () ... Talvez eu tenha olhado no lugar errado ou tenha perdido algum princípio básico sobre como atravessar a API Ruby & Rails docs. Alguém pode compartilhar a melhor prática de como ler documentos da API Ruby?
Vladimir Kroz
2
@aveatflow: sim, veja minha resposta abaixo, que lê o arquivo uma linha de cada vez.
Tom De Leu
1
@ lokeshjain2008, refere-se ao modelo do OP.
Justin D.
3
Este método é ineficiente! Em enormes arquivos CSV, o uso da memória RAM dispara. o abaixo é melhor.
Unom
206

Versão mais simples da resposta de yfeldblum, que é mais simples e funciona bem também com arquivos grandes:

require 'csv'    

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

Não há necessidade de with_indifferent_access ou symbolize_keys e não há necessidade de ler o arquivo em uma sequência primeiro.

Ele não mantém o arquivo inteiro na memória de uma só vez, mas lê linha por linha e cria um Molding por linha.

Tom De Leu
fonte
1
Isso é melhor para gerenciar tamanhos grandes de arquivos, certo? Lê uma linha de cada vez?
NotSimon
1
@ Simon: de fato. Ele não mantém o arquivo inteiro na memória de uma só vez, mas lê linha por linha e cria um Molding por linha.
Tom De Leu
Eu tenho esse erro, você sabe por quê ?: ActiveModel :: UnknownAttributeError: unknown attribute 'siren; nom_ent; adresse; complement_adresse; cp_ville; paga; region; departement; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti ; categorie; tel 'para Transação
nico_lrx
1
@AlphaNico Crie uma pergunta com seu problema. Esse erro não está relacionado a isso, seus objetos Model parecem fora de sincronia.
Unom
Nesse caso, como você escreve TestCases para isso?
Afolabi Olaoluwa Akinwumi
11

A smarter_csvgem foi criada especificamente para este caso de uso: para ler dados do arquivo CSV e criar rapidamente entradas do banco de dados.

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

Você pode usar a opção chunk_sizede ler N linhas CSV por vez e, em seguida, usar Resque no loop interno para gerar tarefas que criarão os novos registros, em vez de criá-los imediatamente - dessa forma, você pode espalhar a carga de gerar entradas para vários trabalhadores.

Veja também: https://github.com/tilo/smarter_csv

Tilo
fonte
3
Como a classe CSV está incluída, acho melhor usá-la em vez de adicionar ou instalar uma gema adicional. Concedido, você não propôs que uma nova gema fosse adicionada ao aplicativo. É muito fácil adicionar uma série de gemas individuais, cada uma para uma finalidade específica e, antes que você perceba, seu aplicativo tem dependências excessivas. (Encontro-me conscientemente evitando a adição de quaisquer gemas na minha loja que precisamos para justificar a adição aos nossos companheiros de equipe..)
Tass
1
@Tass também é muito fácil adicionar uma série de métodos individuais, cada um para uma finalidade específica e, antes que você perceba, seu aplicativo tem uma lógica excessiva que você precisa manter. Se uma gema funciona, é bem mantida e utiliza poucos recursos ou pode ser colocada em quarentena nos ambientes relevantes (por exemplo, Preparação para tarefas de produção), parece-me sempre uma opção melhor para usar a gema. Ruby e Rails são sobre escrever menos código.
zrisher
Eu tenho o seguinte erro, você sabe o porquê? ActiveModel :: UnknownAttributeError: atributo desconhecido 'siren; nom_ent; endereço; complement_adresse; cp_ville; paga; região; departamento; atividade; data; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel' para Transaction
nico_lrx
Eu tentei isso em uma tarefa de rake, o console retorna: rake abortado! NoMethodError: undefined method `fechar' para nil: NilClass stackoverflow.com/questions/42515043/...
Marcos R. Guevara
1
@Tass chunking o processamento CSV, melhorando a velocidade e economia de memória pode ser uma boa justificativa para a adição de uma nova jóia;)
Tilo
5

Você pode tentar 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 é isso que você deseja, considere também livrar-se da chave primária de incremento automático da tabela e definir a chave primária como name. Como alternativa, se houver alguma combinação de atributos que formam uma chave primária, use-a como seletor. Nenhum índice é necessário, apenas o tornará mais rápido.

Seamus Abshere
fonte
2

É melhor agrupar o processo relacionado ao banco de dados dentro de um transactionbloco. O sopro do snippet de código é um processo completo de propagação de um conjunto de idiomas para o 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

O fragmento abaixo é uma parte do languages.csvarquivo,

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

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

class Moulding < ActiveRecord::Base
  acts_as_importable
end

Agora você pode usar:

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

Apenas certifique-se de que seus cabeçalhos correspondam aos nomes das colunas da sua tabela

Michael Nera
fonte
0

A melhor maneira é incluí-lo em uma tarefa de rake. Crie o arquivo import.rake dentro de / lib / tasks / e coloque esse código nesse arquivo.

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

Depois disso, execute este comando no seu terminal rake csv_model_import[file.csv,Name_of_the_Model]

Ipsagel
fonte
0

Eu sei que é pergunta antiga, mas ainda nos primeiros 10 links no google.

Não é muito eficiente salvar as linhas uma a uma, porque causa uma chamada ao banco de dados no loop e é melhor evitar isso, especialmente quando é necessário inserir grandes porções de dados.

É melhor (e significativamente mais rápido) usar a inserção em lote.

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

Você pode criar essa consulta manualmente e, em seguida, fazer Model.connection.execute(RAW SQL STRING)(não recomendado) ou usar gem activerecord-import(foi lançado em 11 de agosto de 2010). Nesse caso, basta colocar os dados na matriz rowse chamarModel.import rows

consulte os documentos da gem para obter detalhes

Yaroslav
fonte
-2

É melhor usar CSV :: Table e usar String.encode(universal_newline: true). Ele converte CRLF e CR em LF

ysk
fonte
1
Qual é a sua solução proposta?
Tass
-3

Se você deseja usar o SmartCSV

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

Isso representa dados delimitados por tabulação em cada linha "\t"com linhas separadas por novas linhas"\n"

Maged Makled
fonte