Python 中 fuzzywuzzy 进行字符串模糊匹配的全过程
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">依赖安装</a></li><li><a href="#_label1">编辑距离</a></li><li><a href="#_label2">相似度得分</a></li><li><a href="#_label3">匹配函数</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_0">完整比对</a></li><li><a href="#_lab2_3_1">局部比对</a></li><li><a href="#_lab2_3_2">排序比对</a></li><li><a href="#_lab2_3_3">核心比对</a></li></ul><li><a href="#_label4">process</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_4">extractOne()</a></li><li><a href="#_lab2_4_5">extract()</a></li><li><a href="#_lab2_4_6">extractBests()</a></li><li><a href="#_lab2_4_7">extractWithoutOrder</a></li></ul></ul></div><p>fuzzywuzzy 是 Python中 基于 Levenshtein 距离算法的字符串模糊匹配库,提供 fuzz.ratio 、 partial_ratio 、 token_sort_ratio 等核心函数,用于高效计算字符串相似度。该库广泛应用于数据清洗、拼写纠错、文本挖掘和用户输入处理等场景。比如识别相同新闻(有些新闻可能会在不同平台进行发布,标题和内容基本上没太大差异,爬取时需要把这种的识别出来,避免重复处理浪费相应资源)。</p><p class="maodian"><a name="_label0"></a></p><h2>依赖安装</h2>
<div class="jb51code"><pre class="brush:bash;">pip install fuzzywuzzy python-Levenshtein</pre></div>
<p>依赖安装可以使用清华的 pip 镜像源,不然会很慢,甚至很可能导致安装失败:</p>
<div class="jb51code"><pre class="brush:bash;">pip install fuzzywuzzy python-Levenshtein -i https://pypi.tuna.tsinghua.edu.cn/simple some-package</pre></div>
<p class="maodian"><a name="_label1"></a></p><h2>编辑距离</h2>
<p>你可以把编辑距离想象成一场“变脸游戏”:给你两个词,比如“kitten”和“sitting”,你要通过最少的操作次数,把第一个词变成第二个词。允许的操作只有三种:</p>
<ul><li><strong>插入 (Insert)</strong>:加个字母</li><li><strong>删除 (Delete)</strong>:删个字母</li><li><strong>替换 (Substitute)</strong>:换个字母</li></ul>
<p>比如将 “kitten” 怎么变成 “sitting” ?</p>
<ol><li>k → s (替换)</li><li>e → i (替换)</li><li>在末尾加个 g (插入)</li></ol>
<p>总共三步完成,所以它们的编辑距离就是3。这种以“最小改动次数”来衡量相似度的方式,非常符合人类的直觉。改动越少,说明俩词越像;改动越多,就越不像。</p>
<p class="maodian"><a name="_label2"></a></p><h2>相似度得分</h2>
<p><code>fuzzywuzzy </code>把编辑距离转化成了一个 0 到 100 的直观评分。分数越高,越像。</p>
<div class="jb51code"><pre class="brush:plain;">相似度得分 = (1 - 编辑距离 / 最长字符串长度) × 100</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>匹配函数</h2>
<p>常用的匹配函数一般是下面这些,可能还会有一些衍生出来的变体,但整体分类上没有太大变化。下面示例中,不同依赖包版本的算法可能会稍有差异,运行的结果可能会有不同属于正常现象。</p>
<p class="maodian"><a name="_lab2_3_0"></a></p><h3>完整比对</h3>
<p><strong>ratio() </strong>要求整体上尽量一致,包括顺序,词汇,长度等。它就像一把尺子,从头到尾量一遍两个字符串的相似度。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
standard = "iPhone 15 Pro Max"
variants = [
"Iphone15ProMax",
"iphone 15 pro max (256GB)",
"IPHONE 15 PRO MAX",
]
for v in variants:
print(f"{v}: {fuzz.ratio(standard.lower(), v.lower())}")
# Iphone15ProMax: 90
# iphone 15 pro max (256GB): 81
# IPHONE 15 PRO MAX: 100</pre></div>
<p class="maodian"><a name="_lab2_3_1"></a></p><h3>局部比对</h3>
<p><strong>partial_ratio() </strong>自动把短的那个字符串当成模板,在长的那个字符串上滑动,挨个位置截取同样长度的子串,然后调用 <code>ratio() </code>去比,最后返回最高的那个得分。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
query = "北京路"
db_entry = "广东省广州市越秀区北京路步行街"
print(fuzz.ratio(query, db_entry))# 只有33多分
print(fuzz.partial_ratio(query, db_entry))# 高达100分!</pre></div>
<p class="maodian"><a name="_lab2_3_2"></a></p><h3>排序比对</h3>
<p><strong>token_sort_ratio() </strong>会先把两个字符串按空格或其他分隔符拆分成词(token),然后排序,最后再调用 ratio() 比对得到结果。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
print(fuzz.token_sort_ratio("红 色 跑车", "跑车 红 色"))# 输出: 100</pre></div>
<p><strong>排序局部比对 partial_token_sort_ratio()</strong></p>
<p>上面这种比对,如果有额外的干扰项的话,还是会影响最终的得分结果。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
print(fuzz.token_sort_ratio("红 色 跑车", "跑车 红 色 这是 干扰 项"))# 输出: 60</pre></div>
<p>所以还有变体 <strong>partial_token_sort_ratio()</strong>,排序后再进行局部比对,就能避免额外项的干扰了</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
print(fuzz.token_sort_ratio("红 色 跑车", "跑车 红 色 这是 干扰 项"))# 输出: 60
print(fuzz.partial_token_sort_ratio("红 色 跑车", "跑车 红 色 这是 干扰 项"))# 输出: 100</pre></div>
<p class="maodian"><a name="_lab2_3_3"></a></p><h3>核心比对</h3>
<p><strong>token_set_ratio() </strong>会提取两个字符串的所有唯一词汇,然后分成三部分:交集(共同有的词)、A独有、B独有。接着,它会组合这些部分进行多次比对,取最高分。在处理电商商品标题、文章标签等富含冗余信息场景时比较有效。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
title1 = "【旗舰店】Apple iPhone 15 Pro Max 256G 黑色"
title2 = "iPhone15 Pro Max 256GB 手机 黑色"
print(fuzz.token_set_ratio(title1, title2))# 73</pre></div>
<p><strong>核心局部比对 partial_token_set_ratio()</strong></p>
<p>当然,同排序比对一样,如果两者有额外的干扰项,也会影响最终的得分。</p>
<p>所以还有变体 <strong>partial_set_sort_ratio()</strong>,核心部分再进行局部比对,就能避免额外项的干扰了</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import fuzz
title1 = "【旗舰店】Apple iPhone 15 Pro Max 256G 黑色"
title2 = "iPhone15 Pro Max 256GB 手机 黑色"
print(fuzz.token_set_ratio(title1, title2))# 73
print(fuzz.partial_token_set_ratio(title1, title2))# 100</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>process</h2>
<p class="maodian"><a name="_lab2_4_4"></a></p><h3>extractOne()</h3>
<p>传给它一个查询词和一个候选列表,它会默默帮你把每个候选都比一遍,然后把得分最高的那位和它的分数打包送回来。默认的 <code>full_process </code>预处理器会帮你做小写转换、去标点等清洗工作。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import process
choices = [
"Apple iPhone 14 Pro",
"iPhone 14 Plus",
"Samsung Galaxy S23",
"Google Pixel 7",
]
query = "iphone 14 pro"
best_match, score = process.extractOne(query, choices)
print(f"最佳匹配: {best_match}, 得分: {score}")
# 输出: 最佳匹配: Apple iPhone 14 Pro, 得分: 95</pre></div>
<p class="maodian"><a name="_lab2_4_5"></a></p><h3>extract()</h3>
<p>提取最好的几个,默认是 5 个,会将最佳匹配的几个候选和得分返回来。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import process
choices = [
"Apple iPhone 14 Pro",
"iPhone 14 Plus",
"Samsung Galaxy S23",
"Google Pixel 7",
]
query = "iphone 14 pro"
bests_match = process.extract(query, choices, limit=3)
for match, score in bests_match:
print(f"匹配:'{match}',得分:{score}")
# 匹配:'Apple iPhone 14 Pro',得分:95
# 匹配:'iPhone 14 Plus',得分:81
# 匹配:'Google Pixel 7',得分:35</pre></div>
<p class="maodian"><a name="_lab2_4_6"></a></p><h3>extractBests()</h3>
<p>目前来看和 extract() 差别不大,唯一的区别是可以设置 score_cutoff 的阈值,使得只返回得分高于阈值的选项。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import process
choices = [
"Apple iPhone 14 Pro",
"iPhone 14 Plus",
"Samsung Galaxy S23",
"Google Pixel 7",
]
query = "iphone 14 pro"
bests_match = process.extractBests(query, choices, limit=3, score_cutoff=80)
for match, score in bests_match:
print(f"匹配:'{match}',得分:{score}")
# 匹配:'Apple iPhone 14 Pro',得分:95
# 匹配:'iPhone 14 Plus',得分:81</pre></div>
<p class="maodian"><a name="_lab2_4_7"></a></p><h3>extractWithoutOrder</h3>
<p>这个就是上面 extractOne()、extract()、extractBests() 内部所调用的函数,就是按照原始的输入顺序(不会按照评分进行排序)返回每个可选项及其评分。</p>
<div class="jb51code"><pre class="brush:py;">from fuzzywuzzy import process
choices = [
"Apple iPhone 14 Pro",
"iPhone 14 Plus",
"Samsung Galaxy S23",
"Google Pixel 7",
]
query = "iphone 14 pro"
bests_match = process.extractWithoutOrder(query, choices)
for match, score in bests_match:
print(f"匹配:'{match}',得分:{score}")
# 匹配:'Apple iPhone 14 Pro',得分:95
# 匹配:'iPhone 14 Plus',得分:81
# 匹配:'Samsung Galaxy S23',得分:19
# 匹配:'Google Pixel 7',得分:35</pre></div> 感谢楼主的详细分享!
这篇文章写得非常清晰,把fuzzywuzzy的各个功能都讲解得很透彻,特别是编辑距离的那个"变脸游戏"比喻特别形象,一听就懂![/:i]
补充一点小经验:
[*]在处理大规模数据时,建议把python-Levenshtein装上,它是用C写的,速度比纯Python实现快很多,官方说能快10倍以上
[*]实际项目中token_set_ratio和partial_token_set_ratio确实更实用,特别是电商标题这种噪音比较多的场景
[*]如果遇到匹配效果不理想的情况,可以试试先对文本做预处理,比如统一全角半角、去除特殊符号等
另外想请教一下,楼主有没有遇到过中文分词的问题?比如"苹果手机"和"苹果 手机"这种情况怎么处理比较妥当?
最后mark一下,下次做数据清洗的时候可以好好参考楼主的这篇教程!
#收藏 #顶
頁:
[1]