正确的字符串标准化以进行比较


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’

我们该如何解决这一问题?

规则1:如果可以,请不要比较人类文字

如果可以,请不要将字符串与异构系统进行比较。正确地做到这一点出奇的棘手(即使有可能处理大多数情况,如下文所示)。而是比较序列,UUID或任何其他不带空格或“特殊”字符的基于ASCII的字符串。来自不同信息系统的字符串可能会以不同方式存储数据(小写/大写,有/没有变音符号等)。相反,好的ID由纯ASCII字符串组成,因此没有编码问题。

例子:

系统1: {"id":"8b286f72-b366-47a4-9537-59d39411979a","desc":"Œeuf brouillé"}

系统2:{"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'

规则2:在记忆体中标准化

最好在可能的最后时刻比较字符串,并在内存中进行比较,而不要在存储时对字符串进行规范化。至少有两个原因:

  1. 如果仅存储字符串的规范化版本,则会丢失信息。出于显示目的或其他原因,您稍后可能需要适当的变音符号。作为IT专业人员,您的任务之一是永远不会丢失人类为您提供的信息。
  2. 如果在设置标准化例程之前已存储了某些项目怎么办?如果归一化函数随时间变化怎么办? 为了避免这些常见的陷阱,只需在内存中normalize(<data system 1>)与进行比较normalize(<data system 2>)。如果您不每秒比较数千个项目,则CPU开销应该可以忽略不计。

规则3:始终在外部和内部进行修剪

处理人类键入的字符串时,另一个常见的陷阱是在字符序列的开头或中间出现空格。

例如,查看以下字符串:(' Wiliam'请注意开头的空格),'Henry '(请注意结尾的空格),'Gates III'(请参阅此姓氏中间的双精度空格,您是第一次注意到吗?)。

适当的解决方案:

  1. 修剪文本以删除文本开头和结尾的空格。
  2. 删除字符串中间多余的空格。

在Java中,实现此目标的方法之一是:

s = s.replaceAll("\\s{2,}", " ").trim();

规则4:协调字母框

这是最著名和最直接的规范化方法:只需将每个字母大小写都可以。据我所知,没有一个或另一个选择的偏好。大多数开发人员(包括我自己)使用小写字母。

在Java中,只需使用toLowerCase()

s = s.toLowerCase();

规则5:将带有变音符号的字符转换为ASCII

输入时,通常会省略变音符号,而使用ASCII版本。例如,您可以输入德语单词'schon'而不是'schön'。

Unicode提出了四种可用于该目的的规范化形式(NFC,NFD,NFKD和NFKC)。查看此启发性插图。

详细介绍所有这些形式将超出本文的范围,但是,基本上,某些Unicode字符可以编码为单个组合字符或分解形式。例如,之后'é'可以编码为\u00e9代码点或分解形式'\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”。

对于这些类型的“暹罗双胞胎”字符,我们必须应用NFKD(“兼容性分解”)规范化形式,该形式既分解字符(请参见前面的“规则5”),又将连字映射到几个“基本”字符。然后,您可以删除其余的变音符号。

在Java中,使用:

s = Normalizer.normalize(s, Normalizer.Form.NFKD)
    .replaceAll("[^\\p{ASCII}]", "");

现在是个坏消息:由于晦涩的原因,Unicode不支持某些广泛使用的连字的分解等价形式,例如法语“ œ”“ æ”或德语“ eszett ß”。如果需要处理它们,则必须在应用NFKD规范化之前编写自己的替换项:

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