-
React & Node.jsの組合せによるCSVファイル取込(機種依存文字対応)
- 2021年8月27日
- CSV
- Node.js
- React
最近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アップロードにお困りの方の助力になれば幸いです。
さいごに
ソリューションウェアでは、さまざまな分野の案件を幅広く持ち合わせておりスキルアップには最適の環境です。自身のスキル向上に悩んでいる方、エンジニアとしてもう一皮むけたいと考えている方、一緒に私達と働きませんか?お気軽に一度ソリューションウェアにご相談ください。
参考
資料を公開してくださっている方々に感謝です。
この記事を書いた人 : 児嶋寛通
AWS bluebird css CSV docker docker compose electron ES6 es2015 Git Heroku ITコンサルティング JavaScript justinmind less mongoDB Node.js php PostgreSQL Private Space Promise React react-router reactjs Salesforce scss Selenium Builder selenium IDE Selenium WebDriver stylus TypeScript VirtualBox VisualStudioCode vue vuejs webpack システム開発プロジェクト セキュリティ ワイヤーフレーム 上流工程 卒FIT 帳票 要件定義 設計 電力小売業界