Python使用unicodedata实现字符串标准化
作者:古明地觉的编程教室
unicodedata.normalize
负责对 unicode 字符串进行规范化处理,因为有些字符看似长度为 1,但其实不是,举个例子:
s1 = "é" s2 = "é" s3 = "e\u0301" print(len(s1)) # 1 print(len(s2)) # 2 print(len(s3)) # 2 print(s1 == s2) # False print(s2 == s3) # True print(s1, s2, s3) # é é é
相信打印结果会让你感到诧异,s2 的长度看似为 1,但其实是 2,这是因为 Python3 的字符串存储的其实是码位序列。而某些字符可以有多种表示方式,比如 é:
- 预组合字符:代码中的 s1,这是一个单一的 unicode 码点,表示带有重音符号的小写字母 e;
- 分解序列:代码中的 s3,将小写字母 e 和重音符号(\u0301)分开,使用两个 unicode 码点表示,而 s2 虽然看起来和 s1 一样,但它的表示方式和 s3 是相同的;
# 两个码点 s = "é" print(s[0] == "e") # True print(s[1] == "\u0301") # True
这种情况直接处理的话会比较头疼,所以 normalize 的作用就是规范化,让多个码点表示的字符使用一个码点表示。规范化前后的字符串长得一样,但是长度变短了。
import unicodedata s1 = "lové" s2 = "lové" s3 = "love\u0301" print(s1 == s2 == s3) # False print(len(s1), len(s2), len(s3)) # 4 5 5 # 对字符串进行规范化,第一个参数是规范化方式,第二个参数是字符串 # "NFC":将多个码点表示的字符替换为等效的单个码点形式 normalize_s2 = unicodedata.normalize("NFC", s2) normalize_s3 = unicodedata.normalize("NFC", s3) print( s1 == normalize_s2 == normalize_s3 ) # True print(s2, normalize_s2) # lové lové print(len(s2), len(normalize_s2)) # 5 4
规范化方式除了 NFC 之外还有 NFD,它和 NFC 是相反的,表示让一个码点表示的字符使用多个码点表示。
import unicodedata s1 = "lové" s2 = "lové" s3 = "love\u0301" print(len(s1), len(s2), len(s3)) # 4 5 5 # s1 中的 é 使用一个码点表示,这里让它变成等效的两个码点 normalize_s1 = unicodedata.normalize("NFD", s1) print(len(normalize_s1)) # 5 print(s1 == s3) # False print(normalize_s1 == s3) # True
因此这就是 unicode 的规范化处理,因为有的 unicode 字符可以有多种表示方式,可以是一个码点,也可以是两个码点,但它们长得都一样。NFC 是让两个码点表示的字符使用一个码点表示,NFD 是让一个码点表示的字符使用两个码点表示。
但需要注意:如果字符只有一种表示方式,那么规范化前后的结果是一样的。
import unicodedata # 这六个字符都只有一种表示方式 s = "satori" nfc_s = unicodedata.normalize("NFC", s) nfd_s = unicodedata.normalize("NFD", s) print(s == nfc_s == nfd_s) # True # 这个 emoji 同样只有一种表示方式,并且占用两个码点 s = "😀" nfc_s = unicodedata.normalize("NFC", s) nfd_s = unicodedata.normalize("NFD", s) print(s == nfc_s == nfd_s) # True
所以 NFC 相当于组合,NFD 相当于分解。
然后除了 NFC 和 NFD 之外,还有 NFKC 和 NFKD,它们有什么区别呢?用大白话解释就是:
- NFC 和 NFD 标准化前后的字符,虽然占用的码点不同,但起码长得一样,两者在外观上是等价的。这种替换方式也被称为标准替换。
- 而 NFKC 和 NFKD 则是兼容替换,它们更关注文字语义的表达。
import unicodedata print(unicodedata.normalize("NFKD", "㍿")) # 株式会社 print(unicodedata.normalize("NFKD", "㊥")) # 中 print( unicodedata.normalize("NFKD", "①②③④⑤") ) # 12345 # 上述几个字符串如果使用 NFC、NFD 标准化,那么结果还是原来的样子 # 而 '㍿' 和 '株式会社' 显然不是一个东西,它们长的都不一样 # 所以这两者不可能是同一个字符的不同表达形式 # 但从人类的思维来说,这两者就是一个东西,在语义上是等价的 # 所以 NFKD 是兼容替换,说白了就是按照语义替换 # 比如全角转半角,组合字符分解成多个独立字符 comma1 = "," comma2 = "," print( unicodedata.normalize("NFD", comma1) == comma2 ) # False print( unicodedata.normalize("NFKD", comma1) == comma2 ) # True # comma1 是中文的逗号,comma2 是英文的逗号 # 采用 NFD 标准化的结果和之前一样,因为全角和半角压根不是一个东西 # 但对于人类而言,一眼就知道它们都是逗号 # 所以 NFKD 标准化之后,会将中文逗号转成英文逗号
所以这几种规范化方式区别如下:
- NFC:某些字符可以有多种表达方式,将多个码点表示的字符转成使用一个码点表示,字符在替换前后的外观是一样的;
- NFD:和 NFC 相反,将一个码点表示的字符转成使用多个码点表示,字符在替换前后的外观也是一样的;
- NFKD:按照语义对字符进行兼容分解(全角转半角,组合字符分解),前后外观会发生变化,但现实语义不变。比如 ㊥ 和 中,㊋ 和 火;
- NFKC:NFKD 是兼容分解,直接就完事了,而 NFKC 还会进行组合;
大部分情况下,我们只需要使用 NFC 规范化即可,而 NFKD 在处理带圆圈的数字的时候也会使用。
unicodedata.category
该函数可以返回一个字符的类别,而类别有以下几种:
'Lu':'大写字母(Letter, uppercase)' 'Ll':'小写字母(Letter, lowercase)' 'Lt':'标题大小写字母(Letter, titlecase)' 'Lm':'修饰字母(Letter, modifier)' 'Lo':'其他字母(Letter, other)' 'Mn':'非间断标记(Mark, nonspacing)' 'Mc':'间断标记(Mark, spacing combining)' 'Me':'封闭标记(Mark, enclosing)' 'Nd':'十进制数字(Number, decimal digit)' 'Nl':'字母数字(Number, letter)' 'No':'其他数字(Number, other)' 'Pc':'连接符号(Punctuation, connector)' 'Pd':'破折号符号(Punctuation, dash)' 'Ps':'开放的标点符号(Punctuation, open)' 'Pe':'关闭的标点符号(Punctuation, close)' 'Pi':'初引号(Punctuation, initial quote)' 'Pf':'末引号(Punctuation, final quote)' 'Po':'其他标点符号(Punctuation, other)' 'Sm':'数学符号(Symbol, math)' 'Sc':'货币符号(Symbol, currency)' 'Sk':'修饰符号(Symbol, modifier)' 'So':'其他符号(Symbol, other)' 'Zs':'空格符号(Separator, space)' 'Zl':'分行符(Separator, line)' 'Zp':'分段符(Separator, paragraph)' 'Cc':'控制字符(Other, control)' 'Cf':'格式字符(Other, format)' 'Cs':'代理字符(Other, surrogate)' 'Co':'私用字符(Other, private use)' 'Cn':'未分配字符(Other, not assigned)'
举个例子:
import unicodedata print(unicodedata.category('A')) # Lu print(unicodedata.category('a')) # Ll print(unicodedata.category('1')) # Nd print(unicodedata.category('$')) # Sc print(unicodedata.category(' ')) # Zs
'A' 是大写字母,所以它的类别是 'Lu';'a' 是小写字母,所以它的类别是 'Ll';'1' 是十进制数字,所以它的类别是 'Nd';'$' 是货币符号,所以它的类别是 'Sc';' ' 是空格符号,所以它的类别是 'Zs'。
unicodedata.lookup
有些 unicode 字符是有名字的,可以根据它的名字查找相应的字符。
import unicodedata print( unicodedata.lookup("LATIN SMALL LETTER A"), unicodedata.lookup("COPYRIGHT SIGN"), unicodedata.lookup("PEACH"), ) # a © 🍑
如果给定的名字不是一个有效的 Unicode 字符名,那么会抛出 KeyError。
unicodedata.name
和 lookup 功能相反,负责返回字符的名称。
import unicodedata print(unicodedata.name("z")) # LATIN SMALL LETTER Z print(unicodedata.name("@")) # COMMERCIAL AT print(unicodedata.name("🍑")) # PEACH
比较简单,如果字符没有名称,则抛出 ValueError,或者也可以指定一个默认值。
unicodedata.numeric
将 unicode 字符转成等效的数值,如果无法转换则返回默认值(没有则抛出 ValueError)。
import unicodedata print(unicodedata.numeric("零")) # 0.0 print(unicodedata.numeric("〇")) # 0.0 print(unicodedata.numeric("一")) # 1.0 print(unicodedata.numeric("贰")) # 2.0 print(unicodedata.numeric("叁")) # 3.0 print(unicodedata.numeric("四")) # 4.0 print(unicodedata.numeric("伍")) # 5.0 print(unicodedata.numeric("⑥")) # 6.0 print(unicodedata.numeric("漆")) # 7.0 print(unicodedata.numeric("捌")) # 8.0 print(unicodedata.numeric("玖")) # 9.0 print(unicodedata.numeric("拾")) # 10.0 text = "加 v́ 壹捌⑤壹零贰❤️捌⑥捌〇②,看 Python❤️" def chr_to_num(char): try: return str(unicodedata.numeric(char))[0] except ValueError: return char print( "".join(map(chr_to_num, text)) ) # 加 v́ 185102❤️86802,看 Python❤️
当然还有几个函数没有说,个人觉得用不上,这里面最有用的应该就是 normalize 函数了,更多内容可以参考官网。
到此这篇关于Python使用unicodedata实现字符串标准化的文章就介绍到这了,更多相关Python unicodedata内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!