文字コード変換ライブラリICUの紹介

■はじめに
まずはじめに、「文字化けとは何か」と聞かれたらあなたは何と答えますか。それは、

日本に生を受けたプログラマが背負う宿命的な問題

と私なら答えるでしょう。(何、この冒頭w)

皆さん、Webページ制作の経験があったり、自然言語処理をかじった人なら必ず文字化けに苦しまれたことがあると思います。英語のようにアルファベットだけなら何の問題もないのですが、日本語のように漢字や仮名が混じっている場合には文字データを正しい文字コードエンコーディングしないと文字化けを起こします。このように文字コードには複数あり、その指定ミスが文字化けの原因となります。

例えば、日本語の文字コードには、大きく分けて以下の4つがあります。

  • JIS(ISO-2022-JP):JIS規格のX0208で定められた文字コード。7ビットコード2バイトで日本語文字を表現する。
  • Shift_JISMicrosoft社が決めた文字コード。パソコン上のファイル内で日本語文字を表現するために使われている。
  • EUC-JP:日本語UNIX環境で使用される文字コード。8ビットコード2バイトで日本語文字を表現する。
  • Unicode:すべての文字を2バイトで表現した文字コード。世界中の文字を表現しようとしている。

これにより、文字化けを直すには正しい文字コードに変換する必要がある訳ですが、個人的に使えそうだと思った文字コード変換ライブラリを発見したのでそれを紹介したいと思います。文字コード変換ライブラリと言ってもいろいろあるのですが、オープンソースという観点から今回はIBMUnicodeを扱うためのライブラリであるICU(International Components for Unicode)について紹介したいと思います。

ICU(International Components for Unicode)
ICUは、プログラムの国際化を支援するために作成された、世界中で使われている多くの文字コードUnicodeの相互変換を提供するライブラリです。オープンソースであるため、自作のアプリケーションに移植して配布することも可能です。C/C++Javaから使用することができます。

■ダウンロード
まずは以下のサイトからダウンロードします。
ICU - International Components for Unicode

「Download ICU」から辿って、各自の開発環境に対応した圧縮ファイルを選択して下さい。
C/C++版には「C」が、Java版には「J」がファイル名に付いているようです。
2012年11月現在の最新版はバージョン50.1です。

■インストール
Windowsの場合

    • 準備と環境設定
VC++Cygwinが必要になり、これらは前もってインストールされており、環境設定も済んでいるものとして説明していきます。
VC++ 2008 を立ち上げます。
次に、メニューの[ツール]->[オプション]でオプションダイアログを起動し、[プロジェクトおよびソリューション]->[VC++ディレクトリ]でICUディレクトリを設定します。
設定は、ダウンロードした圧縮ファイルを C:\ICU に解凍した場合、"インクルードファイル"に"C:\ICU\icu\source\common"を追記し、"ライブラリファイル"、"実行可能ファイル"に"C:\ICU\icu\source\lib"を追記します。
    • VC++のプロジェクトの設定
[プロジェクト]->[プロパティ]でプロパティダイアログを起動し、[構成プロパティ]->[リンカ]->[入力]で、"追加の依存ファイル"に"icuuc.lib"(デバッグモードの場合は、"icuude.lib")を追記します。今回は文字コードの変換処理だけを使用するので"icuuc.lib"(デバッグモードの場合は、"icuude.lib")だけです。
    • DLLの配置
ソリューションで、exeファイルが生成されるディレクトリに"icudt44.dll"と"icuuc44.dll"(デバッグモードの場合は、"icuuc44d.dll")をコピーします。今回は文字コードの変換処理だけを使用するのでこの2つだけです。
    • サンプルプログラムのビルド
VC++のプロジェクト作成で、空のコンソールアプリケーションを作成します。
作成したプロジェクトへ以下のソースコードを貼り付けたhppファイル、cppファイルなどを作成します。
ビルドして、コンパイルおよびリンクができれば完了です。

MacOSXの場合

    • 圧縮ファイルの解凍
ダウンロードした圧縮ファイル(ここではicu4c-49_1_2-src.tgzを選択)を好きなフォルダに入れて解凍します。
$ tar -xvzf icu4c-49_1_2-src.tgz /(好きなフォルダ)
$ cd /(好きなフォルダ)/icu/source
    • ライブラリの構築
次に、ICUライブラリを構築します。
$ ./configure
$ make
    • インストール
最後に、ICUライブラリとICUインクルードファイルをインストールして完了です。
$ sudo make install
ICUライブラリが /usr/local/lib に、ICUインクルードファイルが /usr/local/include にあることを確認して下さい。
    • パスの追加
