javascript で作成したCSVファイルをエクセルで表示可能にする

ブラウザからCSVをダウンロードさせる機能を、サーバサイドではなくフロント(js)でやった時に、紆余曲折して色々試したみた結果のまとめ。

#変更履歴
- **2015/09/13  サンプルコードを追加しました**

#文字コードやBOM等について

■参考
[RubyでMac版ExcelでインポートできるCSVデータを作成する](http://qiita.com/shinichinomura/items/88879711245a420f713d)
[【Rails】CSVをエクスポート→エクセル で文字化け問題](http://qiita.com/shunsuke227ono/items/a9648f8c043ed637dc69)

今回rubyではなくjsですが、文字コードについては共通なのでこれらの記事を参考にしたところ、どうやら

- BOMあり (Win対応)
- UTF-16 (Mac対応)
- TAB区切り (Mac対応)

この組合せが良さそうです。TAB区切りなのに拡張子がcsvなのを許容出来れば、ですが。

#Excel形式でのエクスポートについて

実は色々試しました。前提が「エクセルで表示可能にする」なので、CSVではなくエクセルファイルでダウンロード出来るようにしたらいいじゃない！と真っ先に考えたのですが、

1. HTML形式でxls（またはxlsx形式）にして出力
2. XML形式でxls（またはxlsx形式）にして出力

どちらもMac上では問題ないのに、Windowsで表示確認すると

> ファイル形式と拡張子が一致しません。ファイルが破損しているか、安全ではない可能性があります。発行元が信頼できない場合は、このファイルを開かないでください。ファイルを開きますか？

という無慈悲な警告メッセージが表示され、この問題をどうしても解決出来ませんでした。もし解決出来る方法をご存知であれば、どなたかコメントで教えていただけると大変ありがたいです。

#文字コード / BOM / 拡張子 の組み合わせテスト結果

<table>
<tr>
<th></th>
<th>文字コード</th>
<th>BOM</th>
<th>区切り</th>
<th>拡張子</th>
<th>Win</th>
<th>Mac</th>
</tr>
<tr>
<td>1</td>
<td>UTF-8</td>
<td>なし</td>
<td>カンマ</td>
<td>csv</td>
<td>✕</td>
<td>✕</td>
</tr>
<tr>
<td>2</td>
<td>UTF-8</td>
<td>有り</td>
<td>カンマ</td>
<td>csv</td>
<td>△</td>
<td>✕</td>
</tr>
<tr>
<td>3</td>
<td>UTF-16</td>
<td>有り</td>
<td>カンマ</td>
<td>csv</td>
<td>◯</td>
<td>✕</td>
</tr>
<tr>
<td>4</td>
<td>UTF-16</td>
<td>有り</td>
<td>TAB</td>
<td>xls</td>
<td>✕</td>
<td>◯</td>
</tr>
<tr>
<td>5</td>
<td>UTF-16</td>
<td>有り</td>
<td>TAB</td>
<td>csv</td>
<td>◯</td>
<td>◯</td>
</tr>
</table>

**■パターン1**
Win / Mac共に文字化け

**■パターン2**
Winでは私の環境では文字化け発生しなかったが、ググってみるとバージョンによってはUTF-8だと文字化けするという声が多いので、ちょっと不明。
Macで文字化け。Macに対応させるにはUTF-16が必須。

**■パターン3**
Macでの文字化けは解消されるものの、エクセルで表示させるとカンマの存在も虚しく、1セルにすべて表示されてしまう。
↓こんな感じ
![スクリーンショット 2015-09-11 15.13.01.png](https://qiita-image-store.s3.amazonaws.com/0/52142/9b28df1c-0f1c-2b03-4823-1299de90aa57.png)

**■パターン4**
拡張子をxlsにすると、winで無慈悲な警告メッセージが再び表示されてしまう。

**■パターン5**
何のエラーも表示されなかったベストパターン。（拡張子詐称なのを除けば）

##私なりの結論
- Macユーザーを無視出来るなら、UTF-16/BOM有りにするだけでもOK
- TAB区切りなのにcsv拡張子にするのが気持ち悪い＆許容できないなら、Macユーザには自力でセル表示してもらうように促す（エクセルで表示する場合にカンマ区切りファイルである指定をしてもらえばOK）
- jsで頑張らないでサーバーサイドでエクセルファイル作成すれば万事解決（それを言ったらおしまい）

#jsで「パターン5」のCSVファイルを作成する
ダウンロード処理部分で便宜的にjQueryを利用していますが、jQueryは必須でないです。

##1. エンディアンを考慮しない場合
```js:sample1.js
//CSVに記載するデータ配列
var csv_array = [
    ['生徒番号', '名前', '点数', '評価'],
    ['A1', '藤原', '80', 'A'],
    ['A2', '増川', '20', 'C'],
    ['A3', '直井', '60', 'B'],    
    ['A4', '升', '100', 'S']
];
var file_name = 'test.csv';

//配列をTAB区切り文字列に変換
var csv_string = "";
for (var i=0; i<csv_array.length; i++) {
    csv_string += csv_array[i].join("\t");
    csv_string += '\r\n';
}

//BOM追加
csv_string = "\ufffe" + csv_string; //UTF-16
console.log (csv_string);

//UTF-16に変換...(1)
var array = [];
for (var i=0; i<csv_string.length; i++){
   array.push(csv_string.charCodeAt(i));
}
var csv_contents = new Uint16Array(array);

//ファイル作成
var blob = new Blob([csv_contents] , {
    type: "text/csv;charset=utf-16;"
});

//ダウンロード実行...(2)
if (window.navigator.msSaveOrOpenBlob) {
    //IEの場合
    navigator.msSaveBlob(blob, file_name);
} else {
    //IE以外(Chrome, Firefox)
    var downloadLink = $('<a></a>');
    downloadLink.attr('href', window.URL.createObjectURL(blob));
    downloadLink.attr('download', file_name);
    downloadLink.attr('target', '_blank');

    $('body').append(downloadLink);
    downloadLink[0].click();
    downloadLink.remove();
}
```

##説明
**(1) UTF-16に変換**
このサンプルでは Blob を使ってファイルを作成します。Blobの第1引数はStringのまま渡すとUTF-8となってしまうため、高速なTypedArrayで `Uint16Array` に変換したデータを渡します。

ただし、TypedArrayはエンディアンの指定ができず、実行環境のCPUのエンディアンと同じになってしまう点に注意です。最近ではほぼすべての環境がリトルエンディアン（LE）なので、このままでも多くの場合は問題ないのですが、厳密にLEを指定したい場合にはもう少しコードを変更する必要があります。
（サーバサイドではなく完全にフロントのjsで実行する前提であれば、LE固定にするのではなく、実行環境のエンディアンとなるTypedArrayで作成するので良い気もしますが）

**(2) ダウンロード実行**
ダウンロード処理は、html5のdownload属性を利用しています。ただしこれはChromeとFirefoxしか対応していません。IEの場合はその代替となる `msSaveBlob` を利用しています。

##2. エンディアンを指定する場合
厳密にLEを指定したい場合は、以下のようになります。

```js:sample2.js
//CSVに記載するデータ配列
var csv_array = [
    ['生徒番号', '名前', '点数', '評価'],
    ['A1', '藤原', '80', 'A'],
    ['A2', '増川', '20', 'C'],
    ['A3', '直井', '60', 'B'],    
    ['A4', '升', '100', 'S']
];
var file_name = 'test.csv';

//配列をTAB区切り文字列に変換
var csv_string = "";
for (var i=0; i<csv_array.length; i++) {
    csv_string += csv_array[i].join("\t");
    csv_string += '\r\n';
}

//BOM追加
csv_string = "\ufffe" + csv_string; //UTF-16
console.log (csv_string);

//実行環境がLEかどうか判別...(3)
if (isLittleEndian()) {

    //実行環境のエンディアンがLEならTypedArrayを利用
    var array = [];
    for (var i=0; i<csv_string.length; i++){
        array.push(csv_string.charCodeAt(i));
    }
    var csv_contents = new Uint16Array(array);

} else {

    //LEでない場合はDataViewでUTF-16LEのArrayBufferを作成
    var array_buffer = new ArrayBuffer(csv_string.length * 2);
    var data_view = new DataView(array_buffer);
    for (var i=0,j=0; i<csv_string.length; i++,j=i*2) {
        data_view.setUint16( j, csv_string.charCodeAt(i), true ); //第3引数にtrueを渡すとLEになる
    }
    var csv_contents = array_buffer
}

//ファイル作成
var blob = new Blob([csv_contents] , {
    type: "text/csv;charset=utf-16;"
});

//ダウンロード実行
if (window.navigator.msSaveOrOpenBlob) {
    //IEの場合
    navigator.msSaveBlob(blob, file_name);
} else {
    //IE以外(Chrome, Firefox)
    var downloadLink = $('<a></a>');
    downloadLink.attr('href', window.URL.createObjectURL(blob));
    downloadLink.attr('download', file_name);
    downloadLink.attr('target', '_blank');

    $('body').append(downloadLink);
    downloadLink[0].click();
    downloadLink.remove();
}

// --------------------------------------
// 実行環境のエンディアンがLEかどうか判別
// --------------------------------------
function isLittleEndian(){
	if ((new Uint8Array((new Uint16Array([0x00ff])).buffer))[0]) return true;
	return false;
}
```

##説明
**(3) 実行環境がLEかどうか判別**
実行環境のエンディアンを確認します。LEの場合は先ほどと同じですが、そうでない場合にはエンディアンを指定出来るDataViewを使用して、UTF-16LEのArrayBufferを作成しています。DataViewはget/setのメソッドが用意されていたり高機能な分、TypedArrayよりも低速になります。

##注意点
- ファイル作成の処理部分は、IE9以前では動きません
- ダウンロード部分は、Safari / IE9以前 では動きません
- 配列の上限数を超えるような大きなデータの場合には、分割した複数の配列をBlobの第1引数に引き渡すようにする必要があります。（※未確認ですが、ブラウザによっては配列数の上限が6万ちょいらしいので、例えば6万文字以上かどうかの判断が必要なのかも）

```js:例
var blob = new Blob([csv1, csv2, csv3]) , {
    type: "text/csv;charset=utf-16;"
});
```

##参考

- [Blob と File クラスについて | JavaScript プログラミング講座](http://hakuhin.jp/js/file.html)
- [型付き配列について（TypedArray） | JavaScript プログラミング講座](http://hakuhin.jp/js/typed_array.html#TYPED_ARRAY_05)
- [javascriptでバイナリを扱うために](http://qiita.com/yaegaki/items/909587a2dae20467c74a)
- [JavaScript/HTML5で，SJISの日本語CSVファイルをWebページ上で動的に生成してダウンロードするサンプルコードと解説 （動作デモ付き。Excelで開いても文字化けしない）](http://d.hatena.ne.jp/TipsMemo+computer-technology/20150710/p2)
- [How to convert ArrayBuffer to and from String](https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String)