LoginSignup
0
0

More than 1 year has passed since last update.

オリジナル文字コード(UTF-16, JIS8混在)からユニコード(UTF-16)へ変換する文字コード変換プログラムを作ってみました

Last updated at Posted at 2021-07-14

はじめに

 同じような悩みを持つ方がどれだけいるかは全く謎ですが,オリジナル文字コードを変換して,別の環境に移植する必要があったため,その方法を記載します。取り上げる内容は以下のとおりです。

  • 対象は文字コードですが,何らしかのコードを変換するという仕組みとして共通に使用できます
  • 文字コード初心者ならではの躓きを取り上げます
  • サロゲートペアという unicode(ucs2) の範囲を超えてしまった文字を扱います
  • C#(初心者)でスマートに実装しようとしたのですが,途中ライブラリの使用をあきらめています
  • 今後の課題を取り上げます

環境

  • Windows 10
  • VS Code
  • .Net Core SDK 3.1

工数

  • 約1人日(以下,内訳)
  1. 実装・テスト:1/4人日
  2. qiitaへの投稿記載:1/4人日
  3. サロゲートペア変換API(UNICODE符号位置からサロゲートペアへの変換)の捜索:2/4人日
3については結局既存のライブラリを使用して実装することを断念して,計算することにしました。

背景

 以下の理由でシステムの若返りを図りたいと考えています。

  • 技術者が足りないため,保守ができない
  • 開発環境の選択が少ない
  • データの利用が難しい(必ず難解な文字コード変換を挟むことになる)。

今回作成したもの

 下の赤枠の文字コード変換するためのツール
文字コード.png

変換ツールの材料

  • 1バイトデータ(JIS8)を2バイトデータ(BMP)に置換する変換テーブル
.\1ByteT\1ByteToBMP.csv
 :
23,0023
24,0024
25,0025
 :
  • 2バイトデータを2バイトデータ(BMP)又は面02(U+2XXXX)に置換する変換テーブル

【特徴】変換元の環境では,BMP面の一部をサロゲートペア領域として使用しています(固定長データ処理系のため)。

.\2ByteT\2ByteToBMPandSIP.csv
  :
2127,2127
212B,212B
  :
A0C3,2373F
A0C4,23763
  :
  • 項目リスト(固定長データの区切り単位:レコードサイズ254バイト)
引数3
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 変換プログラムは以下のとおりです。

Program.cs
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]の指定が逆になる。)。
BELE.png

ビッグエンディアンデータの読みだしとキー指定
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本立てて同時実行する実装を現在検討しています。
performance.png

追記1

@jzkeyさんのコメントより,サロゲートペアの計算を以下のライブラリで計算できることがわかった。

Char.ConvertFromUtf32での実装
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]));
0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0