環境設定でライブラリパスとインクルードファイルパスにICUのそれぞれのパスを追記します。
    • サンプルプログラムの実行
以下のソースコードを貼り付けたhppファイル、cppファイルなどを作成します。
コンパイルおよびリンクができれば完了です。
# 今は、コンパイルは通るがリンク時にエラーが発生して実行できていない状態です。
# それが解決したら追記するので、もう少し待って下さい。

実は、ターミナルで、

$ sudo port installed

として確認すれば分かるように、MacOSXにはもともとOSが内部で使用するためにICUが組み込まれています。しかし、これをアプリケーション側から利用することはできないため、今までのような事を行う必要があるという訳です。
もし、ICUソースコードにバンドルされていない場合は、portコマンドなどでインストールしておいて下さい。ついでに最新のlibxml2パッケージも必要となるのでここでインストールしておきましょう。

$ sudo port icu install
$ sudo port libxml2 install

■サンプルプログラムの説明
ここでは、文字コード変換プログラムを理解するために必要なAPIの説明を行っていきます。さらに詳しく知りたい人はICUのドキュメントを参照して下さい。

●前準備

文字コード変換APIを使用するためにインクルードします。ソースコードの先頭付近に
#include
を記述します。
    • UErrorCode : エラーコード
処理中に発生したエラーは全てここに書き込まれるようになっています。

●UConverterの生成と解放

    • ucnv_open : UConverterの生成
ここで変換の対象となる文字コード名を指定します。
    • ucnv_close : UConverterの解放

●対Unicode変換

    • ucnv_toUnicode : マルチバイト文字列をUnicode文字列へ変換
あるマルチバイト文字列から別のマルチバイト文字列へ相互変換するために必要な前半のステップ
    • ucnv_fromUnicode : Unicode文字列をマルチバイト文字列へ変換
あるマルチバイト文字列から別のマルチバイト文字列へ相互変換するために必要な後半のステップ

●バッファの大きさ

    • ucnv_getMinCharSize(UConverter* converter) : 指定された文字コードにおける1文字を表現するために必要な最小のバイト数
マルチバイト文字列/Unicode文字列の相互変換をする場合に、変換結果を格納するバッファの大きさを求められます。
入力されるマルチバイト文字列のバイト数をこの値で割れば、出力Unicode文字列バッファに必要な文字数が得られます。
    • ucnv_getMaxCharSize(UConverter* converter) : 指定された文字コードにおける1文字を表現するために必要な最大のバイト数
マルチバイト文字列/Unicode文字列の相互変換をする場合に、変換結果を格納するバッファの大きさを求められます。
入力されるUnicode文字列の文字数にこの値を掛ければ、出力マルチバイト文字列バッファに必要な文字数が得られます。

■サンプルプログラム
以下に、ICU文字コード変換APIを用いてeuc-jpで書かれた"日本"をshift_jisに変換するプログラムを示します。ここで、出力バッファの確保や引数の設定を行うラッパが用意されているので、次のような各変換がシンプルな呼び出しで利用できるようになっています。

  • widen:マルチバイト -> Unicode
    • const char* -> std::wstring
    • std::string -> std::wstring
  • narrow:Unicode -> マルチバイト
    • const wchar_t* -> std::string
    • std::wstring -> std::string
  • convert:マルチバイト -> マルチバイト
    • const char_t* -> std::string
    • std::string -> std::string

使い方についてはヘッダ(UnicodeConverter.hpp)を参照して下さい。

●UnicodeConverter.hpp

#ifndef UNICODECONVERTER_H
#define UNICODECONVERTER_H

#include <unicode/utypes.h>
#include <exception>
#include <string>

struct UConverter;
enum   UErrorCode;

namespace s34 {
    // 'icu_error' exception
    class icu_error : public std::exception {
        UErrorCode error_;
    public:
        explicit icu_error(UErrorCode error)
            : error_(error)
        {}

        virtual const char* what() const throw();
        //virtual const char* what() const;

        UErrorCode code() const {
            return error_;
        }
    };

    // 'open_converter'
    UConverter* open_converter(const char* name, UErrorCode* error =0);

    // 'close_converter'
    void close_converter(UConverter*);

    // 'widen' : multibyte -> doublebyte
    std::wstring widen(const char* source,
                       size_t      sourceLength,
                       UConverter* converter,
                       bool        flush =true,
                       UErrorCode* error = 0);

