2013-02-04 31 views
5

Tôi cần có khả năng tìm ra dấu phân cách nào đang được sử dụng trong tệp csv (dấu phẩy, dấu cách hoặc dấu chấm phẩy) trong dự án Ruby của tôi. Tôi biết, có một lớp Sniffer trong Python trong mô-đun csv có thể được sử dụng để đoán dấu phân tách của tệp đã cho. Có gì tương tự với Ruby không? Bất kỳ loại trợ giúp hoặc ý tưởng nào được đánh giá cao.Ruby: Làm thế nào tôi có thể phát hiện/đoán thông minh dấu phân cách được sử dụng trong tệp CSV?

+2

Về mặt kỹ thuật, chỉ là một trong những là một tập tin CSV ... –

Trả lời

9

Dường như việc triển khai py chỉ kiểm tra một vài phương ngữ: excel hoặc excel_tab. Vì vậy, việc thực hiện đơn giản của một cái gì đó mà chỉ kiểm tra "," hoặc "\t" là:

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 

Lưu ý: rằng sẽ trả lại đầy đủ delimiter nó tìm thấy, ví dụ ",", do đó, để có được , bạn có thể thay đổi snif[0][0] thành snif[0][0][1].

Ngoài ra, tôi đang sử dụng count(delim) vì nó nhanh hơn một chút, nhưng nếu bạn thêm dấu phân cách gồm hai (hoặc nhiều hơn) ký tự cùng loại như -- thì có thể mỗi lần xuất hiện hai lần (hoặc nhiều hơn) khi cân nặng loại, vì vậy trong trường hợp đó, nó có thể là tốt hơn để sử dụng scan(delim).length.

2

Tôi không biết về bất kỳ triển khai trình thám thính nào trong thư viện CSV có trong Ruby 1.9. Nó sẽ cố gắng tự động phát hiện ra dấu tách hàng, nhưng dấu tách cột được giả định là dấu phẩy theo mặc định.

Một ý tưởng sẽ là thử phân tích một số hàng mẫu (5% tổng số có thể?) Bằng cách sử dụng từng dấu phân cách có thể. Bất kỳ dấu phân tách nào dẫn đến cùng một số cột nhất quán có lẽ là dấu phân cách chính xác.

5

Đây là câu trả lời Gary S. Weaver khi chúng tôi đang sử dụng nó trong quá trình sản xuất. Giải pháp tốt hoạt động tốt.

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