日本語を扱う

charcode 変換

説明はどうでもいいから動くソースを出せという人は、後ろのほうへ…

文字コード?

日本語をはじめとする多くのアジア言語は、0〜255 までしか表せない char 型ではとても表現できません。そこで、複数の char 型を組み合わせて表現しています。ところが、日本語の場合この組み合わせ型に大きく3通りの方法があり、主に使用している OS によってその傾向が異なります。

この複数によって表される文字の数値「文字コード」と呼び、文字コードの番号付け、というか組み合わせの体系ことを「文字符号化方法 (character encoding)」とか「文字セット (character set)」などと呼びます。

伝統的に、Microsoft Windows や Apple Mac OS では Shift_JIS (内部的には Unicode ですが) が使用され、サーバーで用いられる UNIX では EUC が用いられています。また、電子メールやニュースグループでは ISO-2022-JP と呼ばれる文字セットを用います。

なお、最後のものを JIS コードと呼ぶこともありますが、実際には Shift_JIS も JIS 規格 (参考規格ですが) ですので、JIS コードだけがJIS規格というわけではありません。

文字セットについては 文字コードとエンコーディング にわかりやすく記述されています。

文字セットの構成

各文字セットは、以下の文字で構成されています。(参考)

ISO-2022-JP

制御コード
0x00〜0x1F、0x7F
ASCII文字
0x20〜0x7E
半角カタカナ
0x21〜0x5F
0xA1〜0xDF
漢字
第1バイト : 0x21〜0x7E
第2バイト : 0x21〜0x7E
補助漢字
第1バイト : 0x21〜0x7E
第2バイト : 0x21〜0x7E

Shift_JIS

制御コード
0x00〜0x1F, 0x7F
ASCII文字
0x20〜0x7E
半角カタカナ
0xA1〜0xDF
漢字
第1バイト : 0x81〜0x9F, 0xE0〜0xFC
第2バイト : 0x40〜0x7E, 0x80〜0xFC

日本語EUC

制御コード
0x00〜0x1F, 0x7F
ASCII文字
0x20〜0x7E
漢字
第1バイト : 0xA1〜0xFE
第2バイト : 0xA1〜0xFE
半角カタカナ
第1バイト : 0x8E
第2バイト : A1〜0xDF
補助漢字
第1バイト : 0x8F
第2バイト : 0xA1〜0xFE
第3バイト : 0xA1〜0xFE

文字セットの判定

与えられた文字列の文字コードを変換するためには、まずその文字コードがどの種類なのかを認識する必要があります。

上記の文字セットの構成を、バイトごとにグラフに書いてみると、文字コードは以下のように判定できることがわかります。

  1. 7ビット文字で、エスケープシーケンスがない
  2. エスケープシーケンスを含んでいる
  3. 「1文字目が 0x81〜0x9F かつ 2文字目が 0x40〜0x7E / 0x80〜0xFC」あるいは「1文字目が 0xE0〜0xFC かつ 2文字目が 0x40〜0x7E」
  4. 「1文字目が 0xA1〜0xDF / 0xFD〜0xFE かつ 2文字目が 0xA1〜0xFE」
  5. それ以外

そこで、これを素直にインプリメントしてみましょう。

int get_charset(string& source)
{
    unsigned int xpos = 0;
    while (xpos < source.length()) {
        unsigned char lead_byte = static_cast<unsigned char>(source[xpos]);
        unsigned char trail_byte = static_cast<unsigned char>(source[xpos+1]);

        if (lead_byte == 0x1E && (trail_byte == '$' || trail_byte == '(')) {
            cout << "ISO-2022-JP" << endl;
            return 1;
        }
        if (((0x81 <= lead_byte && lead_byte <= 0x9F) ||
                (0xE0 <= lead_byte && lead_byte <= 0xEF)) &&
                (0x40 <= trail_byte && trail_byte <= 0x9F)) {
            cout << "Shift_JIS" << endl;
            return 2;
        }
        if (0xF0 <= lead_byte || (0xFD <= trail_byte && trail_byte <= 0xFF)) {
            cout << "EUC" << endl;
            return 3;
        }
        xpos++;
    }
    cout << "Unknown..." << endl;
    return -1;
}

