CSVを編集する簡易htmlツール

少し前にこんなの↓を作りました。

https://qiita.com/44dayo/items/2aef92ca9c423ee49d35

作ってから思ったのですが、「表示させて確認中にへんなところがぴょこっとあったら、ぱぱっと修正したいよなぁ～」と思いました。
というわけで、今度は編集できる簡易ツールを作ってみました。


# 1.作り方


- メモ帳で新規テキストを作成し、以下のコードを張り付けます。

<details><summary>Rev1</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSV Editor</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
    <style>
        table, th, td {
            border: 1px solid black;
            border-collapse: collapse;
        }
        th, td {
            padding: 8px;
        }
    </style>
</head>
<body>
    <h1>CSV Editor</h1>
    <input type="file" id="csvFileInput" accept=".csv">
    <br><br>
    <button onclick="addRow()">行追加</button>
    <button onclick="addColumn()">列追加</button>
    <button onclick="deleteRow()">行削除</button>
    <button onclick="deleteColumn()">列削除</button>
    <br><br>
    <table id="csvTable"></table>
    <br>
    <label for="encodingSelect">Select Encoding:</label>
    <select id="encodingSelect">
        <option value="UTF8">UTF-8</option>
        <option value="UTF8_BOM">UTF-8 with BOM</option>
        <option value="UTF16LE">UTF-16LE</option>
        <option value="UTF16LE_BOM">UTF-16LE with BOM</option>
        <option value="UTF16BE">UTF-16BE</option>
        <option value="UTF16BE_BOM">UTF-16BE with BOM</option>
        <option value="SJIS">Shift_JIS</option>
        <option value="EUCJP">EUC-JP</option>
        <option value="ISO2022JP">ISO-2022-JP</option>
    </select>
    <button onclick="downloadCSV()">CSVを保存</button>

    <script>
        document.getElementById('csvFileInput').addEventListener('change', function(event) {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    const text = e.target.result;
                    displayCSV(text);
                };
                reader.readAsText(file);
            }
        });

        function displayCSV(text) {
            const rows = text.split('\n');
            const table = document.getElementById('csvTable');
            table.innerHTML = '';
            rows.forEach((row) => {
                const tr = document.createElement('tr');
                const cells = row.split(',');
                cells.forEach((cell) => {
                    const td = document.createElement('td');
                    td.contentEditable = true;
                    td.textContent = cell;
                    tr.appendChild(td);
                });
                table.appendChild(tr);
            });
        }

        function addRow() {
            const table = document.getElementById('csvTable');
            const newRow = document.createElement('tr');
            const columnCount = table.rows[0] ? table.rows[0].cells.length : 1;
            for (let i = 0; i < columnCount; i++) {
                const newCell = document.createElement('td');
                newCell.contentEditable = true;
                newRow.appendChild(newCell);
            }
            table.appendChild(newRow);
        }

        function addColumn() {
            const table = document.getElementById('csvTable');
            for (let row of table.rows) {
                const newCell = document.createElement('td');
                newCell.contentEditable = true;
                row.appendChild(newCell);
            }
        }

        function deleteRow() {
            const table = document.getElementById('csvTable');
            if (table.rows.length > 0) {
                table.deleteRow(-1);
            }
        }

        function deleteColumn() {
            const table = document.getElementById('csvTable');
            if (table.rows.length > 0) {
                for (let row of table.rows) {
                    if (row.cells.length > 0) {
                        row.deleteCell(-1);
                    }
                }
            }
        }

        function downloadCSV() {
            const table = document.getElementById('csvTable');
            let csvContent = '';
            for (let row of table.rows) {
                let rowData = [];
                for (let cell of row.cells) {
                    rowData.push(cell.textContent);
                }
                csvContent += rowData.join(',') + '\n';
            }

            const encoding = document.getElementById('encodingSelect').value;
            let bom = [];
            let encodedData;

            switch (encoding) {
                case 'UTF8_BOM':
                    bom = [0xEF, 0xBB, 0xBF];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF8', 'UNICODE');
                    break;
                case 'UTF16LE_BOM':
                    bom = [0xFF, 0xFE];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF16LE', 'UNICODE');
                    break;
                case 'UTF16BE_BOM':
                    bom = [0xFE, 0xFF];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF16BE', 'UNICODE');
                    break;
                default:
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), encoding, 'UNICODE');
                    break;
            }

            const blob = new Blob([new Uint8Array([...bom, ...encodedData])], { type: 'text/csv;charset=' + encoding.toLowerCase() + ';' });
            
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'edited.csv';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }
    </script>
</body>
</html>



