案例真相程序员应该知道的

2018年北湾Python大会上,我发表了关于用户名演讲演讲中的大部分信息都是我在支持django注册的12年中收集的这次经历给我带来了比我计划获得的更多有关“简单”事物的知识。



但是,在演讲开始时,我提到这不会是“程序员相信的关于X的一系列误解”系列的另一篇文章。您可以找到许多这样的启示。但是,我不喜欢这些文章。他们列出了各种被认为是错误的东西,但是很少解释为什么会这样,应该怎么做。我怀疑人们会读这些文章,祝贺自己取得的成就,然后去寻找有趣的新方法来犯这些文章中未提及的错误。这是因为他们并不真正了解导致这些错误的问题。



因此,在我的报告中,我试图尽可能地解释一些问题并解释如何解决它们-我更喜欢这种方法。我只是顺便谈到了一个话题(这只是一张幻灯片,而在其他幻灯片上有几次提及)是与字符大小写相关的复杂性。对于我讨论的问题,有一个官方的正确答案™–不区分大小写的ID比较,在我的演讲中,我仅使用Python标准库提供了我所知道的最佳解决方案。



但是,我简短地提到了Unicode情况的更深层次的复杂性,我想花一些时间来描述细节。这很有趣,理解它可以帮助您在设计和编写文本处理代码时做出决定。因此,我为您提供了文章``程序员对X的误解''-程序员应了解的真相的反面。



还有一件事:Unicode充满了术语。在本文中,由于Unicode标准,我将主要使用“大写”和“小写”定义。使用这些术语。如果您喜欢其他术语,例如小写/大写字母,也可以。另外,我经常会使用“符号”一词,有些人可能会认为这是不正确的。是的,在Unicode中,“字符”的概念并不总是人们所期望的,因此通常最好使用其他术语来避免使用它。但是,在本文中,我将使用Unicode中使用的术语-描述可以声明的抽象实体。每当重要时,我都会使用更具体的术语(例如代码点)来阐明。



有两个以上的寄存器



以欧洲语言为母语的人习惯于他们的语言使用大小写字母来表示特定事物的事实。例如,在英语(和俄语)语言中,我们通常以大写字母开头的句子,最经常以小写字母开头的句子。同样,专有名称以大写字母开头,许多首字母缩写词和缩写都以大写字母书写。



我们通常认为只有两个寄存器。有字母“ A”和字母“ a”。一个大写,一个小写-是不是?



但是,Unicode中有三个寄存器。有一个大写字母,有一个小写字母,还有一个标题大写[titlecase]。用英语,名字是用这种方式写的。例如,“复仇者联盟:无限战争”。通常,每个单词的第一个字母为此只是大写(并且根据不同的规则和样式,某些单词(例如文章)不会大写)。



Unicode标准给出了一个大写字母的示例:U + 01F2带小Z的拉丁大写字母D。看起来像这样::。



有时需要使用此类字符来处理Unicode标准最早解决方案之一的负面影响:与现有文本编码的向后兼容性。对于Unicode,使用标准字符组合来构造序列会更加方便。但是,在许多现有系统中,已经为现成的序列分配了空间。例如,在ISO-8859-1(“ latin-1”)中,“é”字符具有一个现成的形式,编号为0xe9。在Unicode中,最好将此字母写成一个单独的“ e”和一个重音符号。但是,为了确保与现有的编码(例如latin-1)完全向后兼容,Unicode还会为现成的字符分配代码点。例如,U + 00E9带有小写字母的拉丁文小写字母E。



尽管此字符的代码位置与其拉丁1字节值相同,但您不应依赖此字符。Unicode中的字符编码不太可能保留这些位置。例如,在UTF-8中,代码位置U + 00E9被写为字节序列0xc3 0xa9。



并且,当然,在使用大写字母的情况下,现有编码中的字符需要进行特殊处理,这就是为什么它们按原样包含在Unicode中的原因。如果要查看它们,请在您喜欢的Unicode数据库中搜索Lt类别中的字符(“ Letter,titlecase”)。



有几种定义大小写的方法