    inline
    std::wstring widen(const char* source,
                       UConverter* converter,
                       bool        flush =true,
                       UErrorCode* error =0) {
        return widen(source,
                     std::char_traits<char>::length(source),
                     converter,
                     flush,
                     error);
    }

    inline
    std::wstring widen(const char* source,
                       const char* sourceLimit,
                       UConverter* converter,
                       bool        flush =true,
                       UErrorCode* error =0) {
        return widen(source,
                     sourceLimit - source,
                     converter,
                     flush,
                     error);
    }

    inline
    std::wstring widen(const std::string& source,
                       UConverter*        converter,
                       bool               flush =true,
                       UErrorCode*        error =0) {
        return widen(source.data(),
                     source.length(),
                     converter,
                     flush,
                     error);
    }

    // 'narrow' : doublebyte -> multibyte
    std::string narrow(const wchar_t* source,
                       size_t         sourceLength,
                       UConverter*    converter,
                       bool           flush =true,
                       UErrorCode*    error =0);

    inline
    std::string narrow(const wchar_t* source,
                       UConverter*    converter,
                       bool           flush =true,
                       UErrorCode*    error =0) {
        return narrow(source,
                      std::char_traits<wchar_t>::length(source),
                      converter,
                      flush,
                      error);
    }

    inline
    std::string narrow(const wchar_t* source,
                       const wchar_t* sourceLimit,
                       UConverter*    converter,
                       bool           flush =true,
                       UErrorCode*    error =0) {
        return narrow(source,
                      sourceLimit - source,
                      converter,
                      flush,
                      error);
    }

    inline
    std::string narrow(const std::wstring& source,
                       UConverter*         converter,
                       bool                flush =true,
                       UErrorCode*         error =0) {
        return narrow(source.data(),
                      source.length(),
                      converter,
                      flush,
                      error);
    }

    // 'convert' : multibyte -> multibyte
    std::string convert(const char* source,
                        size_t      sourceLength,
                        UConverter* sourceConverter,
                        UConverter* targetConverter,
                        bool        flush =true,
                        UErrorCode* error =0);

    inline
    std::string convert(const char* source,
                        UConverter* sourceConverter,
                        UConverter* targetConverter,
                        bool        flush =true,
                        UErrorCode* error =0) {
        return convert(source,
                       std::char_traits<char>::length(source),
                       sourceConverter,
                       targetConverter,
                       flush,
                       error);
    }

    inline
    std::string convert(const char* source,
                        const char* sourceLimit,
                        UConverter* sourceConverter,
                        UConverter* targetConverter,
                        bool        flush =true,
                        UErrorCode* error =0) {
        return convert(source,
                       sourceLimit - source,
                       sourceConverter,
                       targetConverter,
                       flush,
                       error);
    }

    inline
    std::string convert(std::string& source,
                        UConverter*  sourceConverter,
                        UConverter*  targetConverter,
                        bool         flush =true,
                        UErrorCode*  error =0) {
        return convert(source.data(),
                       source.length(),
                       sourceConverter,
                       targetConverter,
                       flush,
                       error);
    }
}

#endif

●UnicodeConverter.cpp

#include <unicode/ucnv.h>
#include "UnicodeConverter.h"

namespace s34 {

    const char* icu_error::what() const {
        return u_errorName(error_);
    }

    // make_converter
    UConverter* open_converter(const char* name, UErrorCode* error) {
        UErrorCode  innerError = U_ZERO_ERROR;
        UErrorCode* perror = error ? error : &innerError;
        UConverter* converter = ucnv_open(name, perror);

        if( U_FAILURE(*perror) && !error )
            throw icu_error(innerError);

        return converter;
    }

    void close_converter(UConverter* converter) {
        ucnv_close(converter);
    }

    // widen : multibyte --> wide
    std::wstring widen(const char* source,
                       size_t      sourceLength,
                       UConverter* converter,
                       bool        flush,
                       UErrorCode* error) {
        const size_t poolLength = 256;
        wchar_t      pool[poolLength];
        size_t       targetLength = sourceLength / ucnv_getMinCharSize(converter);
        wchar_t*     buffer = ( targetLength > poolLength ) ? new wchar_t[targetLength] : pool;
        wchar_t*     target = buffer;
        UErrorCode   innerError = U_ZERO_ERROR;
        UErrorCode*  perror = error ? error : &innerError;

        ucnv_toUnicode(converter,
                       &target, target + targetLength,
                       &source, source + sourceLength,
                       0, flush, perror);

        if(U_FAILURE(*perror) && !error) {
            if ( buffer != pool )
                delete[] buffer;

            throw icu_error(innerError);
        }

        std::wstring result(buffer,target);

        if(buffer != pool)
            delete[] buffer;

        return result;
    }

