ウェブブラウザ上のJavaScriptでExcelファイルをゴニョゴニョしたい

特に需要はないと思いますが、極たま~~~にウェブでエクセルファイルを扱うことがあります。
なんつっても、世の中の文書フォーマットは、マイクロソフトのエクセル(Excel)がデファクトスタンダードです💦
これはもうどうにもなりません。CSVで、つっても、エクセルファイルをよこしやがります。

まぁ、やっぱり業務システムにはエクセルは欠かせません、というか、これなしには、日本は動きません。日本の企業とか国、地方公共団体は、エクセルで動いているんです。これはもう動かしようのない事実であり当分の間は変わりませんし、変化の兆しも見えません。

しょうがない。

と、開発者の方が思ったかどうかわかりませんが、ブラウザのJavaScriptでエクセルファイルをパースして編集して、出力してくれる ライブラリがあるんですね。サーバーサイドのNode.jsでも使えます、というかそっちがメインのライブラリなのかも。

一般的にMS-Officeをインストールしたパソコンにはおまけ?として、EXCELのオートメーションが使えるようになってます。あくまでOfficeを買えば!の話ですが。
Windows Scripting HostなどからVBScriptやJScriptを使ってカンタンにエクセルファイルをアーダコーダできます。

・・・が、今回はこういう噺ではありません。
一般的なブラウザ上で、エクセルファイルをアーダコーダできるライブラリがあったんです。需要がなかったので今まで知らなかった!

SheetJS Spreadsheets simplified

で、ググったりしていろいろ調べると、意外にカンタンに使えちゃいますね。ただし、javascriptの blobとかFileReaderとかArrayBufferとかUint8Arrayとか・・・そういうちょっとややこしめの知識が必要です。

昔と違ってブラウザでローカルファイルとか普通に扱えます。ですが、その時は必ず、上記のFile/Blob/FileReader/ArrayBuffer/TypedArrayとかが絡んできます。
FileとBlobの関係、ArrayBufferとTypedArray(Uint8Arrayとか)の関係、さらにはFileReaderとFile/Blobの関係。このあたりは鬼門です。なんでこんなめんどくさいんだよ!っていつも思います。

MDNとかのリファレンスを読むのが手っ取り早いのですが・・・とりあえず、MDNのサイトでは、こういう場合は、こうする、という「お約束」の手順が書かれているので、まずそれを丸覚えするのがいいと思います。公文式です(笑) 理屈は後から学べばいいんです。ただ、ググってブログ記事を参考にするのは結局は理解するのが遅くなってしまうので、リファレンスとサンプルを読んで、書いて、試してみたほうがいいと思ってます。

※ たぶんIEでは動かないと思う。試してないけど。IEは既にMSも認めたオワコンなんで、どーでもいい。

下のデモ(テストコード)

さて、HTMLファイルをサクッと書きます。(Ryzenの価格表コピペしました。)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style type="text/css">
      table,th,td {
        border-collapse: collapse;
        border: 1px solid #aaa;
      }
      th,td {
        padding: 5px;
        text-align: center;
        font-size: 85%;
      }
      th { 
        background-color: #f6f6f6;
      }
    </style>

  <body>
    <h2>sheet-js サンプル</h2>
    <table id="table-0">
      <tr>
        <th>モデルナンバー</th>
        <th>プロセスルール</th>
        <th>コア/スレッド数</th>
        <th>TDP</th>
        <th>周波数(ブースト時/ベース)</th>
        <th>合計キャッシュサイズ(MB)</th>
        <th>GPU</th>
        <th>PCIe 4.0 レーン(X570利用時)</th>
        <th>店頭予想価格(税別)</th>
        <th>提供開始時期</th>
      </tr>
      <tr>
        <td>Ryzen 9 3950X</td>
        <td>7nm</td>
        <td>16/32</td>
        <td>105W</td>
        <td>4.7/3.5GHz</td>
        <td>72MB</td>
        <td>-</td>
        <td>40</td>
        <td>不明</td>
        <td>9月</td>
      </tr>
      <tr>
        <td>Ryzen 9 3900X</td>
        <td>7nm</td>
        <td>12/24</td>
        <td>105W</td>
        <td>4.6/3.8GHz</td>
        <td>70MB</td>
        <td>-</td>
        <td>40</td>
        <td>59,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 7 3800X</td>
        <td>7nm</td>
        <td>8/16</td>
        <td>105W</td>
        <td>4.5/3.9GHz</td>
        <td>36MB</td>
        <td>-</td>
        <td>40</td>
        <td>46,980円</td>
        <td>7月7日</td>
      </tr>
      <tr class="y5 odd">
        <td>Ryzen 7 3700X</td>
        <td>7nm</td>
        <td>8/16</td>
        <td>65W</td>
        <td>4.4/3.6GHz</td>
        <td>36MB</td>
        <td>-</td>
        <td>40</td>
        <td>39,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3600X</td>
        <td>7nm</td>
        <td>6/12</td>
        <td>95W</td>
        <td>4.4/3.8GHz</td>
        <td>35MB</td>
        <td>-</td>
        <td>40</td>
        <td>29,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3600</td>
        <td>7nm</td>
        <td>6/12</td>
        <td>65W</td>
        <td>4.2/3.6GHz</td>
        <td>35MB</td>
        <td>-</td>
        <td>40</td>
        <td>23,980円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3400G</td>
        <td>12nm</td>
        <td>4/8</td>
        <td>65W</td>
        <td>4.2/3.7GHz</td>
        <td>6MB</td>
        <td>Radeon RX Vega 11</td>
        <td>-</td>
        <td>18,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 3 3200G</td>
        <td>12nm</td>
        <td>4/4</td>
        <td>65W</td>
        <td>4/3.6GHz</td>
        <td>6MB</td>
        <td>Radeon RX Vega 8</td>
        <td>-</td>
        <td>11,800円</td>
        <td>7月7日</td>
      </tr>
    </table>
        
    <p>
      <label for="select-file">テーブルをエクセルファイルに追加します。</label>
      <input type="file" id="select-file">
    </p>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.14.3/xlsx.full.min.js"></script>
    <script type="text/javascript" src="./index.js"></script><!-- 下記 javascriptコード -->
  </body>
</html>

JSライブラリは適当にCDNから引っ張ってきましょう。僕は jQuery好き好き人間なので、jQueryを使用します。すみません。
INPUT[type=file]タグでローカルファイルの口としましょう。ドラッグ&ドロップを仕込んでもいいのですが、コードをカンタンにするため、普通のファイル選択にしました。ここでローカルファイルを選んでjavascriptコードに放り込みます。

で、次に実際の処理を書いていきます。まず、基本。INPUT[type=file]のonchangeイベントハンドラを起点にしています。(13行目付近)
ローカルファイルを読み込んでゴニョゴニョするときは、必ずFileReaderのインスタンスを作って、onloadイベントで処理を行います。(18行目付近)
下記例では、FileReader.readAsArrayBuffer() していますが、単純に Data URIが必要であれば FileReader.readAsDataUrl() を使用します。
ローカルの画像ファイルを読み込んで表示するときは、readAsDataUrlメソッドを使いますよねぇ。

ちなみにreadAsArrayBufferメソッドを使うと、onloadイベントハンドラ内で ev.target.result によってArrayBufferオブジェクトを得ることができますが、直接このArrayBufferオブジェクトにアクセスすることができません。必ず、TypedArray・・・たとえば、Uint8Arrayなどのオブジェクトのインスタンスからアクセスします。めんどくさいですねぇ。

