ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

为了正确比较而规范Unicode字符串

2021-11-14 11:02:09  阅读:278  来源: 互联网

标签:normalize 字符 unicodedata NFC 正确 s2 micro Unicode 字符串


为了正确比较而规范Unicode字符串

因为Unicode有组合字符(变音字符和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
例如,"café"这个词可以使用两种方式构成,分别由4个和5个码位,但是结果完全一样:

s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
('café', 'café')
len(s1), len(s2)
(4, 5)
s1 == s2
False

U+0301是COMBINING ACUTE ACCENT,加在'e'后面得到'é'。在Unicode标准中,'é'和'e\u0301'这样的序列叫"标准等价物"(canonical equivalent),应用程序应该把它们视作相同的字符,但是Python看到的是不同的码位序列,因此判定二者不相等

这个问题的解决方案是使用unicodedata.normalize函数提供的Unicode规范化。这个函数的第一个参数是这4个字符串中的一个:'NFC'、'NFD'、'NFKC'和'NFKD'。

NFC(Normalization Form C)使用最少码位构成等价的字符串,而NFD把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期

from unicodedata import normalize
s1 = 'café'
s2 = 'cafe\u0301'
len(s1), len(s2)
(4, 5)
len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
normalize('NFC', s1) == normalize('NFC', s2)
True
normalize('NFD', s1) == normalize('NFD', s2)
True

西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC形式。不过,安全起见,保存文本之前,最好使用normalize('NFC',user_text)清洗字符串。NFC也是W3C(Character Model for the World Wide Web: String Matching and Searching)的规范推荐的规范化形式。

使用NFC时,有些单字符会被规范成另一个单字符。例如电阻的单位欧姆(Ω)会被规范成希腊字母大写的欧米茄。这两个字符在视觉上是一样的,但是比较时并不相等,因此要规范化,防止出现意外:

from unicodedata import normalize,name
ohm='\u2126'
name(ohm)
'OHM SIGN'
ohm_c=normalize('NFC',ohm)
name(ohm_c)
'GREEK CAPITAL LETTER OMEGA'
ohm==ohm_c
False
normalize('NFC',ohm)==normalize('NFC',ohm_c)
True

在另外两个规范化形式(NFKC和NFKD)的首字母缩略词中,字母K表示'compatibility'(兼容性)。这两种是比较严格的规范化形式,对'兼容字符'有影响。虽然Unicode的目标是为各个字符提供“规范的”码位,但是为了兼容现有标准,有些字符会出现多次。例如,虽然希腊字母表中有“μ”这个字母(码位是U+03BC,GREEK SMALL LETTER MU),但是Unicode还是加入了微符号'µ'(U+00B5),以便与latin1相互转换。因此微符号是一个“兼容字符”。

在NFKC和NFKD格式中,各个兼容字符会被替换成一个或多个“兼容分解”字符,即便这样有些格式损失,但仍是“首选”表述——理想情况下,格式化是外部标记的职责,不应该由Unicode处理。

二分之一"½"(U+00BD)经过1兼容分解后得到的是三个字符序列'1/2';微符号‘µ’(U+00B5)经过兼容分解后得到的是小写字母'μ'(U+03BC)

from unicodedata import normalize, name
half = '½'
normalize('NFKC', half)
'1⁄2'
four_squared='4²'
normalize('NFKC',four_squared)
'42'
micro = 'µ'
micro_kc = normalize('NFKC', micro)
micro, micro_kc
('µ', 'μ')
ord(micro), ord(micro_kc)
(181, 956)
name(micro), name(micro_kc)
('MICRO SIGN', 'GREEK SMALL LETTER MU')

使用'1/2'替‘½’可以接受,微符号也确实是小写的希腊字母'μ',但是把'4²'转换成‘42’就改变原意了。某些应用可以把‘4²’保存为'42',但是可以为搜索和索引提供便利的中间表述:用户搜索'1/2 inch'时,如果还能找到包含'½ inch'的文档,那么用户会感到满意

使用NFKC和NFKD规范化形式时要小心,而且不能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因此这两种转换会导致数据丢失

大小写折叠

大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能有str.casefold()方法支持。

对于只包含latin1字符的字符串s,s1.casefold()得到的结果和s.lower()一样,唯有两个例外:微符号‘µ’会变成小写的希腊字母‘μ’(在大多数字体中二者看起来一样);德语Eszett(‘sharp s',ß)会变成'ss'。

micro='µ'
name(micro)
'MICRO SIGN'
micro_cf=micro.casefold()
name(micro_cf)
'GREEK SMALL LETTER MU'
micro,micro_cf
('µ', 'μ')
eszett='ß'
name(eszett)
'LATIN SMALL LETTER SHARP S'
eszett_cf=eszett.casefold()
eszett,eszett_cf
('ß', 'ss')

自从Python3.4开始,str.casefold()和str.lower(0得到不同结果的有116个码位。Unicode6.3命名了110122个字符,这只占0.11%。

与Unicode相关的任何问题一样,大小写折叠是个复杂的问题,有很多语言上的特殊情况,但是Python核心团队尽力提供了一种方案,能满足大多数用户的需求

规范化文本匹配实用函数

NFC和NFD可以放心使用,而且能合理比较Unicode字符串。对大多数应用来说,NFC是最好的规范化形式。不区分大小写的比较应该使用str.casefold()。

如果要处理多语言文本,可以自定义函数nfc_equal和fold_equal

from unicodedata import normalize


def nfc_equal(str1, str2):
    return normalize('NFC', str1) == normalize('NFC', str2)


def fold_equal(str1, str2):
    return (normalize('NFC', str1).casefold() == normalize('NFC', s2).casefold())

极端“规范化”:去掉变音符号

Google搜索涉及很多技术,其中一个显然是忽略变音符号(如重音符、下加符等),至少在某些情况下会这么做。去掉变音符号不是正确的规范化方式,因为人们有时很懒,或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化,因此实际语言中的重音经常变来变去。

除了搜索,去掉变音字符还能让URL更易于阅读,至少对拉丁语系语言是如此

# 去掉全部组合记号的函数
import unicodedata 
import string
def shave_marks(text):
    '''去掉全部变音符号'''
    norm_txt=unicodedata,normalize('NFD',txt) # 把所有字符分解成基字符和组合记号
    shaved=''.join(c for c in norm_txt if not unicodedata.conbining(c)) # 过滤掉所有组合记号
    return unicodedata.normalize('NFC',shaved)  # 重组所有字符

通常去掉变音字符是为了把拉丁文本变成纯粹的ASCII,但是shave_marks函数还会修改非拉丁字符(如希腊字符),而只去掉重音符并不能把它们变成ASCII字符。因此,应该分析各个基字符,仅当字符在拉丁字母表中时才删除附加的记号

def shave_marks_latin(txt):
    '''把拉丁基字符中所有的变音符号删除'''
    norm_txt = unicodedata.normalize('NFD', txt) # 把所有字符分解成基字符和组合记号
    latin_base = False
    keepers = []
    for c in norm_txt:
        if unicodedata.combining(c) and latin_base: # 基字符为拉丁字母时,跳过组合记号
            continue # 忽略拉丁基字符上的变音符号
        keepers.append(c) # 
        if not unicodedata.combining(c):# 检测新的基字符,判断是不是拉丁字母
            latin_base = c in string.ascii_letters
    shaved = ''.join(keepers)
    return unicodedata.normalize('NFC', shaved) # 重组所有字符
# 把西文印刷字符转换成ASCII字符
def dewinize(txt):
    return txt.translate(mu)

标签:normalize,字符,unicodedata,NFC,正确,s2,micro,Unicode,字符串
来源: https://www.cnblogs.com/Reion/p/15551010.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有