从 Java 中的字符串中删除重音符号和变音符号
1.概述
许多字母包含重音和变音符号。为了可靠地搜索或索引数据,我们可能想把带有变音符的字符串转换成只包含ASCII字符的字符串。Unicode定义了一个文本规范化程序,以帮助实现这一目标。
在本教程中,我们将看到什么是Unicode文本规范化,我们如何使用它来去除变音符号,以及需要注意的陷阱。然后,我们将看到一些使用Java Normalizer类和Apache Commons StringUtils的例子。
2.问题一目了然
假设我们正在处理含有我们想要删除的变音符号范围的文本:
āăąēîïĩíĝġńñšŝśûůŷ
读完这篇文章后,我们就会知道如何摆脱变音符,并最终得到:
aaaeiiiiggnnsssuuy
3.Unicode基础知识
在直接跳入代码之前,让我们先学习一些Unicode的基本知识。
为了表示一个带有变音或重音符号的字符,Unicode可以使用不同的码位序列。其原因是与旧的字符集的历史兼容性。
Unicode 规范化是使用标准定义的等价形式分解字符。
3.1.Unicode等价形式
为了比较代码点的序列,Unicode定义了两个术语:规范等效性和兼容性。
在显示的时候,经典的等价代码点具有相同的外观和意义。例如,字母“ś”(拉丁字母“s”与锐角)可以用一个码位+U015B表示,或者用两个码位+U0073(拉丁字母“s”)和+U0301(锐角符号)表示。
另一方面,兼容的序列可以有不同的外观,但在某些情况下有相同的含义。例如,代码点+U013F(拉丁文连接词“Ŀ”)与序列+U004C(拉丁字母“L”)和+U00B7(符号“-”)兼容。此外,有些字体可以在L里面显示中间的点,有些则在它后面。
从规范上讲,等价的序列是兼容的,但相反的情况并不总是如此。
3.2.字符分解
字符分解是用一个基本字母的码位来替换复合字符,然后再组合字符(根据等价形式)。例如,这个程序将把字母“ā”分解为字符“a”和“-“。
3.3.匹配变音符号和重音符号
一旦我们将基本字符与变音符号分开,我们必须创建一个匹配不需要的字符的表达式。我们可以使用一个字符块或一个类别。
最受欢迎的Unicode代码块是 Combining Diacritical Marks 。它不是很大,只包含112个最常见的组合字符。另一方面,我们也可以使用Unicode类别 标记 。它由组合标记的代码点组成,并进一步划分为三个子类别:
- Nonspacing_Mark: 该类别包括1839个代码点。
- Enclosing_Mark: 包含13个代码点
- Spacing_Combining_Mark :包含443个点
Unicode字符块和类别之间的主要区别是,字符块包含一个连续的字符范围。另一方面,一个类别可以有许多字符块。例如,这正是Combining Diacritical Marks的情况:所有属于这个块的码位也都包括在Nonspacing_Mark类别中。
4.算法
现在我们了解了Unicode的基本术语,我们可以计划一下从String中去除变音符号的算法。
首先,我们将使用 Normalizer 类将基本字符与重音符号和变音符号分开。此外,我们将执行以 Java 枚举 NFKD 表示的兼容性分解。此外,我们使用兼容性分解,因为它比规范方法分解更多的连字(例如,连字“fi”)。
其次,我们将使用 \p{M} 正则表达式删除所有匹配 Unicode Mark 类别的字符。我们选择这个类别是因为它提供了最广泛的标记。
5.使用Core 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的强度。这个属性定义了在比较过程中被认为是重要的最小差异水平。
Java为一个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删除重音和变音符号。我们还看到了一些例子,了解了如何比较含有重音的文本,以及在处理含有重音的文本时需要注意的一些问题。
像往常一样,文章的完整源代码在GitHub上。