/*******************************************************************************
  filename:  index.js
 
  description: 
   ローカルのエクセルファイルを選択すると(<input type="file" id="select-file">、
   選択したエクセルファイルへシートを追加し、テーブル要素(<table id="table-0">)を書込み、
   そのエクセルシートをダウンロードするためのリンクをdocument.body に追加します。

   ※このままのコードだと、セルの書式属性は全部消えます。
*******************************************************************************/
(function($) {

  $('input#select-file[type=file]').on('change',function(ev) {
    var files = this.files;
    var f = files[0];
    var reader = new FileReader();

    reader.onload = function(e) {
     
      // 読み込んだエクセルファイル(ArrayBuffer)をUint8Array配列にし、XLSX.readに渡します。
      var data = new Uint8Array(e.target.result);
      var workbook = XLSX.read(data, {type: 'array'});

      // 上記 HTMLのテーブルを table_to_sheetメソッドに渡しエクセルシート(ブック?)を作って、
      var new_workbook = XLSX.utils.table_to_sheet( $('#table-0').get(0) );
      
      //読み込んだエクセルに上記テーブルを変換したシート(ブック?)を新しいシートとして追加します。
      XLSX.utils.book_append_sheet(workbook, new_workbook, 'Ryzen price');

      // 新しく作成するエクセルファイルの作成オプションを設定します。
      var options = {
          bookType: 'xlsx',
          bookSST: false,
          type: 'array',
          compression: true
        };
      
      // 上記オプションを使って Blobオブジェクトに出力します。
      var blob = new Blob(
        [XLSX.write(workbook, options)],
        {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
      );

      // Blobオブジェクトをダウンロードさせるための仮想的な?URLを作って A要素をdocument.bodyに追加します。
      // クリックすると、読み込んだエクセルファイルに表をシートに追加し、新しく作ったエクセルファイルをダウンロードできます。
      $('<a>')
        .attr({'href':window.URL.createObjectURL(blob),'download': 'シートを追加したエクセルファイル.xlsx'})
        .appendTo(document.body)
        .text('シートを追加したエクセルファイル.xls');
    };
    reader.readAsArrayBuffer(f);
  });

})(jQuery);

FileReader.onload内で、ev.target.result を そのまま Uint8Arrayコンストラクタに渡して、ArrayBufferへアクセスするための Uint8Arrayオブジェクトを作り(21行目付近)、それを XLSX.readメソッドに渡します。ただそれだけで、エクセルファイルをパースしてくれます。あとは、XLSX.utils オブジェクトのメソッドをコールしていけば、だいたいのことはできると思います。
CSVデータが欲しければ、XLSX.utils.sheet_to_csv, JSONデータが欲しければ XLSX.utils.sheet_to_json、シートを増やしたければ、XLSX.utils.append_sheet、他にも javascript配列をシートに、DOM TABLE要素をシートに・・・とか対応するメソッドがあるようです。この辺のドキュメントはgithubをたよりに試行錯誤するしかないのかな。。。

エクセルファイルにシートを追加して、そのエクセルファイルをダウンロードする、これだけのことをクライアント内(ブラウザ内)で完結することができてしまいます。
ローカル内でサーバーを立てる必要もありません。上記二つのHTMLとJSを任意のフォルダに適当な名前で保存して、file://スキームで試すことも可能です。

(1) HTMLファイル ローカルで開いたところ。

(2) エクセルを選択すると、ダウンロードリンクが追加されます。

(3) 元のエクセルはいつもダミーで使わせてもらってる疑似会社情報のエクセルファイル
( http://hogehoge.tk/personal/ )

(4) HTMLのテーブルデータをエクセルシートにぶっこんでくれます。
ただしセル書式は全部吹っ飛びます。

今回は、エクセルシートを追加してみましたけど、セルの属性とかは全部ぶっとんでしまいました💦
が、単にJSONやCSVが取れればいい、というのが大半の需要かと思いますので問題はないでしょう。

僕は エクセルファイルをサーバー側で変換するのではなく、クライアントで一旦CSVに変換してから、サーバーにアップロードして、処理を行う用途に使用しました。サーバーでエクセルファイルを処理すると重くなるんで・・・。
エクセルを読むだけなら、カンタンにできるので、本当にありがたいライブラリです。

ただ一点、ちゃんとしたリファレンスドキュメントがありません。。。これはPro版を買えってことなのかなぁ。。。Community版はApache License 2.0なんで、「おまえら、ソース読んで、自分でなんとかしろよ」ってことなんでしょうね。

hasOwnPropertyがない

Internet Explorer 8でチェックしてたら、window.hasOwnProperty()のところでエラーが発生。あれ?

自分の無知が恥ずかしい。。。

MSDNドキュメントをチェックしてみると、なんかドキュメントがええかげん。.hasOwnPropertyの説明では確かにIE8はサポートされない、と書いてあるんだけど・・・javascriptのバージョン情報のところを見ると、hasOwnPropertyは’Y’になっとる。

だけど、Object.hasOwnProperty はエラーは起こらず。どゆこと?

結局、IE8の時だけ、下記のようにする。

// これってもしかして、常識なの???(^^;;;
var undefined;
if(window.hasOwnProperty === undefined)
{
  window.hasOwnProperty = function(property_name)
    {
       return Object.prototype.hasOwnProperty.call(window,property_name); 
    };
}

なんか、釈然としない。はやくIE8消えてくれ。つか、会社のPC、いいかげん、XPから7にバージョンアップさせてほしい・・・自分で金出すから。

packer(.NET版)を勝手に改造

これ、すごい。こんなスマートなビルの壊し方があるんすね。。。ビル爆破より地味だけど、なんかお金かかりそう。
http://www.bbc.co.uk/news/world-asia-21406927

それはともかく。

javascriptコードを圧縮するのに packer (http://dean.edwards.name/download/#packer) の.NET版をよく使っています。
が、.NET版は(たぶん)メンテされておらず?ソースコードも .NET Framework 1.0時代のものなので、見栄えがかなり悪いし、テキストボックスの入力が32KBに制限されていたり、とちょいと不満がくすぶっていました。
最近Javascriptコードを書くことが多くなって、頻繁に使い出すと、どうもこのUI自体が(僕にとっては)使いにくい。

で、あまり見た目を触らずボタン類の配置、ビジュアルスタイルの適用などを含めて改造。
自分用の書庫の意味もかねているので、ここにアップロード。

左がオリジナル。右がUI改造版
jspacker myjspacker

改造点は、

  • ボタン配置・レイアウトを変更した。
  • ビジュアルスタイルを適用するようにした。
  • ラベルを日本語化した。.configファイルでラベル編集可能。
  • テキストボックスの32KB制限を外した。
  • ファイルからの読込(Loadボタン)時に文字コードを判別するようにした。
    (文字判定にInternet Explorerのモジュールを使っているのでIEを削除している方(いないと思うけど)の環境では文字判定できません

ユーザーインターフェイスの部分(Visual C#のIDE上で編集できる部分)のみ修正したのでpacker本体のアルゴリズムには一切タッチせず。packer自体のバージョンは上がっていると思うので、たぶん古いアルゴリズムのままのはず。分からんけど。

プロジェクトのプラットフォームを.NET Framework 3.5にして、フォームを定義しているソースファイルでIDE機能のデザイナーで編集される部分のコードを分離( partial class化)してます。

<< Download from here if you need.>> もし必要ならダウンロードはこちら
(改造後のVisual C# 2008のプロジェクトソースも同梱してます。)
(なんか、chromeだと、危険だ!とか出てダウンロードできませんが、別になんも仕込んではいないです。)

使い方は・・・説明しなくても・・・(^^;;;
ラベルを英語にしたい場合は、Javascript packer.exe.config ファイルを開いて、

&lt;setting name=&quot;Lang&quot; serializeAs=&quot;String&quot;&gt;
&lt;value&gt;ja&lt;/value&gt;
&lt;/setting&gt;

の中の、ja の部分を ja以外、たとえば、en とかにしてください。
ラベルを修正したい場合は、configファイルの該当箇所を修正してください。ただ、ボタン幅は固定なので長さによってははみ出るかも?

でわでわ。

ID値がグローバル変数に自動追加される

超初歩的なことに、はまってしまった。自分自身の備忘録として残しておこう。

自分で組んだjavascriptコードが chrome で動いたり動かなかったりしたことがきっかけだった。
そのスクリプトは、ブラウザ判定をしてて、chrome、いえ、webkit の検出に以下のようなコードを使用していた。
これ自体は、グーグル検索で辿り着いた、W3G(world wide web guide)というサイトに掲載されていたものを丸々拝借させてもらいました。

// chrome/safari等のオブジェクトを使った判別。
(function(undefined)
{
  var isWebkit = !document.uniqueID &amp;&amp; !window.opera &amp;&amp; !window.sidebar &amp;&amp; window.localStorage &amp;&amp; window.orientation === undefined;
})();

さて、ここで、chromeでアクセスしているにもかかわらず、HTMLファイルによってはtrueになったりfalseになったりして ????という状態で、falseと判定されるHTMLを開いてブラウザのコンソールで、調べていったら、 !window.sidebarfalseになることが判明。

これでピンときた(思い出した)。

あ!、HTMLファイルのある要素(DIVタグ)に id=”sidebar” って属性をつけてるわ・・・。

とほほ。ご存じのように、タグのid属性に指定した値は、グローバル変数に(厳密に言うと、windowオブジェクト)に追加されるんだった・・・orz

いや、確かに昔、Internet Explorer が中心だったころ、よくこれでラクしてたんだよな・・・最近は、document.getElementByIdというDOMメソッドを使用することがほとんどだったので忘れてた。でもIEならともかく・・・webkitでこれに直面するとわおもわなんだ。

結局、元になるHTMLファイルから、sidebar という名のid属性値を、違う値に全部変更することで一件落着。

これから、タグにid属性を含めるときは、必ずハイフンを使用した値にするようにしよう・・・と固く誓うのであった・・・。
というより、こんなIEの独自仕様は端っからwebkitから消し去ってくれればいいのに・・・。

iframeのloadイベント 【書き直し】

今月の初めに書いた投稿が、完全に勘違いだったので、削除して改めて書き直し。

jQuery(1.7.2)でiframeを動的に生成して、そのloadイベントを書いてたときに、どうしても理解できないことが一つあります。
たぶん、僕が無知なだけかもしれませんが・・・

// &lt;a id=&quot;test&quot; href=&quot;http://somewhere.url/&quot;&gt;iframeにリンク先をロードさせる&lt;/a&gt;
// というような、HTMLがあって、

;(function($)
  {
    $('a#test').click(function(ev)
                      {
                        ev.preventDefault();
                        ev.stopPropagation();
                                          
                        //クリックされたアンカータグのhref属性を保存。
                        var href = this.href;
                        
                        //デバッグ用のカウンタ
                        var count = 1

                        //iframeを生成して、そこに読み込ませます。
                        var $iframe = $(document.createElement('iframe')).appendTo(document.body);
                        
                        //onloadイベントでチェック
                        $iframe.load(function()
                                    {
                                      console.log([count++,this.src]);
                                    });

                        //普通に読み込むと、ログには [1,'http://somewhere.url/'] が表示されます。
                        $iframe.attr('src',href);
                        
                        //window.setTimeout(function(){$iframe.attr('src',href);},100);
                        //しかし、IEやFireFoxだとsetTimeoutメソッドで強制的に非同期で読み込ませると、
                        //ログには・・・
                        //---------------------------
                        //[1,'']
                        //[2,'http://somewhere.url/']
                        //---------------------------
                        //と表示され、2回コールされています。
                        //しかし、safariやchromeなどのwebkit系のブラウザでは1回しかコールされません。
                      });
  })(jQuery);

単純にsrc属性を設定してロードした場合だと、どのブラウザでも1回しかコールされない。
だけど、setTimeoutを使用してロードを遅延(非同期)実行させると、IE/Mozilla系ブラウザは2回コールされる。
ただ、タイムアウトミリ秒を1とか10とかのようにすると1回しかコールされないときもある。

よく分からない。そういうものなの???