    // narrow : wide --> multibyte
    std::string narrow(const wchar_t* source,
                       size_t         sourceLength,
                       UConverter*    converter,
                       bool           flush,
                       UErrorCode*    error) {
        const size_t   poolLength = 256;
        char           pool[poolLength];
        size_t         targetLength = sourceLength * ucnv_getMaxCharSize(converter);
        char*          buffer = ( targetLength > poolLength ) ? new char[targetLength] : pool;
        char*          target = buffer;
        UErrorCode   innerError = U_ZERO_ERROR;
        UErrorCode*  perror = error ? error : &innerError;

        ucnv_fromUnicode(converter,
                         &target, target + targetLength,
                         &source, source + sourceLength,
                         0, flush, perror);

        if(U_FAILURE(*perror) && !error) {
            if(buffer != pool)
                delete[] buffer;

            throw icu_error(innerError);
        }

        std::string result(buffer, target);

        if(buffer != pool)
            delete[] buffer;

        return result;
    }

    // convert : multibyte --> multibyte
    std::string convert(const char* source,
                        size_t      sourceLength,
                        UConverter* sourceConverter,
                        UConverter* targetConverter,
                        bool        flush,
                        UErrorCode* error) {
        const size_t poolLength = 256;
        size_t       targetLength;

        targetLength = sourceLength / ucnv_getMinCharSize(sourceConverter);
        wchar_t  wpool[poolLength];
        wchar_t* wbuffer = ( targetLength > poolLength ) ? new wchar_t[targetLength] : wpool;
        wchar_t* wtarget = wbuffer;
        UErrorCode   innerError = U_ZERO_ERROR;
        UErrorCode*  perror = error ? error : &innerError;

        ucnv_toUnicode(sourceConverter,
                       &wtarget, wtarget + targetLength,
                       &source,  source  + sourceLength,
                       0, flush, perror);

        if(U_FAILURE(*perror)) {
            if(wbuffer != wpool)
                delete[] wbuffer;
            if(!error)
                throw icu_error(innerError);

            return std::string();
        }

        targetLength = (wtarget - wbuffer) * ucnv_getMaxCharSize(targetConverter);
        char     pool[poolLength];
        char*    buffer = ( targetLength > poolLength ) ? new char[targetLength] : pool;
        const wchar_t* wsource = wbuffer;
        char*    target = buffer;

        ucnv_fromUnicode(targetConverter,
                         &target,  target + targetLength,
                         &wsource, wtarget,
                         0, flush, perror);

        if(U_FAILURE(*perror) && !error) {
            if(wbuffer != wpool)
                delete[] wbuffer;
            if(buffer != pool)
                delete[] buffer;

            throw icu_error(innerError);
        }

        std::string result(buffer, target);

        if(wbuffer != wpool)
            delete[] wbuffer;
        if(buffer != pool)
            delete[] buffer;

        return result;
    }
}

●test.cpp

#include <iostream>
#include <unicode/ucnv.h>
#include "UnicodeConverter.h"

int main() {
    std::string euc_jp = "\xC6\xFC\xCB\xDC";

    try {
        UConverter* sourceConverter = s34::open_converter("euc-jp");
        UConverter* targetConverter = s34::open_converter("shift_jis");

        std::cout << s34::convert(euc_jp, sourceConverter, targetConverter) << std::endl;
        s34::close_converter(sourceConverter);
        s34::close_converter(targetConverter);
    }
    catch(s34::icu_error& ex) {
        std::cerr << ex.what() << std::endl;
    }

    return 0;
}

■おわりに
お分かりの通り、プログラム内で文字コード変換が必要になった場合に、特にICUを使わなくても別の方法で行うこともできます。しかし、ICUを利用するメリットとして、「コードの移植性が高い」という点が挙げられます。そのために、何らかのアプリケーションに組み込んで配布することを目的に使用する人にとっては必要になると思ったので、今回紹介することにしました。

■参考にしたページ
株式会社エス・スリー・フォー » ICUの文字コード変換を使いたいのですが…
株式会社エス・スリー・フォー » ICU 2.x : UnicodeString による文字コード変換
文字コード変換ライブラリ「ICU」セットアップ: プログラマーの雑記帳
ICU(1) ICU Unicodeライブラリを古いVC++で環境設定する : OFF-SOFT.net