使用不同的编码和库解析CSV文件

abb*_*jam 5 ruby csv parsing google-adwords

尽管有关主题的SO线程很多,但我在解析CSV方面遇到了麻烦.这是从Adwords关键字规划师下载的.csv文件.以前,Adwords可以选择将数据导出为"普通CSV"(可以使用Ruby CSV库进行解析),现在选项可以是Adwords CSV或Excel CSV.这些格式中的两个都会导致此问题(由终端会话说明):

file = File.open('public/uploads/testfile.csv')
 => #<File:public/uploads/testfile.csv> 

file.read.encoding
 => #<Encoding:UTF-8> 

require 'csv'
 => true 

CSV.foreach(file) { |row| puts row }
ArgumentError: invalid byte sequence in UTF-8
Run Code Online (Sandbox Code Playgroud)

让我们改变编码,看看是否有帮助:

file.close
 => nil 

file = File.open("public/uploads/testfile.csv", "r:ISO-8859-1")
 => #<File:public/uploads/testfile.csv> 

file.read.encoding 
=> #<Encoding:ISO-8859-1> 

CSV.foreach(file) { |row| puts row }
ArgumentError: invalid byte sequence in UTF-8
Run Code Online (Sandbox Code Playgroud)

让我们尝试使用不同的CSV库:

require 'smarter_csv'
 => true 

file.close
 => nil 

file = SmarterCSV.process('public/uploads/testfile.csv')
ArgumentError: invalid byte sequence in UTF-8
Run Code Online (Sandbox Code Playgroud)

这是一个不赢的局面吗?我是否必须滚动自己的CSV解析器?

我正在使用Ruby 1.9.3p374.谢谢!

更新1:

使用评论中的建议,这是当前版本:

file_contents = File.open("public/uploads/new-format/testfile-adwords.csv", 'rb').read

require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
  file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
  file_contents.encode!('UTF-8', 'UTF-16')
else
  ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
  file_contents = ic.iconv(file_contents)
end

file_contents.gsub!(/\0/, '') #needed because otherwise, I get "string contains null byte (ArgumentError)"

CSV.foreach(file_contents, :headers => true, :header_converters => :symbol) do |row|
  puts row
end
Run Code Online (Sandbox Code Playgroud)

这不起作用 - 现在我得到一个"文件名太长"的错误.

mat*_*att 17

查看有问题文件:

 $ curl -s http://jamesabbottdd.com/examples/testfile.csv | xxd | head -n3
0000000: fffe 4300 6100 6d00 7000 6100 6900 6700  ..C.a.m.p.a.i.g.
0000010: 6e00 0900 4300 7500 7200 7200 6500 6e00  n...C.u.r.r.e.n.
0000020: 6300 7900 0900 4200 7500 6400 6700 6500  c.y...B.u.d.g.e.
Run Code Online (Sandbox Code Playgroud)

开头字节顺序标记ffee表明文件编码是小端UTF-16,而00每个其他位置的字节都支持这一点.

这表明您应该能够这样做:

CSV.foreach('./testfile.csv', :encoding => 'utf-16le') do |row| ...
Run Code Online (Sandbox Code Playgroud)

但是,这让我invalid byte sequence in UTF-16LE (ArgumentError)来自CSV库.我认为这是由于IO#在使用CSV调用 BOM时因某种原因仅返回单个字节,从而导致UTF-16无效.

通过使用bom|utf-16-le编码,您可以获取CSV以剥离BOM :

CSV.foreach('./testfile.csv', :encoding => 'bom|utf-16le') do |row| ...
Run Code Online (Sandbox Code Playgroud)

您可能更喜欢将字符串转换为更熟悉的编码,在这种情况下,您可以执行以下操作:

CSV.foreach('./testfile.csv', :encoding => 'utf-16le:utf-8') do |row| ...
Run Code Online (Sandbox Code Playgroud)

这两个似乎都可行.