簡易CSVパーサを書く

JScript on WSH 5.6でちょっとしたツールを書いている。CSVファイルを2つ読み込んで処理するので、CSVファイルをフィールドごとに分割した2次元配列を返す簡易パーサを書いてみた。

最初はCOM経由でExcelを使ってCSVファイルを直接読み込んでしまおうと考えていたのだけど、

  • 動作が結構重かった。
  • 読み込んだレコード数/フィールド数を取得する方法が分からなかった。

ということで、仕方なくパーサを自作することに。

CSVのフォーマットについては、

をベースにしつつ、

  • レコードの区切りはCR/LF/CRLFに一応対応。
  • 微妙なデータへの対応は殆ど考えてないけど、幾つか気になった点はExcelの挙動を参考に一応対応。

といった感じに若干変更。

ファイルサイズがそれ程大きくないので、一旦ファイル全体をStringオブジェクトとして取得して、それを解析するという構成で実装。試してないからアレだけど、使用している機能的にJScript以外のJavaScriptの処理系でも動きそうな気がする。

/// @brief  CSVファイルをパースする
///
/// @param[in] text   CSVファイルの全内容を保持する文字列
/// @param[in] delim  フィールドの区切り文字。省略時は","と見なす。
///
/// @return  フィールドごとに分割した二次元配列。
///          フィールドの中身は文字列として格納している。
///
/// @note
///   RFC 4180を参照(WikipediaのCSVの記事も参考になる)。
///   CSV を扱う既存の実装は色々とあるが、ごく一部にしか対応していない。
///
function parse_csv(text, delim) {
	if (!delim) {
		delim = ",";
	}

	var escaped = false;        // ダブルクォートで囲まれたフィールドを処理中ならtrue
	var cells = new Array();
	var rec = new Array();
	var field = new Array();

	for (var i = 0; i < text.length; ++i) {
		var c = text.charAt(i);

		switch (escaped) {
		case false:
			switch (c) {
			case "\r":
				if (i+1 < text.length && text.charAt(i+1) == "\n") {
					++i;
				}
				/*FALLTHRU*/
			case "\n":
				rec.push(field.join(""));
				cells.push(rec);
				field = new Array();
				rec = new Array();
				break;
			case "\"":
				if (field.length == 0) {
					escaped = true;
				} else {
					field.push(c);
				}
				break;
			case delim:
				rec.push(field.join(""));
				field = new Array();
				break;
			default:
				field.push(c);
				break;
			}
			break;
		case true:
			switch (c) {
			case "\"":
				if (i+1 < text.length) {
					if (text.charAt(i+1) == "\"") {
						field.push(c);
						++i;
					} else {
						escaped = false;
					}
				} else {
					rec.push(field.join(""));
					cells.push(rec);
					field = new Array();
					rec = new Array();
				}
				break;
			default:
				field.push(c);
				break;
			}
			break;
		default:
			break;
		}
	}

	// 最後のレコードの末尾に改行コードがない場合の追加処理
	if (field.length > 0 || c ==  delim) {
		rec.push(field.join(""));
	}
	if (rec.length > 0) {
		cells.push(rec);
	}

	return cells;
}

書いてみて思ったけど、発想がCプログラマ的かも知れない。例えば1文字ずつ処理している辺りとか。C++にならそれほど手間を掛けずに移植できそうだし、C言語への移植もいけそうな気がする。見た目がC言語系なので錯覚しているだけかもしれないけど。