如何在Ruby的CSV模块中“观察”流?

时间:2019-11-19 07:51:23

标签: ruby export-to-csv

我正在编写一个接受CSV文件的类,对其进行转换,然后将新数据写出。

module Transformer
  class Base
    def initialize(file)
      @file = file
    end

    def original_data(&block)
      opts = { headers: true }
      CSV.open(file, 'rb', opts, &block)
    end

    def transformer
      # complex manipulations here like modifying columns, picking only certain
      # columns to put into new_data, etc but simplified to `+10` to keep
      # example concise
      -> { |row| new_data << row['some_header'] + 10 }
    end

    def transformed_data
      self.original_data(self.transformer) 
    end

    def write_new_data
      CSV.open('new_file.csv', 'wb', opts) do |new_data|
        transformed_data
      end
    end
  end
end

我想做的是:

  • 查看转换后的数据而不将其写出(这样我就可以测试它是否正确转换了数据,并且我不需要立即将其写入文件:也许我想这样做在写出之前进行更多的操作)
  • 不要一次处理所有文件,因此无论原始数据大小如何,它都可以工作
  • 将此作为具有空transformer的基类,以便实例仅需要实现自己的转换器,而读写行为则由基类给出。

但是显然以上方法不起作用,因为我在new_data中实际上没有引用transformer

我如何优雅地做到这一点?

1 个答案:

答案 0 :(得分:1)

我可以根据您的需求和个人喜好推荐两种方法之一。

为清楚起见,我特意将代码精简到最低限度(没有包装类)。

1。简单的读-修改-写循环

由于您不想对文件进行处理,因此请使用CSV::Foreach。例如,对于快速调试会话,请执行以下操作:

CSV.foreach "source.csv", headers: true do |row|
  row["name"] = row["name"].upcase
  row["new column"] = "new value"
  p row
end

如果您希望在同一迭代中写入文件:

require 'csv'

csv_options = { headers: true }

# Open the target file for writing
CSV.open("target.csv", "wb") do |target|
  # Add a header
  target << %w[new header column names]

  # Iterate over the source CSV rows
  CSV.foreach "source.csv", **csv_options do |row|
    # Mutate and add columns
    row["name"] = row["name"].upcase
    row["new column"] = "new value"

    # Push the new row to the target file
    target << row
  end
end

2。使用CSV::Converters

内置的功能可能会有所帮助-CSV::Converters-(请参阅CSV::New文档中的:converters定义)

require 'csv'

# Register a converter in the options hash
csv_options = { headers: true, converters: [:stripper] }

# Define a converter
CSV::Converters[:stripper] = lambda do |value, field|
  value ? value.to_s.strip : value
end

CSV.open("target.csv", "wb") do |target|
  # same as above

  CSV.foreach "source.csv", **csv_options do |row|
    # same as above - input data will already be converted
    # you can do additional things here if needed
  end
end

3。输入和输出与转换器类分开

根据您的评论,并且由于您希望最大程度地减少I / O和迭代次数,因此可能有兴趣从变压器的职责中提取读/写操作。像这样的东西。

require 'csv'

class NameCapitalizer
  def self.call(row)
    row["name"] = row["name"].upcase
  end
end

class EmailRemover
  def self.call(row)
    row.delete 'email'
  end
end

csv_options = { headers: true }
converters = [NameCapitalizer, EmailRemover]

CSV.open("target.csv", "wb") do |target|
  CSV.foreach "source.csv", **csv_options do |row|
    converters.each { |c| c.call row }
    target << row
  end
end

请注意,如果更改了标题,上面的代码仍然无法处理标题。您可能必须保留最后一行(在所有转换之后),并将其#headers放在输出CSV之前。

可能还有很多其他方法,但是Ruby中的CSV类没有最干净的接口,因此我尝试使处理它的代码保持尽可能的简单。