1. 概要

多くのアルファベットにはアクセント記号と発音区別符号が含まれています。 データを確実に検索または索引付けするために、発音区別符号を含む文字列をASCII文字のみを含む文字列に変換したい場合があります。 Unicodeは、これを行うのに役立つテキスト正規化手順を定義しています。

このチュートリアルでは、Unicodeテキストの正規化とは何か、それを使用して発音区別符号を削除する方法、および注意すべき落とし穴について説明します。 次に、Java NormalizerクラスとApacheCommonsStringUtils。を使用したいくつかの例を示します。

2. 一目でわかる問題

削除する発音区別符号の範囲を含むテキストを処理しているとしましょう。

āăąēîïĩíĝġńñšŝśûůŷ

この記事を読んだ後、発音区別符号を取り除く方法を理解し、最終的に次のようになります。

aaaeiiiiggnnsssuuy

3. Unicodeの基礎

コードに直接飛び込む前に、Unicodeの基本をいくつか学びましょう。

発音区別符号またはアクセント記号で文字を表すために、Unicodeは異なるコードポイントのシーケンスを使用できます。その理由は、古い文字セットとの歴史的な互換性です。

Unicode正規化は、標準で定義された等価形式を使用した文字の分解です。

3.1. Unicode等価フォーム

コードポイントのシーケンスを比較するために、Unicodeは正規等価互換性の2つの用語を定義しています。

正規に同等のコードポイントは、表示されたときに同じ外観と意味を持ちます。 たとえば、文字「ś」(アキュート付きのラテン文字「s」)は、1つのコードポイント+U015Bまたは2つのコードポイント+U0073(ラテン文字「s」)と+ U0301(アキュート記号)で表すことができます。

一方、互換性のあるシーケンスは、外観は異なりますが、コンテキストによっては同じ意味を持つ場合があります。 たとえば、コードポイント+ U013F(ラテン文字「Ŀ」)は、シーケンス+ U004C(ラテン文字「L」)および+ U00B7(記号「・」)と互換性があります。 さらに、一部のフォントはLの内側に中央のドットを表示し、一部のフォントはそれに続くドットを表示できます。

正規に同等のシーケンスは互換性がありますが、その逆が常に当てはまるとは限りません。

3.2. キャラクターの分解

文字分解は、複合文字をベース文字のコードポイントに置き換え、その後に文字を結合します(等価形式に従って)。 たとえば、この手順では、文字「ā」を文字「a」と「-」に分解します。

3.3. 発音区別符号とアクセント記号の一致

分音記号から基本文字を分離したら、不要な文字に一致する式を作成する必要があります。 文字ブロックまたはカテゴリのいずれかを使用できます。

最も人気のあるUnicodeコードブロックは、合成能力ダイアクリティカルマークです。 それほど大きくはなく、112個の最も一般的な結合文字が含まれています。 一方、UnicodeカテゴリMarkを使用することもできます。 これは、マークを組み合わせ、さらに3つのサブカテゴリに分割されるコードポイントで構成されます。

  • Nonspacing_Mark :このカテゴリには1,839個のコードポイントが含まれます
  • Enclosing_Mark :13個のコードポイントが含まれています
  • Spacing_Combining_Mark :443ポイントが含まれています

Unicode文字ブロックとカテゴリの主な違いは、文字ブロックに連続した範囲の文字が含まれていることです。 一方、カテゴリには多くの文字ブロックを含めることができます。 たとえば、これはまさに合成可能なダイアクリティカルマークの場合です。このブロックに属するすべてのコードポイントは、Nonspacing_Markカテゴリにも含まれます。

4. アルゴリズム

基本のUnicode用語を理解したので、Stringから発音区別符号を削除するアルゴリズムを計画できます。

