Ruby on Rails - 从CSV文件导入数据

228

我希望能够将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

你能给我一些代码,展示最佳实现方式吗?感谢。

12个回答

419
require 'csv'    

csv_text = File.read('...')
csv = CSV.parse(csv_text, :headers => true)
csv.each do |row|
  Moulding.create!(row.to_hash)
end

3
你可以将其放在 Rake 任务中、控制器操作中或任何你喜欢的地方... - yfeldblum
1
它完美地运行了。然而,我有一个初学者级别的问题——当我尝试在Ruby和Rails API文档中浏览所描述的方法时,我无法在正确的位置找到它们(我在官方的Ruby和Rails网站、API文档上查找过)。例如,我找不到CSV.parse()返回什么对象,我没有找到to_hash()和with_indifferent_access()方法...也许我在错误的地方查找或者错过了一些遍历Ruby和Rails API文档的基本原则。有人能分享一下如何阅读Ruby API文档的最佳实践吗? - Vladimir Kroz
2
@daveatflow:是的,请看下面我的答案,它一次读取文件中的一行。 - Tom De Leu
1
@lokeshjain2008,它指的是原帖中的模型。 - Justin D.
4
这种方法效率低下!在处理巨大的CSV文件时,内存使用量会激增。下面这个更好些。 - unom
显示剩余7条评论

227

以下是简化版本的yfeldblum答案,它更加简单易懂,同时也适用于大文件:

require 'csv'    

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

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

它不会一次性将整个文件保留在内存中,而是逐行读取并为每一行创建一个Moulding对象。


1
这对于管理大文件大小更好,对吗?它是一次读取一行吗? - NotSimon
1
@Simon:确实。它不会一次性将整个文件保存在内存中,而是逐行读取并为每行创建一个模型。 - Tom De Leu
我遇到了这个错误,你知道为什么吗?:ActiveModel :: UnknownAttributeError:Transaction中未知属性'siren; nom_ent; adresse; complement_adresse; cp_ville; pays; region; departement; activite; date; nb_salaries; nom; prenom; civilite; adr_mail; libele_acti; categorie; tel' - nico_lrx
1
@AlphaNico 请提出您的问题。那个错误与此无关,您的模型对象似乎不同步。 - unom
在这种情况下,您如何编写此类的测试用例? - Afolabi Olaoluwa
当我执行这个程序时,出现了 Errno:ENOENT 错误 - 它说找不到文件或目录,但是在错误日志中我可以看到 CSV 文本,所以它读取了文件。但是它特别在这一行失败:CSV.foreach(csv_text, headers: true) do |row|以下是代码: require 'csv' csv_text = File.read('app/assets/csv/plant_table.csv') CSV.foreach(csv_text, headers: true) do |row| Test.create!(row.to_hash) end - cratag

14

smarter_csv宝石是专门为这种用例而创建的:从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

4
由于CSV类已经包含在内,我觉得最好使用它,而不是添加或安装额外的gem。当然,您没有建议将新的gem添加到应用程序中。添加一系列针对特定目的的个别gems很容易,但在不知不觉中,您的应用程序就会有过多的依赖关系。(我发现自己有意避免添加任何gems,在我的工作室里,我们需要向团队成员证明添加的必要性。) - Tass
2
@Tass,添加一系列特定用途的个别方法也很容易,在你不知不觉中你的应用程序就有了过多的逻辑,需要进行维护。如果一个gem可用、维护良好且使用资源少或可以隔离到相关环境(即为生产任务设置暂存区),在我看来,始终使用此gem是更好的选择。Ruby和Rails都是关于编写更少的代码。 - zrisher
我遇到了以下错误,你知道为什么吗?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' for Transaction。 - nico_lrx
我在一个rake任务上尝试了这个,控制台返回:rake中止! NoMethodError:未定义方法“close”用于nil:NilClass http://stackoverflow.com/questions/42515043/unable-to-run-rake-task-with-smarter-csv?noredirect=1#comment72171357_42515043 - Marcos R. Guevara
看起来已经有人回复了 - 只需在您的CSV文件位置前加上 ./ 前缀。 - Tilo
3
将CSV处理进行分块,提高速度并节省内存可能是添加新的gem的充分理由。 - Tilo

5

您可以尝试使用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。或者,如果有一些属性的组合形成了一个主键,请使用该组合作为选择器。不需要索引,它只会使它更快。


4

链接已经失效,请查看我在这里开的一个关于使用rake任务的问题 http://stackoverflow.com/questions/42515043/unable-to-run-rake-task-with-smarter-csv - Marcos R. Guevara

3

最好将与数据库相关的过程放在 transaction 块中。下面的代码片段是将一组语言种子添加到 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
...

1
更好的方法是将其包含在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,模型名称]

1

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

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

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

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

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

参考gem文档了解详情


0

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

注意:

  • 这是一个很好的内部工具,对于客户使用,建议添加保护和净化措施
  • CSV 中的列名必须与数据库架构完全相同,否则无法正常工作
  • 可以通过使用表名获取标题来进一步改进,而不是在文件中定义它们

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

0
使用这个宝石: https://rubygems.org/gems/active_record_importer
class Moulding < ActiveRecord::Base
  acts_as_importable
end

然后,您现在可以使用:

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接