从 Java 中的字符串中删除重音符号和变音符号

评论 0 浏览 0 2021-10-17

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上

最后更新2023-03-29
0 个评论
标签