如何在Python中通过折叠实现Unicode字符串匹配

11 投票
5 回答
5737 浏览
提问于 2025-04-15 14:15

我有一个应用程序实现了增量搜索。它有一个包含unicode字符串的目录,用来和给定的“关键”字符串进行匹配。如果目录中的字符串包含关键字符串中的所有字符,并且这些字符的顺序也对,那么这个目录字符串就算是“命中”。如果关键字符在目录字符串中聚集得更紧密,那么这个匹配的排名会更高。

总之,这个功能运行得很好,能够准确匹配unicode字符串,比如“öst”可以匹配“Östblocket”或者“röst”或者“röd sten”。

不过,现在我想实现字符折叠,因为在某些情况下,像“á”或“é”这样的目录字符和关键字符“a”或“e”之间并不需要区分。

举个例子:“Ole”应该能够匹配“Olé”。

我该如何在Python中最好地实现这个unicode折叠匹配器呢?效率很重要,因为我需要将成千上万的目录字符串与短的关键字符串进行匹配。

它不需要转换成ascii;实际上,算法的输出字符串可以是unicode。保留一个字符总比去掉它要好。


我不知道该接受哪个答案,因为我用了一点两者的结合。采用NKFD分解并去掉组合标记几乎就完成了,我只是添加了一些自定义的音译。现在这个模块看起来是这样的:(警告,包含unicode字符,因为这样编辑起来更方便。)

# -*- encoding: UTF-8 -*-

import unicodedata
from unicodedata import normalize, category

def _folditems():
    _folding_table = {
        # general non-decomposing characters
        # FIXME: This is not complete
        u"ł" : u"l",
        u"œ" : u"oe",
        u"ð" : u"d",
        u"þ" : u"th",
        u"ß" : u"ss",
        # germano-scandinavic canonical transliterations
        u"ü" : u"ue",
        u"å" : u"aa",
        u"ä" : u"ae",
        u"æ" : u"ae",
        u"ö" : u"oe",
        u"ø" : u"oe",
    }

    for c, rep in _folding_table.iteritems():
        yield (ord(c.upper()), rep.title())
        yield (ord(c), rep)

folding_table = dict(_folditems())

def tofolded(ustr):
    u"""Fold @ustr

    Return a unicode str where composed characters are replaced by
    their base, and extended latin characters are replaced by
    similar basic latin characters.

    >>> tofolded(u"Wyłącz")
    u'Wylacz'
    >>> tofolded(u"naïveté")
    u'naivete'

    Characters from other scripts are not transliterated.

    >>> tofolded(u"Ἑλλάς") == u'Ελλας'
    True

    (These doctests pass, but should they fail, they fail hard)
    """
    srcstr = normalize("NFKD", ustr.translate(folding_table))
    return u"".join(c for c in srcstr if category(c) != 'Mn')

if __name__ == '__main__':
    import doctest
    doctest.testmod()

(另外,如果有人对实际匹配感兴趣:我会提前为所有目录构建折叠字符串,并将折叠版本放入已经存在的目录对象别名属性中。)

5 个回答

2

一个通用的解决方案(特别适合搜索标准化和生成网址后缀)是 unidecode 模块:

http://pypi.python.org/pypi/Unidecode

这个模块是 Perl 语言中的 Text::Unidecode 模块的移植版。虽然它不是很完整,但它可以翻译所有我找到的拉丁字母字符,还能把西里尔字母、中文等转换成拉丁字母,甚至能正确处理全角字符。

通常来说,最好是把你不想在最终结果中出现的字符去掉,或者用其他字符替代(比如 "äßœ$" 会被转换成 "assoe$",所以你可能想去掉那些不是字母或数字的字符)。对于那些应该被转换但不该出现的字符(比如 § 变成 SS 变成 EU),你需要先清理输入:

input_str = u'äßœ$'
input_str = u''.join([ch if ch.isalnum() else u'-' for ch in input_str])
input_str = str(unidecode(input_str)).lower()

这段代码会把所有非字母数字的字符替换成一个占位符,然后再进行转换,并把字符串转换成小写。

4

对于我的应用,我在其他评论中已经提到过:我想要一个unicode的结果,并且不处理未处理的字符

在这种情况下,正确的方法是创建一个UCA排序器对象,并将其强度设置为仅进行初级比较,这样就完全忽略了重音符号。

我在这个回答中展示了如何使用Perl来实现这一点。第一个排序器对象的强度是你需要的,而第二个则考虑了重音符号以解决平局。

你会注意到,在进行这些比较时,没有任何字符串受到影响:原始数据保持不变。

8

你可以使用这个 strip_accents 函数来去掉重音符号:

def strip_accents(s):
   return ''.join((c for c in unicodedata.normalize('NFD', unicode(s)) if unicodedata.category(c) != 'Mn'))

>>> strip_accents(u'Östblocket')
'Ostblocket'

撰写回答