本次我们来聊一聊 Python 的一个内置模块:unicodedata,它是专门用来处理 unicode 字符串的,下面来看看它的用法。
unicodedata.normalize
负责对 unicode 字符串进行规范化处理,因为有些字符看似长度为 1,但其实不是,举个例子:
s1 = "é"
s2 = "é"
s3 = "eu0301"
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 = "loveu0301"
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 = "loveu0301"
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❤️
以上就是 unicodedata 模块的一些用法,当然还有几个函数没有说,个人觉得用不上。这里面最有用的应该就是 normalize 函数了,更多内容可以参考官网。
原文始发于微信公众号(古明地觉的编程教室):使用 unicodedata 模块对字符串标准化
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/251344.html