ICU: 文字コードの変換 [C++]

(2019.10)

ICU を使った文字コード変換.

ある文字コードから別の文字コードへ変換することもできるが、その場合でも必ず Unicode を経由する。

Unicodeへの変換

ICUでは, Unicode code point は UChar32 型, UTF-16 code unit は UChar 型で表す。文字列は UnicodeString クラスのオブジェクトになる。

データ列がシフトJIS、日本語EUCなどUnicode以外のときは、読込み時に, 何らかの方法で変換してやる必要がある。

UNIXであれば iconv() が使えるので、それを使ってもよい。変換表をいくつもあれこれ使うと、思わぬところで文字化けしたりする恐れがある。iconv() を使わないのなら、ICUの変換器を使う。UConverter クラスが文字コードの変換器。

iconv

次のソースコードは、日本語EUCからUnicodeへ変換したうえで、UnicodeStringオブジェクトを作る。

まずは、必要なヘッダを include する。

  3| #undef USE_ICONV
  4| 
  5| #include <stdio.h>
  6| #include <string.h>
  7| #include <assert.h>
  8| #ifdef USE_ICONV
  9|   #include <iconv.h>
 10| #else
 11|   #include <unicode/ucnv.h>
 12| #endif
 13| #include <unicode/unistr.h>
 14| #include <unicode/uchar.h>
 15| #include <unicode/schriter.h>

次の部分は、iconvを使った変換。iconv_open()で文字コードを指定し、iconv()で変換する。ただ、変換後の大きさが1000バイトに収まるという仮定を置いているので、実用にするときは、修正が必要。