Unicode标准(第4.2节)列出了三种不同的大小写定义。可能要选择三种语言中的一种取决于您的编程语言。否则,您的选择将取决于您的特定目标。这些定义是:

  1. 如果字符在Lu类别中(“字母大写”),则大写;如果在L1类别中(“ Letter,小写”),则小写。该标准认识到此定义的局限性:每个特定符号仅应归于其中一个类别。因此,许多大写或小写“必须”的字符将无法满足此要求,因为它们属于其他类别。
  2. 如果该字符继承大写属性,则为大写;如果继承小写属性,则为小写。它是一个字符定义与其他字符属性(可能包括大小写)的组合。
  3. 如果字符在映射为大写字母后没有变化,则为大写字母。如果字符在映射为小写字母后没有变化,则为小写字母。这是一个相当笼统的定义,但它的行为也可能不直观。




如果您使用符号的有限子集(特别是字母),则1个定义可能就足够了。如果您的曲目范围更广-它包含不是字母的类似字母的符号,则第二个定义可能适合您。Unicode标准§4.2建议:

如果操作Unicode字符串的程序员不直接使用字符属性,则它们应使用诸如isLowerCase(及其功能表亲toLowerCase)之类的字符串函数。




此处提到的功能在Unicode标准的第3.13节中定义。形式上,定义3使用第3.13节中的isLowerCase和isUpperCase函数,分别根据toLowerCase和toUpperCase中的固定位置定义。



如果您的编程语言具有检查或转换字符串或单个字符的大小写的功能,则值得研究一下实现中使用了哪些提及的定义。如果您感到好奇,Python中的isupper()和islower()方法使用第二个定义。



无法通过字符的外观或名称来了解字符的大小写



通过显示许多字符,您可以判断出它们是什么情况。例如,“ A”为大写。从符号名称也很清楚:“拉丁文大写字母A”。但是,有时此方法不起作用。取代码点U + 1D34。看起来像这样:ᴴ。在Unicode中,它被分配了名称:MODIFIER LETTER CAPITALH。所以它是大写的,对吗?



实际上,它继承了小写字母属性,因此根据定义2,尽管它在外观上类似于大写字母H,并且名称中包含单词“ CAPITAL”,但它还是小写字母。



有些字符根本没有大小写



Unicode标准的第3.13节中的定义135规定:

当且仅当C具有小写或大写属性,或General_Category为Titlecase_Letter时,C才区分大小写。




这意味着许多Unicode字符-实际上,其中大多数-都是不区分大小写的。关于案件的问题没有任何意义,案件的变更也不影响他们。但是,我们可以通过定义#3来获得该问题的答案。



一些字符的行为就像它们具有多个寄存器



含义是,如果您使用定义#3并询问一个无大小写的字符是大写还是小写,您将得到一个肯定的答案。



Unicode标准给出了一个字符U + 02BD MODIFIER LETTER REVERSED COMMA(看起来像:ʽ)的示例(表4-1,第7行)。它没有继承的小写或大写属性,不属于Lt类别,因此没有大小写。同时,转换为大写字母不会改变它,转换为小写字母不会改变它,因此,根据第三个定义,它对两个问题都回答“是”:“您属于大写字母吗?”和“你是小写吗?”



看来这可能会引起不必要的混乱,但要点是,定义#3适用于任何Unicode字符序列,并允许您简化大小写转换算法(无大小写的字符会变成自己)。



区分大小写



您可能会认为,如果Unicode大小写转换表覆盖了所有字符,那么此转换仅是在表中找到正确的位置。例如,Unicode数据库说U + 0041拉丁大写字母A是小写的U + 0061拉丁小写字母A。很简单,不是吗?



希腊语是这种方法行不通的一个例子。转换为小写字母时,字符Σ-即U + 03A3希腊大写字母SIGMA-被映射为两个不同的字符,具体取决于其在单词中的位置。如果它在单词的结尾,则为小写ς(U + 03C2希腊小写字母最终SIGMA)。在其他地方为σ(U + 03C3希腊小写字母SIGMA)。



