Ruby on Rails - Importer des données à partir d'un fichier CSV

205

Je voudrais importer des données d'un fichier CSV dans une table de base de données existante. Je ne veux pas enregistrer le fichier CSV, il suffit de prendre les données et de les placer dans la table existante. J'utilise Ruby 1.9.2 et Rails 3.

Voici ma table:

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

Pouvez-vous me donner du code pour me montrer la meilleure façon de le faire, merci.

le plus frais
la source

Réponses:

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
la source
2
Vous pouvez le mettre dans une tâche Rake, ou dans une action de contrôleur, ou n'importe où vous voulez ....
yfeldblum
1
Cela a parfaitement fonctionné. Cependant, j'ai une question de niveau débutant - lorsque j'ai essayé de parcourir les méthodes décrites dans la documentation de l'API Ruby and Rails, je n'ai pas pu les trouver sur place (j'ai consulté les sites officiels Ruby and Rails, les documents API). Par exemple, je n'ai pas pu trouver quel objet renvoie CSV.parse (), je n'ai pas trouvé les méthodes to_hash () et with_indifferent_access () ... Peut-être que j'ai regardé au mauvais endroit ou que j'ai raté un principe de base sur la façon de traverser l'API Ruby & Rails docs. Quelqu'un peut-il partager les meilleures pratiques pour lire les documents de l'API Ruby?
Vladimir Kroz
2
@daveatflow: oui, voir ma réponse ci-dessous, qui lit dans le fichier une ligne à la fois.
Tom De Leu
1
@ lokeshjain2008, il fait référence au modèle de l'OP.
Justin
3
Cette méthode est inefficace! Sur d'énormes fichiers CSV, l'utilisation du ram monte en flèche. celui ci-dessous est meilleur.
unom
206

Version plus simple de la réponse de yfeldblum, qui est plus simple et fonctionne bien également avec des fichiers volumineux:

require 'csv'    

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

Pas besoin de with_indifferent_access ou symbolize_keys, et pas besoin de lire d'abord le fichier dans une chaîne.

Il ne conserve pas le fichier entier en mémoire à la fois, mais lit ligne par ligne et crée un moulage par ligne.

Tom De Leu
la source
1
C'est mieux pour gérer de gros fichiers, n'est-ce pas? Se lit-il sur une ligne à la fois?
NotSimon,
1
@Simon: en effet. Il ne conserve pas le fichier entier en mémoire à la fois, mais lit ligne par ligne et crée un moulage par ligne.
Tom De Leu
J'ai cette erreur, savez-vous pourquoi?: ActiveModel :: UnknownAttributeError: attribut inconnu 'siren; nom_ent; adresse; complement_adresse; cp_ville; pays; region; departement; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti ; categorie; tel 'pour Transaction
nico_lrx
1
@AlphaNico Créez une question avec votre problème. Cette erreur n'est pas liée à cela, vos objets de modèle semblent désynchronisés.
unom
Dans ce cas, comment écrivez-vous des TestCases pour cela?
Afolabi Olaoluwa Akinwumi
11

Le smarter_csvbijou a été spécialement créé pour ce cas d'utilisation: pour lire les données du fichier CSV et créer rapidement des entrées de base de données.

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

Vous pouvez utiliser l'option chunk_sizepour lire N lignes csv à la fois, puis utiliser Resque dans la boucle interne pour générer des travaux qui créeront les nouveaux enregistrements, plutôt que de les créer immédiatement - de cette façon, vous pouvez répartir la charge de génération d'entrées à plusieurs travailleurs.

Voir aussi: https://github.com/tilo/smarter_csv

Tilo
la source
3
Comme la classe CSV est incluse, je pense qu'il est préférable de l'utiliser au lieu d'ajouter ou d'installer une gemme supplémentaire. Certes, vous n'avez pas proposé d'ajouter un nouveau joyau à l'application. Il est si facile d'ajouter une série de gemmes individuelles, chacune dans un but spécifique et avant de vous en rendre compte, votre application présente des dépendances excessives. (Je me retrouve à éviter consciemment l'ajout de gemmes. Dans ma boutique, nous devons justifier l'ajout à nos coéquipiers.)
Tass
1
@Tass, il est également assez facile d'ajouter une série de méthodes individuelles, chacune dans un but spécifique et avant de le savoir, votre application a une logique excessive que vous devez maintenir. Si une gemme fonctionne, est bien entretenue et utilise peu de ressources ou peut être mise en quarantaine dans les environnements pertinents (c'est-à-dire la mise en scène pour les tâches de production), il me semble toujours une meilleure option pour utiliser la gemme. Ruby et Rails consistent à écrire moins de code.
zrisher
J'ai l'erreur suivante, savez-vous pourquoi? ActiveModel :: UnknownAttributeError: attribut inconnu 'siren; nom_ent; adresse; complément_adresse; cp_ville; pays; région; département; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel' for Transaction
nico_lrx
J'ai essayé ceci sur une tâche de râteau, la console revient: râteau abandonné! NoMethodError: méthode non définie `close 'pour nil: NilClass stackoverflow.com/questions/42515043/…
Marcos R. Guevara
1
@Tass segmentant le traitement CSV, améliorant la vitesse et économisant la mémoire pourrait être une bonne justification pour ajouter un nouveau bijou;)
Tilo
5