変換したバイト列と文字コード名をUnicodeStringに与えて文字列オブジェクトを生成する。

 17| #ifdef USE_ICONV
 18| static const char* UTF8_CES = "UTF-8";
 19| static const char* EUCJP_CES = "eucJP-open";
 20| #endif
 21| 
 22| int main()
 23| {
 24|     const char* euc = "ABC日本語のテキスト\\/¥/\";
 25| #ifdef USE_ICONV
 26|     // エンコーディングの変換:iconvを使う場合
 27|     iconv_t cd = iconv_open(UTF8_CES, EUCJP_CES);
 28|     size_t left = strlen(euc);
 29|     char buf[1000];
 30|     char* p = buf;
 31|     size_t bufleft = 1000;
 32|     printf("euc = %x, p = %x\n", euc, p);
 33|     size_t r = iconv(cd, const_cast<char**>(&euc), &left, &p, &bufleft);
 34|     printf("euc = %x, p = %x\n", euc, p);
 35|     printf("r = %d, left = %d, bufleft = %d\n", r, left, bufleft);
 36|     *p = '\0';
 37|     iconv_close(cd);
 38|     UnicodeString str(buf, UTF8_CES);

ICU のコンバータ

今度はICUのコンバータを使ってみる。ucnv_open()UConverterオブジェクトを生成し、それと変換元のバイト列をUnicodeStringに与えるだけでいい。

ucnv_open() に渡す文字コード名だが、日本語EUCの場合は、mappings/convrtrs.txtを見ると、次の3つがある。ibm-954でいいだろう。 ICU では EUC-JP がまともな変換表。

ucnv_open()に渡す文字コード名 コメント
ibm-33722_VPUA ibm-5050. JIS X 0208記号も足らない。不可。
ibm-33722_P120-1999 '\' => yen sign. NG
ibm-954 収録が足らない。不可。
EUC-JP これが一番まとも。

その他、aliasなどは、次のページで見れる;

 39| #else
 49|     UErrorCode error = U_ZERO_ERROR;
 50|     UConverter* cnv = ucnv_open("ibm-954", &error);
 51|     assert(U_SUCCESS(error));
 52|     UnicodeString str(euc, strlen(euc), cnv, error);
 53|     assert(U_SUCCESS(error));
 54| #endif

文字コード間の変換

ある文字コードのバイト列を、別の文字コードに変換する。

レガシーな文字コードの場合だけでなく、UTF-8などの場合も、このようにすればいい。

1回で完結

メモリ上にバイト列が納まる場合は、ucnv_convert() が一番簡単。

C++
[RAW]
  1. #include <unicode/ucnv.h>
  2. #include <string.h>
  3. #include <stdio.h>
  4. #include <time.h>
  5. int main()
  6. {
  7. FILE* fp = fopen("sjis.txt", "r");
  8. char buf[10000];
  9. int r = fread(buf, 1, sizeof(buf), fp);
  10. buf[r] = '\0';
  11. fclose(fp);
  12. char buf2[10000];
  13. UErrorCode err = U_ZERO_ERROR;
  14. // convenience function.
  15. // "ibm-954" は NEC特殊文字が全滅。使えない.
  16. r = ucnv_convert(/*"ibm-954"*/ "EUC-JP", // toConverterName
  17. "MS932", // fromConverterName
  18. buf2, // target
  19. sizeof(buf2), // targetCapacity
  20. buf, // source
  21. strlen(buf), // sourceLength
  22. &err); // pErrorCode
  23. buf2[r] = '\0';
  24. fp = fopen("euc.txt", "w");
  25. if (!fp) {
  26. fprintf(stderr, "fopen failed.\n");
  27. return 1;
  28. }
  29. time_t now = time(nullptr);
  30. fprintf(fp, "%s", ctime(&now)); // ctime(): 改行が付く.
  31. fwrite(buf2, 1, r, fp);
  32. fclose(fp);
  33. return 0;
  34. }

ストリームを変換

メモリに載りきらない場合や、ストリームを読込みながら変換したい場合は、やや複雑になる。

1文字の途中で入力のバイト列がいったん切れることもあるし、エスケープシーケンスで文字集合を切り替えるようなものだと、変換器が状態を持つ。

まずは、呼び出す部分。

C++
[RAW]
  1. int main()
  2. {
  3. FILE* input = fopen("sjis.txt", "r");
  4. FILE* output = fopen("euc.txt", "w");
  5. if (!output) {
  6. fprintf(stderr, "fopen failed.\n");
  7. return 1;
  8. }
  9. convert_and_copy(input, output);
  10. fclose(input);
  11. fclose(output);
  12. return 0;
  13. }

本題の変換する部分。

こういう用途では ucnv_convertEx() も考えられるが、使い方があまりに複雑なため、よくない。

ucnv_toUnicode()ucnv_fromUnicode() を組み合わせるのがよい。中間表現として pivot バッファを用意し、いったん Unicode に変換する。

ucnv_open() で変換器を生成し、ループを回している間、これを使いまわす。ucnv_close() を忘れずに。

手抜きだが、こんな感じで上手くいく。

C++
[RAW]
  1. void convert_and_copy(FILE* input, FILE* output)
  2. {
  3. assert(input);
  4. assert(output);
  5. char source_buf[9]; // 極端な例。多バイト文字の最長より長いこと。
  6. constexpr int ELEM_SIZE = 30; // source_buf に対して十分な大きさが必要.
  7. UChar pivot[ELEM_SIZE]; // 中間表現. UTF-16
  8. char target_buf[100];
  9. UErrorCode err = U_ZERO_ERROR;
  10. UConverter* sourceCnv = ucnv_open("MS932", &err);
  11. assert(sourceCnv);
  12. UConverter* targetCnv = ucnv_open(/*"ibm-954"*/ "EUC-JP", &err);
  13. assert(targetCnv);
  14. int remaining = 0;
  15. while (true) {
  16. // バッファの先頭とは限らない.
  17. int r = fread(source_buf + remaining, 1, sizeof(source_buf) - remaining,
  18. input);
  19. bool flush = feof(input) || ferror(input);
  20. if ( flush && (remaining + r == 0) )
  21. break; // 未変換のものはない.
  22. // 中間表現に変換.
  23. UChar* pivot_p = pivot;
  24. const char* source_p = source_buf;
  25. ucnv_toUnicode(sourceCnv, &pivot_p, pivot + ELEM_SIZE,
  26. &source_p, // source
  27. source_buf + remaining + r, // sourceLimit
  28. nullptr, flush, &err);
  29. assert(U_SUCCESS(err));
  30. print_uchars(pivot, pivot_p);
  31. // ターゲットに変換.
  32. char* target_p = target_buf;
  33. const UChar* pivot_out = pivot;
  34. ucnv_fromUnicode(targetCnv, &target_p, target_buf + sizeof(target_buf),
  35. &pivot_out, pivot_p, nullptr, flush, &err);
  36. assert(U_SUCCESS(err));
  37. assert(pivot_out == pivot_p);
  38. // 書き込む
  39. fwrite(target_buf, 1, target_p - target_buf, output);
  40. if (flush)
  41. break;
  42. // sourceバッファを詰める
  43. memmove(source_buf, source_p, (remaining + r) - (source_p - source_buf));
  44. remaining = (remaining + r) - (source_p - source_buf);
  45. }
  46. ucnv_close(sourceCnv);
  47. ucnv_close(targetCnv);
  48. }