標籤:
1、導語:
模糊比對可以算是現代編輯器(如 Eclipse 等各種 IDE)的一個必備特性了,它所做的就是根據使用者輸入的部分內容,猜測使用者想要的檔案名稱,並提供一個推薦列表供使用者選擇。
範例如下:
‘模糊比對’這是一個極為有用的特性,同時也非常易於實現。
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’,而最簡單的實現方法就是使用Regex。
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來說不算什麼事,因為當我們使用Regex進行字串匹配時,匹配到的字串就已經被存放在了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“是一個非常適合用來做自動校正拼字錯誤的技術,但在從部分子串匹配長檔名時表現的不太好(所以這裡沒有使用)。
Refer:
[1] FuzzyFinder - in 10 lines of Python
http://blog.amjith.com/fuzzyfinder-in-10-lines-of-python
[2] MyCli:支援自動補全和文法高亮的 MySQL 用戶端
http://hao.jobbole.com/mycli-mysql/
https://github.com/dbcli/mycli
[3] Postgres CLI with autocompletion and syntax highlighting
https://github.com/dbcli/pgcli
10 行 Python 代碼實現模糊查詢/智能提示