rails ruby csv

ruby - rails csv:: row



¿Cómo fuerzo que un campo en la salida CSV de Ruby se ajuste con comillas dobles? (6)

Estoy generando una salida CSV usando el CSV incorporado de Ruby. Todo funciona bien, pero el cliente desea que el campo de nombre en la salida incluya comillas dobles para que la salida se vea como el archivo de entrada. Por ejemplo, la entrada se ve algo como esto:

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

La salida de CSV, que es correcta, se ve así:

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

Sé que CSV está haciendo lo correcto al no citar el tercer campo por el simple hecho de que tiene espacios en blanco incrustados, y envolver el campo con comillas dobles cuando tiene la coma incrustada. Lo que me gustaría hacer, para ayudar al cliente a sentirse cálido y confuso, es decirle a CSV que siempre doble comillas en el tercer campo.

Intenté envolver el campo con comillas dobles en mi método to_a , lo que crea un campo "Firstname Lastname" se pasa a CSV, pero CSV se rió de mi intento y salida """Firstname Lastname""" . Eso es lo que se debe hacer correctamente porque se escapa de las comillas dobles, por lo que no funcionó.

Luego intenté establecer CSV :force_quotes => true en el método open , que :force_quotes => true comillas dobles en todos los campos como se esperaba, pero al cliente no le gustó eso, lo cual también esperaba. Entonces, eso tampoco funcionó.

He revisado los documentos de la tabla y la fila y no parece haber nada que me permita acceder al método "generar un campo de cadena", o una forma de establecer un indicador "para el campo n siempre usar comillas".

Estoy a punto de sumergirme en la fuente para ver si hay algunos ajustes súper secretos, o si hay una manera de parchar el mono CSV y doblarlo para hacer mi voluntad, pero me pregunté si alguien tenía algún conocimiento especial o se había topado con esto. antes de.

Y, sí, sé que podría rodar mi propia salida CSV, pero prefiero no reinventar ruedas bien probadas. Y, también estoy al tanto de FasterCSV; Ahora es parte de Ruby 1.9.2, que estoy usando, así que usar FasterCSV explícitamente no me ofrece nada especial. Además, no estoy usando Rails y no tengo ninguna intención de reescribirlo en Rails, así que a menos que tengas una forma linda de implementarlo usando un pequeño subconjunto de Rails, no te preocupes. Abajo voté cualquier recomendación para usar cualquiera de esas formas solo porque no te molestaste en leer hasta aquí.


CSV ha cambiado un poco en Ruby 2.1 como lo menciona @jwadsack, sin embargo, aquí hay una versión de trabajo de @ the-tin-man-man MyCSV. Poco modificado, establece los forzados_campos_campos mediante opciones.

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

El código modificado

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


Bueno, hay una forma de hacerlo pero no estaba tan limpia como esperaba que el código CSV pudiera permitir.

Tuve que subclasificar CSV, luego anular CSV::Row.<<= method y agregar otro método forced_quote_fields= para hacer posible definir los campos en los que quiero forzar el presupuesto, además de extraer dos lambdas de otros métodos. Al menos funciona para lo que quiero:

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

Ese es el código. Llamándolo:

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

resultados en:

# >> Ruby version: 1.9.2 # >> Quoting fields: 1 # >> # >> 1,"2",3 # >> 2,"two too",3 # >> 3,"two, too",3


Dudo que esto ayude al cliente a sentirse cálido y confuso después de todo este tiempo, pero esto parece funcionar:

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


Esta publicación es antigua, pero no puedo creer que nadie haya pensado en esto.

Por que no hacer

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

donde / 0 es un carácter nulo, luego simplemente agregue comillas a cada campo donde se necesiten:

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

Entonces al final puedes hacer una

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


No parece que haya ninguna manera de hacer esto con la implementación de CSV existente, a menos que sea parcheado / reescrito.

Sin embargo, suponiendo que tenga control total sobre los datos de origen, podría hacer esto:

  1. Agregue una cadena personalizada que incluya una coma (es decir, una que nunca se encontraría de forma natural en los datos) al final del campo en cuestión para cada fila; tal vez algo como " FORCE_COMMAS ".
  2. Generar la salida CSV.
  3. Ahora que tiene una salida CSV con comillas en cada fila para su campo, elimine la cadena personalizada: csv.gsub!(/FORCE_COMMAS,/, "")
  4. El cliente se siente cálido y confuso.

CSV tiene una opción force_quotes que lo obligará a citar todos los campos (puede que no haya estado allí cuando publicaste esto originalmente). Me doy cuenta de que esto no es exactamente lo que propusiste, pero es menos parches de mono.

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"

El inconveniente es que el primer valor entero termina listado como una cadena, que cambia las cosas cuando se importa en Excel.