如何强制Ruby的CSV输出中的一个字段用双引号括起来?

the*_*Man 21 ruby csv

我正在使用Ruby的内置CSV生成一些CSV输出.一切正常,但客户希望输出中的name字段包含双引号,因此输出看起来像输入文件.例如,输入看起来像这样:

1,1.1.1.1,"Firstname Lastname",more,fields
2,2.2.2.2,"Firstname Lastname, Jr.",more,fields
Run Code Online (Sandbox Code Playgroud)

CSV的输出是正确的,如下所示:

1,1.1.1.1,Firstname Lastname,more,fields
2,2.2.2.2,"Firstname Lastname, Jr.",more,fields
Run Code Online (Sandbox Code Playgroud)

我知道CSV正在做正确的事情,因为它没有引用第三个字段只是因为它嵌入了空格,并且当它有嵌入的逗号时用双引号包装字段.为了帮助客户感到温暖和模糊,我想做的是告诉CSV总是双引号第三个字段.

我尝试在我的to_a方法中用双引号包装该字段,这会创建一个"Firstname Lastname"传递给CSV 的字段,但是CSV嘲笑我的微不足道的尝试和输出"""Firstname Lastname""".这是正确的事情,因为它正在逃避双引号,所以这不起作用.

然后我尝试:force_quotes => trueopen方法中设置CSV ,输出双引号按预期包装所有字段,但客户不喜欢,我也期望.所以,这也没有用.

我查看了Table和Row文档,似乎没有任何内容可以让我访问"生成字符串字段"方法,或者设置"for field n always use quoting"标志的方法.

我即将深入了解消息来源,看看是否有一些超级秘密的调整,或者是否有办法修补CSV并弯曲它以实现我的意愿,但想知道是否有人有一些特殊知识或者遇到过这个问题之前.

而且,是的,我知道我可以滚动自己的CSV输出,但我更喜欢不重新发明经过良好测试的轮子.而且,我也知道FasterCSV; 这是我正在使用的Ruby 1.9.2的一部分,因此明确使用FasterCSV并没有什么特别之处.另外,我没有使用Rails并且无意在Rails中重写它,所以除非你有一个可爱的方式使用一小部分Rails实现它,所以不要打扰.我会向你推荐任何使用这些方法的建议,因为你没有费心去读这篇文章.

the*_*Man 9

好吧,有一种方法可以做到,但它并不像我希望CSV代码允许的那样干净.

我必须继承CSV,然后重写CSV::Row.<<=方法并添加另一个方法forced_quote_fields=,以便可以定义我想强制引用的字段,再从其他方法中拉出两个lambdas.至少它适用于我想要的东西:

require 'csv'

class MyCSV < CSV
    def <<(row)
      # make sure headers have been assigned
      if header_row? and [Array, String].include? @use_headers.class
        parse_headers  # won't read data for Array or String
        self << @headers if @write_headers
      end

      # handle CSV::Row objects and Hashes
      row = case row
        when self.class::Row then row.fields
        when Hash            then @headers.map { |header| row[header] }
        else                      row
      end

      @headers = row if header_row?
      @lineno  += 1

      @do_quote ||= lambda do |field|
        field         = String(field)
        encoded_quote = @quote_char.encode(field.encoding)
        encoded_quote                                +
        field.gsub(encoded_quote, encoded_quote * 2) +
        encoded_quote
      end

      @quotable_chars      ||= encode_str("\r\n", @col_sep, @quote_char)
      @forced_quote_fields ||= []

      @my_quote_lambda ||= lambda do |field, index|
        if field.nil?  # represent +nil+ fields as empty unquoted fields
          ""
        else
          field = String(field)  # Stringify fields
          # represent empty fields as empty quoted fields
          if (
            field.empty?                          or
            field.count(@quotable_chars).nonzero? or
            @forced_quote_fields.include?(index)
          )
            @do_quote.call(field)
          else
            field  # unquoted field
          end
        end
      end

      output = row.map.with_index(&@my_quote_lambda).join(@col_sep) + @row_sep  # quote and separate
      if (
        @io.is_a?(StringIO)             and
        output.encoding != raw_encoding and
        (compatible_encoding = Encoding.compatible?(@io.string, output))
      )
        @io = StringIO.new(@io.string.force_encoding(compatible_encoding))
        @io.seek(0, IO::SEEK_END)
      end
      @io << output

      self  # for chaining
    end
    alias_method :add_row, :<<
    alias_method :puts,    :<<

    def forced_quote_fields=(indexes=[])
      @forced_quote_fields = indexes
    end
