タグ別アーカイブ: CSV

React & Node.jsの組合せによるCSVファイル取込(機種依存文字対応)

最近ReactとNode.jsの組み合わせによるプロジェクトにおきまして、CSVファイルの取込にてデータ登録するという実装がありました。
当初SJISからUTF-8への変換モジュールを使用しReact側にて実装したのですが、機種依存文字(はしご髙やたて﨑など)の変換ができないという問題にぶち当たりました。
PHPなどではSJIS-winが実装されておりmb_convert_encoding関数などで比較的簡単に変換できていたのですが、Javascriptでは機種依存文字まで対応するための資料が少なく、今回かなりハマったため記事にしてみました。

当初の対応(React側での処理)

当初React側でCSVファイルを取込したらそれを文字コード変換、CSV解析という流れを取っていました。
モジュールはencoding.jsとpapaparseを使用させていただきました。

npm install encoding-japanese
npm install papaparse
    reader.onload = (e) => {
      const codes = new Uint8Array(e.target.result);
      const encoding = Encoding.detect(codes);
      const unicodeString = Encoding.convert(codes, {
        to: 'unicode',
        from: encoding,
        type: 'string',
      });
      Papa.parse(unicodeString, {
        header: false,
        dynamicTyping: true,
        skipEmptyLines: "greedy",
        complete: (results) => {
         ...
          },
        });
      };

encoding.jsは本当に素晴らしい文字変換モジュールで、
papaparseとの組み合わせで高速に処理ができておりましたが
encoding.jsが機種依存文字に未対応でした。
機種依存文字が含まれる文字列を解析した場合、ncoding.detect(codes)にてShift_JISという判定が出せず、Shift_JISをUTF-8で取り込みしてしまうという事態が発生しました。
強制的にShift_JIS判定を出せば変換はできるわけですが、機種依存文字は「?」となってしまいます。
取り込まれる文字列に機種依存文字が含まれないということであればこちらで完結で十分だと思いますが、今回は不特定ユーザーからのアップロードになるため機種依存文字を対応しなければなりません。

encoding.js – https://github.com/polygonplanet/encoding.js
papaparse – https://www.npmjs.com/package/papaparse

どうもいろいろ調べてみたところReact側での機種依存文字を含めた変換はできなさそう。。
ということで一旦こちらの調査は止め、node.js側での変換を目指しました。

Node.js側での実装を検討してみる

Node.jsでの変換モジュールを探していたところ
node-iconvというモジュールを発見いたしました。ただしこちらもSJIS-winは未対応でしたが、更に調べてみるとどうもiconv-jpというモジュールがありそう。
ということで早速、

npm install iconv-jp

を実行したところ、、サービスが終了している。。orz

とりあえず再度振り出しに戻りnode-iconvを調べてみると、libiconvに対して修正パッチをあてることで対応できるとのこと。
早速試してみました。

まずはiconvモジュールをインストールするのですが、node-moduleに含まれる変換ライブラリ自体に修正パッチを当てるので、localとしてモジュールを持つ形にします。(今回はnode-iconv-2.2.3を使用しました)
package.jsonと同階層にlocal-node-modulesというディレクトリを作成し、その中にiconvのパッケージを置きます。

修正パッチを落としてきます。
libiconv-1.14 日本語パッチを使用しました。

libiconv-1.14 日本語パッチ – http://apolloron.org/software/libiconv-1.14-ja/

depsディレクトリに修正パッチを置き、libconvディレクトリに移動、上記配布サイトに記載の通りにコマンドを実行します。

$ cd libiconv-1.14
$ patch -p1 < ../libiconv-1.14-ja-2.patch

次にインストールです。

npm install iconv file:local-node-modules/node-iconv-2.2.3

local-node-modulesディレクトリのパッケージを指定し、インストールしました。
これでSJIS-win対応の変換モジュール完成です!

変換コードの作成

これがまた色々とハマりポイントが多くかなり苦戦しました。
当初記述したコードがこちら

React側

    const reader = new FileReader();
    const fl = event.target.files[0];

    ...

    reader.onload = (e) => {
        console.log(e.target.result);

        ....

       // e.target.resultをnodejs側にPOST
      
       ...

    };
    reader.readAsText(fl);

Node.js側

const Iconv = require('iconv').Iconv;
const csvParse = require('csv-parse/lib/sync');

...

console.log(string);
const iconv = new Iconv('SJIS-WIN', 'UTF-8//TRANSLIT//IGNORE');
const utfString = iconv.convert(string).toString();
console.log(utfString);

こちらを実行してみたところ

string:��������
utfString:ソスソスソスソスソスソスソスソス

ソスソス、、、orz

ということで再度試したのがこちら、
一旦16進数に変換してしまえば何者にも影響を受けないだろうと考えました。

