You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Levenshtein Distance实现两个术语列表合并的技术求助

我明白你现在的需求了——要把两个格式差异不小的术语列表做匹配,还要保留所有条目,不管有没有匹配上对吧?刚好我之前处理过类似的场景,给你梳理下清晰的思路和可直接运行的代码示例,用现成的NuGet包就能搞定,不用自己从头写复杂的匹配逻辑。

核心思路拆解

要解决这个问题,关键分三步:

  • 术语标准化:先把两个列表里的术语统一格式(比如转小写、去掉特殊符号、合并重复空格),消除命名规则差异带来的干扰。
  • 模糊匹配:用成熟的模糊字符串匹配算法,找到相似度足够高的术语对。
  • 结果整合:生成包含原术语、两个列表ID的结果集,无匹配的条目对应ID留空,同时确保所有原始条目都被保留。
推荐的NuGet包

我最推荐FuzzySharp——这个包专门做模糊字符串匹配,支持多种相似度算法,API简单易用,处理2万条数据的性能也完全够用。你可以通过NuGet管理器搜索安装,或者用命令行:

Install-Package FuzzySharp
完整代码示例

第一步:定义数据类

先定义对应原始列表和结果的类,方便数据处理:

// 原始术语列表的实体类
public class SourceTerm
{
    public string Term { get; set; }
    public string Id { get; set; }
}

// 最终输出的匹配结果类
public class MatchedTerm
{
    public string Term { get; set; }
    public string List1Id { get; set; }
    public string List2Id { get; set; }
}

第二步:术语标准化函数

这个函数会把各种格式的术语转换成统一的“干净”格式,比如把"The Term (Some Subcategory)"和"the term - some subcategory"都转换成"the term some subcategory":

private static string NormalizeTerm(string term)
{
    if (string.IsNullOrWhiteSpace(term))
        return string.Empty;
    
    // 1. 统一转小写
    var normalized = term.ToLowerInvariant();
    // 2. 把所有非字母数字的字符替换成空格(括号、破折号、斜杠等)
    normalized = System.Text.RegularExpressions.Regex.Replace(normalized, @"[^a-z0-9\s]", " ");
    // 3. 合并连续空格为单个,再去掉首尾空格
    normalized = System.Text.RegularExpressions.Regex.Replace(normalized, @"\s+", " ").Trim();
    
    return normalized;
}

第三步:核心匹配逻辑

这部分是关键,会处理两个列表的匹配,同时保留所有未匹配的条目:

using FuzzySharp;
using FuzzySharp.PreProcess;

public static List<MatchedTerm> MatchTermLists(List<SourceTerm> list1, List<SourceTerm> list2)
{
    // 先预处理列表2,建立「标准化术语」到「原始术语+ID」的映射,方便快速查找
    var list2NormalizedMap = list2.ToDictionary(
        t => NormalizeTerm(t.Term),
        t => new { t.Term, t.Id },
        StringComparer.OrdinalIgnoreCase
    );
    
    // 用来存储最终结果
    var matchedResults = new List<MatchedTerm>();
    // 用来记录列表2中已经匹配过的ID,避免重复匹配
    var matchedList2Ids = new HashSet<string>();

    // 先处理列表1的所有术语
    foreach (var term1 in list1)
    {
        var normalizedTerm1 = NormalizeTerm(term1.Term);
        if (string.IsNullOrWhiteSpace(normalizedTerm1))
        {
            // 空术语直接标记为未匹配
            matchedResults.Add(new MatchedTerm
            {
                Term = term1.Term,
                List1Id = term1.Id,
                List2Id = null
            });
            continue;
        }

        // 在列表2的标准化术语中找最匹配的项
        var bestMatch = Process.ExtractOne(
            normalizedTerm1,
            list2NormalizedMap.Keys,
            s => s,
            Preprocessors.None, // 已经预处理过,这里不需要再处理
            Fuzz.Ratio // 用全匹配相似度算法,也可以根据需求换成PartialRatio/TokenSortRatio
        );

        // 设置相似度阈值(比如80分以上认为有效匹配,可根据你的数据调整)
        if (bestMatch.Score >= 80 && !matchedList2Ids.Contains(list2NormalizedMap[bestMatch.Value].Id))
        {
            var matchedList2Item = list2NormalizedMap[bestMatch.Value];
            matchedResults.Add(new MatchedTerm
            {
                Term = term1.Term,
                List1Id = term1.Id,
                List2Id = matchedList2Item.Id
            });
            // 标记这个列表2的ID已经被匹配,避免重复
            matchedList2Ids.Add(matchedList2Item.Id);
        }
        else
        {
            // 没有找到匹配项,标记List2Id为空
            matchedResults.Add(new MatchedTerm
            {
                Term = term1.Term,
                List1Id = term1.Id,
                List2Id = null
            });
        }
    }

    // 处理列表2中还未被匹配的术语
    foreach (var term2 in list2)
    {
        if (!matchedList2Ids.Contains(term2.Id))
        {
            matchedResults.Add(new MatchedTerm
            {
                Term = term2.Term,
                List1Id = null,
                List2Id = term2.Id
            });
        }
    }

    return matchedResults;
}

第四步:测试使用示例

你可以用模拟数据测试这个逻辑:

// 模拟两个测试列表
var list1 = new List<SourceTerm>
{
    new SourceTerm { Term = "The Term (Some Subcategory)", Id = "L1-001" },
    new SourceTerm { Term = "Data Processing - Advanced", Id = "L1-002" },
    new SourceTerm { Term = "Unmatched Term", Id = "L1-003" }
};

var list2 = new List<SourceTerm>
{
    new SourceTerm { Term = "the term - some subcategory", Id = "L2-001" },
    new SourceTerm { Term = "data processing advanced", Id = "L2-002" },
    new SourceTerm { Term = "Another Unmatched One", Id = "L2-003" }
};

// 执行匹配
var results = MatchTermLists(list1, list2);

// 输出结果
foreach (var result in results)
{
    Console.WriteLine($"Term: {result.Term}, List1Id: {result.List1Id ?? "NULL"}, List2Id: {result.List2Id ?? "NULL"}");
}
调整优化建议
  • 相似度阈值:如果你的术语差异比较大,可以把阈值降到70;如果要求严格匹配,提到90即可,根据实际测试结果调整。
  • 匹配算法:FuzzySharp提供了多种算法,比如PartialRatio适合术语有部分重叠的情况,TokenSortRatio适合单词顺序不同的术语(比如"apple banana"和"banana apple"),可以根据你的数据特点替换。
  • 性能优化:如果2万条数据运行较慢,可以在匹配前先过滤掉长度差异超过50%的术语,减少不必要的匹配计算;或者用Process.ExtractTop限制返回的候选数量。
  • 一对多/多对一场景:如果列表1有多个细分术语对应列表2的一个条目,可以去掉matchedList2Ids的逻辑,允许重复匹配;如果需要更精准的多对多映射,可以考虑构建双向匹配的结果后再去重。

内容的提问来源于stack exchange,提问作者Nugs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:09:37