Vous pourriez essayer 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 c'est ce que vous voulez, vous pouvez également envisager de vous débarrasser de la clé primaire à incrémentation automatique de la table et de définir la clé primaire sur name. Sinon, s'il existe une combinaison d'attributs qui forment une clé primaire, utilisez-la comme sélecteur. Aucun index n'est nécessaire, il le rendra plus rapide.

Seamus Abshere
la source
2

Il est préférable d'envelopper le processus lié à la base de données dans un transactionbloc. Le coup d'extrait de code est un processus complet d'amorçage d'un ensemble de langues vers le modèle de langue,

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

L'extrait ci-dessous est une partie du languages.csvfichier,

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

Utilisez ce joyau: https://rubygems.org/gems/active_record_importer

class Moulding < ActiveRecord::Base
  acts_as_importable
end

Ensuite, vous pouvez maintenant utiliser:

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

Assurez-vous simplement que vos en-têtes correspondent aux noms de colonne de votre table

Michael Nera
la source
0

La meilleure façon est de l'inclure dans une tâche de râteau. Créez le fichier import.rake dans / lib / tasks / et mettez ce code dans ce fichier.

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

Après cela, exécutez cette commande dans votre terminal rake csv_model_import[file.csv,Name_of_the_Model]

Ipsagel
la source
0

Je sais que c'est une vieille question, mais elle figure toujours dans les 10 premiers liens de Google.

Il n'est pas très efficace d'enregistrer les lignes une par une car cela provoque un appel à la base de données dans la boucle et il vaut mieux éviter cela, surtout lorsque vous devez insérer d'énormes portions de données.

Il est préférable (et beaucoup plus rapide) d'utiliser l'insertion par lots.

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

Vous pouvez créer une telle requête manuellement et que faire Model.connection.execute(RAW SQL STRING)(non recommandé) ou utiliser gem activerecord-import(il a été publié pour la première fois le 11 août 2010) dans ce cas, mettez simplement les données dans le tableau rowset appelezModel.import rows

reportez-vous à la documentation de gem pour plus de détails

Iaroslav
la source
-2

Il vaut mieux utiliser CSV :: Table et utiliser String.encode(universal_newline: true). Il convertit CRLF et CR en LF

ysk
la source
1
Quelle est votre solution proposée?
Tass
-3

Si vous souhaitez utiliser SmartCSV

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

Cela représente des données délimitées par des tabulations dans chaque ligne "\t"avec des lignes séparées par de nouvelles lignes"\n"

Maged Makled
la source