2013-02-04 13 views
5

Devo essere in grado di capire quale delimitatore viene utilizzato in un file CSV (virgola, spazio o punto e virgola) nel mio progetto Ruby. Lo so, esiste una classe Sniffer in Python nel modulo csv che può essere utilizzata per indovinare il delimitatore di un determinato file. C'è qualcosa di simile a questo in Ruby? Ogni tipo di aiuto o idea è molto apprezzato.Ruby: come posso rilevare/indovinare in modo intelligente il delimitatore utilizzato in un file CSV?

+2

Tecnicamente, solo uno di questi è un file CSV ... –

risposta

9

Sembra che l'implementazione py controlli solo alcuni dialetti: excel o excel_tab. Quindi, una semplice implementazione di qualcosa che solo controlla per "," o "\t" è:

COMMON_DELIMITERS = ['","',"\"\t\""] 

def sniff(path) 
    first_line = File.open(path).first 
    return nil unless first_line 
    snif = {} 
    COMMON_DELIMITERS.each {|delim|snif[delim]=first_line.count(delim)} 
    snif = snif.sort {|a,b| b[1]<=>a[1]} 
    snif.size > 0 ? snif[0][0] : nil 
end 

Nota: che sarebbe restituire l'intero delimitatore che trova, ad esempio, ",", quindi per ottenere , è possibile modificare snif[0][0] in snif[0][0][1].

Inoltre, sto usando count(delim) perché è un po 'più veloce, ma se hai aggiunto un delimitatore composto da due (o più) caratteri dello stesso tipo come --, allora potrebbe ogni occorrenza due volte (o più) quando si pesa il tipo, quindi in tal caso, potrebbe essere meglio usare scan(delim).length.

2

Non sono a conoscenza di alcuna implementazione dello sniffer nella libreria CSV inclusa in Ruby 1.9. Proverà a scoprire automaticamente il separatore di riga, ma si presume che il separatore di colonne sia una virgola per impostazione predefinita.

Un'idea sarebbe provare a analizzare un numero di esempio di righe (il 5% del totale forse?) Utilizzando ciascuno dei possibili separatori. Indipendentemente dal separatore, lo stesso numero di colonne è il separatore corretto.

5

Ecco la risposta Gary S. Weaver mentre la stiamo usando in produzione. Buona soluzione che funziona bene.

class ColSepSniffer 
    NoColumnSeparatorFound = Class.new(StandardError) 
    EmptyFile = Class.new(StandardError) 

    COMMON_DELIMITERS = [ 
    '","', 
    '"|"', 
    '";"' 
    ].freeze 

    def initialize(path) 
    @path = path 
    end 

    def self.find(path) 
    new(path: path).find 
    end 

    def find 
    fail EmptyFile unless first 

    if valid? 
     delimiters[0][0][1] 
    else 
     fail NoColumnSeparatorFound 
    end 
    end 

    private 

    def valid? 
    !delimiters.collect(&:last).reduce(:+).zero? 
    end 

    # delimiters #=> [["\"|\"", 54], ["\",\"", 0], ["\";\"", 0]] 
    # delimiters[0] #=> ["\";\"", 54] 
    # delimiters[0][0] #=> "\",\"" 
    # delimiters[0][0][1] #=> ";" 
    def delimiters 
    @delimiters ||= COMMON_DELIMITERS.inject({}, &count).sort(&most_found) 
    end 

    def most_found 
    ->(a, b) { b[1] <=> a[1] } 
    end 

    def count 
    ->(hash, delimiter) { hash[delimiter] = first.count(delimiter); hash } 
    end 

    def first 
    @first ||= file.first 
    end 

    def file 
    @file ||= File.open(@path) 
    end 
end 

Spec

require "spec_helper" 

describe ColSepSniffer do 
    describe ".find" do 
    subject(:find) { described_class.find(path) } 

    let(:path) { "./spec/fixtures/google/products.csv" } 

    context "when , delimiter" do 
     it "returns separator" do 
     expect(find).to eq(',') 
     end 
    end 

    context "when ; delimiter" do 
     let(:path) { "./spec/fixtures/google/products_with_semi_colon_seperator.csv" } 

     it "returns separator" do 
     expect(find).to eq(';') 
     end 
    end 

    context "when | delimiter" do 
     let(:path) { "./spec/fixtures/google/products_with_bar_seperator.csv" } 

     it "returns separator" do 
     expect(find).to eq('|') 
     end 
    end 

    context "when empty file" do 
     it "raises error" do 
     expect(File).to receive(:open) { [] } 
     expect { find }.to raise_error(described_class::EmptyFile) 
     end 
    end 

    context "when no column separator is found" do 
     it "raises error" do 
     expect(File).to receive(:open) { [''] } 
     expect { find }.to raise_error(described_class::NoColumnSeparatorFound) 
     end 
    end 
    end 
end 
Problemi correlati