【问题标题】:Ruby on Rails - Import Data from a CSV fileRuby on Rails - 从 CSV 文件导入数据
【发布时间】:2011-05-23 14:04:17
【问题描述】:

我想将 CSV 文件中的数据导入现有的数据库表。我不想保存 CSV 文件,只需从中获取数据并将其放入现有表中。我正在使用 Ruby 1.9.2 和 Rails 3。

这是我的桌子:

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

你能给我一些代码来告诉我最好的方法吗,谢谢。

【问题讨论】:

    标签: ruby-on-rails csv import


    【解决方案1】:

    以下模块可以在任何模型上扩展,它会根据 CSV 中定义的列标题导入数据。

    注意:

    • 这是一个很棒的内部工具,供客户使用,我建议添加安全措施和消毒
    • CSV 中的列名必须与 DB 架构完全相同,否则将不起作用
    • 可以通过使用表名来获取标题而不是在文件中定义它们来进一步改进

    在您的 models/concerns 文件夹中创建一个名为 "csv_importer.rb" 的文件

    module CsvImporter
      extend ActiveSupport::Concern  
      require 'csv'
      
      def convert_csv_to_book_attributes(csv_path)
        csv_rows = CSV.open(csv_path).each.to_a.compact
        columns = csv_rows[0].map(&:strip).map(&:to_sym)
        csv_rows.shift
        
        return columns, csv_rows
      end
      
      def import_by_csv(csv_path)
        columns, attributes_array = convert_csv_to_book_attributes(csv_path)
        
        message = ""
        begin
          self.import columns, attributes_array, validate: false
          message = "Import Successful."
        rescue => e
          message = e.message
        end
        
        return message
      end
    end
    

    extend CsvImporter 添加到您希望将此功能扩展到的任何型号。

    在您的控制器中,您可以执行如下操作来利用此功能:

    def import_file
       model_name = params[:table_name].singularize.camelize.constantize
       csv = params[:file].path
       @message = model_name.import_by_csv(csv)
    end
    

    【讨论】:

      【解决方案2】:

      yfeldblum 答案的简单版本,更简单,也适用于大文件:

      require 'csv'    
      
      CSV.foreach(filename, headers: true) do |row|
        Moulding.create!(row.to_hash)
      end
      

      不需要with_indifferent_accesssymbolize_keys,也不需要先将文件读入字符串。

      它不会一次将整个文件保存在内存中,而是逐行读取并每行创建一个 Molding。

      【讨论】:

      • 这更适合管理大文件大小,对吧?它一次读一行吗?
      • @Simon:确实。它不会一次将整个文件保存在内存中,而是逐行读取并每行创建一个 Molding。
      • 我有这个错误,你知道为什么吗?:ActiveModel::UnknownAttributeError: unknown attribute 'siren;nom_ent;adresse;complement_adresse;cp_ville;pays;region;departement;activite;date;nb_salaries;nom ;prenom;civilite;adr_mail;libele_acti;categorie;tel' 用于交易
      • @AlphaNico 针对您的问题创建一个问题。该错误与此无关,您的模型对象似乎不同步。
      • 这种情况下,你怎么写TestCases呢?
      【解决方案3】:

      我知道这是个老问题,但它仍在谷歌的前 10 个链接中。

      逐一保存行不是很有效,因为它会导致循环中的数据库调用,您最好避免这种情况,尤其是当您需要插入大量数据时。

      使用批量插入更好(而且速度明显更快)。

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

      您可以手动构建这样的查询,而不是 Model.connection.execute(RAW SQL STRING)(不推荐) 或使用 gem activerecord-import(它于 2010 年 8 月 11 日首次发布)在这种情况下只需将数据放入数组 rows 并调用 Model.import rows

      refer to gem docs for details

      【讨论】:

        【解决方案4】:

        更好的方法是将其包含在 rake 任务中。在 /lib/tasks/ 中创建 import.rake 文件并将此代码放入该文件。

        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
        

        然后在终端中运行此命令rake csv_model_import[file.csv,Name_of_the_Model]

        【讨论】:

          【解决方案5】:

          使用这个宝石: https://rubygems.org/gems/active_record_importer

          class Moulding < ActiveRecord::Base
            acts_as_importable
          end
          

          那么你现在可以使用:

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

          请确保您的标题与表格的列名匹配

          【讨论】:

            【解决方案6】:

            最好将数据库相关进程包装在transaction 块中。代码sn -p blow是将一组语言播种到Language模型的全过程,

            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
            

            下面的片段是languages.csv文件的一部分,

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

            【讨论】:

              【解决方案7】:

              smarter_csv gem 是专门为此用例创建的:从 CSV 文件中读取数据并快速创建数据库条目。

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

              您可以使用选项 chunk_size 一次读取 N 个 csv 行,然后在内部循环中使用 Resque 生成将创建新记录的作业,而不是立即创建它们 - 这样您可以将生成条目的负载分散到多个工作人员。

              另请参阅: https://github.com/tilo/smarter_csv

              【讨论】:

              • 由于包含了 CSV 类,我觉得最好使用它而不是添加或安装额外的 gem。当然,您并没有建议将新的 gem 添加到应用程序中。添加一系列单独的 gem 非常容易,每个 gem 都有特定的用途,并且在您知道它之前,您的应用程序具有过多的依赖关系。 (我发现自己有意识地避免添加任何宝石。在我的商店中,我们需要证明添加给我们队友的合理性。)
              • @Tass 添加一系列单独的方法也很容易,每个方法都用于特定目的,并且在您知道它之前,您的应用程序具有您必须维护的过多逻辑。如果 gem 工作正常、维护良好、使用很少的资源或可以隔离到相关环境(即生产任务的暂存),那么在我看来总是是使用 gem 的更好选择。 Ruby 和 Rails 都是为了减少代码编写。
              • 我出现以下错误,你知道为什么吗? ActiveModel::UnknownAttributeError: 未知属性 'siren;nom_ent;adresse;complement_adresse;cp_ville;pays;region;departement;activite;date;nb_salaries;nom;prenom;civilite;adr_mail;libele_acti;categorie;tel' 用于交易
              • 我在 rake 任务上试过这个,控制台返回:rake aborted! NoMethodError: nil:NilClass stackoverflow.com/questions/42515043/…的未定义方法“关闭”
              • @Tass 对 CSV 处理进行分块,提高速度并节省内存可能是添加新 gem 的一个很好的理由;)
              【解决方案8】:

              最好使用 CSV::Table 并使用String.encode(universal_newline: true)。它将 CRLF 和 CR 转换为 LF

              【讨论】:

              • 您提出的解决方案是什么?
              【解决方案9】:

              如果你想使用 SmartCSV

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

              这表示每行中的制表符分隔数据"\t",行由新行分隔"\n"

              【讨论】:

                【解决方案10】:
                require 'csv'    
                
                csv_text = File.read('...')
                csv = CSV.parse(csv_text, :headers => true)
                csv.each do |row|
                  Moulding.create!(row.to_hash)
                end
                

                【讨论】:

                • 你可以把它放在一个 Rake 任务中,或者一个控制器动作中,或者任何你喜欢的地方......
                • 效果很好。但是我有一个初学者级别的问题 - 当我尝试浏览 Ruby 和 Rails API 文档中描述的方法时,我无法找到它们(我查看了官方 Ruby 和 Rails 网站,API 文档)。例如。我找不到返回 CSV.parse() 的对象,我没有找到 to_hash() 和 with_indifferent_access() 方法......也许我看错了地方,或者错过了一些关于如何遍历 Ruby & Rails API 文档的基本原则.谁能分享如何阅读 Ruby API 文档的最佳实践?
                • @daveatflow:是的,请参阅下面的答案,它一次读取文件中的一行。
                • @lok​​eshjain2008,它指的是OP的模型。
                • 这种方法效率低下!在巨大的 CSV 文件中,内存使用量猛增。下面那个更好。
                【解决方案11】:

                你可以试试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
                

                如果这是您想要的,您还可以考虑从表中删除自动增量主键并将主键设置为name。或者,如果有一些属性组合形成主键,则将其用作选择器。不需要索引,它只会让它更快。

                【讨论】:

                  【解决方案12】:

                  这会有所帮助。它也有代码示例:

                  http://csv-mapper.rubyforge.org/

                  或者对于执行相同操作的 rake 任务:

                  http://erikonrails.snowedin.net/?p=212

                  【讨论】:

                  猜你喜欢
                  • 2011-11-29
                  • 2017-08-21
                  • 1970-01-01
                  • 2012-02-14
                  • 2016-03-24
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-05-25
                  • 1970-01-01
                  相关资源
                  最近更新 更多