在Python中使用TRE的近似正则表达式:奇怪的Unicode行为

5 投票
3 回答
1464 浏览
提问于 2025-04-16 22:54

我正在尝试在Python中使用TRE库来匹配拼写错误的输入。
重要的是,它能够很好地处理utf-8编码的字符串。

举个例子:
德国首都的名字是柏林,但从发音上来说,如果人们写成“Bärlin”,听起来是一样的。

到目前为止,这个库的工作效果不错,但如果检测到的字符串的第一个或第二个位置有非ASCII字符,那么范围或检测到的字符串本身都是不正确的。

# -*- coding: utf-8 -*-
import tre

def apro_match(word, list):
    fz = tre.Fuzzyness(maxerr=3)
    pt = tre.compile(word)
    for i in l:
        m = pt.search(i,fz)
        if m:
            print m.groups()[0],' ', m[0]

if __name__ == '__main__':
    string1 = u'Berlín'.encode('utf-8')
    string2 = u'Bärlin'.encode('utf-8')    
    string3 = u'B\xe4rlin'.encode('utf-8')
    string4 = u'Berlän'.encode('utf-8')
    string5 = u'London, Paris, Bärlin'.encode('utf-8')
    string6 = u'äerlin'.encode('utf-8')
    string7 = u'Beälin'.encode('utf-8')

    l = ['Moskau', string1, string2, string3, string4, string5, string6, string7]

    print '\n'*2
    print "apro_match('Berlin', l)"
    print "="*20
    apro_match('Berlin', l)
    print '\n'*2

    print "apro_match('.*Berlin', l)"
    print "="*20
    apro_match('.*Berlin', l)

输出

apro_match('Berlin', l)
====================
(0, 7)   Berlín
(1, 7)   ärlin
(1, 7)   ärlin
(0, 7)   Berlän
(16, 22)   ärlin
(1, 7)   ?erlin
(0, 7)   Beälin



apro_match('.*Berlin', l)
====================
(0, 7)   Berlín
(0, 7)   Bärlin
(0, 7)   Bärlin
(0, 7)   Berlän
(0, 22)   London, Paris, Bärlin
(0, 7)   äerlin
(0, 7)   Beälin

对于正则表达式'.*Berlin',它工作得很好,而对于正则表达式'Berlin'

u'Bärlin'.encode('utf-8')    
u'B\xe4rlin'.encode('utf-8')
u'äerlin'.encode('utf-8')

则没有效果,而

u'Berlín'.encode('utf-8')
u'Berlän'.encode('utf-8')
u'London, Paris, Bärlin'.encode('utf-8')
u'Beälin'.encode('utf-8')

的确是按预期工作的。

我在编码上是不是做错了什么?你知道有什么技巧吗?

3 个回答

-1

你给的链接是一个博客文章,里面提到另一篇关于最新版本的博客,评论区有很多人不满,包括一个人提到这个包可能不支持“非拉丁”编码(这是什么意思呢?)。你是怎么认为TRE可以处理UTF-8编码的文本的呢?它是按字符处理,而不是按字节处理吗?

你没有告诉我们,接受多少个错误(插入、删除、替换)算作模糊匹配。你也没有说明它是使用char例程还是wchar例程。你真的指望潜在的回答者去下载这个包并阅读Python接口的代码吗?

如果有wchar的C++例程,理应有一个Python接口可以处理Python的unicode和Python的str(用UTF-16LE编码)以及C++的wchar之间的转换,但似乎并没有这样做?

根据测试结果,6个字符的测试案例返回了(0, 7),而一个不工作的案例(字符串6)把一个两字节的字符拆开了(因为答案不是有效的UTF-8,所以打印成了?),这看起来是在以字节(char)编码无关的模式下工作,这可不是个好主意。

如果其他方法都不行,而你的输入数据全是德语,你可以尝试用latin1或cp1252编码,并使用字节模式。

还有一些进一步的说明:

你的string3是多余的——它和string2是一样的。

你说string5“有效”的说法似乎和你说string2和string3“有效”的说法不一致。

你的测试覆盖面很少;需要有几个不匹配的案例,它们应该比“Moskau”更接近匹配!

你应该先确保它在只处理ASCII数据时“有效”;这里有一些测试案例:

Berlxn Berlxyn
Bxrlin Bxyrlin
xerlin xyerlin
Bexlin Bexylin
xBerlin xyBerlin
Bxerlin Bxyerlin
Berlinx Berlinxy
erlin Brlin Berli

然后用非ASCII字符替换上面列表中的每个xy进行测试。

使用像“.*Berlin”这样的模式对于诊断并没有太大帮助,尤其是当你没有有意义的“应该不匹配”的测试案例时。

2

TRE在内部是以字节为单位工作的,它返回的是字节的位置。我之前也遇到过你一样的问题——其实没有什么特别的技巧!

我对Python的绑定做了一些修改,添加了一个utf8函数,还有一个可以把字节位置转换为字符位置的函数,以及一个小的包装器。使用这个包装器时,你的测试用例可以正常工作。我没有发布这些修改,因为当时只是为了测试TRE做的一个快速修改——如果你需要这些修改,随时告诉我。

据我所知,TRE已经有一段时间没有更新了,目前的版本(0.8.0)中还有一些未修复的bug,主要是关于在字符串末尾进行模式匹配的问题(比如用模式“2004$”搜索“2004 ”时,成本是2,而预期的成本应该是1)。

正如其他人提到的,对于Python来说,新的正则表达式模块似乎非常有趣!

6

你可以使用新的 regex 库,它支持Unicode 6.0和模糊匹配:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from itertools import ifilter, imap
import regex as re

def apro_match(word_re, lines, fuzzy='e<=1'):
    search = re.compile(ur'('+word_re+'){'+fuzzy+'}').search
    for m in ifilter(None, imap(search, lines)):
        print m.span(), m[0]

def main():
    lst = u'Moskau Berlín Bärlin B\xe4rlin Berlän'.split()
    lst += [u'London, Paris, Bärlin']
    lst += u'äerlin Beälin'.split()
    print
    print "apro_match('Berlin', lst)"
    print "="*25
    apro_match('Berlin', lst)
    print 
    print "apro_match('.*Berlin', lst)"
    print "="*27
    apro_match('.*Berlin', lst)

if __name__ == '__main__':
    main()

'e<=1' 的意思是最多允许出现一个错误。错误有三种类型:

  • 插入错误,用 "i" 表示
  • 删除错误,用 "d" 表示
  • 替换错误,用 "s" 表示

输出

apro_match('Berlin', lst)
=========================
(0, 6) Berlín
(0, 6) Bärlin
(0, 6) Bärlin
(0, 6) Berlän
(15, 21) Bärlin
(0, 6) äerlin
(0, 6) Beälin

apro_match('.*Berlin', lst)
===========================
(0, 6) Berlín
(0, 6) Bärlin
(0, 6) Bärlin
(0, 6) Berlän
(0, 21) London, Paris, Bärlin
(0, 6) äerlin
(0, 6) Beälin

撰写回答