JavaScriptでExcel（XLSX）を読み込む

# 概要

[前回](http://qiita.com/tomgoodsun/items/0107e5d778b803935fc0)HTMLのTABLEタグをExcel（XLSX）に出力する方法を投稿したんですが、ここ最近業務の効率化でJavaScriptでExcelを読み込むという方法を使えないかと思ったら、すぐに情報が出てきたので、まとめておきます。

ちなみにExcelを使わないという選択肢も効率化の中にはあるのですが、それはまた別の機会に取り組みます。
とりあえず今回のテーマは__JavaScriptでExcelを読み込む__です。

# Sheet.js

http://sheetjs.com/

今回も必要なものはSheetJSです。
フルパッケージのスクリプトを読み込めば必要なものはすべて入っています。
今回もCDNから取得しました。

## 構成

オブジェクトを作ったわけなんですが、基本的には参考のページに書いてあることを踏襲しているだけです。


## 実装

この辺りを参考に作ります。

- https://stackoverflow.com/questions/41249187/import-excel-file-in-javascript
- http://codetheory.in/parse-read-excel-files-xls-xlsx-javascript/

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <title></title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  </head>
  <body>
    <form action="" method="get">
      <input type="file" name="upload_file" id="import-excel" />
      <h2>Result</h2>
      <div id="result"></div>
    </form>

    <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.11.1/xlsx.full.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
```

```js:script.js
(function (window, document) {
  window.ExcelJs = {};
  ExcelJs.File = function (_file, _workbook) {
    var that = this;
    var file = _file;
    var workbook = _workbook;

    return {
      getFile: function () {
        return file;
      },
      getWorkbook: function () {
        return workbook;
      },
      toJson: function () {
        var result = {};
        workbook.SheetNames.forEach(function(sheetName) {
          var roa = XLSX.utils.sheet_to_row_object_array(workbook.Sheets[sheetName]);
          if(roa.length > 0){
            result[sheetName] = roa;
          }
        });
        return result;
      },
      toCsv: function () {
        var result = [];
        workbook.SheetNames.forEach(function(sheetName) {
          var csv = XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]);
          if(csv.length > 0){
            result.push('SHEET: ' + sheetName);
            result.push('');
            result.push(csv);
          }
        });
        return result.join("\n");
      },
      toFormulae() {
        var result = [];
        workbook.SheetNames.forEach(function(sheetName) {
          var formulae = XLSX.utils.get_formulae(workbook.Sheets[sheetName]);
          if(formulae.length > 0){
            result.push('SHEET: ' + sheetName);
            result.push('');
            result.push(formulae.join("\n"));
          }
        });
        return result.join("\n");
      }
    };
  };

  ExcelJs.Reader = function (_file, onload) {
    var that = this;

    var file = _file;
    var reader = new FileReader();

    reader.onload = function(e) {
      var data = e.target.result;

      // データが多いとString.fromCharCode()でMaximum call stack size exceededエラーとなるので、
      // 別途関数で処理をする。
      //var arr = String.fromCharCode.apply(null, new Uint8Array(data));
      var arr = handleCodePoints(new Uint8Array(data));

      if (typeof onload == 'function') {
        onload(e, new ExcelJs.File(file, XLSX.read(btoa(arr), {type: 'base64'})));
      }
    };
    reader.readAsArrayBuffer(file);
  };
})(window, window.document);

// see: https://github.com/mathiasbynens/String.fromCodePoint/issues/1
function handleCodePoints(array) {
  var CHUNK_SIZE = 0x8000; // arbitrary number here, not too small, not too big
  var index = 0;
  var length = array.length;
  var result = '';
  var slice;
  while (index < length) {
    slice = array.slice(index, Math.min(index + CHUNK_SIZE, length)); // `Math.min` is not really necessary here I think
    result += String.fromCharCode.apply(null, slice);
    index += CHUNK_SIZE;
  }
  return result;
}

function renderResult(name, content) {
  var elem = document.getElementById('result');
  var html = elem.innerHTML;
  html += '<h3>' + name + '</h3>';
  html += '<pre>' + content + '</pre>';
  elem.innerHTML = html;
}

document.getElementById('import-excel').addEventListener('change', function (evt) {
  var files = evt.target.files;
  var i, f;
  for (i = 0, f = files[i]; i != files.length; ++i) {
    var er = new ExcelJs.Reader(f, function (e, xlsx) {
      renderResult(xlsx.getFile().name, JSON.stringify(xlsx.toJson(), null, 2));
    });
  }
}, false);
```

ファイルのアップロード用のフォームにxlsxファイルをアップロードすると画面に出力されます。

サンプル作りました。
[demo](http://tom-gs.com/demo/test/excel_import/)

## 注意点、疑問点

ファイルのロードタイミングは処理のタイミングとずれる（onloadイベントで通知される）ので、すべてを読み込んで処理させたい場合は、そういう処理を書いてあげる必要があります。
あとJSON形式で取得するときに複数のシートにデータが入っているのに、1シート分のデータしかJSONに変換されないことがあるみたいです。
あまり詳しく調べてないんで、何とも言えませんが、これなんか使えそう。
ローカルでも動くみたいだから、ちょっとした業務効率もできそう。（みたいな本が売ってたような気がする）

一応、Chrome、Firefox、Edgeでは動くみたいです。