公開日:2022/07/30
title.png

21st Season


#21-10 文字コード変換

 今回のテーマは文字コード変換です。

日本語文字コード

 ベースとすべきは、JIS規格です。

  区点コード表はこちら

 JIS規格は単なる一覧表でしかないので、コンピュータでテキストデータとして取り扱うためには、符号化しなくてはなりません。それが「xxxコード」と呼ばれる符号化方式です。いくつか代表的な符号化方式を以下に示します。

 ここまで全てJIS規格をベースにしており、多少変則的な計算は必要となりますが、機械的に相互変換可能なことが特徴です。各方式、長所・短所があるみたいなので、必要に応じて調べてみてください。
 オススメはS-JISコードなので、以降、S-JISコードを対象として話を進めます。

 この他、最近ではunicodeでも、日本語文字を使うことができます。
 unicode(UTF-8コード)は、JIS規格とは関係のない所で決められた文字コードなので、JIS規格を元にしたS-JIS、EUC-JP等の間で機械的に変換することができません。なんと、数1,000文字の対応表を持っておいて、テーブル参照で変換するしかないのです。
 これは、ナンセンス。

※本ページでは、以降、「コード」を省略して単にS-JISなどと記す場合があります。

unicode

 世界共通で使えるunicodeが主流になりつつあります。

 unicodeは当初、世界中の文字を16ビット(65,536文字)に収録することを目指してスタートしたらしいです。ところが、収録作業を進めていくと全然足りなくて、今では16ビットを超えるunicodeも存在します(ありがち、アグリィ)。
 インターネットとか、WEBとか、世界中からアクセスできる情報の記述方法としては、必要なのでしょう。そこを否定するつもりはありません。

 ただ、日本語テキストデータの記録方法としては、unicode(UTF-8コード)は非効率極まりないのです。何でもかんでも「unicodeにしておけば間違いない」なんてバカなことは言わない方が良い。
 使い所をわきまえるのが肝要だと思います。

unicodeのダメな所

 最大の短所は、世界中の文字が含まれることです。← 長所の裏返し。
 日本語の文字だけを使いたい場合でも、unicodeには明らかに日本語では使わない文字がたくさん含まれます。しかも、日本語のコードはバラバラ、飛び飛びに割り当たっていて扱い難いのです(下図)。

a21a_sjisdist.png

 上図は、S-JISコードに含まれる全角文字(6,962文字)が、16ビットunicodeでは、どのコード範囲に入っているのか、解析した棒グラフです。
 横軸には16ビットunicodeに於いて上位8ビット(0x00〜0xff)のコード範囲を取り、縦軸には該当するコード範囲にいくつのS-JISコード文字が含まれているかを示しています。

 上のグラフで、棒グラフの無いコード範囲には、S-JISコードに含まれる文字が割り当たっていません。つまり、棒グラフの無い範囲は、無駄にコード領域を占めています。
 unicodeの上位8ビットでグループ分けしているので、1つのコード範囲に含まれる最大文字数は256文字です。日本語文字が多く含まれている0x4e〜0x9fの範囲でも、S-JISコード文字は平均約78文字しか含まれておらず、残りの約178文字は、S-JISコード文字に含まれない文字が混ざっています。

 次節で解説するUTF-8コードでは、日本語文字のほとんどが3バイトコードになります。そのため冗長で、データ量が多くなります。

 日本語フォントを作ろうとすると、使わない多数の文字を無視して、日本語だけのフォントを作成することになります。えっ?それとも、「絶対使わないでしょ?」って思うような文字まで全部フォント起こします?ご苦労様ですm(1__)m。
 コードがバラバラに飛んでいるため、文字コード(UTF-8)から該当するフォントデータを引き出そうにも、一苦労します。

 Arduino等の組み込み用途で、限られたCPUパワー、限られたメモリ容量で取り扱うには、unicodeは欠点が大きすぎるのです。