这意味着寄存器不是一对一的或可传递的。另一个示例是ß(U + 00DF拉丁小写字母SHARP S或escet)。尽管现在还有另一种大写形式(ẞ,U + 1E9E拉丁大写字母SHARP S),但它将大写为“ SS”。将“ SS”转换为小写会导致“ ss”,因此(使用Unicode术语进行大小写转换):toLowerCase(toUpperCase(ß))!= Ss。



大小写取决于语言环境



不同的语言具有不同的大小写转换规则。最受欢迎的示例:i(U + 0069拉丁文小写字母I)和I(U + 0049拉丁文大写字母I)在大多数语言环境中-大多数(但不是全部)都相互转换。在语言环境az和tr(突厥语语言)中,大写字母i将是İ(U + 0130带有小点的拉丁文大写字母I),小写字母I将是ı(U + 0131拉丁文小写字母I)。有时候,做到正确确实意味着生与死之间的区别。



Unicode本身不能处理所有语言环境的所有可能的大小写转换规则。 Unicode数据库仅具有转换所有字符的通用规则,而并非特定于语言环境。对于某些语言和复合形式也有特殊规定-立陶宛语,突厥语和希腊语的某些功能。其他一切都不在那里。标准的第3.13节提到了这一点,并建议在必要时引入特定于语言环境的翻译规则。



一个示例是英语符号-这是某些名称的标题大小写。 “ O'brian”应转换为“ O'Brian”(而不是“ O'brian”)。但是,这样做时,“ it's”必须转换为“ It's”而不是“ It'S”。另一个未使用Unicode处理的示例是荷兰字母组合“ij”,当转换为标题时,如果出现在单词的开头,则必须将其转换为全部大写。因此,所有权登记册中荷兰最大的海湾将是“IJsselmeer”而不是“ Ijsselmeer”。如果需要,Unicode具有字符IJU + 0132拉丁大写字母IJ和ijU + 0133拉丁小写字母IJ。默认情况下,大小写转换将它们彼此转换(尽管使用兼容等效性的Unicode规范化形式会将它们分成两个单独的字符)。





返回报告中介绍的材料。 Unicode大小写管理的复杂性意味着无法使用许多编程语言中提供的标准小写或大写转换函数进行不区分大小写的比较。对于此类比较,Unicode具有大小写折叠的概念,并且标准的第3.13节定义了toCaseFold和isCaseFolded函数。



您可能会认为转换为折叠大小写与转换为小写字母相似,但事实并非如此。 Unicode标准警告折叠大小写的字符串不必是小写的。例如,给出了切罗基语-在折叠状态下的字符串中,大写字符也将出现。



在我演讲的幻灯片之一中,Unicode技术报告#36尽可能在Python中完全实现。执行NFKC规范化,然后对结果字符串调用casefold()方法(仅在Python 3+中可用)。即便如此,一些边缘情况仍然存在,这并不是ID比较真正推荐的方法。首先,这是一个坏消息:Python没有公开足够的Unicode属性来过滤掉不在XID_Start或XID_Continue中的字符,或者具有Default_Ignorable_Code_Point属性的字符。据我所知,它不支持NFKC_Casefold映射。也没有简单的方法来使用修改后的NFKC UAX#31§5.1。



好消息是,这些边缘情况中的大多数都不涉及所涉及符号所构成的任何实际安全风险。而且,原则上将案例折叠不定义为规范化保留操作(因此NFKC_Casefold映射,在案例折叠后将其重新规范化为NFC)。通常,在比较时,您不必担心预处理后两个字符串是否均已标准化。您关心的是预处理是否前后不一致,是否可以保证只有“之后”应该不同的行才会有所不同。如果您对此感到担心,则可以在添加寄存器后手动重新规范化。



现在足够了



与上一份报告一样,本文也不是详尽无遗的,并且几乎不可能将所有这些材料都放在一篇文章中。希望本文对本主题的复杂性有所帮助,并提供了足够的起点来寻找更多信息。因此,原则上您可以在这里停止。



希望其他人停止写“程序员相信的关于X的误解”系列文章的曝光,并开始写诸如“程序员应该知道的真相”之类的文章是否天真?



All Articles