アルゴリズム/CSVファイルデータの読み込み

CSVファイルのデータを扱いたい。
でも「,」を含む値も取り扱いたい。。。ということがあったのでメモです。。。

CSVの仕様によると、

  • 各値はダブルクォートで囲まれることがある
  • ダブルクォート内ではコンマや改行はエスケープされる
  • ダブルクォート内でダブルクォートをエスケープするにはダブルクォートを二重に書く

とのことなので、(参考/RFC4180 http://www.ietf.org/rfc/rfc4180.txt)
ダブルクォートのエスケープが面倒くさいけど
「,」を含むデータを取り扱うにはダブルクォートで囲めばよいそうです。

今回は、ダブルクォート内に改行が含まれない1行のCSVデータを読み込み、各値を配列で返す処理を考えます。

簡単に考えるために1行の文字を先頭から見ていき、遷移する状態として次の5状態を考えます

  • 初期状態
    • 各データ先頭文字の状態、データ状態のいずれかへ遷移
  • データ状態(非クォート)
    • ダブルクォートで囲まれない場合で、コンマが現れるまでデータとして取り込んで行く
  • データ状態(クォート)
    • ダブルクォートで囲まれている場合、ダブルクォートが現れるまでデータとして取り込んで行く
  • ダブルクォート中のダブルクォート状態
    • 次の文字がダブルクォートなら再びクォート内のデータ状態へ移り、コンマならデータ末尾
  • データ末尾状態
    • 各データの末尾まで読み込んだ状態で、配列に詰め込む

以上の状態遷移をもとにPHPで記述してみました。

<?php
function csv($dataline){
	$result = array();
	$len = strlen($dataline);
	//各状態の初期化
	// start, not_quoted, quoted, end_or_escape, end
	$state = "start";
	
	$tmp = "";
	for($i=0; $i<$len; $i++){
		$ch = $dataline[$i];
		switch($state){
			case 'start'://初期状態の処理
				if($ch == '"') $state = 'quoted';
				elseif($ch == ',') $state = 'end';
				else {
					$state = 'not_quoted';
					$tmp .= $ch;
				}
				break;
			case 'not_quoted'://クォーティングされてない場合
				if($ch == ',') $state = 'end';
				else $tmp .= $ch;
				break;
			case 'quoted'://クォーティングされている場合
				if($ch == '"') $state = 'end_or_escape';
				else $tmp .= $ch;
				break;
			case 'end_or_escape'://クォーティング中のクォート
				if($ch == '"'){
					$state = 'quoted';
					$tmp .= '"';
				} else $state = 'end';
				break;
		}
		if($state == 'end'){
			$state = 'start';
			$result[] = $tmp;
			$tmp = "";
		
		}
	}
	//最後の要素を結果に詰め込む
	//行末がコンマで終わる時はデータとして含めない
	if($tmp != "") $result[] = $tmp;
	
	return $result;
}
?>

状態遷移の仕方などよく考えるともっとコードは短くなりそうです。