ブラウザでCSV(CP932)⇔XLSXの相互変換

極力サーバーサイドでExcelファイルを処理させたくなくて、(なんちゃって)フロントエンド開発をワチャワチャやっております。

データベースからデータを抜き出すのも、Excelファイルだし、データベースにインポートするデータもExcelファイルです。 もうエクセルを見るのも嫌になってます。

サーバーサイドのプログラムを組む時、データをJSONもしくはCSV形式で扱うと、書くのがラクになります。そこで、ブラウザ上で、CSVとXLSXを相互変換できれば便利だな、と思ったのがきっかけ。

例によって、node.jsモジュールを browserify でワンパッケージにして利用します。(バンドラーって呼ぶみたいです。よく分からん)今回は npmコマンドで exceljs・encoding.js・file-saver の三つのモジュールを取得して、browserify で一つのファイルにパックします。

exceljs は、Excel(xlsx)ファイルを扱うモジュール。ExcelファイルをJavaScriptで扱うライブラリは SheetJS が有名で xlsxだけじゃなくて旧型式のxlsファイルも読むことができて非常に便利なのですが、Pro版じゃないと制限が多いので、最近では ExcelJSの方を使っています。

encoding.js は JavaScript でUTF-8,ShiftJIS(CP932),UTF16などを相互に変換できるライブラリです。ShiftJISのCSVファイルを扱う時に使用します。

file-saverは・・・説明要らないですよね💦 ダウンロードできるようにするヤツです。


動作デモはこちら


手順はいつもどおり。
まずは、package.json 。

今回は 3つのモジュールを一つのファイル(bundle.min.js)にまとめて、実際に使用するときは、そのファイル内(bundle.min.js)に定義されているrequire関数でロードするようにします。

上記package.jsonを適当なディレクトリにおいて下記を実行して、bundle.min.js を生成しておきます。

>> npm install
>> npm run bundle

この bundle.min.js を下記のサンプルのHTMLファイルのように、scriptタグでロードしておきます。

そして・・・肝心の 変換コードは下記のようになります。

特筆すべき点はありませんが・・・encoding-japaneseモジュールで、ShiftJISに変換した後、ShiftJISに変換したものを保存するとき、単純 string型でBlobのコンストラクタに渡すと、再変換されて文字化けを起こしてしまいハマってしまいました。
イマイチあんまり分かってません。

CSVの行区切り、本来は・・・CRLF(0x0D0A)・・・ですよね・・・。workbookからCSVにするとき、worksheetを行毎にループして変換したあと、自前で join(‘\r\n’)とかすればいいんでしょうけど・・・。
workbook.csv.writeBuffer()する際、formatterOptionでrowDelimiterで改行コードを指定すればいいみたい。

フロントエンド、メンドクサイし、ムズカシイし、あんまりやりたくない。。。


動作デモはこちら


PHPでのExcel吐き出しCSVファイルの処理

何度となくハマったPHPでのCSV処理・・・もういやだ。
最初は、fgetcsv で setlocaleし忘れでハマり、改行を含むセルでSplFileObjectを知り、SJIS-WIN でハマり・・・もういやだ。

もうCSVで涙目になるのは嫌なので、一個クラスを作る。

要はCP932エンコードされたCSVファイル用の SplFileObject が欲しい!ってことなんですけどね。
マルチバイト用のSplFileObject、mb_SplFileObjectみたいなもの標準で入れてくんないのかな・・・。

