java实现中英⽂拼写检查和错误纠正?可我只会写CRUD啊!简单的需求
临近下班,⼩明忙完了今天的任务,正准备下班回家。
⼀条消息闪烁了起来。
“最近发现公众号的拼写检查功能不错,帮助⽤户发现错别字,体验不错。给我们系统也做⼀个。”
看着这条消息,⼩明在内⼼默默问候了⼀句。
“我 TND 的会做这个,就直接去⼈家总部上班了,在这受你的⽓。”
“好的”,⼩明回复到,“我先看看”
今天,天王⽼⼦来了我也得下班,耶稣也留不住。
⼩明想着,就回家了。
冷静分析
说到这个拼写检查,⼩明其实是知道的。
⾃⼰没吃过猪⾁,还是见过猪跑的。
平时看过⼀些公众号⼤佬分享,说是公众号推出了拼写检查功能,以后再也不会有错别字了。
后来,⼩明还是在他们的⽂章中看到了不少错别字。后来,就没有后来了。
为什么不去问⼀问万能的 github 呢?
⼩明打开了 github 发现好像没有成熟的 java 相关的开源项⽬,有的⼏颗星,⽤起来不太放⼼。
估计 NLP 是搞 python 的⽐较多吧,java 实现中英⽂拼写检查和错误纠正?可我只会写 CRUD 啊!画画玩玩
⼩明默默地点起了⼀根华⼦……
窗外的夜⾊如⽔,不禁陷⼊了沉思,我来⾃何⽅?去往何处?⼈⽣的意义⼜是什么?
尚有余热的烟灰落在了⼩明某东买的拖鞋上,把他脑海中脱缰的野马烫的⼀机灵。
没有任何思路,没有任何头绪,还是先洗洗睡吧。
那⼀夜,⼩明做了⼀个长长的美梦。梦⾥没有任何的错别字,所有的字句都坐落在正确的位置上……
转机
第⼆天,⼩明打开了搜索框,输⼊ spelling correct。
可喜的是,找到了⼀篇英⽂拼写纠正算法讲解。
吾尝终⽇⽽思矣,不如须臾之所学也。⼩明叹了⼀句,就看了起来。
算法思路
英⽂单词主要有 26 个英⽂字母组成,所以拼写的时候可能出现错误。
⾸先可以获取正确的英⽂单词,节选如下:
apple,16192
applecart,41
applecarts,1
appledrain,1
appledrains,1
applejack,571
applejacks,4
appleringie,1
appleringies,1
apples,5914
applesauce,378
applesauces,1
applet,2
每⼀⾏⽤逗号分隔,后⾯是这个单词出现的频率。
以⽤户输⼊appl的为例,如果这个单词不存在,则可以对其进⾏ inrt/delete/replace 等操作,找到最接近的单词。(本质上就是找到编辑距离最⼩的单词)如果输⼊的单词存在,则说明正确,不⽤处理。
词库的获取
那么英⽂词库去哪⾥获得呢?
⼩明想了想,于是去各个地⽅查了⼀圈,最后找到了⼀个⽐较完善的英⽂单词频率词库,共计 27W+ 的单词。
节选如下:
aa,1831
aah,45774
aahed,1
aahing,30
aahs,23
...
zythums,1
zyzzyva,2
zyzzyvas,1
zzz,76
zzzs,2
核⼼代码
获取⽤户当前输⼊的所有可能情况,核⼼代码如下:
/**
* 构建出当前单词的所有可能错误情况
*
* @param word 输⼊单词
* @return 返回结果
* @since 0.0.1
* @author ⽼马啸西风
*/
private List<String> edits(String word) {新春祝福短语
List<String> result = new LinkedList<>();
for (int i = 0; i < word.length(); ++i) {
result.add(word.substring(0, i) + word.substring(i + 1));
}
for (int i = 0; i < word.length() - 1; ++i) {
result.add(word.substring(0, i) + word.substring(i + 1, i + 2) + word.substring(i, i + 1) + word.substring(i + 2));
}
for (int i = 0; i < word.length(); ++i) {
for (char c = 'a'; c <= 'z'; ++c) {
result.add(word.substring(0, i) + c + word.substring(i + 1));
}
}
for (int i = 0; i <= word.length(); ++i) {
for (char c = 'a'; c <= 'z'; ++c) {
result.add(word.substring(0, i) + c + word.substring(i));
}
}
return result;
}
然后和词库中正确的单词进⾏对⽐:
List<String> options = edits(formatWord);
List<CandidateDto> candidateDtos = new LinkedList<>();重返索马里
for (String option : options) {花冈
if (ainsKey(option)) {
CandidateDto dto = CandidateDto.builder()
.word(option).(option)).build();
candidateDtos.add(dto);
}
}
最后返回的结果,需要根据单词出现的频率进⾏对⽐,整体来说还是⽐较简单的。
中⽂拼写
失之毫厘
中⽂的拼写初看起来和英⽂差不多,但是中⽂有个很特殊的地⽅。
因为所有的汉字拼写本⾝都是固定的,⽤户在输⼊的时候不存在错字,只存在别字。
有关地球的知识
单独说⼀个字是别字是毫⽆意义的,必须要有词,或者上下⽂。
这⼀点就让纠正的难度上升了很多。
⼩明⽆奈的摇了摇头,中华⽂化,博⼤精深。
算法思路
针对中⽂别字的纠正,⽅式⽐较多:
(1)困惑集。
⽐如常⽤的别字,万变不离其宗错写为万变不离其中。
(2)N-Gram
也就是⼀次字对应的上下⽂,使⽤⽐较⼴泛的是 2-gram。对应的语料,sougou 实验室是有的。
也就是当第⼀个词固定,第⼆次出现的会有对应的概率,概率越⾼的,肯定越可能是⽤户本意想要输⼊的。⽐如跑的飞快,实际上跑地飞快可能才是正确的。
纠错
当然,中⽂还有⼀个难点就是,⽆法直接通过 inrt/delete/replace 把⼀个字变成另⼀个字。
不过类似的,还是有许多⽅法:
(1)同⾳字/谐⾳字
(2)形近字
(3)同义词净水机维修
(4)字词乱序、字词增删
算法实现
迫于实现的难度,⼩明选择了最简单的困惑集。
⾸先找到常见别字的字典,节选如下:
⼀丘之鹤⼀丘之貉
⼀仍旧惯⼀仍旧贯
⼀付中药⼀服中药
...
黯然消魂黯然销魂
⿍⽴相助⿍⼒相助
⿎躁⽽进⿎噪⽽进
龙盘虎据龙盘虎踞
前⾯的是别字,后⾯的是正确⽤法。
以别字作为字典,然后对中⽂⽂本进⾏ fast-forward 分词,获取对应的正确形式。
当然⼀开始我们可以简单点,让⽤户固定输⼊⼀个词组,实现就是直接解析对应的 map 即可public List<String> correctList(String word, int limit, IWordCheckerContext context) {
final Map<String, List<String>> wordData = context.wordData().correctData();
// 判断是否错误
if(isCorrect(word, context)) {
return Collections.singletonList(word);
}
List<String> allList = (word);
final int minLimit = Math.min(allList.size(), limit);
List<String> resultList = wArrayList(minLimit);
for(int i = 0; i < minLimit; i++) {
resultList.(i));
}
return resultList;
}
中英⽂混合长⽂本
算法思路
实际的⽂章,⼀般是中英⽂混合的。
要想让⽤户使⽤起来更加⽅便,肯定不能每次只输⼊⼀个词组。
那要怎么办呢?
答案是分词,把输⼊的句⼦,分词为⼀个个词。然后区分中英⽂,进⾏对应的处理。
关于分词,推荐开源项⽬:
算法实现
修正的核⼼算法,可以复⽤中英⽂的实现。
@Override
public String correct(String text) {
小孩子发烧怎么办if(StringUtil.isEnglish(text)) {
return text;
}
StringBuilder stringBuilder = new StringBuilder();
final IWordCheckerContext zhContext = buildChineContext();
final IWordCheckerContext enContext = buildEnglishContext();
// 第⼀步执⾏分词
List<String> gments = (text);
// 全部为真,才认为是正确。
for(String gment : gments) {
// 如果是英⽂
if(StringUtil.isEnglish(gment)) {
String correct = t(gment, enContext);
stringBuilder.append(correct);
} el if(StringUtil.isChine(gment)) {
String correct = t(gment, zhContext);
stringBuilder.append(correct);
} el {
// 其他忽略
stringBuilder.append(gment);
}
}
String();
}
其中分词的默认实现如下:
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.ICommonSegment;
import com.github.impl.CommonSegments;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 默认的混合分词,⽀持中⽂和英⽂。
*
* @author binbin.hou
* @since 0.0.8
*/
public class DefaultSegment implements ICommonSegment {
@Override
public List<String> gment(String s) {
//根据空格分隔
List<String> strings = CommonSegments.defaults().gment(s);
if(CollectionUtil.isEmpty(strings)) {
ptyList();
}
List<String> results = new ArrayList<>();
ICommonSegment chineSegment = InnerCommonSegments.defaultChine();
for(String text : strings) {
// 进⾏中⽂分词
List<String> gments = (text);
results.addAll(gments);
}
return results;
}
}
⾸先是针对空格进⾏分词,然后对中⽂以困惑集的别字做 fast-forward 分词。当然,这些说起来也不难。
真的实现起来还是⽐较⿇烦的,⼩明把完整的实现已经开源:
觉得有帮助的⼩伙伴可以 fork/star ⼀波~
快速开始
word-checker ⽤于单词拼写检查。⽀持英⽂单词拼写检测,和中⽂拼写检测。话不多说,我们来直接体验⼀下这个⼯具类的使⽤体验。
特性说明
可以迅速判断当前单词是否拼写错误
可以返回最佳匹配结果
可以返回纠正匹配列表,⽀持指定返回列表的⼤⼩
成功的方法错误提⽰⽀持 i18n
⽀持⼤⼩写、全⾓半⾓格式化处理
⽀持⾃定义词库
内置 27W+ 的英⽂词库
⽀持基本的中⽂拼写检测