Как заставить одно поле в выводе CSV в Ruby заключить в двойные кавычки? - PullRequest
20 голосов
/ 31 января 2011

Я генерирую некоторый вывод CSV, используя встроенный в Ruby CSV. Все работает нормально, но клиент хочет, чтобы в поле имени в выводе были заключены двойные кавычки, чтобы вывод был похож на входной файл. Например, вход выглядит примерно так:

1,1.1.1.1,"Firstname Lastname",more,fields
2,2.2.2.2,"Firstname Lastname, Jr.",more,fields

Вывод CSV, который является правильным, выглядит следующим образом:

1,1.1.1.1,Firstname Lastname,more,fields
2,2.2.2.2,"Firstname Lastname, Jr.",more,fields

Я знаю, что CSV делает правильные вещи, не заключая в двойные кавычки третье поле только потому, что в нем есть встроенные пробелы, и оборачивая поле двойными кавычками, когда в нем есть запятая. Чтобы помочь клиенту почувствовать тепло и нечеткость, я бы хотел, чтобы CSV всегда заключал в кавычки третье поле.

Я попытался обернуть поле в двойные кавычки в моем методе to_a, который создает поле "Firstname Lastname", передаваемое в CSV, но CSV посмеялся над моей попыткой маленького человека и вывел """Firstname Lastname""". Это правильная вещь, потому что она избегает двойных кавычек, так что это не сработало.

Затем я попытался установить CSV :force_quotes => true в методе open, который выводит двойные кавычки, оборачивая все поля, как и ожидалось, но клиенту это не понравилось, что я и ожидал. Так что это тоже не сработало.

Я просмотрел документы таблиц и строк, и ничего не дало мне доступа к методу «генерировать поле строки» или способу установить флаг «для поля n всегда использовать квотирование».

Я собираюсь погрузиться в источник, чтобы увидеть, есть ли какие-то сверхсекретные твики, или есть ли способ обезопасить CSV и согнуть его, чтобы выполнить мою волю, но подумал, есть ли у кого-то какие-то специальные знания столкнуться с этим раньше.

И, да, я знаю, что могу прокрутить свой собственный выход CSV, но я предпочитаю не изобретать хорошо проверенные колеса. И я также знаю о FasterCSV; Теперь это часть Ruby 1.9.2, который я использую, поэтому явное использование FasterCSV не дает мне ничего особенного. Кроме того, я не использую Rails и не собираюсь переписывать его в Rails, поэтому, если у вас нет симпатичного способа реализовать его с помощью небольшого подмножества Rails, не беспокойтесь. Я опущу все рекомендации по использованию любого из этих способов только потому, что вы не удосужились прочитать это далеко.

Ответы [ 6 ]

9 голосов
/ 07 февраля 2011

Ну, есть способ сделать это, но он не был таким чистым, как я надеялся, что код CSV может позволить.

Мне пришлось создать подкласс CSV, затем переопределить метод CSV::Row.<<= и добавить еще один метод forced_quote_fields=, чтобы можно было определить поля, для которых требуется принудительное цитирование, плюс извлечь две лямбды из других методов.По крайней мере, это работает для того, что я хочу:

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

Это код.Вызов:

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

приводит к:

# >> Ruby version:   1.9.2
# >> Quoting fields: 1
# >> 
# >> 1,"2",3
# >> 2,"two too",3
# >> 3,"two, too",3
5 голосов
/ 30 мая 2011

Это сообщение старое, но я не могу поверить, что никто не подумал об этом.

Почему бы не сделать:

csv = CSV.generate :quote_char => "\0" do |csv|

, где \ 0 - нулевой символ, затем просто добавьтекавычки для каждого поля, где они необходимы:

csv << [product.upc, "\"" + product.name + "\"" # ...

Затем в конце вы можете сделать

csv.gsub!(/\0/, '')
4 голосов
/ 10 января 2013

Я сомневаюсь, что это поможет клиенту чувствовать тепло и нечеткость после всего этого времени, но, похоже, это сработает:

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(",")}}
0 голосов
/ 13 декабря 2016

CSV немного изменился в Ruby 2.1, как упомянуто @jwadsack, однако вот рабочая версия MyCSV @ the-tin-man. Немного изменив, вы устанавливаете значения полюсных_применений с помощью опций.

MyCSV.generate(forced_quote_fields: [1]) do |_csv|...

Модифицированный код

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

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

    self  # for chaining
  end

  def init_separators(options)
    # store the selected separators
    @col_sep    = options.delete(:col_sep).to_s.encode(@encoding)
    @row_sep    = options.delete(:row_sep)  # encode after resolving :auto
    @quote_char = options.delete(:quote_char).to_s.encode(@encoding)
    @forced_quote_fields = options.delete(:forced_quote_fields) || []

    if @quote_char.length != 1
      raise ArgumentError, ":quote_char has to be a single character String"
    end

    #
    # automatically discover row separator when requested
    # (not fully encoding safe)
    #
    if @row_sep == :auto
      if [ARGF, STDIN, STDOUT, STDERR].include?(@io) or
         (defined?(Zlib) and @io.class == Zlib::GzipWriter)
        @row_sep = $INPUT_RECORD_SEPARATOR
      else
        begin
          #
          # remember where we were (pos() will raise an exception if @io is pipe
          # or not opened for reading)
          #
          saved_pos = @io.pos
          while @row_sep == :auto
            #
            # if we run out of data, it's probably a single line
            # (ensure will set default value)
            #
            break unless sample = @io.gets(nil, 1024)
            # extend sample if we're unsure of the line ending
            if sample.end_with? encode_str("\r")
              sample << (@io.gets(nil, 1) || "")
            end

            # try to find a standard separator
            if sample =~ encode_re("\r\n?|\n")
              @row_sep = $&
              break
            end
          end

          # tricky seek() clone to work around GzipReader's lack of seek()
          @io.rewind
          # reset back to the remembered position
          while saved_pos > 1024  # avoid loading a lot of data into memory
            @io.read(1024)
            saved_pos -= 1024
          end
          @io.read(saved_pos) if saved_pos.nonzero?
        rescue IOError         # not opened for reading
          # do nothing:  ensure will set default
        rescue NoMethodError   # Zlib::GzipWriter doesn't have some IO methods
          # do nothing:  ensure will set default
        rescue SystemCallError # pipe
          # do nothing:  ensure will set default
        ensure
          #
          # set default if we failed to detect
          # (stream not opened for reading, a pipe, or a single line of data)
          #
          @row_sep = $INPUT_RECORD_SEPARATOR if @row_sep == :auto
        end
      end
    end
    @row_sep = @row_sep.to_s.encode(@encoding)

    # establish quoting rules
    @force_quotes   = options.delete(:force_quotes)
    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)

    @quote         = if @force_quotes
      do_quote
    else
      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
    end
  end
end
0 голосов
/ 27 марта 2015

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"

Недостатком является то, что первое целочисленное значение отображается в виде строки, которая меняет вещи при импорте в Excel.

0 голосов
/ 01 февраля 2011

Не похоже, что есть какой-то способ сделать это с существующей реализацией CSV, за исключением ее исправления / переписывания обезьяной.

Однако, если вы имеете полный контроль над исходными данными, вы можете сделать это:

  1. Добавить пользовательскую строку , включая запятую (т. Е. Ту, которая никогда не будет естественным образом найдена в данных) в конец поля, о котором идет речь, для каждой строки; может быть что-то вроде " FORCE_COMMAS, ".
  2. Создать выходной сигнал CSV.
  3. Теперь, когда у вас есть вывод CSV с кавычками в каждой строке вашего поля, удалите пользовательскую строку: csv.gsub!(/FORCE_COMMAS,/, "")
  4. Клиент чувствует себя тепло и нечетко.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...