外部プラグイン利用不可、サーバサイド処理不可という条件で、JavaScriptのみでCSVのパースが必要だったので、実装した処理をメモです。
必要要件
- システム側から出力されるタブ区切りテキストの取り込み
- ユーザーがExcelから出力したCSVの取り込み
- セルの中にカンマが入る事もある
- Excel出力したファイルは、必要なセルしかダブルクォーテーションで囲まれない
以上の事から、単純にsplitできる物ではなかったため、実装処理を備忘録として残します。
実際には、さらにSJISとUTF-8(BOM付き)のどちらのパターンも対応する必要がありましたが、ここではそれは割愛しますが、データを読み込んで、先頭にBOMがついていれば、文字コード指定して再読み込みする処理をいれて対応しました。
実装
簡単に言えば、
- 行末改行で区切ってループ
- ダブルクォーテーションの数が奇数なら次の行と結合して行データにする
- 行データごとにループ
- 先頭からセルを切り出す
という流れで構築しています。
Excelから出力したCSV(UTF-8)
id,"nam,e",price,about,,d
1,スイカ,1200,熊本産,a,e
2,"メロン
メロン2","2,500","マスクメロン
贈答品にお勧め",b,f
3,そら豆,500,塩茹でしてビールとともに,c,g
タブ区切りテキスト
id nam,e price about d
1 スイカ 1200 熊本産 a e
2 "メロン
メロン2" 2,500 "マスクメロン
贈答品にお勧め" b f
3 そら豆 500 塩茹でしてビールとともに c g
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<form name="form">
<input type="file" name="filechoose">
</form>
<table></table>
<script src="script.js"></script>
</body>
</html>
style.css
@charset "UTF-8";
table {
background: #ccc;
border-collapse: separate;
border-spacing: 1px;
}
th, td {
min-width: 80px;
padding: 5px 10px;
}
th {
background: #efefef;
}
td {
background: #fff;
}
script.js
const table = document.querySelector('table');
const input = document.querySelector('[name="filechoose"]');
const reader = new FileReader();
const make_col = (label, tag) => {
const col = document.createElement(tag);
if (label !== null)
col.innerHTML = label.replace('\n', '<br />').replace('\x22\x22', '\x22');
return col;
}
const make_row = (line, tag) => {
const tr = document.createElement('tr');
line.forEach(label => {
tr.appendChild(make_col(label, tag));
});
return tr;
}
// 画面出力
const show = (header, items) => {
table.innerHTML = '';
table.appendChild(make_row(header, 'th'));
// 値
items.forEach(line => {
table.appendChild(make_row(line, 'td'));
});
}
// ファイル選択確認
const file_choosed = () => {
if (input.files.length === 0) return;
const file = input.files[0];
reader.readAsText(file);
}
// ファイル読み込み
const file_load = () => {
let csv = reader.result;
// 空データなら処理しない
if (csv.length === 0) return false;
// Excel出力されたCSVの最終行の空行を無視
// Excel出力されたセル中の改行は\n、行末の改行は\r\n(macは行末\nかも...)
csv = csv.replace(/\r\n$/, '');
console.log(csv);
// 改行区切りしたデータ
const parse = csv.split(/\r\n/m);
let data = [];
// 行データごとにまとめる
let line = [];
let quot = 0;
for (const current of parse) {
const this_quots = current.match(/\x22/g);
if (this_quots == null && line.length == 0) {
data.push(current);
} else if (this_quots == null) {
line.push(current);
} else {
quot += this_quots.length;
line.push(current);
if (quot % 2 == 0) {
data.push(line.join('\r\n'));
quot = 0;
line = [];
}
}
}
/**
* セル区切りパターン
*/
// 空セル
const pt_1 = new RegExp(/^\x22?[,\t]\x22?/, 'm');
// ダブルクォーテーションつきのセルはダブルクォーテーション後カンマ、タブが出てくるまで
const pt_2 = new RegExp(/^\x22([^\x22]+)\x22(\,|\t)/, 'm');
// ダブルクォーテーションなしのタブ区切りでカンマが出てくるパターン
const pt_3 = new RegExp(/^([^\t]+)(\t)/);
// ダブルクォーテーションで囲まれていない場合はカンマかタブが出現するまで
const pt_4 = new RegExp(/^([^\,\t]+)(\,|\t|$)/);
// ヘッダ、内容出力データ用配列
const header = [];
const items = [];
before = null;
data.forEach((v, i) => {
cell = null;
while (v.length > 0) {
if (pt_1.exec(v)) {
cell = [pt_1.exec(v)[0], null, null];
} else if (pt_2.exec(v)) {
cell = pt_2.exec(v);
} else if (pt_3.exec(v)) {
cell = pt_3.exec(v);
} else if (pt_4.exec(v)) {
cell = pt_4.exec(v);
} else {
// 最終行
cell = [v, v, v];
}
if (i === 0) {
header.push(cell[1]);
} else {
if (typeof(items[i - 1]) === 'undefined') items[i - 1] = [];
items[i - 1].push(cell[1]);
}
v = v.substring(cell[0].length, v.length);
}
});
// 画面出力
show(header, items);
}
// Events
input.addEventListener('change', file_choosed, false);
reader.addEventListener('load', file_load, false);
取り込み結果
あとがき
タブ区切りテキストで、ダブルクォーテーションで囲まれていないセルの中に、ダブルクォーテーションが入っている時の処理も入れたら、ほぼほぼカバーできます。この処理はさらに精度を高めた処理にしていきたいと思います。