もちろん、これでは不十分です(たとえば trail_byte == '\0' の場合など)が、たいていの文書であればこれだけでも文字セットを判別できると思います。

なお、文字コードにおける 0x8E などは、char == unsigned char での話です。通常の処理系では char == signed char ですので、(静的)キャストして char → unsigned char という変換をする必要があります。また、これをせずに文字コードを 0x8F → -0x71 などと読み替えるのも一つの方法です。速度的には、定数値を変化させるだけの後者が速いはずです (実際の所はどうなんでしょう?) が、読みやすさから言うと前者がお奨めです。

文字セットを変換する

各文字セットの文字は、それぞれ以下のように相互変換可能です。

ISO-2022-JP → Shift_JIS

void j2s(unsigned char* buf, unsigned char* dest)
{
    unsigned char row_offset = (buf[0] < 0x5F) ? 0x70 : 0xB0;
    unsigned char cell_offset = (buf[0] % 2) ? ((buf[1] > 0x5F) ? 0x20 : 0x1F) : 0x7E;

    dest[0] = ((buf[0] + 1) / 2) + row_offset;
    dest[1] = buf[1] + cell_offset;
}

ISO-2022-JP → utf-8

void j2e(unsigned char* buf, unsigned char* dest)
{
  	dest[0] = buf[0] | 0x80;
   	dest[1] = buf[1] | 0x80;
  	
  	/* ただし、「半角」文字の場合
  	    dest[0] = '\x8E';
  	    dest[1] = buf[0] | 0x80;
  	   補助漢字の場合は
  	    dest[0] = '\x8F';
  	    dest[1] = buf[0] | 0x80;
  	    dest[2] = buf[1] | 0x80;
  	*/
}

Shift_JIS → ISO-2022-JP

void s2j(unsigned char* buf, unsigned char* dest)
{
    unsigned char row_offset  = (buf[0] < 0xA0) ? 0x70 : 0xB0;
    unsigned char cell_offset = (buf[1] < 0x9F) ? ((buf[1] > 0x7F) ? 0x20 : 0x1F) : 0x7E;

    dest[0] = ((buf[0] - row_offset) / 2) - (c2 < 0x9F ? 1 : 0);
    dest[1] = buf[1] - cell_offset;
}

EUC → ISO-2022-JP

void e2j(unsigned char* buf, unsigned char* dest)
{
    switch (buf[0]) {
      case 0x8E:
        dest[0] = buf[1] & 0x7f;
        // ただし、「半角」文字のエスケープ ESC ( I が必要
        // 8ビット JIS が使用できるなら dest[0] = buf[1] でもよい。
        break;
      case 0x8F:
          dest[0] = buf[1] & 0x7f;
          dest[1] = buf[2] & 0x7f;
        // ただし、補助漢字のエスケープ ESC $ ( D が必要
        break;
      default:
        dest[0] = buf[0] & 0x7F;
        dest[1] = buf[1] & 0x7F;
    }
    return;
}

Shift_JIS → EUC : 上記の s2j と j2e を組み合わせます。

EUC → Shift_JIS : 上記の e2j と j2s を組み合わせます。

その他の実装

あとは、ISO-2022-JP におけるエスケープシーケンスの実装と、「半角」文字や「機種依存」文字をいかに処理するかですが、ここでは割愛します。各自考えてみてください。

あるいは、nkf などのソースを読むか…

また、下に掲載していますが、一通り動作するソースもダウンロードできますので、ご利用ください。

ダウンロード

実際に実行形式として使えるものを作りましたので、そのソースを公開しておきます。

kanji.020313.zip
LHA 圧縮 / 9,472 バイト
使い方については同梱の readme.txt をご覧ください。
kanji.020313.tar.gz
TAR + GZIP 圧縮 / 7,412 バイト
上の tar.gz 版。
kanji.w32.010204.lzh
LHA 圧縮 / 66,024 バイト
Windows 用のバイナリーです。あまりこれ自体に意味はありませんが、動作確認にご利用ください。

以前はオブジェクト指向を目指して、クラス化していたのですが、意外と使いにくかったので従来の方式に戻してしまいました。が、ちゃんとC++の味は残している…つもりです。

その他参考にしたサイト