Python 代碼實現模糊查詢


Python 代碼實現模糊查詢

1、導語:

模糊匹配可以算是現代編輯器(如 Eclipse 等各種 IDE)的一個必備特性了,它所做的就是根據用戶輸入的部分內容,猜測用戶想要的文件名,並提供一個推薦列表供用戶選擇。

樣例如下:

  • Vim (Ctrl-P)

  • Sublime Text (Cmd-P)

‘模糊匹配’這是一個極為有用的特性,同時也非常易於實現。

 

2、問題分析:

我們有一堆字符串(文件名)集合,我們根據用戶的輸入不斷進行過濾,用戶的輸入可能是字符串的一部分。我們就以下面的集合為例:

>>> collection = ['django_migrations.py',
                'django_admin_log.py',
                'main_generator.py',
                'migrations.py',
                'api_user.doc',
                'user_group.doc',
                'accounts.txt',
                ]

 

當用戶輸入’djm‘字符串時,我們假定是匹配到’django_migrations.py’和’django_admin_log.py’,而最簡單的實現方法就是使用正則表達式。

3、解決方案:

3.1 常規的正則匹配

將 "djm" 轉換成 "d.*j.*m" 然后用這個正則嘗試匹配集合中的每一個字符串,如果匹配到了就被列為候選。

>>> import re
>>> def fuzzyfinder(user_input, collection):
        suggestions = []
        pattern = '.*'.join(user_input) # Converts 'djm' to 'd.*j.*m'
        regex = re.compile(pattern)     # Compiles a regex.
        for item in collection:
            match = regex.search(item)  # Checks if the current item matches the regex.
            if match:
                suggestions.append(item)
        return suggestions

>>> print fuzzyfinder('djm', collection)
['django_migrations.py', 'django_admin_log.py']

>>> print fuzzyfinder('mig', collection)
['django_migrations.py', 'django_admin_log.py', 'main_generator.py', 'migrations.py']

 

這里根據用戶的輸入我們得到了一個推薦列表,但是推薦列表中的字符串是沒有進行重要性區分的。有可能出現最合適的匹配項被放到了最后的情況。

實際上,還是這個例子,當用戶輸入’mig‘時,最佳選項’migrations.py’就被放到了最后。

3.2 帶有rank排序的匹配列表

這里我們對匹配到的結果按照匹配內容第一次出現的起始位置來進行排序。

'main_generator.py'     - 0
'migrations.py'         - 0
'django_migrations.py'  - 7
'django_admin_log.py'   - 9

下面是相關代碼:

>>> import re
>>> def fuzzyfinder(user_input, collection):
        suggestions = []
        pattern = '.*'.join(user_input) # Converts 'djm' to 'd.*j.*m'
        regex = re.compile(pattern)     # Compiles a regex.
        for item in collection:
            match = regex.search(item)  # Checks if the current item matches the regex.
            if match:
                suggestions.append((match.start(), item))
        return [x for _, x in sorted(suggestions)]

>>> print fuzzyfinder('mig', collection)
['main_generator.py', 'migrations.py', 'django_migrations.py', 'django_admin_log.py']

 

這次我們生成了一個由二元 tuple 組成的列表,即列表中的每一個元素為一個二元tuple,而該二元tuple的第一個值為匹配到的起始位置、第二個值為對應的文件名,然后使用列表推導式按照匹配到的位置進行排序並返回文件名列表。

現在我們已經很接近最終的結果了,但還稱不上完美——用戶想要的是’migration.py’,但我們卻把’main_generator.py’作為第一推薦。

3.3 根據匹配的緊湊程度進行排序

當用戶開始輸入一個字符串時,他們傾向於輸入連續的字符以進行精確匹配。比如當用戶輸入’mig‘他們更傾向於找的是’migrations.py’或’django_migrations.py’,而不是’main_generator.py’,所以這里我們所做的改變就是查找匹配到的最緊湊的項目。