まず、ノーマライザークラスを使用して、ベース文字をアクセント記号と発音区別符号から分離します。 さらに、Java列挙型NFKDとして表される互換性分解を実行します。 さらに、互換性分解を使用します。これは、正規の方法よりも多くの合字を分解するためです(たとえば、合字「fi」)。

次に、\ p {M}正規表現を使用して、Unicodeマークカテゴリに一致するすべての文字を削除します。 マークの範囲が最も広いため、このカテゴリを選択します。

5. コアJavaの使用

コアJavaを使用したいくつかの例から始めましょう。

5.1. 文字列が正規化されているかどうかを確認します

正規化を実行する前に、Stringがまだ正規化されていないことを確認することをお勧めします。

assertFalse(Normalizer.isNormalized("āăąēîïĩíĝġńñšŝśûůŷ", Normalizer.Form.NFKD));

5.2. 文字列の分解

String が正規化されていない場合は、次の手順に進みます。 ASCII文字を発音区別符号から分離するために、互換性分解を使用してUnicodeテキストの正規化を実行します。

private static String normalize(String input) {
    return input == null ? null : Normalizer.normalize(input, Normalizer.Form.NFKD);
}

この手順の後、文字「â」と「ä」の両方が「a」に縮小され、その後にそれぞれの発音区別符号が続きます。

5.3. 発音区別符号とアクセント記号を表すコードポイントの削除

String を分解したら、不要なコードポイントを削除します。 したがって、Unicode正規表現 \ p{M}を使用します。

static String removeAccents(String input) {
    return normalize(input).replaceAll("\\p{M}", "");
}

5.4. テスト

実際に分解がどのように機能するかを見てみましょう。 まず、Unicodeで定義された正規化形式の文字を選び、すべての発音区別符号を削除することを期待しましょう。

@Test
void givenStringWithDecomposableUnicodeCharacters_whenRemoveAccents_thenReturnASCIIString() {
    assertEquals("aaaeiiiiggnnsssuuy", StringNormalizer.removeAccents("āăąēîïĩíĝġńñšŝśûůŷ"));
}

次に、分解マッピングなしでいくつかの文字を選択しましょう。

@Test
void givenStringWithNondecomposableUnicodeCharacters_whenRemoveAccents_thenReturnOriginalString() {
    assertEquals("łđħœ", StringNormalizer.removeAccents("łđħœ"));
}

予想通り、私たちの方法ではそれらを分解できませんでした。

さらに、分解された文字の16進コードを検証するためのテストを作成できます。

@Test
void givenStringWithDecomposableUnicodeCharacters_whenUnicodeValueOfNormalizedString_thenReturnUnicodeValue() {
    assertEquals("\\u0066 \\u0069", StringNormalizer.unicodeValueOfNormalizedString("fi"));
    assertEquals("\\u0061 \\u0304", StringNormalizer.unicodeValueOfNormalizedString("ā"));
    assertEquals("\\u0069 \\u0308", StringNormalizer.unicodeValueOfNormalizedString("ï"));
    assertEquals("\\u006e \\u0301", StringNormalizer.unicodeValueOfNormalizedString("ń"));
}

5.5. Collator を使用して、アクセントを含む文字列を比較します

パッケージjava.textには、もう1つの興味深いクラスCollatorが含まれています。 は、ロケールに依存する文字列比較を実行できるようにします。 重要な構成プロパティは、Collatorの強度です。 このプロパティは、比較中に重要と見なされる差異の最小レベルを定義します。

Javaは、Collatorに4つの強度値を提供します。

  • PRIMARY :大文字と小文字とアクセントを省略した比較
  • SECONDARY :大文字と小文字を省略し、アクセントと発音区別符号を含む比較
  • TERTIARY :大文字と小文字とアクセントを含む比較
  • IDENTICAL :すべての違いは重要です

いくつかの例を確認してみましょう。まず、主な強みを示します。

