ICUでUTF-8の文字列を1文字ごとに分割する

More than 1 year has passed since last update.


ICUとは

ICU(International Components for Unicode)は、Unicodeの取り扱いに関するさまざまな機能を提供するオープンソースのライブラリです。C/C++およびJava版が用意されているほか、各種言語へのラッパーも開発されています。

C/C++にはUnicodeを扱うための標準的なライブラリがないため、マルチバイト文字を含むテキストの処理を自力で実装するのは一筋縄ではいきません。特に現在デファクトスタンダードとなっているUTF-8は、1文字あたりのバイト数が固定ではないため、それぞれの文字を正確に切り出だけでもなかなか大変です。

ICUはそういったUnicode周りの悩みを解決するために作られたかなり強力なライブラリで、実際に多くのプロダクトによって利用されています。

ICU - International Components for Unicode


ダウンロードとインストール

以下のページからダウンロードできます。

http://site.icu-project.org/download

本稿執筆時点での最新版は60.2で、Unicode 10.0およびCLDR 32に対応しているそうです。ICU4CがC/C++版、ICU4JがJava版です。Java版はJava 9対応だそうです。今回はC言語で試すのでICU4Cを使います。

FedraやUbuntu、Windows用にはバイナリが用意されていますが、Macの場合にはソースコードからビルドする必要があるので、ソースコード版(icu4c-60_2-src.tgz)をダウンロードします。

ダウンロードできたら、任意のディレクトリに配置して、次のように解凍・ビルド・インストールしましょう。

$ tar xf icu4c-60_2-src.tgz

$ cd icu/source
$ ./configure
$ make
$ sudo make install

インストールが完了すると、ライブラリは /usr/local/lib に、インクルードファイルは /usr/local/include/unicode に配置されるので、ここにパスを通しておけば使用できます(通常はそのままでOK)。


UString(ustring.h)を使ってコードポイントを取得する

次のコードは、標準入力から1行の文字列を読み込んで、それをコードポイントごとに分解して表示するプログラムの例です。各コードポイントを、16進数形式と通常の文字形式でそれぞれ表示します。

#include <stdio.h>

#include <string.h>
#include <inttypes.h>
#include <unicode/ustring.h>

void get_codepoint(const char *text)
{
UChar dest[128]; // UString文字列の格納先
int32_t destCapacity = 128; // UString文字列の格納先のバッファサイズ
int32_t destLength; // 生成されたUString文字列のサイズ
int32_t length = -1; // 元文字列のサイズ、NULL終端文字列の場合は-1
UErrorCode errorCode = U_ZERO_ERROR; // エラーコード

// UStringの生成
u_strFromUTF8(dest, destCapacity, &destLength, text, length, &errorCode);

// コードポイントを1つずつ取得して16進数で出力
int32_t pos = 0;
UChar32 out;
while (pos < destLength) {
U16_NEXT(dest, pos, destLength, out);
printf("%"PRIX32" ", out);
}
printf("\n");

// コードポイントを1つずつ取得して文字として出力
uint8_t buf[5];
uint32_t s = 5;
uint32_t p = 0;
UBool error;
pos = 0;
while (pos < destLength) {
U16_NEXT(dest, pos, destLength, out)
U8_APPEND(buf, p, s, out, error);
printf("%.*s ", s, buf);
memset(buf, 0, s);
p = 0;
}
printf("\n");
}

int main(void)
{
char buf[128];

if (fgets(buf, sizeof(buf), stdin)) {
if (buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1] = '\0';
get_codepoint(buf);
}
}


サンプルコードの解説

ICUでは、UTF-16の文字を扱うための UChar型が定義されています。これは内部的には16ビットのunsigned intです。そして、複数の

UCharを組み合わせて文字列(UString)として扱います。charとchar*の関係と同様です。u_strFromUTF8()関数を使うことで、UTF-8文字列を

UStrung(UChar文字列)に変換できます。

UChar dest[128];            // UString文字列の格納先

int32_t destCapacity = 128; // UString文字列の格納先のバッファサイズ
int32_t destLength; // 生成されたUString文字列のサイズ
int32_t length = -1; // 元文字列のサイズ、NULL終端文字列の場合は-1
UErrorCode errorCode = U_ZERO_ERROR; // エラーコード

// UStringの生成
u_strFromUTF8(dest, destCapacity, &destLength, text, length, &errorCode);

textがオリジナルのUTF-8文字列です。変換に成功すると、UTF-16形式の文字列がdestに格納されます。UErrorCodeはエラーコード格納用に定義されたenumです。

U16_NEXT()関数を使うことで、UStringからUTF-16形式の文字を一文字ずつ取得することができます。

UChar32 out;

U16_NEXT(dest, pos, destLength, out);

posは取得する文字の位置で、関数実行後に次の文字の位置に自動でインクリメントされます。Char32は32ビットのsinged intで、Unicodeのコードポイントを表現するために定義されています。内部的にはint32_tと等価とのことなので、int32_tと同様にprintf()で表示できます。

UChar32のコードポイントをUTF-8形式の文字に変換する方法のひとつとして、U8_APPEND()関数があります。この関数は、UTF-8文字列に、指定されたUChar32コードポイントを結合するというものです。この例では、U16_NEXT()で取り出したコードポイントを、U8_APPEND()を使ってUTF-8文字列にした上で、文字として出力しています。


コンパイルと実行例

コンパイルの際には、次のようにicuucライブラリをリンクする必要があります。実行すると、標準入力から読み込んだ文字列をコードポイントごとに分解して表示します。「こんにちはさようなら」は10個のコードポイントに分解されます。

$ cc -licuuc get_char_with_UString.c -o get_char_with_UString

$ echo こんにちはさようなら | ./get_char_with_UString
3053 3093 306B 3061 306F 3055 3088 3046 306A 3089
こ ん に ち は さ よ う な ら


UCharIterator(uitr.h)を使ってコードポイントを取得する

ICUには、文字列をイテレータで処理する方法も用意されています。次のコードは、先ほどと同じように文字列をコードポイント別に分解するプログラムを、イテレータを利用して実装した例です。イテレータは uitr.h に定義されています。

#include <stdio.h>

#include <string.h>
#include <inttypes.h>
#include <unicode/utypes.h>
#include <unicode/uiter.h>

void get_codepoint(const char *text)
{
UCharIterator iterator;
int32_t length = -1; // 元文字列のサイズ、NULL終端文字列の場合は-1

// Iteratorを作成
uiter_setUTF8(&iterator, text, length);

// コードポイントを1つずつ取得して16進数で出力
while (iterator.hasNext(&iterator) != 0) {
UChar32 codepoint = iterator.next(&iterator);
printf("%"PRIX32" ", codepoint);
}
printf("\n");

// ポジションを先頭に戻す
iterator.move(&iterator, 0, UITER_START);

// コードポイントを1つずつ取得して文字として出力
uint8_t buf[5];
uint32_t s = 5;
uint32_t p = 0;
UBool error;
while (iterator.hasNext(&iterator) != 0) {
UChar32 codepoint = iterator.next(&iterator);
U8_APPEND(buf, p, s, codepoint, error);
printf("%.*s ", s, buf);
memset(buf, 0, s);
p = 0;
}
printf("\n");
}

int main(void)
{
char buf[128];

if (fgets(buf, sizeof(buf), stdin)) {
if (buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1] = '\0';
get_codepoint(buf);
}
}

イテレータは UCharIterator型として定義されており、uiter_setUTF8()関数を使うことでUTF-8文字列から生成できます。

UCharIterator iterator;

int32_t length = -1; // 元文字列のサイズ、NULL終端文字列の場合は-1

// Iteratorを作成
uiter_setUTF8(&iterator, text, length);

UCharIteratorでは、hasNext()で次の文字が存在するかどうかを調べ、next()で次の文字を取得できます。next()は次の文字をChar32型として返し、ポジションをひとつ先に進めます。

while (iterator.hasNext(&iterator) != 0) {

UChar32 codepoint = iterator.next(&iterator);
printf("%"PRIX32" ", codepoint);
}

ポジジョンを任意の位置に設定するにはmove()を使います。move()は、第3引数に基準となるポジションを、第2引数に基準からの位置を指定します。基準位置は列挙型でUCharIteratorOriginとして定義されています。次の例では、UITER_STARTが開始点を表し、そこから0ポイントの位置、すなわち開始点そのものに移動することを意味します。

iterator.move(&iterator, 0, UITER_START);


コンパイルと実行例

実行結果はUStringの場合と同様です。

$ cc -licuuc get_char_with_Iterator.c -o get_char_with_Iterator

$ echo こんにちはさようなら | ./get_char_with_Iterator
3053 3093 306B 3061 306F 3055 3088 3046 306A 3089
こ ん に ち は さ よ う な ら


複数のコードポイントから成る書記素を含むケース

さて、今度は前述のいずれかのプログラムに、次の文字列をそのままコピペして読み込ませてみましょう。


ポプテピピック


$ echo ポプテピピック | ./get_char_with_Iterator

30DD 30D7 30C6 30D4 30D4 30C3 30AF
ポ プ テ ピ ピ ッ ク

7個のコードポイントに分解されました。

では、次の文字列の場合はどうでしょうか。


ポプテピピック


$ echo ポプテピピック | ./get_char_with_Iterator

30DB 309A 30D5 309A 30C6 30D2 309A 30D2 309A 30C3 30AF
ホ ゚ フ ゚ テ ヒ ゚ ヒ ゚ ッ ク

今度は11個のコードポイントに分解されました。2つ目の例では、「ポ」「プ」「ピ」がそれぞれ2つのコードポイントを組み合わせて構成されているためです。したがって、見た目は1文字ですが、コードポイントごとに取得しようとすると2つに別れてしまいます。

Unicodeでは一般的に1文字として認識される単位を「書記素」と呼びます。そして、ひとつの書記素を複数のコードポイントの組み合わせで構成することを許容しています。つまり、単純にコードポイントの数を数えても、それが文字数と一致するとは限らないということです。


BreakIterator(ubrk.h)を使って書記素を取得する

そこでICUには、(コードポイントではなく)書記素単位で文字を取得する手段が提供されています。それがUBreakIteratorです。UBreakIteratorは、文字列から書記素単位で文字を取得するイテレータです。UCharIteratorと違って、コードポイントの区切りではなく、書記素の区切りを基準にします。UBreakIteratorは urbk.h に定義されています。

以下は、UBreakIteratorを利用して書記素単位で文字列を分割するコード例です。

#include <stdio.h>

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

void get_grapheme(const char *text)
{
const char *locale = uloc_getDefault(); // ロケール
int32_t length = -1; // 元文字列のサイズ、NULL終端文字列の場合は-1
UErrorCode errorCode = U_ZERO_ERROR; // エラーコード

// 空のイテレータを生成
UBreakIterator *iterator = ubrk_open(UBRK_CHARACTER, locale, NULL, 0, &errorCode);
// UTextを生成
UText *utext = utext_openUTF8(NULL, text, length, &errorCode);
// イテレータにUTextをセット
ubrk_setUText(iterator, utext, &errorCode);

int32_t current = 0;
int32_t next = 0;
int32_t size = 0;

// 1書記素ごと取り出して出力
current = ubrk_current(iterator);
while (current != UBRK_DONE) {
next = ubrk_next(iterator);

if (next == UBRK_DONE) break;

size = next - current;

printf("%d-%d: %.*s\n", current, next, size, text + current);

current = next;
}
}

int main(void)
{
char buf[128];

if (fgets(buf, sizeof(buf), stdin)) {
if (buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1] = '\0';
get_grapheme(buf);
}
}


サンプルコードの解説

UBreakIteratorは、ubrk_open()関数で生成します。ここでは、最初に空のイテレータを作成してから、ubrk_setUText()関数を利用して処理対象のテキストをセットしています。

UBreakIterator *iterator = ubrk_open(UBRK_CHARACTER, locale, NULL, 0, &errorCode);

UText *utext = utext_openUTF8(NULL, text, length, &errorCode);
ubrk_setUText(iterator, utext, &errorCode);

UTextは、UTF-8文字列を扱うための(UChar*とは別の)構造体です。特定のフォーマットの文字列をUBreakIteratorで扱いたい場合には、ubrk_setUText()やubrk_setText()を使って明示的に任意のテキストオブジェクトをセットする必要があります。今回はUTF-8文字列が対象なので、UTextを利用する方法をとっています。

UBreakIteratorに対しては、ubrk_current()で現在の位置を、ubrk_next()で次の書記素の開始位置を取得できます。これらの関数が返す値はあくまでの書記素の開始位置であり、文字そのものではありません。現在の位置から次の書記素の開始位置までのbyte列を取得すれば、それがひとつの書記素を構成することになります。なお、文字列の最後に到達した場合には、ubrk_current()やubrk_next()はUBRK_DONEを返します。


コンパイルと実行例

コンパイルして実行すると、次のようになります。

$ cc -licuuc get_grapheme.c -o get_grapheme

$ echo ポプテピピック | ./get_grapheme
0-3: ポ
3-6: プ
6-9: テ
9-12: ピ
12-15: ピ
15-18: ッ
18-21: ク
$ echo ポプテピピック | ./get_grapheme
0-6: ポ
6-12: プ
12-15: テ
15-21: ピ
21-27: ピ
27-30: ッ
30-33: ク

ひとつ目は単一のコードポイントで構成されている書記素のみの例で、ふたつ目が複数のコードポイントで構成される書記素を含む例です。数字は書記素の区切り位置(範囲)ですが、単一コードポイントの書記素は1文字あたり3つずつ(非ASCIIの一般的な日本語文字は3バイトなので)なのに対して、2つのコードポイントを含む「ポ」「プ」「ピ」はそれぞれ倍の6つを使っていることがわかります。


最後に

ICUは非常に強力なライブラリであり、ここで紹介したのはあくまでも基本中の基本にすぎません。また、今回のような1文字ずつ取り出すという処理についても、ここで紹介した以外にさまざまな方法が用意されています。Unicodeに対する理解を深める意味でも、ぜひ公式のAPIドキュメントを眺めてみましょう。

http://icu-project.org/apiref/icu4c/index.html