英文单词主要有 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
的为例,如果这个单词不存在,则可以对其进行 insert/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 (wordDataMap.containsKey(option)) {
CandidateDto dto = CandidateDto.builder()
.word(option).count(wordDataMap.get(option)).build();
candidateDtos.add(dto);
}
}
复制代码
最后返回的结果,需要根据单词出现的频率进行对比,整体来说还是比较简单的。
中文拼写
失之毫厘
中文的拼写初看起来和英文差不多,但是中文有个很特殊的地方。
因为所有的汉字拼写本身都是固定的,用户在输入的时候不存在错字,只存在别字。
单独说一个字是别字是毫无意义的,必须要有词,或者上下文。
这一点就让纠正的难度上升了很多。
小明无奈的摇了摇头,中华文化,博大精深。
算法思路
针对中文别字的纠正,方式比较多:
(1)困惑集。
比如常用的别字,万变不离其宗
错写为 万变不离其中
。
(2)N-Gram
也就是一次字对应的上下文,使用比较广泛的是 2-gram。对应的语料,sougou 实验室是有的。
也就是当第一个词固定,第二次出现的会有对应的概率,概率越高的,肯定越可能是用户本意想要输入的。
比如 跑的飞快
,实际上 跑地飞快
可能才是正确的。
纠错
当然,中文还有一个难点就是,无法直接通过 insert/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 = wordData.get(word);
final int minLimit = Math.min(allList.size(), limit);
List<String> resultList = Guavas.newArrayList(minLimit);
for(int i = 0; i < minLimit; i++) {
resultList.add(allList.get(i));
}
return resultList;
}
复制代码
中英文混合长文本
算法思路
实际的文章,一般是中英文混合的。
要想让用户使用起来更加方便,肯定不能每次只输入一个词组。
那要怎么办呢?
答案是分词,把输入的句子,分词为一个个词。然后区分中英文,进行对应的处理。
关于分词,推荐开源项目:
算法实现
修正的核心算法,可以复用中英文的实现。
@Override
public String correct(String text) {
if(StringUtil.isEnglish(text)) {
return text;
}
StringBuilder stringBuilder = new StringBuilder();
final IWordCheckerContext zhContext = buildChineseContext();
final IWordCheckerContext enContext = buildEnglishContext();
// 第一步执行分词
List<String> segments = commonSegment.segment(text);
// 全部为真,才认为是正确。
for(String segment : segments) {
// 如果是英文
if(StringUtil.isEnglish(segment)) {
String correct = enWordChecker.correct(segment, enContext);
stringBuilder.append(correct);
} else if(StringUtil.isChinese(segment)) {
String correct = zhWordChecker.correct(segment, zhContext);
stringBuilder.append(correct);
} else {
// 其他忽略
stringBuilder.append(segment);
}
}
return stringBuilder.toString();
}
复制代码
其中分词的默认实现如下:
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.houbb.nlp.common.segment.ICommonSegment;
import com.github.houbb.nlp.common.segment.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> segment(String s) {
//根据空格分隔
List<String> strings = CommonSegments.defaults().segment(s);
if(CollectionUtil.isEmpty(strings)) {
return Collections.emptyList();
}
List<String> results = new ArrayList<>();
ICommonSegment chineseSegment = InnerCommonSegments.defaultChinese();
for(String text : strings) {
// 进行中文分词
List<String> segments = chineseSegment.segment(text);
results.addAll(segments);
}
return results;
}
}
复制代码
首先是针对空格进行分词,然后对中文以困惑集的别字做 fast-forward 分词。
当然,这些说起来也不难。
真的实现起来还是比较麻烦的,小明把完整的实现已经开源:
觉得有帮助的小伙伴可以 fork/star 一波~
快速开始
word-checker 用于单词拼写检查。支持英文单词拼写检测,和中文拼写检测。
话不多说,我们来直接体验一下这个工具类的使用体验。
特性说明
-
可以迅速判断当前单词是否拼写错误
-
可以返回最佳匹配结果
-
可以返回纠正匹配列表,支持指定返回列表的大小
-
错误提示支持 i18n
-
支持大小写、全角半角格式化处理
-
支持自定义词库
-
内置 27W+ 的英文词库
-
支持基本的中文拼写检测
快速开始
maven 引入
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>word-checker</artifactId>
<version>0.0.8</version>
</dependency>
复制代码
测试案例
会根据输入,自动返回最佳纠正结果。
final String speling = "speling";
Assert.assertEquals("spelling", EnWordCheckers.correct(speling));
复制代码
核心 api 介绍
核心 api 在 EnWordCheckers
工具类下。
功能 | 方法 | 参数 | 返回值 | 备注 |
---|---|---|---|---|
判断单词拼写是否正确 | isCorrect(string) | 待检测的单词 | boolean | |
返回最佳纠正结果 | correct(string) | 待检测的单词 | String | 如果没有找到可以纠正的单词,则返回其本身 |
判断单词拼写是否正确 | correctList(string) | 待检测的单词 | List | 返回所有匹配的纠正列表 |
判断单词拼写是否正确 | correctList(string, int limit) | 待检测的单词, 返回列表的大小 | 返回指定大小的的纠正列表 | 列表大小 小于等于 limit |
测试例子
是否拼写正确
final String hello = "hello";
final String speling = "speling";
Assert.assertTrue(EnWordCheckers.isCorrect(hello));
Assert.assertFalse(EnWordCheckers.isCorrect(speling));
复制代码
返回最佳匹配结果
final String hello = "hello";
final String speling = "speling";
Assert.assertEquals("hello", EnWordCheckers.correct(hello));
Assert.assertEquals("spelling", EnWordCheckers.correct(speling));
复制代码
默认纠正匹配列表
final String word = "goox";
List<String> stringList = EnWordCheckers.correctList(word);
Assert.assertEquals("[good, goo, goon, goof, gook, goop, goos, gox, goog, gool, goor]", stringList.toString());
复制代码
指定纠正匹配列表大小
final String word = "goox";
final int limit = 2;
List<String> stringList = EnWordCheckers.correctList(word, limit);
Assert.assertEquals("[good, goo]", stringList.toString());
复制代码
中文拼写纠正
核心 api
为降低学习成本,核心 api 和 ZhWordCheckers
中,和英文拼写检测保持一致。
是否拼写正确
final String right = "正确";
final String error = "万变不离其中";
Assert.assertTrue(ZhWordCheckers.isCorrect(right));
Assert.assertFalse(ZhWordCheckers.isCorrect(error));
复制代码
返回最佳匹配结果
final String right = "正确";
final String error = "万变不离其中";
Assert.assertEquals("正确", ZhWordCheckers.correct(right));
Assert.assertEquals("万变不离其宗", ZhWordCheckers.correct(error));
复制代码
默认纠正匹配列表
final String word = "万变不离其中";
List<String> stringList = ZhWordCheckers.correctList(word);
Assert.assertEquals("[万变不离其宗]", stringList.toString());
复制代码
指定纠正匹配列表大小
final String word = "万变不离其中";
final int limit = 1;
List<String> stringList = ZhWordCheckers.correctList(word, limit);
Assert.assertEquals("[万变不离其宗]", stringList.toString());
复制代码
长文本中英文混合
情景
实际拼写纠正的话,最佳的使用体验是用户输入一个长文本,并且可能是中英文混合的。
然后实现上述对应的功能。
核心方法
WordCheckers
工具类提供了长文本中英文混合的自动纠正功能。
功能 | 方法 | 参数 | 返回值 | 备注 |
---|---|---|---|---|
文本拼写是否正确 | isCorrect(string) | 待检测的文本 | boolean | 全部正确,才会返回 true |
返回最佳纠正结果 | correct(string) | 待检测的单词 | String | 如果没有找到可以纠正的文本,则返回其本身 |
判断文本拼写是否正确 | correctMap(string) | 待检测的单词 | Map | 返回所有匹配的纠正列表 |
判断文本拼写是否正确 | correctMap(string, int limit) | 待检测的文本, 返回列表的大小 | 返回指定大小的的纠正列表 | 列表大小 小于等于 limit |
拼写是否正确
final String hello = "hello 你好";
final String speling = "speling 你好 以毒功毒";
Assert.assertTrue(WordCheckers.isCorrect(hello));
Assert.assertFalse(WordCheckers.isCorrect(speling));
复制代码
返回最佳纠正结果
final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("hello 你好", WordCheckers.correct(hello));
Assert.assertEquals("spelling 你好以毒攻毒", WordCheckers.correct(speling));
复制代码
判断文本拼写是否正确
每一个词,对应的纠正结果。
final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("{hello=[hello], =[ ], 你=[你], 好=[好]}", WordCheckers.correctMap(hello).toString());
Assert.assertEquals("{ =[ ], speling=[spelling, spewing, sperling, seeling, spieling, spiling, speeling, speiling, spelding], 你=[你], 好=[好], 以毒功毒=[以毒攻毒]}", WordCheckers.correctMap(speling).toString());
复制代码
判断文本拼写是否正确
同上,指定最多返回的个数。
final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("{hello=[hello], =[ ], 你=[你], 好=[好]}", WordCheckers.correctMap(hello, 2).toString());
Assert.assertEquals("{ =[ ], speling=[spelling, spewing], 你=[你], 好=[好], 以毒功毒=[以毒攻毒]}", WordCheckers.correctMap(speling, 2).toString());
复制代码
格式化处理
有时候用户的输入是各式各样的,本工具支持对于格式化的处理。
大小写
大写会被统一格式化为小写。
final String word = "stRing";
Assert.assertTrue(EnWordCheckers.isCorrect(word));
复制代码
全角半角
全角会被统一格式化为半角。
final String word = "string";
Assert.assertTrue(EnWordCheckers.isCorrect(word));
复制代码
自定义英文词库
文件配置
你可以在项目资源目录创建文件 resources/data/define_word_checker_en.txt
内容如下:
my-long-long-define-word,2
my-long-long-define-word-two
复制代码
不同的词独立一行。
每一行第一列代表单词,第二列代表出现的次数,二者用逗号 ,
隔开。
次数越大,在纠正的时候返回优先级就越高,默认值为 1。
用户自定义的词库优先级高于系统内置词库。
测试代码
我们在指定了对应的单词之后,拼写检测的时候就会生效。
final String word = "my-long-long-define-word";
final String word2 = "my-long-long-define-word-two";
Assert.assertTrue(EnWordCheckers.isCorrect(word));
Assert.assertTrue(EnWordCheckers.isCorrect(word2));
复制代码
自定义中文词库
文件配置
你可以在项目资源目录创建文件 resources/data/define_word_checker_zh.txt
内容如下:
默守成规 墨守成规
复制代码
使用英文空格分隔,前面是错误,后面是正确。