2013-02-04 2 views
5

Ich muss herausfinden, welches Trennzeichen in einer CSV-Datei (Komma, Leerzeichen oder Semikolon) in meinem Ruby-Projekt verwendet wird. Ich weiß, es gibt eine Sniffer-Klasse in Python im csv-Modul, mit der man den Begrenzer einer bestimmten Datei erraten kann. Gibt es etwas Ähnliches in Ruby? Jede Art von Hilfe oder Idee wird sehr geschätzt.Ruby: Wie kann ich das in einer CSV-Datei verwendete Trennzeichen erkennen/intelligent erraten?

+2

Technisch nur einer von denen ist eine CSV-Datei ... –

Antwort

9

Sieht aus wie die Py-Implementierung überprüft nur ein paar Dialekte: Excel oder Excel_Tab. So ist eine einfache Implementierung von etwas, das nur überprüft für "," oder "\t" ist:

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 

Hinweis: das wäre den vollständigen Begrenzer zurückkehrt es findet, zum Beispiel ",", um , zu erhalten, könnten Sie die snif[0][0] zu snif[0][0][1] ändern.

Auch ich benutze count(delim), weil es ein wenig schneller ist, aber wenn Sie ein Trennzeichen, das aus zwei (oder mehr) Zeichen des gleichen Typs wie -- zusammengesetzt ist, dann könnte es jedes Auftreten zweimal (oder mehr) beim Wiegen des Typs, so dass es in diesem Fall besser sein kann, scan(delim).length zu verwenden.

2

Mir ist keine Sniffer-Implementierung in der in Ruby 1.9 enthaltenen CSV-Bibliothek bekannt. Es wird versucht, das Zeilentrennzeichen automatisch zu erkennen, aber das Spaltentrennzeichen wird standardmäßig als Komma angenommen.

Eine Idee wäre es, versuchen eine Probe Anzahl von Zeilen (5% der Gesamtmenge vielleicht?) Parsing mit jedem der möglichen Trennzeichen. Unabhängig davon, welcher Separator zu der gleichen Anzahl von Spalten führt, ist wahrscheinlich der richtige Separator.

5

Hier ist Gary S. Weaver Antwort, wie wir es in der Produktion verwenden. Gute Lösung, die gut funktioniert.

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