はじめに
JavaScriptでの外部からデータを受け取る時に、JSONでやり取りできればいいのですが、時としてCSVで受け取らねばならないことも多くあるかと思います。
ということで、CSVでデータを渡してくる代表的な環境から渡されるCSVをいい感じにオブジェクトとして取り込む仕組みを考えていきます。
CSVは「セルや行をただ単純にカンマや改行で区切ったもの」ではない
他の言語においてもCSVが使われる機会はそれなりにあるため、色々な方々がCSVを読み込むプログラムを作った経験があるかと考えられます。
一定の法則のデータしか存在しない前提であれば単純に改行とカンマでパースすればよいのですが、CSVを作ることができるツールとして代表的なExcelやGoogleスプレッドシートなどではセルの中にカンマを含めたり、改行したりでき、そのままCSVとして出力もできるようになっています。
セル内の改行と行区切りの改行は、「LF単体」(セル内改行)か「CR LF」(行区切りの改行)かで容易に判別できるようになっています。
ですので、CSVの文字列を取得して、BOMを処理した後はまず「CR LF」("\r\n")を区切り文字としてsplitするとよいでしょう。
また、この時に最後の行が空文字列であれば除去します。
// 最後の空白行を除去する
function RemoveEmptyFinal(textArray)
{
if (textArray[textArray.length - 1] == "")
{
textArray.splice(textArray.length - 1);
}
return textArray;
}
// 受け取ったCSVを行ごとの文字列にパースする
var csvText = "(何らかのCSV文字列)";
RemoveEmptyFinal(csvText.split("\r\n"))
また、セル内のカンマとセル区切りのカンマについては、改行とは異なり文字コードとしては同一で、前後に何か文字を付与するタイプの区別方法ではありません。
CSVには、カンマ(,)、ダブルクォーテーション(")、改行を含むフィールド(セル)をダブルクォーテーションで囲う、という仕様があります。
そのため、フィールドの先頭文字がダブルクォーテーションでなければそのまま次のカンマの直前もしくは行末までを、フィールドの先頭文字がダブルクォーテーションであれば、そのダブルクォーテーションの次の文字から、「",」の組み合わせの直前までをフィールドの文字列として解釈します。また、セル内のダブルクォーテーションは2個続けて記述されるため、フィールドの解釈が終わった後に""を"に置換しましょう。
// CSVの行を分割する(成功時はフィールド文字列の配列、失敗時はnullを返却する)
function SplitCsvRow(rowText)
{
var rawSplit = [];
for (var i = 0; i < rowText.length; ++i)
{
const currentCharacter = rowText[i];
if (currentCharacter == ',')
{
rawSplit.push("");
if (i == rowText.length - 1)
{
rawSplit.push("");
}
continue;
}
var isTerminatorNotFound = true;
if (currentCharacter == '"')
{
const quotedStartIndex = i++;
for (; i < rowText.length; ++i)
{
const quotedCharacter = rowText[i];
if (quotedCharacter == '"')
{
const nextQuotedCharacter = rowText[++i];
if (nextQuotedCharacter == ',')
{
const quotedText = rowText.substring(quotedStartIndex + 1, i - 1).replaceAll("\"\"", "\"");
rawSplit.push(quotedText);
if (i == rowText.length - 1)
{
rawSplit.push("");
}
isTerminatorNotFound = false;
break;
}
else if (i == rowText.length)
{
const quotedText = rowText.substring(quotedStartIndex + 1, i - 1).replaceAll("\"\"", "\"");
rawSplit.push(quotedText);
isTerminatorNotFound = false;
}
}
}
if (isTerminatorNotFound) return null;
continue;
}
const startIndex = i;
for (; i < rowText.length; ++i)
{
const regularCharacter = rowText[i];
if (regularCharacter == ',')
{
rawSplit.push(rowText.substring(startIndex, i));
if (i == rowText.length - 1)
{
rawSplit.push("");
}
isTerminatorNotFound = false;
break;
}
if (regularCharacter == '"' || regularCharacter == '\n')
{
return null;
}
}
if (isTerminatorNotFound)
{
rawSplit.push(rowText.substring(startIndex));
}
}
return rawSplit;
}
(上のコードではセル内の改行についてはダブルクォーテーションがなくても通ってしまいますが、一般的な環境では手書きで改行文字を使い分ける方が稀かと思われますし、そもそも表ツールからはそんなCSVは出力されないため省略しています)
キー名とフィールドを紐付ける
この項目は、1行目が「以降の行のどの列が何かを示すヘッダーである」ことを前提としたCSVについての話です。
まず、1行目は上に示した処理でフィールド文字列の配列にしておきます。
そして、2行目以降を以下のような処理で連想配列にします。
フィールドをパースする処理自体は1行目と同じですが、その後に「同じ位置にあるヘッダー行の文字列をキーとして連想配列に格納する」処理を行うことによって、後でデータを使う時に行数と列名でデータにアクセスできるようになって実装が見やすくなるかと思われます。
// 行の文字列をキーとフィールドが紐づけられたオブジェクトにする
function SplitRow(rowText, keyNames)
{
const rawSplit = SplitCsvRow(rowText);
if (rawSplit === null || rawSplit.length != keyNames.length)
{
return null;
}
var data = {};
for (var i = 0; i < rawSplit.length; ++i)
{
data[keyNames[i]] = rawSplit[i];
}
return data;
}