```

</details>

**追記**
とこれで終わりのつもりだったのですが、読み込みの部分でアイディアを頂戴したのでマージしてみました。↓のRev2はデータ選択前に文字コードを選択しておくと、そのコードで読み込んでくれる(はず)です。

<details><summary>Rev2_</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSV Editor</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
    <style>
        table, th, td {
            border: 1px solid black;
            border-collapse: collapse;
        }
        th, td {
            padding: 8px;
        }
    </style>
</head>
<body>
    <h1>CSV Editor</h1>
    <input type="file" id="csvFileInput" accept=".csv">
    <br><br>
    <button onclick="addRow()">行追加</button>
    <button onclick="addColumn()">列追加</button>
    <button onclick="deleteRow()">行削除</button>
    <button onclick="deleteColumn()">列削除</button>
    <br><br>
    <table id="csvTable"></table>
    <br>
    <label for="encodingSelect">Select Encoding:</label>
    <select id="encodingSelect">
        <option value="utf-8">UTF-8</option>
        <option value="utf-8-sig">UTF-8 with BOM</option>
        <option value="utf-16le">UTF-16LE</option>
        <option value="utf-16le-sig">UTF-16LE with BOM</option>
        <option value="utf-16be">UTF-16BE</option>
        <option value="utf-16be-sig">UTF-16BE with BOM</option>
        <option value="shift_jis">Shift_JIS</option>
        <option value="euc-jp">EUC-JP</option>
        <option value="iso-2022-jp">ISO-2022-JP</option>
    </select>
    <button onclick="downloadCSV()">CSVを保存</button>

    <script>
        document.getElementById('csvFileInput').oninput = function() {
            const file = this.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    const text = new TextDecoder(document.getElementById('encodingSelect').value).decode(new Uint8Array(e.target.result));
                    displayCSV(text);
                };
                reader.readAsArrayBuffer(file);
            }
        };

        function displayCSV(text) {
            const rows = text.split('\n');
            const table = document.getElementById('csvTable');
            table.innerHTML = '';
            rows.forEach((row) => {
                const tr = document.createElement('tr');
                const cells = row.split(',');
                cells.forEach((cell) => {
                    const td = document.createElement('td');
                    td.contentEditable = true;
                    td.textContent = cell;
                    tr.appendChild(td);
                });
                table.appendChild(tr);
            });
        }

        function addRow() {
            const table = document.getElementById('csvTable');
            const newRow = document.createElement('tr');
            const columnCount = table.rows[0] ? table.rows[0].cells.length : 1;
            for (let i = 0; i < columnCount; i++) {
                const newCell = document.createElement('td');
                newCell.contentEditable = true;
                newRow.appendChild(newCell);
            }
            table.appendChild(newRow);
        }

        function addColumn() {
            const table = document.getElementById('csvTable');
            for (let row of table.rows) {
                const newCell = document.createElement('td');
                newCell.contentEditable = true;
                row.appendChild(newCell);
            }
        }

        function deleteRow() {
            const table = document.getElementById('csvTable');
            if (table.rows.length > 0) {
                table.deleteRow(-1);
            }
        }

        function deleteColumn() {
            const table = document.getElementById('csvTable');
            if (table.rows.length > 0) {
                for (let row of table.rows) {
                    if (row.cells.length > 0) {
                        row.deleteCell(-1);
                    }
                }
            }
        }

        function downloadCSV() {
            const table = document.getElementById('csvTable');
            let csvContent = '';
            for (let row of table.rows) {
                let rowData = [];
                for (let cell of row.cells) {
                    rowData.push(cell.textContent);
                }
                csvContent += rowData.join(',') + '\n';
            }

            const encoding = document.getElementById('encodingSelect').value;
            let bom = [];
            let encodedData;

            switch (encoding) {
                case 'utf-8-sig':
                    bom = [0xEF, 0xBB, 0xBF];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF8', 'UNICODE');
                    break;
                case 'utf-16le-sig':
                    bom = [0xFF, 0xFE];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF16LE', 'UNICODE');
                    break;
                case 'utf-16be-sig':
                    bom = [0xFE, 0xFF];
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), 'UTF16BE', 'UNICODE');
                    break;
                default:
                    encodedData = Encoding.convert(Encoding.stringToCode(csvContent), encoding.toUpperCase(), 'UNICODE');
                    break;
            }

            const blob = new Blob([new Uint8Array([...bom, ...encodedData])], { type: 'text/csv;charset=' + encoding.toLowerCase() + ';' });
            
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'edited.csv';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }
    </script>
</body>
</html>
```
</details>

- 張り付けたテキストデータを`CsvEditor.htm`というファイル名に書き換えます。

::: note info 
- ここでは`CsvEditor`としましたが、名称は任意のものでよいです。
- 拡張子表示していないとテキストデータは`新規 テキスト ドキュメント`などという名称になっています。拡張子表示にしたうえで、拡張子も`txt`から`htm`に変更してください。


これはインターネットに繋がっていない環境の場合は動作しません。
インターネットに繋がらない環境で使用したい場合は

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
```
を↓のように書き換えて、このツールと同じディレクトリに`encoding.min.js`を保存することで使えるようになるはずです。
```
<script src="./encoding.min.js"></script>
```

:::

# 2.使い方

- `CsvEditor.htm`を実行します。
- 既存のファイルがある場合は`ファイルを選択`をクリックしてCSVファイルを選択します。
- こんな感じで表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599902/4760f270-6dcf-b59d-9100-8b90bad9543c.png)

前回までとは違って、入力できるようになっているのがわかるでしょうか？実際に入力できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599902/90b3866d-35f5-915f-b49f-c74a7c28d7bd.png)

そして今度は行と列の追加、削除ができるようになっているのと、文字コードを指定して保存できるようにしました。

::: note warn
- 保存時に文字コードを正しく保存するために、encoding.jsを読み込むようになっています。そのため、インターネットにつながっていないとだめです。
- 作るだけ作ってこの辺はあまり検証できていませんので保証はできません。実際の動作を様子見してください。
:::


# 3.最後に
本職は多少スクリプト書く位で、javascriptもこのためにちょっと触ってみただけだったのですが、自分に必要なツールと思って作ってみたら思いのほかよさげなのができたと思いました。

必要は人を強くしますね。

多少動作が怪しいところはあるのですが、そもそもの「CSVを参照したいけどエクセルがないし、入れられない。あってもインストールが手間。」という課題はもうこのツールが使えればよい気がするので、これはもうここまででいったん終わりたいと思います。
また困りごとがでたらこれを改修するなり新たな簡易ツールを作ったりしてみたいと思います。

ちなみにこのツールは有用だと思ったらご自由にお使い下さいませ。

もし改造とかしたら教えていただけると嬉しいです。