【发布时间】:2020-05-19 10:27:24
【问题描述】:
我遇到了几个问题,我们的董事会成员上传的 CSV 文件由于格式不一致而无法正确解析:逗号分隔、分号分隔、制表符分隔...通常他们甚至不知道哪个已使用分隔符,因为 Excel / LibreOffice Calc 在导出为 CSV 时未指定它。
【问题讨论】:
我遇到了几个问题,我们的董事会成员上传的 CSV 文件由于格式不一致而无法正确解析:逗号分隔、分号分隔、制表符分隔...通常他们甚至不知道哪个已使用分隔符,因为 Excel / LibreOffice Calc 在导出为 CSV 时未指定它。
【问题讨论】:
此函数假定 CSV 至少有 4 列。它在文件的第一行(= 通常是标题,不太可能有额外的逗号或奇怪的字符)中查找分隔符和非分隔符的交替,然后返回最常见的分隔符。
def self.detect_separator(file)
firstline = File.open(file, &:readline)
if firstline
separators = ",;\t|#"
non_sep = "[^" + separators + "]+"
sep = "([" + separators + "])"
reg = Regexp.new(non_sep + sep + non_sep + sep + non_sep + sep + non_sep + sep)
m = firstline.match(reg)
if m
four_separators = m[1..-1].join('') # this line should have four separators but may have less if the data is less conclusive
detected_separator = separators.split('').map {|x| [four_separators.count(x),x]}.max
return detected_separator[1] if detected_separator
end
end
nil
end
【讨论】:
"[^" + separators + "]+" 通常写成 "[^#{separators}]+" (ruby 倾向于插值而不是串联) 2) 鉴于这是一个 Regexp,你可以直接在里面插值non_sep = /[^#{separators}]+/ 3) | 在正则表达式中有意义,所以你可能想逃避它。
让我们从构建一个可能的 CSV 文件开始。
arr = [
["abc", "d;ef", "hi;j", "k;l;mnp"],
["efg", "i;jk", "mn;p", "q;r;stu"],
["tuv", "w;xy", "zg;b", "c;d;e;f"]
]
FName = 't.csv'
require 'csv'
我们将使用逗号分隔列1。
CSV.open(FName, mode='w', col_sep: ',') { |csv| arr.each { |s| csv << s } }
让我们看看我们创建的文件。
puts File.read(FName)
# abc,d;ef,hi;j,k;l;mnp
# efg,i;jk,mn;p,q;r;stu
# tuv,w;xy,zg;b,c;d;e;f
现在让我们读取这个文件,首先用逗号作为列分隔符,然后用分号作为分隔符。
arr_comma = CSV.read(FName, col_sep: ',')
#=> [["abc", "d;ef", "hi;j", "k;l;mnp"],
# ["efg", "i;jk", "mn;p", "q;r;stu"],
# ["tuv", "w;xy", "zg;b", "c;d;e;f"]]
arr_semicolon = CSV.read(FName, col_sep: ';')
#=> [["abc,d", "ef,hi", "j,k", "l", "mnp"],
# ["efg,i", "jk,mn", "p,q", "r", "stu"],
# ["tuv,w", "xy,zg", "b,c", "d", "e", "f"]]
正如预期的那样,arr_comma 是一个由三个 4 元素数组组成的数组。相比之下,arr_semicolon 包含两个 5 元素数组和一个 6 元素数组。假设缺少的字段包含空字符串(例如,...ef;;gh;...),这告诉我们文件与用作列分隔符的逗号一致,但不是分号。
我们可以编写一个小方法来检查每一行对于给定的列分隔符是否具有相同的列数。
def sep_check(filename, sep)
CSV.read(FName, col_sep: sep).map(&:size).uniq.size == 1
end
sep_check(FName, ',')
#=> true
sep_check(FName, ';')
#=> false
请注意,CSV 行可能包含带引号的字符串。如果带引号的字符串包含列分隔符,则它们不会被视为列分隔符。这是一个例子。
arr = [
["abc", "d;ef", "h\"q,r\"i;j", "k;l;mnp"],
["efg", "i;jk", "mn;p", "q;r;stu"],
["tuv", "w;xy", "zg;b", "c;d;e;f"]
]
CSV.open(FName, mode='w', col_sep: ',') { |csv| arr.each { |s| csv << s } }
puts File.read(FName)
# abc,d;ef,"h""q,r""i;j",k;l;mnp
# efg,i;jk,mn;p,q;r;stu
# tuv,w;xy,zg;b,c;d;e;f
sep_check(FName, ',')
#=> true
sep_check(FName, ';')
#=> CSV::MalformedCSVError (Illegal quoting in line 1.)
这是另一个将分号排除在列分隔符之外的条件,而不是逗号。
1.包括, col_sep: ',' 是可选的,因为col_sep 的默认值是逗号。
【讨论】: