はじめに
これまでの記事で、CSVファイルの基礎知識、文字コードの変換、読み込みについて紹介しました。
今回は、読み込んだCSV文字列を二次元配列にパースする処理について解説します!☘️
💡 この記事でわかること
- なぜ単純なカンマ分割ではCSVを正しく処理できないのか
- TypeScriptによるCSVパーサーの実装
📚 関連記事
- 【CSV入門】CSVを扱う前に知っておきたい基礎知識
- 【CSV入門】ブラウザでCSVファイルを読み込む
- ブラウザでCSVファイルを読み込む Part2:encoding-japaneseで文字化けした話
パースとは
Parseとは、「構文解析する」という意味です。
MDN Web Docsでは、構文解析について以下のように説明されています。
構文解析 (Parsing) とは、プログラムを解釈し、例えばブラウザー内の JavaScript エンジンなどの実行環境で、実際に実行できる内部形式に変換することを意味します。
https://developer.mozilla.org/ja/docs/Glossary/Parse
例えば、ブラウザがHTMLを構文解析して、DOMツリーを構築したりすることもパースです。
CSVにおけるパース
前回の記事では、CSVファイルを読み込んで文字列として取得するところまでを紹介しました。文字列のままではレコードやフィールド単位で直接アクセスすることができないので、扱いやすい形式に変換します。
CSVファイルを読み込んで文字列に変換した結果
結果:文字列
"商品名,価格\nヘッドホン,8000\nマウス,3500"
パース後の二次元配列
[
["商品名", "価格"],
["ヘッドホン", "8000"],
["マウス", "3500"]
]
CSVとして意味のある単位に分解して、プログラムで扱いやすいデータ構造に変換するため、二次元配列にパースします。
CSVパースの課題
CSVにはカンマ・改行・ダブルクォートを含むフィールドはダブルクォートで囲むというルールがあります。
そのため、split(",") や split("\n") による分割では、フィールド内のカンマや改行まで区切り文字として認識されてしまい、正しくパースできません。
ダブルクォートの中か外かで、カンマや改行の意味が変わるので、「今ダブルクォートの中にいるかどうか」を確認しながら1文字ずつ処理する必要があります。
CSV文字列を二次元配列にパース
改行コードの統一
const text = content.replace(/\r\n|\r/g, '\n');
改行コードにはLF(\n)、CR(\r)、CRLF(\r\n)の3種類があります。ここでは、改行コードをLF(\n)に統一して、\n だけを改行として扱います。
✍️ 補足
正規表現 /\r\n|\r/g では \r\n を先に確認するようにします。順序を逆にすると、\r 部分が先にマッチして \n に変換され、 \n\n になってしまいます。
状態管理
let inQuotes = false;
inQuotes はダブルクォートの内側にいるかどうかを表すフラグです。
このフラグによって「ダブルクォートの外」と「ダブルクォートの内」の2つの状態を管理します。
inQuotes |
状態 | 説明 |
|---|---|---|
false |
ダブルクォートの外 | カンマは区切り文字、改行はレコードの終端 |
true |
ダブルクォートの内 | カンマや改行もフィールドの一部として扱う |
たとえば、以下のCSV文字列をパースする場合を考えます。
ノートPC,"軽量,高性能",120000
パース結果は以下のようになります。
["ノートPC", "軽量,高性能", "120000"]
"軽量,高性能" の中のカンマはフィールドを分割せず、フィールドの値としてそのまま保持されます。
1文字ずつ処理するループ
for (let i = 0; i < text.length; i++) {
const char = text[i]; // 現在の文字
const nextChar = text[i + 1]; // 次の文字
パーサーの本体は for ループで文字列を1文字ずつ順番に読んでいきます。
nextChar で次の文字を確認しているのは、ダブルクォートのエスケープ("")を判定するためです。
クォート内の処理
// ダブルクォート内の場合
if (inQuotes) {
if (char === '"') {
// "" はエスケープされた " として判断
if (nextChar === '"') {
currentCell += '"';
i += 1;
} else {
// " 単体の場合、クォート終了。通常状態に戻る
inQuotes = false;
}
} else {
// フィールドに文字を追加
currentCell += char;
}
}
inQuotes が true のときは、ダブルクォートの中にいるときの処理です。
-
""の場合:エスケープされたダブルクォートとして、フィールドに"を1つ追加します。i += 1で次の"をスキップします -
"単体の場合:クォートの終了としてinQuotesをfalseに戻します - それ以外の場合:カンマや改行を含め、文字列をフィールドの値として追加します
クォート外の処理
} else if (char === '"') {
// クォート開始 → クォートの中に入る
inQuotes = true;
} else if (char === ',') {
// カンマ → ここまでを1つのフィールドとする
currentRow.push(currentCell);
currentCell = '';
} else if (char === '\n') {
// 改行 → ここまでを1つのフィールドとし、次の行へ
currentRow.push(currentCell);
rows.push(currentRow);
currentRow = [];
currentCell = '';
} else {
// その他 → フィールドに文字を追加
currentCell += char;
}
inQuotes が false のときは、ダブルクォートの外にいるときの処理です。
| 文字 | 処理 |
|---|---|
" |
クォートの中に入る(inQuotes = true) |
, |
カンマまでを1つのフィールドとし、次のフィールドの処理を開始 |
\n |
改行までを1つのフィールドとし、次の行へ |
| その他 | フィールドの文字としてそのまま追加 |
ファイル末尾の処理
ファイル末尾が改行で終わっていない場合、残ったフィールドとレコードを追加します。
if (currentCell || currentRow.length) {
currentRow.push(currentCell);
rows.push(currentRow);
}
CSVのファイル末尾の改行は任意とされています。
ファイルが改行で終わっていない場合、ループ終了時に currentCell や currentRow にデータが残っているので、この条件で残ったフィールドとレコードを追加します。
全体の実装コード
今回紹介したパーサーの全体コードです。
/** CSV文字列を二次元配列にパース */
const parseContent = (content: string): string[][] => {
const rows: string[][] = [];
let currentRow: string[] = [];
let currentCell = '';
let inQuotes = false;
const text = content.replace(/\r\n|\r/g, '\n');
for (let i = 0; i < text.length; i++) {
const char = text[i];
const nextChar = text[i + 1];
if (inQuotes) {
if (char === '"') {
if (nextChar === '"') {
currentCell += '"';
i += 1;
} else {
inQuotes = false;
}
} else {
currentCell += char;
}
} else if (char === '"') {
inQuotes = true;
} else if (char === ',') {
currentRow.push(currentCell);
currentCell = '';
} else if (char === '\n') {
currentRow.push(currentCell);
rows.push(currentRow);
currentRow = [];
currentCell = '';
} else {
currentCell += char;
}
}
if (currentCell || currentRow.length) {
currentRow.push(currentCell);
rows.push(currentRow);
}
return rows;
};
まとめ
- パースとは、テキストデータを解析してプログラムで扱えるデータ構造に変換すること
- 単純な
split(",")ではダブルクォート内のカンマや改行を正しく扱えない - フラグで状態を管理しながら1文字ずつ処理することでCSVをパースできる
参考