ってなわけで?、
SplFileObjectから派生したクラスを定義。ついでに、read / readAll / each メソッドを追加。これは自己満。
コンストラクタでCP932なCSVファイルを引き受けて、UTF-8に変換した作業用ファイルを作成して、デストラクタで消去。
はじめは file_get_contentsで一気に変換しようかと思ったが、巨大サイズのファイルを渡されるとmemory_limitに引っかかるので・・・(^^;
/usr/bin/nkf とか /usr/bin/iconv とかに丸投げしようかと思ったけど、Windows環境だとメンドーだし。

とりあえず、 jQuery のeachみたいな感じのものが欲しかったので・・・

クラス作るほどのものじゃないんだけどなー・・・・

Text::CSV_XS::version を確かめなさい・・・

全県の郵便番号CSV(ZIP書庫)ファイルを日本郵便のサイトからダウンロードして、解凍して、SQLiteのテーブルへぶち込む、一連の流れをperl で書いた。

そもそもの発端は、メールフォームや会員登録フォームなどで住所を入力する手間を省くため郵便番号から自動的に住所が挿入される、よくある仕組みを実装するため、変換のベースとなるCSVデータをSQLiteにインポートすれば、SELECT一発で引けるやん、という訳。

ここで、上記のCSV => SQLite にインポートするため、Text::CSV_XSモジュールを使った。まぁ、それが当然ですよね。このスクリプト自体はすぐ出来たんですが、作成・実行をWindows7上のActivePerl 5.14.1 で行った。なんら問題ない。

#CSVからレコードを読込データベーステーブルに登録する。
# $dbh   :DBIハンドル,
# $table :テーブル名
# $fin   :CSVファイルのIOハンドル
# utf8,h2z_,sjis は 文字コード変換のために定義した関数
# eval_result はエラー処理のために定義した関数
sub csv2db
{
  my ($dbh,$tablename,$fin) = @_;
  my $csv = Text::CSV_XS->new({binary => 1,eol => $/});

  eval
    {
      my $sth = $dbh->prepare(sprintf('insert into %s values(?,?,?,?,?,?,?,?,?)',$tablename));
      $dbh->begin_work;

      my $count = 1;
      while(my $row_ = $csv->getline($fin))
        {
          $row_->[1] =~ s/\s+$//g;
          if(1 != $sth->execute($row_->[0],
                               $row_->[1],
                               $row_->[2],
                               utf8(h2z_($row_->[3],'cp932')),
                               utf8(h2z_($row_->[4],'cp932')),
                               utf8(h2z_($row_->[5],'cp932')),
                               utf8($row_->[6]),
                               utf8($row_->[7]),
                               utf8($row_->[8])))
            {
              print "failed to insert...row($count)\n";
            }

          if((my $c = $count++) % 10000 == 0)
            {
              $dbh->commit;
              print sjis("${c}件登録されました。\n");
              $dbh->begin_work;
            }
        }
    };
  eval_result('insert data successfully.',sub{$dbh->commit;},sub{$dbh->rollback;});
}

で、このコードを含む一連のスクリプトを、テストサーバー(CentOS6 / perl 5.10 ) で実行すると・・・

Wide character in subroutine entry…

でました・・・。やな、メッセージ(;゚ロ゚)

半角カタカナから全角カタカナの変換がおかしいのかなー・・・なんて見当違いな事をいろいろ試してました。というのも、ちゃんと変換できてるレコードもあるので・・・。ちゃんとSQLiteへインポートできたレコードと上記エラーメッセージが出たレコードを比較しても全然分からん。

で、最終的に、分かったのが・・・CSVからレコード配列に変換するときに変なことが起こってるみたい・・・というのが判明して、Text::CSV_XSモジュールを使わず単純にsplitを使うと、あっけなくインポート終了。なぜ???

最終的に、Text::CSV_XSのバージョンが違っていたことが原因と判明。

Windows上のActivePerlにバンドルされていたバージョンは、”0.82″。 CentOS上のperlにバンドルされていたバージョンは”1.19″。どうやら新しい方のバージョンは、Text::CSV_XS->new({…, decode_utf8 => 0})というようにdecode_utf8属性?を無効にしないと、UTF8フラグが付いてしまう?らしい。この辺、まだよく分からん。

こんな、perldoc Text::CSV_XS をちゃんと読めば解決できたであろう、しょーもないことに丸2日もかかっちまったよ。トホホ。。。