Подтвердить что ты не робот

Как заставить одно поле в Ruby CSV файле обернуться двойными кавычками?

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

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, который выводит двойные кавычки, обертывая все поля, как ожидалось, но клиенту это не понравилось, что я ожидал также. Таким образом, это тоже не сработало.

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

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

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

4b9b3361

Ответ 1

Ну, есть способ сделать это, но это было не так чисто, как я надеялся, что 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

Ответ 2

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

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

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

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

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

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

csv.gsub!(/\0/, '')

Ответ 3

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

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(",")}}

Ответ 4

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

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

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

Ответ 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"

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

Ответ 6

CSV немного изменился в Ruby 2.1, как упоминалось в @jwadsack, однако здесь рабочая версия @the-tin-man MyCSV. Бит изменен, вы устанавливаете принудительные_quote_fields через параметры.

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 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 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