React側

    const reader = new FileReader();
    const fl = event.target.files[0];
    
    ...

    reader.onload = (e) => {
      console.log(e.target.result);
      var bytes = new Uint8Array(e.target.result);
      let hexstr = '';
      bytes.forEach(value => {
        // 16進数に変換
        hexstr += ('00' + value.toString(16)).slice(-2);
      });
       // hexstrをnodejs側にPOST
      
       ...

    };
    reader.readAsArrayBuffer(fl);
    event.target.value = null;

Node.js側

const Iconv = require('iconv').Iconv;
const csvParse = require('csv-parse/lib/sync');

...

  console.log(hexstr);
  // 文字列に変換
  const string = Buffer.from(hexstr, 'hex').toString();
  console.log(string);
  // SJISデータをUTF-8に変更
  const iconv = new Iconv('SJIS-WIN', 'UTF-8//TRANSLIT//IGNORE');
  const utfString = iconv.convert(string).toString();
  console.log(utfString);

こちらの実行結果がこちら

hexstr:8e9993888ab092ca2c...
string:��������
utfString:ソスソスソスソスソスソスソスソス

またソスソス、、、orz

何がだめなの…?

結論から言うとiconv以外の箇所でtoString()などを使用し文字列への変換をしたのが原因でした。
ここにたどり着くまでの道のりは長かったのですが、よくよく考えてみるとSJIS-WINとUTF-8との変換を担っているのはiconvであって、その他の部分はUTF-8に沿って変換しているだけなんですよね。

ということで再チャレンジ!!

React側は変更なしで16進数でNodejs側に受け渡し

    const reader = new FileReader();
    const fl = event.target.files[0];
    
    ...

    reader.onload = (e) => {
      console.log(e.target.result);
      var bytes = new Uint8Array(e.target.result);
      let hexstr = '';
      bytes.forEach(value => {
        // 16進数に変換
        hexstr += ('00' + value.toString(16)).slice(-2);
      });
       // hexstrをnodejs側にPOST
      
       ...

    };
    reader.readAsArrayBuffer(fl);
    event.target.value = null;

Node.js側

const Iconv = require('iconv').Iconv;
const csvParse = require('csv-parse/lib/sync');

...

  console.log(hexstr);
  // SJISデータをUTF-8に変更
  const iconv = new Iconv('SJIS-WIN', 'UTF-8//TRANSLIT//IGNORE');
  const utfString = iconv.convert(Buffer.from(hexstr, 'hex')).toString();
  console.log(utfString);
hexstr:8e9993888ab092ca2c...
utfString:児嶋寛通

おぉ!行けた!
では肝心の機種依存文字はどうか

hexstr:fbfc93888ab092ca2c6...
utfString:髙嶋寛通

こちらもクリアしました。

何をしたの?

先の通りiconvで変換する前に文字列に戻してしまうとどうしても文字化けが発生してしまうためiconvが文字列以外なら何が対応できるかを調べた結果Bufferからであれば行けそうだというところにたどり着き、.toString()を省いた形になります。

CSVの変換

当初React側にてparseしたものをNode.js側に渡していたのですが、そのままだとmapなどを使用してエンコードしてあげないといけなくなるため、CSVのparseもNode.js側に移しました。

csv-parseのインストール

npm install csv-parse

コード

const csvParse = require('csv-parse/lib/sync');

...

  // CSV parse
  const dataList = csvParse(utfString, {
    columns: false,
    skip_empty_lines: true,
  });

たったこれだけで配列に変換してくれます。
optionのcolumnsはヘッダー行を入れるかどうか、skip_empty_linesは空白行の扱いです。

感想

ここまでたどり着くのにかなりを要してしまったわけですが、文字変換にしても関数1つにしても基礎知識は大事だなと実感いたしました。
機種依存文字まで対応してある資料が少なかったため、javascriptでCSVアップロードにお困りの方の助力になれば幸いです。

さいごに

ソリューションウェアでは、さまざまな分野の案件を幅広く持ち合わせておりスキルアップには最適の環境です。自身のスキル向上に悩んでいる方、エンジニアとしてもう一皮むけたいと考えている方、一緒に私達と働きませんか?お気軽に一度ソリューションウェアにご相談ください。

参考

資料を公開してくださっている方々に感謝です。

Reactで「CSV一括登録機能」 を開発した話

JavaScriptでShift_JISのcsvを読み込む(文字化け対策)

node-iconvで使える文字コードを調べてみた

Node.jsでCSVファイル操作

Node.jsでの文字コードの変換

Node.jsで16進数文字列を文字列に変換メモ

Node.jsで文字コードの自動判別と自動変換

node-iconvでEUCJP-WIN使おうとしてころんだ。