用JAVA從字符串中刪除重音和變音符號
- java
- Apache Commons
1. 概述
許多字母表包含重音符號和變音符號。為了可靠地搜索或索引數據,我們可能希望將帶有變音符號的字符串轉換為僅包含 ASCII 字符的字符串。 Unicode 定義了有助於實現此目的的文本規範化過程。
在本教程中,我們將了解什麼是 Unicode 文本規範化、我們如何使用它來刪除變音符號以及需要注意的陷阱。然後,我們將看到一些使用 Java Normalizer
類和 Apache Commons StringUtils.
2. 問題概覽
假設我們正在處理包含要刪除的變音符號範圍的文本:
āăąēîïĩíĝġńñšŝśûůŷ
閱讀本文後,我們將知道如何擺脫變音符號並最終得到:
aaaeiiiiggnnsssuuy
3. Unicode 基礎
在直接進入代碼之前,讓我們學習一些 Unicode 基礎知識。
為了用變音符號或重音符號表示字符,Unicode 可以使用不同的代碼點序列。原因是與舊字符集的歷史兼容性。
Unicode 規範化是使用標准定義的等價形式分解字符。
3.1. Unicode 等價形式
為了比較代碼點序列,Unicode 定義了兩個術語: canonical equivalence
和compatibility
。
標準等效代碼點在顯示時具有相同的外觀和含義。例如,字母“ś”(帶有銳角的拉丁字母“s”)可以用一個碼位+U015B 或兩個碼位+U0073(拉丁字母“s”)和+U0301(銳角符號)來表示。
另一方面,兼容序列在某些情況下可以具有不同的外觀但具有相同的含義。例如,代碼點 +U013F(拉丁文連字“Ŀ”)與序列 +U004C(拉丁字母“L”)和 +U00B7(符號“·”)兼容。此外,有些字體可以在 L 內顯示中間點,有些在它後面。
規範等價序列是兼容的,但相反的情況並不總是正確的。
3.2.字符分解
字符分解用基本字母的代碼點替換複合字符,然後組合字符(根據等價形式)。例如,此過程將字母“ā”分解為字符“a”和“-”。
3.3.匹配變音符號和重音符號
一旦我們將基本字符與變音符號分開,我們必須創建一個匹配不需要的字符的表達式。我們可以使用字符塊或類別。
最流行的 Unicode 代碼塊是Combining Diacritical Marks
。它不是很大,僅包含 112 個最常見的組合字符。另一方面,我們也可以使用 Unicode 類別Mark
。它由組合標記的代碼點組成,並進一步分為三個子類別:
-
Nonspacing_Mark
:該類別包括 1,839 個代碼點 -
Enclosing_Mark
: 包含 13 個代碼點 -
Spacing_Combining_Mark
: 包含 443 個點
Unicode 字符塊和類別之間的主要區別在於字符塊包含一個連續的字符範圍。另一方面,一個類別可以有許多字符塊。例如,這正是Combining Diacritical Marks
的情況:屬於該塊的所有代碼點也包含在Nonspacing_Mark
類別中。
4. 算法
現在我們了解了基本的 Unicode 術語,我們可以規划算法以從String
刪除變音符號。
首先,我們將**Normalizer
類將基本字符與重音和變音符號分開**。此外,我們將執行表示為 Java 枚舉NFKD
的兼容性分解。此外,我們使用兼容性分解,因為它比規範方法分解了更多的連字(例如,連字“fi”)。
其次,我們將使用\p{M}
regex expression Mark
類別匹配的所有字符。我們選擇這個類別是因為它提供了最廣泛的標記。
5. 使用核心 Java
讓我們從一些使用核心 Java 的示例開始。
5.1.檢查String
是否規範化
在我們執行規範化之前,我們可能想要檢查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("łđħœ"));
}
正如預期的那樣,我們的方法無法分解它們。
此外,我們可以創建一個測試來驗證分解字符的十六進制代碼:
@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
包含另一個有趣的類 – Collator
。它使我們能夠執行語言環境敏感的String
比較。一個重要的配置屬性是Collator's
強度。此屬性定義在比較期間被視為顯著的最小差異級別。
Collator
提供了四個強度值:
-
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))));
相同的強度使所有差異都變得重要。倒數第二個例子很有趣,因為我們可以檢測 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. 使用 Apache Commons StringUtils
現在我們已經了解瞭如何使用核心 Java 來刪除重音符號,我們將檢查 Apache Commons Text 提供的內容。我們很快就會了解到,它更易於使用,但我們對分解過程的控制較少。在幕後,它使用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 也無法對它們進行規範化。如果我們想擺脫這些字符,我們必須手動定義轉錄映射。
最後,值得考慮的是我們是否需要擺脫重音和變音符號。對於某些語言,去掉變音符號的字母沒有多大意義。在這種情況下,更好的主意是使用Collator
類並比較兩個Strings
,包括區域設置信息。
8. 結論
在本文中,我們研究了使用核心 Java 和流行的 Java 實用程序庫 Apache Commons 刪除重音和變音符號。我們還看到了一些示例並學習瞭如何比較包含重音的文本,以及在處理包含重音的文本時需要注意的一些事項。