LoginSignup
2
2

More than 3 years have passed since last update.

JavaScriptでカンマ区切りCSVとタブ区切りテキストの双方をパース

Posted at

外部プラグイン利用不可、サーバサイド処理不可という条件で、JavaScriptのみでCSVのパースが必要だったので、実装した処理をメモです。

必要要件

  • システム側から出力されるタブ区切りテキストの取り込み
  • ユーザーがExcelから出力したCSVの取り込み
  • セルの中にカンマが入る事もある
  • Excel出力したファイルは、必要なセルしかダブルクォーテーションで囲まれない

以上の事から、単純にsplitできる物ではなかったため、実装処理を備忘録として残します。

実際には、さらにSJISとUTF-8(BOM付き)のどちらのパターンも対応する必要がありましたが、ここではそれは割愛しますが、データを読み込んで、先頭にBOMがついていれば、文字コード指定して再読み込みする処理をいれて対応しました。

実装

簡単に言えば、
1. 行末改行で区切ってループ
2. ダブルクォーテーションの数が奇数なら次の行と結合して行データにする
3. 行データごとにループ
4. 先頭からセルを切り出す

という流れで構築しています。

image.png

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);

取り込み結果

image.png

あとがき

タブ区切りテキストで、ダブルクォーテーションで囲まれていないセルの中に、ダブルクォーテーションが入っている時の処理も入れたら、ほぼほぼカバーできます。この処理はさらに精度を高めた処理にしていきたいと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2