优化基于itertools.combinations的文本相似度比对脚本性能
性能优化方案
你的脚本核心瓶颈不是循环写法、也不是fuzz计算本身,是做了海量完全无效的O(n²)全量比对,12万条数据全量两两组合共72亿次遍历,哪怕循环体只写pass,Python解释器跑72亿次迭代本身就需要数分钟,靠换filter/map这类表层写法根本不可能解决问题,按以下优先级优化即可:
- 第一优先级:按
compare_string分桶,砍掉99.9%以上的无效遍历
你的逻辑里只有compare_string相等的两个条目才需要后续比对,根本不需要生成全量两两组合。预处理阶段直接把所有条目按compare_string分到不同桶里,仅在桶内做两两比对,跨桶条目直接跳过。如果你的compare_string区分度正常,这一步就能把总遍历量从72亿降到几万到几十万级别,空循环的2分钟耗时会直接压缩到毫秒级。 - 第二优先级:调整判断顺序,轻量过滤前置,最重的计算放最后
把判断按执行成本从低到高排序,提前排除不满足条件的对,不要上来就跑重计算:- 先判断文本长度差:
fuzz.ratio基于编辑距离计算,两个文本长度差超过5%时,相似度不可能达到95;长度不相等时相似度不可能到100,这类情况直接跳过,不需要做后续判断 - 再判断token集合是否有交集,无交集直接跳过
- 最后才计算fuzz相似度,这是整个循环里最重的操作
- 先判断文本长度差:
- 第三优先级:替换纯Python实现的fuzz库
把你现在用的fuzzywuzzy换成RapidFuzz,接口100%兼容,底层是C++实现带SIMD指令优化,相同的fuzz.ratio计算速度是原版fuzzywuzzy的10~100倍,不需要额外安装Levenshtein依赖,替换成本为0。 - 第四优先级:预处理阶段存好所有需要的字段,避免循环内重复计算
提前把文本长度、token集合等字段在遍历dataframe的时候就算好存起来,不要在循环里反复调用len()、反复做对象属性/索引取值,减少单次循环的额外开销。
优化后参考代码
import itertools from collections import defaultdict from rapidfuzz import fuzz # 预处理:按compare_string分桶,提前计算所有复用字段 bucket_map = defaultdict(list) for t in dataframe.itertuples(): text = t.text bucket_map[t.compare_string].append( (t.id, text, t.set, len(text)) ) highly_similar = [] for bucket in bucket_map.values(): # 单条目桶没有比对必要,直接跳过 if len(bucket) < 2: continue for a, b in itertools.combinations(bucket, 2): a_id, a_text, a_tokens, a_len = a b_id, b_text, b_tokens, b_len = b # 1. 最轻量判断:长度差不满足要求直接跳过 max_len = max(a_len, b_len) min_len = min(a_len, b_len) if (min_len / max_len) * 100 < 95: continue # 2. 次轻量判断:token无交集直接跳过 if a_tokens.isdisjoint(b_tokens): continue # 3. 最后执行最重的相似度计算 score = fuzz.ratio(a_text, b_text) if (score >= 95 and a_len >= 10) or score == 100: highly_similar.append([a_id, b_id, a_text, b_text, score])
补充说明
如果后续数据量上涨到百万级,可以再加MinHash+LSH局部敏感哈希做近邻预筛选,进一步缩小候选比对范围。针对你当前12万条数据的规模,做完上面几步优化后,总耗时可以从15分钟压缩到10秒以内。
你之前测试filter/map性能更差是正常现象:这类内置函数如果包裹的还是Python层面的判断逻辑,只会额外增加函数调用开销,不会带来速度提升,在总计算量没减少的前提下,换任何循环写法都没有实质作用。
内容的提问来源于stack exchange,提问作者Pedro Daumas
相关产品推荐
相关产品推荐

