TL; DR 在Java中,请执行以下操作:
String normalizedString = Normalizer.normalize(originalString,Normalizer.Form.NFKD) .replaceAll("[^\\p{ASCII}]", "").toLowerCase().replaceAll("\\s{2,}", " ").trim();
字符串归一化简介
如今,大多数字符串都是Unicode编码,我们能够将工作与带编印符号/口音各种原始字符(比如ö,é,À)或连字(如æ或ʥ)。字符可以存储在(例如)UTF-8中,并且如果字体支持,则可以正确显示关联的字形。
但是,在比较从不同信息系统发出的字符串和/或最初由人类键入的字符串时,我们经常会发现反复出现的困难。
人脑是填补空白的机器。因此,用'e'代替完全没有问题'ê'。
但是,如果该单词'tête'('head'法语)正确存储在UTF-8编码的数据库中,但是您必须将其与最终用户创建的带有重音符号的文本进行比较,该怎么办?
我们还必须处理遗留系统或充满不支持Unicode标准的遗留数据的现代系统。
关于此问题的另一个简单说明是连字的使用。想象一下一个产品数据库,其中存储了带有ID和描述的各种商品。有些项目包含连字(几个字母的组合在一起以创建单个字符,例如’Œuf’-法语中的egg)。像大多数法国人一样,即使使用法语键盘,我也不知道如何制作这样的字符。我将使用搜索项目说明oeuf。显然,如果我们想返回包含的有用结果,我们的代码必须注意连字’Œuf’。
’Œuf’-
’Œuf’
我们该如何解决这一问题?
规则1:如果可以,请不要比较人类文字
如果可以,请不要将字符串与异构系统进行比较。正确地做到这一点出奇的棘手(即使有可能处理大多数情况,如下文所示)。而是比较序列,UUID或任何其他不带空格或“特殊”字符的基于ASCII的字符串。来自不同信息系统的字符串可能会以不同方式存储数据(小写/大写,有/没有变音符号等)。相反,好的ID由纯ASCII字符串组成,因此没有编码问题。
例子:
系统1: {"id":"8b286f72-b366-47a4-9537-59d39411979a","desc":"Œeuf brouillé"}
{"id":"8b286f72-b366-47a4-9537-59d39411979a","desc":"Œeuf brouillé"}
系统2:{"id":"8b286f72-b366-47a4-9537-59d39411979a","desc":"OEUF BROUILLE"}
{"id":"8b286f72-b366-47a4-9537-59d39411979a","desc":"OEUF BROUILLE"}
如果您比较ID,一切都很简单,您可以早点回家。如果比较说明,则必须将其标准化为前提条件,否则会遇到很大麻烦。
字符规范化是计算字符串的规范形式的操作。为了避免在比较来自多个信息系统的字符串时出现误报,请对两个字符串进行规范化并比较其规范化的结果。
在前面的例子中,我们会比较normalize("Œeuf brouillé")有normalize("OEUF BROUILLE")。然后使用适当的归一化函数进行比较'oeuf brouille','oeuf brouille'但是如果归一化函数有错误或部分错误,则字符串将不匹配。例如,如果normalize()功能不处理连写正确,你将通过比较得到一个假阳性'œuf brouille'与'oeuf brouille'。
normalize("Œeuf brouillé")
normalize("OEUF BROUILLE")
'oeuf brouille'
'œuf brouille'
规则2:在记忆体中标准化
最好在可能的最后时刻比较字符串,并在内存中进行比较,而不要在存储时对字符串进行规范化。至少有两个原因:
normalize(<data system 1>)
normalize(<data system 2>)
规则3:始终在外部和内部进行修剪
处理人类键入的字符串时,另一个常见的陷阱是在字符序列的开头或中间出现空格。
例如,查看以下字符串:(' Wiliam'请注意开头的空格),'Henry '(请注意结尾的空格),'Gates III'(请参阅此姓氏中间的双精度空格,您是第一次注意到吗?)。
' Wiliam'
'Henry '
'Gates III'
适当的解决方案:
在Java中,实现此目标的方法之一是:
s = s.replaceAll("\\s{2,}", " ").trim();
规则4:协调字母框
这是最著名和最直接的规范化方法:只需将每个字母大小写都可以。据我所知,没有一个或另一个选择的偏好。大多数开发人员(包括我自己)使用小写字母。
在Java中,只需使用toLowerCase():
toLowerCase()
s = s.toLowerCase();
规则5:将带有变音符号的字符转换为ASCII
输入时,通常会省略变音符号,而使用ASCII版本。例如,您可以输入德语单词'schon'而不是'schön'。
Unicode提出了四种可用于该目的的规范化形式(NFC,NFD,NFKD和NFKC)。查看此启发性插图。
详细介绍所有这些形式将超出本文的范围,但是,基本上,某些Unicode字符可以编码为单个组合字符或分解形式。例如,之后'é'可以编码为\u00e9代码点或分解形式'\u0065'('e'letter)+ '\u0301'(变音符号'◌́'')。
'\u0065'('e'letter)+ '\u0301'
'◌́''
我们将对初始文本执行NFD(“规范分解”)规范化方法,以确保将每个带有重音符号的字符都转换为其分解形式。然后,我们要做的就是删除变音符号,只保留“基本”简单字符。
在Java中,两种操作都可以通过以下方式完成:
s = Normalizer.normalize(s, Normalizer.Form.NFD) .replaceAll("[^\\p{ASCII}]", "");
注意:即使代码涵盖了此问题,我也希望NFKD转换也能处理连字(请参见下文)。
规则6:将连字分解为一组ASCII字符
要理解的另一件事是,Unicode在大约5000个“复合”字符(例如连字或预先组合的罗马数字)和常规字符列表之间保持了一些兼容性映射。支持此功能的字符已记录在案(请检查Unicode字符文档中的'分解'属性)。
例如; 罗马数字Ⅻ(U + 216B)可以用NFKD归一化为an'X'和2 'I's分解。同样,ij(U + 0133)字符(如'fijn'荷兰语中的-“ nice”)可以分解为“ i”和“ j”。
(U + 216B)
FKD
n'X'
2 'I's
(U + 0133)
fijn
nice
i
j
对于这些类型的“暹罗双胞胎”字符,我们必须应用NFKD(“兼容性分解”)规范化形式,该形式既分解字符(请参见前面的“规则5”),又将连字映射到几个“基本”字符。然后,您可以删除其余的变音符号。
在Java中,使用:
s = Normalizer.normalize(s, Normalizer.Form.NFKD) .replaceAll("[^\\p{ASCII}]", "");
现在是个坏消息:由于晦涩的原因,Unicode不支持某些广泛使用的连字的分解等价形式,例如法语“ œ”和“ æ”或德语“ eszett ß”。如果需要处理它们,则必须在应用NFKD规范化之前编写自己的替换项:
Unicode
“ œ”
“ æ”
“ eszett ß”
s = s.replaceAll("œ", "oe"); s = s.replaceAll("æ", "ae"); s = Normalizer.normalize(s, Normalizer.Form.NFKD) .replaceAll("[^\\p{ASCII}]", "");
规则7:当心标点符号
这是一个较小的问题,但是根据上下文,您可能还需要规范化一些特殊的标点符号。
例如,在文学方面,例如文本修订软件,将em / long破折号('—')字符映射到常规ASCII连字符('-')是一个好主意。
('—')
('-')
据我所知,Unicode并没有为此提供映射,因此您必须自己使用老式方法:
s = s.replaceAll("—", "-");
最后的话
在比较从不同系统发出的字符串或执行适当的比较时,字符串规范化非常有用。甚至是完全英语本地化的项目也可以从中受益,例如照顾空格或尾随空格,或者处理带有重音符号的外来词时。
本文提供了一些最重要的要考虑的要点,但是还远远不够。例如,我们省略了亚洲字符操纵或语义等同项的文化规范化(例如的'St'缩写'Saint'),但我希望这对于大多数项目都是一个好的开始。
原文链接:http://codingdict.com