使用 unicodedata 模块对字符串标准化

本次我们来聊一聊 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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!