UTF-8が主流

 unicode自体は、単なるバイナリコードなので、テキストデータとして扱い難いです。これは、JIS規格と同じ理屈です。JIS規格と同様、テキストデータとして扱い易い様に符号化して使うのが普通です。
 unicodeの符号化方式として主流なのは、UTF-8コードと呼ばれる符号化方式です。

 文字の規格と符号化方式は、以下の様な対応になっています。

   JIS規格  ⇔  unicode : 文字セットを定めた規格
      ↓
  S-JISコード  ⇔  UTF-8コード : テキストデータとして扱うための符号化方式


 UTF-8コードでは、unicodeを符号化して1〜4バイトで1文字を表現します。符号化する方法を以下表に示します。

unicode ビッ
ト幅
第1バイト 第2バイト 第3バイト 備考
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
0x00〜0x7f 7 0 x x x x x x x     1バイト
コード
0x80〜0x7ff 11 1 1 0 x x x x x 1 0 x x x x x x   2バイト
コード
0x800〜0xffff 16 1 1 1 0 x x x x 1 0 x x x x x x 1 0 x x x x x x 3バイト
コード

 表が横に大きくなりすぎるので省略しましたが、上の単純なルールを読み解くと、4バイトコード(21ビットunicode)も容易に類推できます。
 unicodeのコード範囲毎に、「x」の部分をunicodeの2進数表記でMSBから順に埋めるだけ、簡単です。unicodeのビット幅と「x」のビット数が一致しています。
 3バイトコードを使って、0x00〜0x7f範囲のコードを記述することもできる仕様ですが、それは冗長なので禁止されています。お陰で、符号化も復号もすごく簡単です。

 特徴は、最上位の数ビットを見ると、そのバイトがマルチバイト文字の一部かどうかが判ることです。

 S-JISコードと違って、マルチバイト文字コードの先頭バイトと中間バイトにコードの重なりが無いので、テキストデータの途中から解釈しようとする場合、容易に文字コードの切れ目を見つけ出すことができます。

 日本語文字にとっての最大の短所は、コードサイズが大きいことです。
 S-JISに含まれる日本語文字は7,000文字程度だと判りました。7,000文字は、約13ビット(≒log2 (7,000))に相当します。S-JISでは、日本語文字を2バイト(16ビット)に符号化しています。一方のUTF-8は、日本語文字のほとんどが3バイトコード範囲にあるので、1文字当たり3バイト(24ビット)を要します。
 たった13ビットの情報を24ビットも使って表現するので、冗長でデータ量が多くなります。

S-JISの良い所

 C言語などでプログラムを記述する時には、7ビットASCIIコードと呼ばれる、英数字と記号の含まれた文字コードを使います。でも、コメント文中などには、日本語で解り易い注釈を記入した方が良いでしょう。
 すると、必然的に7ビットASCIIコードと日本語S-JISコードが混在することになります。

 S-JISコードでは、日本語約7,000文字種が2バイト/1文字で記録可能です。また、1バイトコード(7ビットASCII文字、半角カタカナ文字)と2バイトコード(その他全角の日本語文字)が混在できます。日本語を使わなければ1バイト/1文字、その他全ての日本語文字が2バイト/1文字なので、データ効率が良いです。

  UTF-8を引き合いに出してみると:

 はぁ。
 単に日本語テキストデータを記録しようとした場合、S-JISコードは優れた符号化方式だとご理解いただけたでしょうか?

unicode→S-JIS変換の必要性

 unicodeを使う最大の利点は、世界中🌍🌎🌏で1つの文字コード(UTF-8)を使ってテキストデータを共有できることです。だから最善の使い所は、インターネットです。
 Arduinoを使った電子工作に於いても、ESP32等を使ってインターネットにアクセスし、ホームページ(HP)のデータを取得したいケースがあります。HPデータに含まれる情報を一部抜き出して表示したい、とか。例えば、HPデータに含まれる、天気予報、災害情報、株価、為替、ニュースのヘッドライン、などです。

 最近のHPデータ(HTML)の多くはunicode(UTF-8コード)で記述されているので、上例の様な情報がunicodeで提供されるなら、これを扱い易いS-JISに変換して使う機会は多くなるはずです。