Collator collator = Collator.getInstance();
collator.setDecomposition(2);
collator.setStrength(0);
assertEquals(0, collator.compare("a", "a"));
assertEquals(0, collator.compare("ä", "a"));
assertEquals(0, collator.compare("A", "a"));
assertEquals(1, collator.compare("b", "a"));

二次強度はアクセント感度をオンにします:

collator.setStrength(1);
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(0, collator.compare("A", "a"));
assertEquals(0, collator.compare("a", "a"));

三次強度にはケースが含まれます:

collator.setStrength(2);
assertEquals(1, collator.compare("A", "a"));
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(0, collator.compare("a", "a"));
assertEquals(0, collator.compare(valueOf(toChars(0x0001)), valueOf(toChars(0x0002))));

同一の強さはすべての違いを重要にします。 最後から2番目の例は、Unicode制御コードポイント+ U001(「見出しの開始」のコード)と+ U002(「テキストの開始」)の違いを検出できるため、興味深いものです。

collator.setStrength(3);
assertEquals(1, collator.compare("A", "a"));
assertEquals(1, collator.compare("ä", "a"));
assertEquals(1, collator.compare("b", "a"));
assertEquals(-1, collator.compare(valueOf(toChars(0x0001)), valueOf(toChars(0x0002))));
assertEquals(0, collator.compare("a", "a")));

言及する価値のある最後の例は、文字に定義された分解ルールがない場合、同じ基本文字を持つ別の文字と等しいとは見なされないことを示しています。 これは、CollatorがUnicode分解を実行できないためです。

collator.setStrength(0);
assertEquals(1, collator.compare("ł", "l"));
assertEquals(1, collator.compare("ø", "o"));

6. ApacheCommonsの使用StringUtils

コアJavaを使用してアクセントを削除する方法を確認したので、 Apache CommonsTextが提供するものを確認します。 すぐにわかるように、の方が使いやすいですが、分解プロセスを制御することはできません。 内部では、 Normalizer.normalize()メソッドとNFD分解形式および\p{InCombiningDiacriticalMarks}正規表現を使用します。

static String removeAccentsWithApacheCommons(String input) {
    return StringUtils.stripAccents(input);
}

6.1. テスト

この方法を実際に見てみましょう—まず、分解可能なUnicode文字のみ

@Test
void givenStringWithDecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnASCIIString() {
    assertEquals("aaaeiiiiggnnsssuuy", StringNormalizer.removeAccentsWithApacheCommons("āăąēîïĩíĝġńñšŝśûůŷ"));
}

予想通り、すべてのアクセントを取り除きました。

線画と文字を含む文字列を試してみましょう。

@Test 
void givenStringWithNondecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnModifiedString() {
    assertEquals("lđħœ", StringNormalizer.removeAccentsWithApacheCommons("łđħœ"));
}

ご覧のとおり、 StringUtils.stripAccents()メソッドは、ラテン語のłおよびŁ文字の変換規則を手動で定義します。 しかし、残念ながら、他の合字は正規化されません 。

7. Javaでの文字分解の制限

要約すると、一部の文字には分解ルールが定義されていないことがわかりました。 より具体的には、 Unicodeは、ストロークを持つ合字および文字の分解ルールを定義しません。 そのため、Javaもそれらを正規化することはできません。 これらの文字を削除したい場合は、文字起こしマッピングを手動で定義する必要があります。

最後に、アクセント記号や発音区別符号を取り除く必要があるかどうかを検討する価値があります。 一部の言語では、発音区別符号から削除された文字はあまり意味がありません。 このような場合は、 Collector クラスを使用して、ロケール情報を含む2つのStringsを比較することをお勧めします。

8. 結論

この記事では、コアJavaと人気のあるJavaユーティリティライブラリであるApacheCommonsを使用してアクセントと発音区別符号を削除する方法について説明しました。 また、いくつかの例を見て、アクセントを含むテキストを比較する方法と、アクセントを含むテキストを操作するときに注意すべきいくつかのことを学びました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。