end
Run Code Online (Sandbox Code Playgroud)

这就是代码.打电话给:

data = [ 
  %w[1 2 3], 
  [ 2, 'two too',  3 ], 
  [ 3, 'two, too', 3 ] 
]

quote_fields = [1]

puts "Ruby version:   #{ RUBY_VERSION }"
puts "Quoting fields: #{ quote_fields.join(', ') }", "\n"

csv = MyCSV.generate do |_csv|
  _csv.forced_quote_fields = quote_fields
  data.each do |d| 
    _csv << d
  end
end

puts csv
Run Code Online (Sandbox Code Playgroud)

结果是:

# >> Ruby version:   1.9.2
# >> Quoting fields: 1
# >> 
# >> 1,"2",3
# >> 2,"two too",3
# >> 3,"two, too",3
Run Code Online (Sandbox Code Playgroud)


Tom*_*hka 6

这篇文章很老,但我无法相信没有人想到这一点.

为什么不这样做:

csv = CSV.generate :quote_char => "\0" do |csv|
Run Code Online (Sandbox Code Playgroud)

其中\ 0是一个空字符,然后只需在每个需要它们的字段中添加引号:

csv << [product.upc, "\"" + product.name + "\"" # ...
Run Code Online (Sandbox Code Playgroud)

然后在最后你可以做一个

csv.gsub!(/\0/, '')
Run Code Online (Sandbox Code Playgroud)

  • 除了其他答案已经提出了这种解决方案这一事实之外,但是如果您要处理庞大的CSV文件,尤其是必须对其进行缓冲或重新加载以解决黑客问题,则对CSV文件进行后处理输出会很痛苦。CSV是一种难看的格式。最好修复根本原因,即使这意味着触摸或覆盖某些代码,也可以使该过程正常运行。 (2认同)

ste*_*lag 5

我怀疑这是否会帮助客户在这么长时间后感觉温暖和模糊,但这似乎有效:

require 'csv'
#prepare a lambda which converts field with index 2 
quote_col2 = lambda do |field, fieldinfo|
  # fieldinfo has a line- ,header- and index-method
  if fieldinfo.index == 2 && !field.start_with?('"') then 
    '"' + field + '"'
  else
    field
  end
end

# specify above lambda as one of the converters
csv =  CSV.read("test1.csv", :converters => [quote_col2])
p csv 
# => [["aaa", "bbb", "\"ccc\"", "ddd"], ["fff", "ggg", "\"hhh\"", "iii"]]
File.open("test1.txt","w"){|out| csv.each{|line|out.puts line.join(",")}}
Run Code Online (Sandbox Code Playgroud)


jwa*_*ack 5

CSV有一个force_quotes选项将强制它引用所有字段(当您最初发布此内容时它可能不存在)。我意识到这并不完全是你所提议的,但它不是猴子修补。

2.1.0 :008 > puts CSV.generate_line [1,'1.1.1.1','Firstname Lastname','more','fields']
1,1.1.1.1,Firstname Lastname,more,fields
2.1.0 :009 > puts CSV.generate_line [1,'1.1.1.1','Firstname Lastname','more','fields'], force_quotes: true
"1","1.1.1.1","Firstname Lastname","more","fields"
Run Code Online (Sandbox Code Playgroud)

缺点是第一个整数值最终以字符串形式列出,这在导入 Excel 时会发生变化。

  • “...第一个整数值最终作为字符串列出...”,正如您所说,将值更改为字符串。另外,我需要一个特定的字段*总是*用引号引起来,而不是每个字段。 (3认同)