令和の時代に、JavaScriptで Shift-JISファイル作成 全銀データフォーマットに対応する。
2020.03.03
この記事は最終更新日から1年以上が経過しています。
どもです。
いやぁ。
世間は新型コロナウィルスで大変ですよね。
当方も、先週頭からリモートワークに切り替わり、正に今から感染が広がっていくのだろうなぁ。と感じております。
後手後手で、グダグダで無茶苦茶な対応で、国内外でかなりの政権批判が発生している状況ですが、
まぁ仕方ないですよね。現政権だと。。
何かを期待する方がおかしいぐらいになっていますもんね。
おかしいなぁ。と思うことは大事ですよね。
それから色々と調べて行くと色々と見えることありますよね。
という事で、時事に関してはこのくらいで今回は、久々に文字コードと戦った事もあってその備忘録でもと。
いいですよね。
この令和の時代に、 Shift-JISを扱うなんて。
時代を遡った感じあって。
でも、文字コードと戦うのがエンジニアぽくて良いですよね。
Shift-JISファイル作成・ダウンロード
今回、CSVファイルのダウンロードと、全銀データフォーマット形式のファイルの
JavaScriptでファイル生成、ダウンロードの方法を記載しているのですが、
ファイル生成、ダウンロードの方法を先にコードを知りたい方はこちらとなります。
ファイルダウンロードを行う関数
export const downloadAsFile = (data) => { const BLOB = new Blob([data.content], { type: data.type || 'text/plain' }) const CAN_USE_SAVE_BLOB = window.navigator.msSaveBlob !== undefined if (CAN_USE_SAVE_BLOB) { window.navigator.msSaveBlob(BLOB, data.fileName) return } const TEMP_ANCHOR = document.createElement('a') TEMP_ANCHOR.href = URL.createObjectURL(BLOB) TEMP_ANCHOR.setAttribute('download', data.fileName) TEMP_ANCHOR.dispatchEvent(new MouseEvent('click')) }
UTF-8文字列をShift-JIS文字列に変換する関数
import Encoding from 'encoding-japanese' toShiftJIS(utf8String) { const detected = Encoding.detect(utf8String) const unicodeList = [] for (let i = 0; i < utf8String.length; i += 1) { unicodeList.push(utf8String.charCodeAt(i)) } const sjisArray = Encoding.convert(unicodeList, { to: 'SJIS', from: detected }) return new Uint8Array(sjisArray) }
と、言った感じで、主要な関数を2つ先に挙げました。
CSVファイル ダウンロード
Google スプレットシート用に、CSVファイルを作成しダウンロード出来るようにする必要があったのですが、
データ自体はAPIでJSONで受け取り、それをCSV形式に変換。
そして、ファイル生成しダウンロード。
と言った流れになります。
CSV形式にするのは、カンマ区切りにすれば良いだけなので、以下の様にカンマ区切りに文字列にすれば取り敢えずはオッケーですね。
getCSVdata関数
getCSVdata() { return `${this.id},${this.ampseq},${this.transfee},${this.ct},${this.ut}...` }
上記でも挙げさせて頂いた様に、ファイルダウンロードを行う関数を見ていきましょう。
ファイルダウンロードを行う関数
export const downloadAsFile = (data) => { const BLOB = new Blob([data.content], { type: data.type || 'text/plain' }) const CAN_USE_SAVE_BLOB = window.navigator.msSaveBlob !== undefined if (CAN_USE_SAVE_BLOB) { window.navigator.msSaveBlob(BLOB, data.fileName) return } const TEMP_ANCHOR = document.createElement('a') TEMP_ANCHOR.href = URL.createObjectURL(BLOB) TEMP_ANCHOR.setAttribute('download', data.fileName) TEMP_ANCHOR.dispatchEvent(new MouseEvent('click')) }
まず、引数のdataは「fileName」「type」「content」を持ったオブジェクトとなります。
fileName: ダウンロードする際のファイル名
type: MIME タイプ
content: データ中身
と言った感じで受け取ります。
引数から「content」と「type」を受け取って、Blob関数を用いてBlobオブジェクトを生成。
その後、navigator msSaveBlob APIの存在の有無を確認します。
msSaveBlobはIEに用意されているAPIなので、IEの場合をこれらを用いてファイルダウンロードを実行。
それ意外は、URL.createObjectURLでURLを生成し、a要素を生成し擬似的にクリックしてダウンロードさせるといった処理になります。
回してダウンロード
let textData = `${headerString}\n` targetList.forEach(target => { textData += `${target.getCSVdata()}\n` }) downloadAsFile({ fileName: `${fileName}.csv`, type: 'text/csv', content: textData })
CSVヘッダーも付与したい場合を想定しました。
上記のtargetは、getCSVdata関数をメンバに持ったインスタンス想定です。
必要な分だけ回して、CSV形式のstringに変換し、変数textDataに追加し、上記のダウンロード関数を実行しファイルを生成。
と言った流れになります。
特に、文字コード指定なければ簡単にCSVファイルを生成、ダウンロード出来るかと思います。
全銀データフォーマット ダウンロード
さて、本題はここからです。
今回、全銀データフォーマット形式のファイルをJavaScriptで作成し、ファイルダウンロードまで行おうといった内容になります。
全銀データフォーマットとはなんぞや?
という方はこちらを御覧ください。
全銀データフォーマット
http://www.hyogokenshin.co.jp/wp-content/uploads/format1.pdf
全銀協規定フォーマットとは、全国銀行協会連合会がデータ伝送を行うために定めたフォーマットになります。
簡単に説明すると、銀行振込を行うための振り込みデータ形式。といったところでしょうか。
上記の形式に作成しないとすぐエラーとなってしまいます。
大まかにポイントは、
・ヘッダー、データ、トレーラー、エンドレコードのデータを作成する
・各レコードは120文字(バイト)で作成する
・各レコードデータはバイト数が決まっている
・半角カナ文字を使用
・Shift-JIS
来ましたねぇ。Shift-JIS。
もう会うことないと思っていたのに、令和の時代でこんにちは。ですね。
それでは、早速各レコード生成の関数を見ていきましょう。
各レコード
createHeaderRecord(data): string { return `1210${data.consignorCode}${consignorName}${data.transferSpecifiedDate}${data.bankCode}${data.bankName}${data.bankBranchCode}${data.bankBranchName}${data.accountNo} ` } createDataRecord(data): string { const bankCode = data.bankCode const bankName = data.bankName.padEnd(15, ' ') const branchCode = data.branchCode const branchName = data.branchName.padEnd(15, ' ') const accountTypeNo = data.accountTypeNo const accountNo = data.accountNo const accountHolder = data.accountHolder.padEnd(30, ' ') const transferAmount = data.transferAmount.padStart(10, '0') return `2${bankCode}${bankName}${branchCode}${branchName} ${accountTypeNo}${accountNo}${accountHolder}${transferAmount}1 ` } createFooterRecord(remittanceList): string { const totalLength = remittanceList.length const totalLengthString = String(totalLength).padStart(6, '0') const totalPrice = sumBy(remittanceList, 'withdrawAmount') const totalPriceString = String(totalPrice).padStart(12, '0') const endEmptyString = ''.padEnd(101, ' ') return `8${totalLengthString}${totalPriceString}${endEmptyString}` } createEndRecord() { return '9'.padEnd(120, ' ') }
まず、ヘッダーレコードを生成する「createHeaderRecord関数」。
こちらは、動的でないプロパティを想定していますので、そのまま設定された値をstringで返却します。
エンドレコードを生成する「createEndRecord関数」も同様ですね。
表示は、9のみで1行120文字のstringを返却。
そのような感じで、「createHeaderRecord関数、createEndRecord関数」は、ちょっと割愛させてもらいます。
で、大事なのは、「createDataRecord関数」。
データの数だけ呼び出しを行い、レコードを作成しております。
データレコードは2始まりで、ところどころスペースがありますが、データを埋めるためのスペースで、
emptyStringとして、変数を作成しても良いですね。
銀行データに関するものなどは、ECMA2017のAPI、padStartとpadEndでゼロパディング&スペース埋めを行いデータを作成しております。
トレーラーレコード生成の「createFooterRecord関数」は、各振り込みデータ数や振込額合計となっております。
sumByは、lodash使用しております。
といった訳で、こちらを利用してデータを作成。
そして、上記のダウンロード関数でファイルをダウンロード。
これで、行けただろう。と試したところ。
エラー1行120バイト
なんと。。。
何が原因だ?調べてみるところ、改行コードでは?
と行き着いた。
文字コードと改行コード
OS | 文字コード | 改行コード | 改行コードの大きさ |
---|---|---|---|
Windows | シフトJIS | CR+LF | 2バイト |
Mac OS | シフトJIS | CR | 1バイト |
Mac OS X | EUC | LF | 1バイト |
UNIX | EUC | LF | 1バイト |
全銀データフォーマットは、最後2バイト改行。
じゃないと行けないようだ。。
うーん。。確かに。
1バイトで改行を行っていたので、修正。
'\n' → '\r\n'
よし!
これで大丈夫だろう!!
とテストを行ってみると。
またしても、
エラー 1行120バイト
ファ。
生成されたデータを目視で見てみると、うん??
ハイフンがなんか変な気がする。。
そう。
銀行コードには、「zengin-code」のパッケージを使用していて、
github
https://github.com/zengin-code
「半角カタカナ」変換には、「moji」のパッケージを使用しているのですが、
github
https://github.com/niwaringo/moji
(こちら、最後の修正が npm publishされていないところを注意。)
「zengin-code」の 「ミツビシユ-エフジエイ」がどうも変な気がする。。
調べたところ、ASCIIのハイフンということがわかる。
と、同時にこんなにハイフンあったのかよーーーと。
文字 | UTF-8 | Unicode | 説明 |
---|---|---|---|
– | 2D | U+002D | ASCIIのハイフン |
ー | E383BC | U+30FC | 全角の長音 |
‐ | E28090 | U+2010 | 別のハイフン |
‑ | E28091 | U+2011 | 改行しないハイフン |
– | E28093 | U+2013 | ENダッシュ |
— | E28094 | U+2014 | EMダッシュ |
― | E28095 | U+2015 | 全角のダッシュ |
− | E28892 | U+2212 | 全角のマイナス |
ー | EFBDB0 | U+FF70 | 半角カナの長音 |
参考ページ(助かりました)
https://qiita.com/ryounagaoka/items/4cf5191d1a2763667add
なので、以下の様に「moji」を使いつつ、拡張して。
これで、問題ないだろう!!
追加
getReplaceHankakuKana(zenkakuKana) { const hankakuKana = moji(zenkakuKana) .convert('ZKtoHK') .toString() return hankakuKana.replace(/(-|ー)/g, 'ー') }
よし。
これでOKですよね!?
エラー 1行120バイト
ファ。
な、な、なんだよ。。。
うん?
そもそも文字コード変換していないなぁ。。
という事で、文字コード、ファイル形式調べてみる。
$ file --mime ./202002_zengin.txt
$ text/plain; charset=utf-8
はい。UTF-8ですよね。
上記のdownloadAsFile関数で、MIME タイプを以下の様に指定してみるが適応されない。
type: 'text/plain; charset=shift_jis', <strong>downloadAsFile関数</strong> downloadAsFile({ fileName: `${formattedDate(remittanceList[0].remittanceDate, 'YYYYMM')}_zengin.txt`, type: 'text/plain; charset=shift_jis', content: this.toShiftJIS(textData) }) downloadAsFile(data) { const BLOB = new Blob([data.content], { type: data.type || 'text/plain' })
うーん。出来ないのか。。一旦コマンド変換することにしました。
iconvコマンド変換
$ iconv -f utf8 -t sjis 202002_zengin.txt > 202002_zengin.txt $ text/plain; charset=iso-8859-1
と、変換してテストを行うと問題なく通りました。
encoding.jsで、Shift-JIS変換
さて、毎度コマンド叩きたくないのでJS(ブラウザ)で、処理は終わらせたい。と。
良さそうなパッケージを探すところ、「encoding.js」パッケージが良さそうなので早速利用してみる。
encoding-japanese
https://github.com/polygonplanet/encoding.js
npmインストール
$ npm install encoding-japanese<
変換できるフォーマット形式は以下のバリエーションの様です。
- 'UTF32' (detect only)
- 'UTF16'
- 'UTF16BE'
- 'UTF16LE'
- 'BINARY' (detect only)
- 'ASCII' (detect only)
- 'JIS'
- 'UTF8'
- 'EUCJP'
- 'SJIS'
- 'UNICODE' (JavaScript Unicode Array)
UTF-8形式から変換しないとな。とか思っていたのですが、
「Encoding.detect関数」で文字コードを確認出来るのですが、確認するところ「UNICODE(JavaScript Unicode Array)」形式なのかよ。
Encoding.detect(textData) UNICODE
では、これで変換。
toShiftJIS(utf8Array) { const detected = Encoding.detect(utf8Array) return Encoding.convert(unicodeList, { to: 'SJIS', from: detected }) }
行けたでしょ。?
と思っていたら、MIME タイプ 文字コードも変換されず、文字化けする始末。
$ text/plain; charset=utf-8
およよ。。
と、ふと考えるとこれは、一文字ずつ変換する必要があるな。。
ということに気がつく。
というわけで、1文字単位で変換することにする。
また、charCodeAt関数でUTF-16 コードユニット形式に変換し、配列に格納。
Encoding.convertで、SJISフォーマットに変換。
ES2017のAPIである、Uint8Arrayで、負の数なし整数(Unsigned int)の配列のバイナリ変換。
toShiftJIS(utf8Array) { const detected = Encoding.detect(utf8Array) const unicodeList = [] for (let i = 0; i < utf8Array.length; i += 1) { unicodeList.push(utf8Array.charCodeAt(i)) } const sjisArray = Encoding.convert(unicodeList, { to: 'SJIS', from: detected }) return new Uint8Array(sjisArray) }
utf8Array.charCodeAt
[12362, 12414, 12360, 12399, 12354, 12411, 12363, 32, 32, 32, 32, 32, 32, 32, 32,...]
new Uint8Array(sjisArray)
[74, 126, 72, 111, 66, 123, 75]
これで、問題ないでしょ。
変換完了
$ text/plain; charset=iso-8859-1
お!
MIME タイプも変換されました。
でも、iso-8859-1なのですね。
テストするところ。。
オッケーでしたー。
と、長々書いてきましたが、久々 JSで文字コードと戦ったログでした。
ではではぁ。