はじめに
同じような悩みを持つ方がどれだけいるかは全く謎ですが,オリジナル文字コードを変換して,別の環境に移植する必要があったため,その方法を記載します。取り上げる内容は以下のとおりです。
- 対象は文字コードですが,何らしかのコードを変換するという仕組みとして共通に使用できます
- 文字コード初心者ならではの躓きを取り上げます
- サロゲートペアという unicode(ucs2) の範囲を超えてしまった文字を扱います
- C#(初心者)でスマートに実装しようとしたのですが,途中ライブラリの使用をあきらめています
- 今後の課題を取り上げます
環境
- Windows 10
- VS Code
- .Net Core SDK 3.1
工数
- 約1人日(以下,内訳)
- 実装・テスト:1/4人日
- qiitaへの投稿記載:1/4人日
- サロゲートペア変換API(UNICODE符号位置からサロゲートペアへの変換)の捜索:2/4人日
3については結局既存のライブラリを使用して実装することを断念して,計算することにしました。
背景
以下の理由でシステムの若返りを図りたいと考えています。
- 技術者が足りないため,保守ができない
- 開発環境の選択が少ない
- データの利用が難しい(必ず難解な文字コード変換を挟むことになる)。
今回作成したもの
変換ツールの材料
- 1バイトデータ(JIS8)を2バイトデータ(BMP)に置換する変換テーブル
:
23,0023
24,0024
25,0025
:
- 2バイトデータを2バイトデータ(BMP)又は面02(U+2XXXX)に置換する変換テーブル
【特徴】変換元の環境では,BMP面の一部をサロゲートペア領域として使用しています(固定長データ処理系のため)。
:
2127,2127
212B,212B
:
A0C3,2373F
A0C4,23763
:
- 項目リスト(固定長データの区切り単位:レコードサイズ254バイト)
1B ,1,10
1B ,11,2
1B ,13,2
1B ,15,2
2B ,17,100
2B ,117,60
2B ,177,40
1B ,217,30
1B ,247,7
1B ,254,1
変換プログラム
1 変換プログラムのインターフェースは以下のとおりです。
module名 【引数1:入力ファイル名(固定長)】 【引数2:出力ファイル名(csv)】 【引数3:項目リスト】
2 変換プログラムは以下のとおりです。
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
// CSVファイルの読み込み
string filePath = @".\JUFx_KMJ\JUF1_KMJ.csv";
string filePath2 = @".\JUFx_KMJ\JUF2_KMJ.csv";
string filePath3 = args[2];
string filePath4 = args[0];
string filePath5 = args[1];
var myTable = new Dictionary<string, string>();
var myTable2 = new Dictionary<string, string>();
using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("UTF-8")))
{
while (reader.Peek() >= 0)
{
// 読み込んだ文字列をカンマ区切りで配列に格納
string[] cols = reader.ReadLine().Split(',');
// コメント行を飛ばす
if(Regex.IsMatch(cols[0], "^#")){continue;}
for (int n = 0; n < cols.Length; n++)
{
//For Debug
//Console.Write(cols[n] + ",");
}
// Dictionary生成
myTable.Add(cols[0], cols[1]);
}
}
using (StreamReader reader2 = new StreamReader(filePath2, Encoding.GetEncoding("UTF-8")))
{
while (reader2.Peek() >= 0)
{
// 読み込んだ文字列をカンマ区切りで配列に格納
string[] cols = reader2.ReadLine().Split(',');
// コメント行を飛ばす
if(Regex.IsMatch(cols[0], "^#")){continue;}
for (int n = 0; n < cols.Length; n++)
{
//For Debug
//Console.Write(cols[n] + ",");
}
// Dictionary生成
myTable2.Add(cols[0], cols[1]);
}
}
// 最後まで読み込む
List<string[]> readCsvList = new List<string[]>();
using (StreamReader readCsvObject = new StreamReader(filePath3, Encoding.GetEncoding("utf-8")))
{
while (!readCsvObject.EndOfStream)
{
var readCsvLine = readCsvObject.ReadLine();
readCsvList.Add(readCsvLine.Split(','));
}
}
var recSize = Convert.ToInt32(readCsvList[readCsvList.Count - 1][1])
+ Convert.ToInt32(readCsvList[readCsvList.Count - 1][2]) - 1;
//Console.Write("recSize: " + recSize + "\n");
//Console.Write(readCsvList.Count + "\n");
using (FileStream fs = new FileStream(filePath4, FileMode.Open, FileAccess.Read)) {
using (FileStream fsout = new FileStream(filePath5, FileMode.Create, FileAccess.Write)) {
using (var writer = new BinaryWriter(fsout)) {
// バイナリファイル読み出し
int fileSize = (int)fs.Length; // ファイルのサイズ
byte[] buf = new byte[fileSize]; // データ格納用配列
int readSize; // Readメソッドで読み込んだバイト数
int remain = fileSize; // 読み込むべき残りのバイト数
int bufPos = 0; // データ格納用配列内の追加位置
//マルチレイアウトは許可しない
Debug.Assert(fileSize%recSize == 0);
while (remain > 0) {
readSize = fs.Read(buf, bufPos, Math.Min(recSize, remain));
bufPos += readSize;
remain -= readSize;
}
for(int i = 0; i < fileSize; i++) {
for(int j = 0; j < readCsvList.Count; j++) {
if((i != 0) && ((i%recSize) == 0))
{
writer.Write(Convert.ToUInt16("000A", 16));
}
if((j != 0) && ((i%recSize) == (Convert.ToInt32(readCsvList[j][1])-1)))
{
writer.Write(Convert.ToUInt16("002C", 16));
}
// 1バイトテーブル
if(Regex.IsMatch(readCsvList[j][0], "EBC") &&
((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) &&
((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1)))
{
//Console.Write(myTable[buf[i].ToString("X2")] + "\n");
writer.Write(Convert.ToUInt16(myTable[buf[i].ToString("X2")], 16));
break;
}
// 2バイトテーブル
if(Regex.IsMatch(readCsvList[j][0], "EKJ") &&
((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) &&
((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1)))
{
Encoding utf8 = Encoding.GetEncoding("UTF-8");
int num = utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]);
if(utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]) <= 4)
{
//Console.Write(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] + "\n");
writer.Write(Convert.ToUInt16(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")], 16));
}
else
{
//サロゲートペア計算(ライブラリが見つからなかった)
var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
//Console.Write("codePoint: " + codePoint + "\n");
var codePoint2 = (codePoint - 0x10000) >> 10;
//Console.Write("plane: " + plane + "\n");
var lead = (codePoint2 & 0x3F) | 0xD800;
//Console.Write("lead: " + lead + "\n");
var trail = (codePoint & 0x3FF) | 0xDC00;
//Console.Write("trail: " + trail + "\n");
writer.Write(Convert.ToUInt16(lead));
writer.Write(Convert.ToUInt16(trail));
}
i++;
break;
}
}
}
}
}
}
}
}
実装のポイント(妥協を含む)
1 テーブルのキー読み出しでエンディアン変換
以下のコードでbuf(元データ:ビッグエンディアン)のデータを読み出し,そのままビッグエンディアン順でキーの指定を行っています(リトルエンディアンであれば,buf[i]とbuf[i+1]の指定が逆になる。)。
myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]
2 サロゲートペアの計算ロジックを作成した
2バイトデータが変換テーブルを使用して,面02(U+2XXXX)に変換された場合,UTF-16ではサロゲートペアに更に変換が必要となる。その際,C#で変換ライブラリを見つけることができなかったため,以下の変換を実装しています。
var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
//Console.Write("codePoint: " + codePoint + "\n");
var codePoint2 = (codePoint - 0x10000) >> 10;
//Console.Write("plane: " + plane + "\n");
var lead = (codePoint2 & 0x3F) | 0xD800;
//Console.Write("lead: " + lead + "\n");
var trail = (codePoint & 0x3FF) | 0xDC00;
//Console.Write("trail: " + trail + "\n");
writer.Write(Convert.ToUInt16(lead));
writer.Write(Convert.ToUInt16(trail));
ネット上にはいくつか実装方法があったが,乗算・除算器を使用するものだったため,加減算及びシフト演算のみのコードに書き直しました。また,サロゲートペアは32bitデータになりますが,UTF-16なので16ビットごとにリトルエンディアンによる書き込みが必要です。
今後
計算時間が結構掛かっている割に,以下のようにプロセスエクスプローラーで確認するとCPU,DISKIOは全力を出し切っていません。実際の移行時には,HyperThread OFF にして,ファイルを4分割し,Threadを4本立てて同時実行する実装を現在検討しています。
追記1
@jzkeyさんのコメントより,サロゲートペアの計算を以下のライブラリで計算できることがわかった。
var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
string sg = Char.ConvertFromUtf32(codePoint);
writer.Write(Convert.ToUInt16(sg[0]));
writer.Write(Convert.ToUInt16(sg[1]));