unicode→S-JIS変換方法考察

 UTF-8⇔unicode変換は、前の節で解説した通り簡単に変換できるので、1〜4バイトの可変長マルチバイト文字コードであるUTF-8ではなく、16ビット固定長で扱うことができるunicodeを変換の対象とします。

※unicodeには、16ビットを超えるコードも存在しますが、JIS規格に含まれる文字は16ビット以下なので、16ビットunicodeだけを対象とします。

 また、主たる目的はインターネットのHPデータ(HTML)で使われているUTF-8(unicode)をS-JISに変換したい事なので、unicode→S-JIS方向の変換について考察して行きます。


 16ビットunicodeで表現可能な文字数は高々65,536文字です。unicode全65,536文字の各文字毎に、対応するS-JISコードを列記して並べた表(変換表)を作ったと仮定しましょう。S-JISコードは2バイトコードなので、1文字当たり2バイトの記憶領域が必要です。
 よって変換表は、65,536文字×2バイト=128Kバイトあれば充足します。

 な〜んだ、たったこれだけで済むのか。PCやスマホなどでは問題となるサイズでは無いですね。
 変換は順方向なので、unicodeに対応する変換テーブルを引っ張ると、そこには対応するS-JISコードが記載されています。あっという間に変換終了です。変換はすごく速いです。

 でも、Arduino UNOだと、128Kバイトの変換表なんて記憶できません。ROM(Flash)が32Kバイトしか無いからです。128Kバイトなんて入らない。Arduino DUEならば、ROM(Flash)サイズ512Kバイトが搭載されているので記憶可能です。
 つまり、限られた高性能MCUを使わない限り、高々コード変換の為だけに128Kバイトの変換表は持てないのが現実なのです。

 もっと変換表を小さくすることを考えなくては……。

逆に引く?

 16ビットunicodeの中で、日本語文字は13ビット程しか占めていないのに、16ビット(65,536文字)全体の変換表を持つのは無駄なんです。もう一回、unicode中のS-JIS文字分布グラフを見ておきますか?(下図、再掲)

a21a_sjisdist.png

 即ち、逆に13ビットの日本語文字コードをベースにして、対応するunicodeの方を列記して並べた表(変換表)を作った方が効率的なのです。
 理論的には、約7,000文字のS-JISコード文字に対して、2バイトのunicodeを紐づけるので、約7,000文字×2バイト≒14Kバイトで充足します。非力なArduino UNOでも、ROM(Flash)サイズ32Kバイトなので、結構きついけど載らないことは無いサイズとなりました。

 ただし、この変換表は、S-JIS→unicode方向の変換表です。作りたかったunicode→S-JIS方向の変換をするためには「逆引き(逆方向参照)」が必要となってしまいます。
 つまり、この表を使ってunicode→S-JIS変換するためには、変換表を頭から検索して行って、対応するunicodeを見つけなくてはならないのです。unicodeが見つかったら、そのアドレスからS-JISコードを知ることができます。

 何も工夫しないでシーケンシャル検索にしてしまうと、1文字毎に、毎回最大7,000文字分検索しないと、変換できません。変換速度はとっても遅いです。これが、変換テーブルサイズをケチった代償です。

 ソフトウェア的な対処方法としては、検索方法を工夫すると高速化できないこともないです。
 例えば、ハッシュテーブルを使います。

※ハッシュテーブルについてはこちらから検索してください。

 その代わり、ハッシュテーブルにすることで余分なデータが追加で必要となるので、変換表のサイズは大きくなってしまいます。メモリ容量の限られたArduino等の組み込み用途では、痛し痒しです。本案は却下。

せめてハードウェア化して、高速化

 いつもの様に、専用のハードウェアをVerilog-HDLで設計して使おうと思います。従って、FPGAボード(Sipeed Lichee Tang Primer)と、その上で動作するオリジナル16ビットCPUコアMoscoviumを使います。

 以下、コード変換ユニットの目標仕様とします。

  目標仕様:

変換テーブル

 変換テーブルには、TD IDEの32KビットROMマクロ(1個当たり4Kバイト)を使います。

 変換テーブルに記憶すべきは16ビットunicodeなので、データ量は16ビット/1文字です。並列検索するため、複数文字を1サイクルで読み出す必要があります。
 ROMマクロを効率よく使うため、いくつかバリエーションを試した結果、96ビット幅のROMを使うのが最適だと判りました。96ビット÷16ビット=6文字毎に読み出すことができます。更に、TD IDEのROMマクロはデュアルポートなので、2つのポートから同時に読みすことが可能です。
 つまり、1サイクルで12文字分読み出して、並列検索することができます。

 ROMサイズは、第二水準漢字までカバー(〜84区)すると、1,344ライン必要だと解りました。

  1,344ライン=84区×2ceil (log2 (94点÷6文字))

※え〜っと、面倒なので詳しく説明しませんが、ROMアドレス(ライン番号)から区点コードへ変換する必要があるため、1区(94点)分のテーブルを記憶するライン数を2冪数に調整する必要がありました。それが上の計算式に表現されています。


 有効なROMサイズは、96ビット×1,344ライン=15.75Kバイトとなりました。TD IDEのIP Generatorで生成してみたところ、32KビットROM(4Kバイト)を使うと6個(合計24Kバイト)必要だと判りました(下図)。ライン数が1,344ラインと、2冪数ではなく中途半端なサイズのため止む無しです。

a21a_lutmcr.png

 ライン数を2冪数に丸めて1,024ラインに妥協すると、必要なROMマクロは3個(12Kバイト)に削減できます。この場合、第一水準はカバーできますが、第二水準の後半は溢れてしまいます(〜64区まで含有)。
 ちゃんと検証したことはありませんが、普通の文章であれば第一水準だけで十分なハズです。1,024ラインだとROMサイズ12Kバイトと軽くなるので、組み込み用途では大きなアドバンテージとなります。
 最後は、融通可能なROMサイズと相談して決めてください。

 今回作る変換ユニットは、第二水準まで変換可能な1,344ライン(フルバージョン)で作成しておきます。

変換表の生成

 肝心な変換表は、Olive+のスクリプトで自動生成してしまいましょう。従って、最終的にはWindowsのAPIが持っている変換表に従ったことになります。

  変換表生成スクリプト:unisji_mkmif.olv

※スクリプト(.olv)の実行には拙作Olive+をWindowsにインストールする必要があります。

 スクリプトを実行すると、「unsj_rom.mif」と言うファイルが完成しますので、TD IDEのIP GeneratorでROMを生成する時に、「Load Init File」欄に指定してください。


 半角カタカナコード(unicodeで0xff61〜0xff9f)の変換に対応するため、未定義領域の14区(2〜64点)に押し込んでテーブルを生成しています。変換回路を設計する時、検索結果が14区だった時の例外処理として、8ビット半角カタカナコードへの変換を実行します。

  // 半角カタカナコードを空き領域(14区)に差し込む
  if (codk=14 & 2≦codt & codt≦64)
  {
    // 半角カタカナ
    cods=codt + 0xa1 - 2;
  }



 ROMマクロを1,024ライン分で作成してROMマクロ数を削減した場合、スクリプト先頭付近で以下の定数を変更して「unsj_rom.mif」ファイルを生成してください。

  var ROMSIZ=1344;  // ROM size: 96bit×ROMSIZ

    ↓第一水準と第二水準の途中までカバー

  var ROMSIZ=1024;  // ROM size: 96bit×ROMSIZ

 ただし、前節で指摘した通り、第二水準の途中までしかカバーしないので、一部のS-JIS文字は変換できなくなります。ROMサイズの削減には貢献するので、用途によっては、そういう選択肢もあります。


 今回はここまでにしておきましょう。unicode→S-JIS変換の方法と、変換テーブルを組み込み用途で使用可能なほどに小さくする方法についての考察が終わりました。次回、具体的な回路の詳細設計を進めて完成させたいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home