2009/06/16

ExcelとOpenOffice Calcが出力するCSVファイル

CSVといえば、1行1レコードとして、各フィールドをカンマで区切っているファイル形式として、いまさら説明する必要がないほどによく使われています。 あまりにもポピュラーなのに、ちゃんとした形式はドキュメントが整備されてこなかった経緯があって、rfc4180がやっとInformationalとして発行されているぐらいです。

このファイル形式の問題点は、SJISかUTF-8かというマルチバイト固有のエンコーディングの問題もありますが、やはりデリミタが文字列として入っている場合の処理が面倒になるという点でしょう。 自分でデータを作成する場合にはデータにデリミタを含まないと決める事でawkやら各種スクリプト言語でループ+split()を使い簡単に処理をさせる事ができます。 しかし「デリミタを含まない」という前提が崩れてしまったり、もらったデータにはダブルクォートやらデリミタが含まれていて区切り文字でsplitする事ができない場面もあると思います。

デリミタをデータに含んでいる場合にはrfc4180にも書かれている通り、ダブルクォート'"'で囲む事が一般的です。ちなみにWindows版のExcel, OpenOffice(oocalc)でCSV形式で出力させると次のようになります。

Excel 2007が出力するCSVファイル

no#,dirname,dirname2
1,X11R6,"b,in"

OpenOffice Calc (oocalc) 3.1が出力するCSVファイル

"no#","dirname","dirname2"
1,"X11R6","b,in"

まとめ

"bin"の間にカンマ','を含んでいると、excel、oocalcどちらもダブルクォートで囲みますが、”X11R6"のようにexcelではダブルクォート、カンマを含まないデータはそのまま、oocalcでは数字以外はダブルクォートで囲むように処理されています。

CSVで保存する目的は他のソフトウェアやマシンにデータを渡すことが多いと思いますが、処理する先では元のデータに戻すために、ダブルクォートで囲まれていないカンマでレコードを分割し、ダブルクォート2つを1つに置換してから、最終的にはフィールドを囲むダブルクォートを取り去る必要があります。

ありふれたawkを使う方法は微妙に面倒なので、あまり普通のサーバーには入っていないかもしれないrubyを使って切り分けるcsv_split()を作ってみました

#!/usr/bin/ruby

def csv_split(reg_char, line)
  res = []
  quote_flag = false
  prev = nil
  for item in line.split(reg_char)
    if (item.count('"') % 2) == 1
      quote_flag = (not quote_flag)
    end

    ## yield during quotation
    if quote_flag 
      if prev == nil
        prev = item
      else
        prev = "#{prev},#{item}"
      end
      next
    end

    ## return the result
    if not quote_flag and prev != nil
      res << escape_result("#{prev},#{item}")
      prev = nil
    else
      res << escape_result(item)
    end
  end

  yield res
end

def escape_result(item)
  res = item
  res = res.gsub(Regexp.new('""'),'"').gsub(Regexp.new('^"'), '').gsub(Regexp.new('"$'), '')
  return res
end

## main ##
open(ARGV[0]).each do |line|
  line.chop!()
  csv_split(/,/, line) do |f1,f2,f3|
    if header == false
      header = true
      next
    end
    print "#{f1}\t#{f2}\t#{f3}\n"
  end
end

0 件のコメント: