基于 HanLP + 编辑距离的术语智能纠错实战

张开发
2026/4/18 7:46:34 15 分钟阅读

分享文章

基于 HanLP + 编辑距离的术语智能纠错实战
基于 HanLP 编辑距离的医疗术语智能纠错实战1. 背景与痛点在医疗文书、电子病历、药品说明等场景中专业术语的准确性至关重要。一个错别字可能导致完全不同的诊断或药品。例如“心机梗塞” → 应为“心肌梗塞”“糖料病” → 应为“糖尿病”“阿莫西林胶襄” → 应为“阿莫西林胶囊”传统的纠错方案要么依赖巨大的语言模型部署成本高要么直接使用 HanLP 的自定义词典但对于错词、变形词的识别能力有限。本文介绍一种轻量级、纯内存、可嵌入的术语纠错实现利用 HanLP 分词结果圈定中文语块 基于编辑距离的滑窗模糊匹配在不依赖大规模模型的前提下精准纠正专业术语。2. 整体设计思路该纠错器核心流程分为三步HanLP 分词 词性标注使用HanLP.segment()对输入文本进行分词并获取每个词语的词性。合并中文连续语块根据分词结果和词性名词、专名等将相邻的中文字符序列合并成一个待检测的候选区间。这解决了错词被 HanLP 切碎的问题。滑窗模糊扫描 编辑距离匹配在每个候选区间内使用长度滑窗提取子串与预加载的标准术语词典进行编辑距离计算。距离 ≤ 1 且与原文不同的触发替换。最终对多个修正进行位置排序安全重建输出文本。3. 代码结构解析3.1 记录类定义recordCorrectionDetail(Stringoriginal,Stringcorrected,intstart,intend,inteditDistance){}recordCorrectionResult(StringcorrectedText,ListCorrectionDetaildetails){}CorrectionDetail记录每次纠错的原文、修正词、起止位置和编辑距离。CorrectionResult封装修正后的完整文本与所有纠错明细。3.2 核心类HanLPCorrector词典存储与加载privatefinalMapInteger,ListStringdictByLennewConcurrentHashMap();privateintminLenInteger.MAX_VALUE,maxLen0;publicvoidload(SetStringcorrectTerms){dictByLen.clear();minLenInteger.MAX_VALUE;maxLen0;for(Stringterm:correctTerms){dictByLen.computeIfAbsent(term.length(),k-newArrayList()).add(term);if(term.length()minLen)minLenterm.length();if(term.length()maxLen)maxLenterm.length();}}使用长度索引MapInteger, ListString存放标准词便于后续滑窗时快速获取候选词列表避免遍历全部词典。主流程correct(String original)ListTermtermsHanLP.segment(original);// 合并中文语块// ...// 在语块上执行滑窗纠错// ...// 排序并重建文本关键点合并中文语块for(Termterm:terms){Stringwordterm.word;booleanisChineseword.chars().allMatch(c-c\u4e00c\u9fa5);Stringnatureterm.nature!null?term.nature.toString():x;booleanisNounLikenature.startsWith(n)||nature.equals(x);if(isChinese(isNounLike||word.length()minLen)){if(currentStart-1)currentStartcharPos;currentEndcharPosword.length();}else{// 非中文/非名词性词语时结束当前语块}charPosword.length();}仅当词语为纯中文且**词性以 n 开头名词/专名或为 x字符串**时才纳入连续语块。使用charPos精准记录字符位置避免字节偏移错误。模糊扫描fuzzyScan(...)for(intistart;iend-minLen;i){if(covered[i])continue;// 已修正的位置跳过for(intlenMath.min(maxLen,end-i);lenminLen;len--){Stringsubtext.substring(i,ilen);ListStringcandidatesdictByLen.get(len);if(candidatesnull)continue;StringbestMatchsub;intminDistInteger.MAX_VALUE;for(Stringcand:candidates){intdistlevenshtein(sub,cand);if(distminDist){minDistdist;bestMatchcand;}}if(minDistMAX_EDIT_DIST!sub.equals(bestMatch)){changes.add(newCorrectionDetail(sub,bestMatch,i,ilen,minDist));for(intki;kilen;k)covered[k]true;break;}}}滑窗策略窗口长度从maxLen递减到minLen优先匹配长词符合术语优先原则。编辑距离阈值MAX_EDIT_DIST 1即最多允许一个字符的增删改。位置覆盖数组避免同一位置被多次修正造成重叠。编辑距离算法privateintlevenshtein(Strings1,Strings2){// 标准动态规划实现}使用经典的 Wagner–Fischer 算法时间复杂度 O(mn)对于短术语通常 20 字符完全可接受。4. 测试演示在main方法中我们加载了医疗术语词典并输入了一段包含多个错别字的文本HanLPCorrectorcorrectornewHanLPCorrector();corrector.load(Set.of(心肌梗塞,糖尿病,阿莫西林胶囊,冠状动脉粥样硬化,心电图));Stringinput患者确诊为心机梗塞伴有轻度糖料病建议复查心电图。医生开了阿莫西林胶襄。检查单号:20240512-001。;CorrectionResultresultcorrector.correct(input);输出结果 原始文本: 患者确诊为心机梗塞伴有轻度糖料病建议复查心电图。医生开了阿莫西林胶襄。检查单号:20240512-001。 ✅ 修正文本: 患者确诊为心肌梗塞伴有轻度糖尿病建议复查心电图。医生开了阿莫西林胶囊。检查单号:20240512-001。 纠错明细: ❌ 原文: 心机梗塞 | ✅ 替换: 心肌梗塞 | 位置: [6, 10) | 距离: 1 ❌ 原文: 糖料病 | ✅ 替换: 糖尿病 | 位置: [16, 19) | 距离: 1 ❌ 原文: 胶襄 | ✅ 替换: 胶囊 | 位置: [37, 39) | 距离: 1可以看到“心机梗塞” 被正确纠正为 “心肌梗塞”错一字编辑距离 1“糖料病” → “糖尿病”“阿莫西林胶襄” → “阿莫西林胶囊”而“心电图”本身正确未被改动数字和符号部分被 HanLP 语块合并逻辑自然跳过保留原样。5. 方案优势与适用场景优点轻量级无需 GPU无需加载大型语言模型内存占用仅词典大小。精准可控编辑距离阈值可调词典可动态热加载。与 HanLP 无缝集成充分利用分词与词性标注能力避免了对整句无差别纠错。易扩展只需替换词典集合即可适配法律、金融、机械等领域的术语纠错。适用场景电子病历、医疗报告的后处理清洗客服聊天记录中的产品名称纠正垂直领域搜索框的输入提示与纠错任何需要术语级精确纠正的文本预处理环节6. 局限性及改进方向词典覆盖率依赖只能纠正词典中已存在的词无法处理未登录术语。编辑距离 1 的限制对于多字错误如“冠状动脉粥样硬化” 错为 “冠状动卖粥样硬化” 两个错字当前阈值无法纠正。可通过增加MAX_EDIT_DIST或引入拼音相似度来改善。分词准确性依赖若 HanLP 将错误术语切分为非名词例如动词则可能不会被纳入候选区间。可通过自定义 HanLP 词性映射或强制将所有中文片段纳入处理。7. 完整代码获取需引入 HanLP 依赖Maven 依赖dependencygroupIdcom.hankcs/groupIdartifactIdhanlp/artifactIdversionportable-1.8.4/version/dependencypackage com.fenci;importcom.hankcs.hanlp.HanLP;importcom.hankcs.hanlp.seg.common.Term;importjava.util.*;importjava.util.concurrent.ConcurrentHashMap;public class TermCorrectorFinal{record CorrectionDetail(String original, String corrected, int start, int end, int editDistance){}record CorrectionResult(String correctedText, ListCorrectionDetaildetails){}static class HanLPCorrector{// 纯内存标准词索引替代不可靠的 CustomDictionary private final MapInteger, ListStringdictByLennew ConcurrentHashMap();private int minLenInteger.MAX_VALUE, maxLen0;private static final int MAX_EDIT_DIST1;public void load(SetStringcorrectTerms){dictByLen.clear();minLenInteger.MAX_VALUE;maxLen0;for(String term:correctTerms){dictByLen.computeIfAbsent(term.length(), k -new ArrayList()).add(term);if(term.length()minLen)minLenterm.length();if(term.length()maxLen)maxLenterm.length();}}public CorrectionResult correct(String original){if(originalnull||original.isEmpty())returnnew CorrectionResult(original, List.of());// 1. 真正调用 HanLP获取带词性标注的分词流 ListTermtermsHanLP.segment(original);// 2. 合并连续中文语块解决 HanLP 将错词切碎的问题 Listint[]rangesnew ArrayList();int currentStart-1, currentEnd0, charPos0;for(Term term:terms){String wordterm.word;boolean isChineseword.chars().allMatch(c -c\u4e00c\u9fa5);// HanLP 词性n(名词)/nz(专名)/x(字符串)视为潜在术语载体 String natureterm.nature!null ? term.nature.toString():x;boolean isNounLikenature.startsWith(n)||nature.equals(x);if(isChinese(isNounLike||word.length()minLen)){if(currentStart-1)currentStartcharPos;currentEndcharPos word.length();}else{if(currentStart!-1){ranges.add(new int[]{currentStart, currentEnd});currentStart-1;}}charPosword.length();// 精准追踪字符位置}if(currentStart!-1)ranges.add(new int[]{currentStart, currentEnd});// 3. 在 HanLP 圈定的中文语块上执行滑窗纠错 ListCorrectionDetailchangesnew ArrayList();boolean[]coverednew boolean[original.length()];for(int[]range:ranges){fuzzyScan(original, range[0], range[1], covered, changes);}// 4. 安全重建文本 changes.sort(Comparator.comparingInt(d -d.start()));StringBuilder sbnew StringBuilder(original.length());int lastEnd0;for(CorrectionDetail d:changes){sb.append(original, lastEnd, d.start());sb.append(d.corrected());lastEndd.end();}sb.append(original, lastEnd, original.length());returnnew CorrectionResult(sb.toString(), changes);}private void fuzzyScan(String text, int start, int end, boolean[]covered, ListCorrectionDetailchanges){for(int istart;iend - minLen;i){if(covered[i])continue;for(int lenMath.min(maxLen, end - i);lenminLen;len--){String subtext.substring(i, i len);ListStringcandidatesdictByLen.get(len);if(candidatesnull)continue;String bestMatchsub;int minDistInteger.MAX_VALUE;for(String cand:candidates){int distlevenshtein(sub, cand);if(distminDist){minDistdist;bestMatchcand;}}if(minDistMAX_EDIT_DIST!sub.equals(bestMatch)){changes.add(new CorrectionDetail(sub, bestMatch, i, i len, minDist));for(int ki;ki len;k)covered[k]true;break;}}}}private int levenshtein(String s1, String s2){int ms1.length(), ns2.length();int[][]dpnew int[m 1][n 1];for(int i0;im;i)dp[i][0]i;for(int j0;jn;j)dp[0][j]j;for(int i1;im;i){for(int j1;jn;j){int costs1.charAt(i -1)s2.charAt(j -1)?0:1;dp[i][j]Math.min(Math.min(dp[i-1][j]1, dp[i][j-1]1), dp[i-1][j-1]cost);}}returndp[m][n];}}// 测试入口 public static void main(String[]args){HanLPCorrector correctornew HanLPCorrector();corrector.load(Set.of(心肌梗塞,糖尿病,阿莫西林胶囊,冠状动脉粥样硬化,心电图));String input患者确诊为心机梗塞伴有轻度糖料病建议复查心电图。医生开了阿莫西林胶襄。检查单号:20240512-001。;CorrectionResult resultcorrector.correct(input);System.out.println( 原始文本: input);System.out.println(✅ 修正文本: result.correctedText());System.out.println(\n 纠错明细:);if(result.details().isEmpty()){System.out.println( (无术语错误));}else{for(var d:result.details()){System.out.printf( ❌ 原文:\%s\| ✅ 替换:\%s\| 位置: [%d, %d) | 距离: %d%n, d.original(), d.corrected(), d.start(), d.end(), d.editDistance());}}}}注意HanLP 的不同版本 API 略有差异本文基于portable-1.8.4测试通过。8. 总结通过HanLP 分词 编辑距离滑窗匹配的组合拳我们实现了一个简洁、高效的术语纠错工具。它既克服了传统自定义词典“只能识别、不能容错”的短板又避免了重型语言模型的部署开销。如果你的项目中有类似的术语纠错需求不妨基于此代码进行定制——只需准备一份高质量的标准术语表即可快速上线。

更多文章