1. 概述
许多字母表包含重音符号和变音符号。为了可靠地搜索或索引数据,我们可能希望将带有变音符号的字符串转换为仅包含ASCII 字符的字符串。Unicode 定义了有助于实现此目的的文本规范化过程。
在本教程中,我们将了解什么是Unicode 文本规范化、我们如何使用它来删除变音符号以及需要注意的陷阱。然后,我们将看到一些使用JavaNormalizer
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 代码块是 它不是很大,仅包含112 个最常见的组合字符。另一方面,我们也可以使用Unicode 类别 它由组合标记的代码点组成,并进一步分为三个子类别:Combining Diacritical Marks
Mark
Nonspacing_Mark
:该类别包括1,839 个代码点Enclosing_Mark
: 包含13 个代码点Spacing_Combining_Mark
: 包含443 个点
Unicode 字符块和类别之间的主要区别在于字符块包含一个连续的字符范围。另一方面,一个类别可以有许多字符块。例如,这正是Combining Diacritical Marks
Nonspacing_Mark
4. 算法
现在我们了解了基本的Unicode 术语,我们可以规划算法以从String
首先,我们将** 此外,我们将执行表示为Java 枚举 此外,我们使用兼容性分解,因为它比规范方法分解了更多的连字(例如,连字“fi”)。Normalizer
NFKD
其次,我们将 我们选择这个类别是因为它提供了最广泛的标记。\p{M}
Mark
5. 使用核心Java
让我们从一些使用核心Java 的示例开始。
5.1.检查String
在我们执行规范化之前,我们可能想要检查String
assertFalse(Normalizer.isNormalized("āăąēîïĩíĝġńñšŝśûůŷ", Normalizer.Form.NFKD));
5.2.字符串分解
如果我们的 为了将ASCII 字符与变音符号分开,我们将使用兼容性分解来执行Unicode 文本规范化:String
private static String normalize(String input) { return input == null ? null : Normalizer.normalize(input, Normalizer.Form.NFKD); }
在这一步之后,字母“â”和“ä”都将缩减为“a”,后跟各自的变音符号。
5.3.删除代表变音符号和重音符号的代码点
一旦我们分解了我们的 因此,我们将使用String
\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
collator.setStrength(0); assertEquals(1, collator.compare("ł", "l")); assertEquals(1, collator.compare("ø", "o"));
6. 使用Apache Commons StringUtils
现在我们已经了解了如何使用核心Java 来删除重音符号,我们将检查Apache Commons Text 提供的内容。我们很快就会了解到,在幕后,它使用Normalizer.normalize()
NFD
static String removeAccentsWithApacheCommons(String input) { return StringUtils.stripAccents(input); }
6.1.测试
让我们在实践中看看这个方法——首先,
@Test void givenStringWithDecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnASCIIString() { assertEquals("aaaeiiiiggnnsssuuy", StringNormalizer.removeAccentsWithApacheCommons("āăąēîïĩíĝġńñšŝśûůŷ")); }
正如预期的那样,我们摆脱了所有的口音。
让我们尝试一个
@Test void givenStringWithNondecomposableUnicodeCharacters_whenRemoveAccentsWithApacheCommons_thenReturnModifiedString() { assertEquals("lđħœ", StringNormalizer.removeAccentsWithApacheCommons("łđħœ")); }
正如我们所见,但是,不幸的是,它并没有规范其他连字StringUtils.stripAccents()
7. Java 中字符分解的局限性
综上所述,我们看到有些字符没有定义分解规则。更具体地说,因此,Java 也无法对它们进行规范化。如果我们想摆脱这些字符,我们必须手动定义转录映射。
最后,值得考虑的是我们是否需要摆脱重音和变音符号。对于某些语言,去掉变音符号的字母没有多大意义。在这种情况下,更好的主意是使用Collator
Strings
8. 结论
在本文中,我们研究了使用核心Java 和流行的Java 实用程序库Apache Commons 删除重音和变音符号。我们还看到了一些示例并学习了如何比较包含重音的文本,以及在处理包含重音的文本时需要注意的一些事项。
0 评论