剛才提到的問題對於Python來說不算什么事,因為當我們使用正則表達式進行字符串匹配時,匹配到的字符串就已經被存放在了match.group()中了。下面假設輸入為’mig’,對最初定義的’collection’的匹配結果如下:

regex = '(m.*i.*g)'

'main_generator.py'    ->  'main_g'
'migrations.py'        ->  'mig'
'django_migrations.py' ->  'mig'
'django_admin_log.py'  ->  'min_log'

這里我們將推薦列表做成了三元tuple的列表的形式,即推薦列表中的每一個元素為一個三元tuple,而該三元tuple的第一個值為匹配到的內容的長度、第二個值為匹配到的起始位置、第三個值為對應的文件名,然后按照匹配長度和起始位置進行排序並返回。

>>> import re
>>> def fuzzyfinder(user_input, collection):
        suggestions = []
        pattern = '.*'.join(user_input) # Converts 'djm' to 'd.*j.*m'
        regex = re.compile(pattern)     # Compiles a regex.
        for item in collection:
            match = regex.search(item)  # Checks if the current item matches the regex.
            if match:
                suggestions.append((len(match.group()), match.start(), item))
        return [x for _, _, x in sorted(suggestions)]

>>> print fuzzyfinder('mig', collection)
['migrations.py', 'django_migrations.py', 'main_generator.py', 'django_admin_log.py']

 

針對我們的輸入,這時候的匹配結果已經趨向於完美了,不過還沒完。

3.4 非貪婪匹配

由 Daniel Rocco 發現了這一微妙的問題:當集合中有['api_user', 'user_group']這兩個元素存在,用戶輸入’user‘時,預期的匹配結果(相對順序)應該為['user_group', 'api_user'],但實際上的結果為:

>>> print fuzzyfinder('user', collection)
['api_user.doc', 'user_group.doc']

上面的測試結果中:’api_user’要排在’user_group’前面。深入一點,我們發現這是因為在搜索’user’時,正則被擴展成了’u.*s.*e.*r’,考慮到’user_group’有2個’r',因此該模式匹配到了’user_gr‘而不是我們預期的’user‘。更長的匹配導致在最后的匹配rank排序時名次下降這一違反直覺的結果,不過這問題也容易解決,將正則修改為’非貪婪匹配’即可。

>>> import re
>>> def fuzzyfinder(user_input, collection):
        suggestions = []
        pattern = '.*?'.join(user_input)    # Converts 'djm' to 'd.*?j.*?m'
        regex = re.compile(pattern)         # Compiles a regex.
        for item in collection:
            match = regex.search(item)      # Checks if the current item matches the regex.
            if match:
                suggestions.append((len(match.group()), match.start(), item))
        return [x for _, _, x in sorted(suggestions)]

>>> fuzzyfinder('user', collection)
['user_group.doc', 'api_user.doc']

>>> print fuzzyfinder('mig', collection)
['migrations.py', 'django_migrations.py', 'main_generator.py', 'django_admin_log.py']

現在,fuzzyfinder已經可以(在上面的情況中)正常工作了,而我們不過只寫了10行代碼就實現了一個 fuzzy finder。

3.5 結論:

以上就是我在我的 pgcli 項目(一個有自動補全功能的Postgresql命令行實現)中設計實現’fuzzy matching’的過程記錄。

我已經將 fuzzyfinder 提取成一個獨立的Python包,你可以使用命令’pip install fuzzyfinder’在你的項目中進行安裝和使用。

感謝 Micah Zoltu 和 Daniel Rocco 對算法的檢查和問題修復。

如果你對這個感興趣的話,你可以來 twitter 上找我。

4、結語:

當我第一次考慮用Python實現“fuzzy matching”的時候,我就知道一個叫做 fuzzywuzzy的優秀庫,但是 fuzzywuzzy 的做法和這里的不太一樣,它使用的是 “levenshtein distance”(編輯距離) 來從集合中找到最匹配的字符串。”levenshtein distance“是一個非常適合用來做自動更正拼寫錯誤的技術,但在從部分子串匹配長文件名時表現的不太好(所以這里沒有使用)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM