你真的会比较字符串的异同吗?

134 阅读3分钟

比较两个字符串是否相同还不容易吗,直接用 == 判断一下不就好了。

那我们就来看这样一段 Python 代码(在控制台中执行):

Python 3.12.4 (main, Jun  6 2024, 18:26:44) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> s1 = 'naïve'
>>> s2 = 'naïve'
>>> s1 == s2
False

结果竟然是 Falses1s2 分明是一模一样的字符串啊,到底哪里不同了?

先来看一下这两个字符串的长度:

>>> len(s1)
5
>>> len(s2)
6

长度竟然还不一样,为什么 s2s1 一样,里面明明也有 5 个字符,长度却是 6 呢?

那就分别把这两个字符串中的每个字符都先打印出来对一对吧:

>>> list(s1)
['n', 'a', 'ï', 'v', 'e']
>>> list(s2)
['n', 'a', 'i', '̈', 'v', 'e']

好,看到差别了,

  • s1 中的 i 是顶着两个点的 ï,类似汉语拼音中的 ü 是顶着两个点的 u

image-20240713163915881

  • s2 中的 i 就是普通的 i,但后面有个奇怪的字符

image-20240713164048588

这个奇怪的字符叫作分音符(diaeresis),属于 Unicode 中的组合字符。组合字符是能够附加到前⼀个字符上的记号,且在打印时与前面的字符形成⼀个整体。

Unicode 中的组合字符似乎不太常见,但在微信朋友圈或论坛中常见的花体字其实就是通过组合字符实现的。

image-20240713165207492

既然字符串中可能包含组合字符,形成了人们看起来完全一样,但从计算机的角度来看却不相同的字符串,那应该如何比较字符串,才能使比较结果符合人类的直觉呢?

解决⽅案是使⽤ unicodedata.normalize() 函数,先规范化再进行比较:

>>> import unicodedata
>>> s1 == s2
False
>>> unicodedata.normalize('NFC', s1) == unicodedata.normalize('NFC', s2)
True

该函数的第 1 个参数是 'NFC''NFD''NFKC''NFKD' 这 4 个字符串中的⼀个。这里的 'NFC'表示,规范化时应使用最少的 Unicode 字符来构成等价的字符串。

关于这 4 种规范化形式的区别,可以参考 zh.wikipedia.org/wiki/Unicod…

最后再来说说怎么输入 ï。一种方法是长按 I 键,(在 macOS 上)就会出现这样一个小窗口,里面列出了字符 i 的各种变体。

image-20240713172533265

另一种方法是在 Python 代码中使用转义序列 \u\N

>>> 'na\u00EFve'
'naïve'
>>> 'nai\u0308ve'
'naïve'
>>> 'na\N{LATIN SMALL LETTER I WITH DIAERESIS}ve'
'naïve'
>>> 'nai\N{COMBINING DIAERESIS}ve'
'naïve'
  • \u 转义序列用于表示 Unicode 字符,后面跟着 4 位十六进制数。例如,\u0061 表示字符 'a'
  • \N 转义序列用于表示具有 Unicode 名称的字符。名称需要用大括号 {} 括起来,并且必须是 Unicode 标准中定义的名称。例如,\N{LATIN SMALL LETTER A} 也表示字符 'a'

看来在处理 Unicode 字符串时,我们还真是“sometimes naïve”。

不过,现在我们又多了个经验——在处理 Unicode 字符串时,还要注意规范化,否则人们看起来相同的字符可能会被计算机判定为不同。