如何实现拼写纠错功能?

本贴最后更新于 1882 天前,其中的信息可能已经天翻地覆

在使用搜索引擎时,当我们输入错误的关键词时,当然这里的错误是拼写错误,搜索引擎的下拉框中仍会显示以正确关键词为前前辍的提示,当你直接回车搜索错误的关键词时,搜索引擎的结果中仍包括正确关键词的结果。你有没有想过它是如何实现的呢?

显示正确的提示

显示正确的结果

前文已经分享如何使用前辍树实现搜索框的关键词提示功能。大家很容易想到以上纠错功能的实现,关键在于给定一个错误的关键词,如何返回一个正确的关键词。

最简单的方法,我们使用一个数组来存储正确关键词,对于给定的关键词,我们遍历此数组,找到与给定关键词最接近的那个词返回即可。

如何找到最接近的那个词呢?也就是说如何量化两个字符串的相似度。通常有两种方法:一种是求两个字符串的编辑距离,编辑距离越小,两个字符串越相近。另一种是求两个子符串的最长公共子串长度,长度越大,两个字符串越相近。

**编辑距离(莱文斯坦距离)**就是从一个词变成另一个词需要的最小编辑次数。这里的编辑是指删除、替换、或插入。比如 facbok 和 facebook 的编辑距离就是 2 ,因为最小的操作是插入 2 次。比如 faccbook 和 facebook 的编辑距离就是 1 ,因为只需要替换 1 次。

最长公共子串长度从相反的角度来量化相似度,通过最小次数的删除,增加操作后,两个字符串达到相同时的长度。比如 facbok 和 facebook 的最大公共子串长度是 6。

如何求两个字符串的编辑距离?

先考虑如何人脑如何有效的识别编辑距离:

facbok (字符串a)
facebook (字符串b)

初始编辑距离为 0,分别遍历两个字符串,如果一样,则指针 index 后移,如果不一样,有以下三种情况:

1、在字符串 a (或字符串 b) 中 index 处的字符删除,编辑距离 +1,然后比较 a[index+1] 与 b[index]

2、在字符串 a (或字符串 b) 中,a[index]前的位置插入一个字符,编辑距离 +1,然后比较 a[index] 与 b[index+1]

3、在字符串 a (或字符串 b) 中,a[index]的位置替换一个字符,编辑距离 +1,然后比较 a[index+1] 与 b[index+1]

循环结束,比较 3 种情况,找出距离最小的即可。
基于以上思路,我们可以画个表格来尝试找规律:

状态转移

字符 f = f ,因此单元格 B2 的值为 0 ,相应的 f 与 fa 的编辑距离为 1 因此 C2 的位置是 1,同理可得第 1 行和第 A 列的编辑距离。

接下来求 C3,C3 的值可以 C2 增加一个字符,B3 删除一个字符,或者 B2 替换一个字符转化而来,这三者的最小距离为
min(1+1,1+1,0+0) = 0 ,同样的道理可以得出其余所有格子的数值。

比如:E5 = min(E4+1,D5+1, 0+INT(E1!=A5)) = 1

最终的结果即 I7 的结果为 2。

以上过程可以很容易翻译成代码。

    def levenshtein_dp(s: str, t: str) -> int:
        '''
        计算莱文斯坦距离(Levenshtein distance),距离越小,说明两个单词越相近,时间复杂度为 O(mxn)
        :param s:
        :param t:
        :return:
        '''
        m, n = len(s), len(t)
        table = [[0] * (n + 1) for _ in range(m + 1)]
        table[0] = [j for j in range(n + 1)]
        # print(table)
        for i in range(m + 1):
            table[i][0] = i
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                table[i][j] = min(1 + table[i - 1][j], 1 + table[i][j - 1],
                                  int(s[i - 1] != t[j - 1]) + table[i - 1][j - 1])
        return table[-1][-1]

为了得到正确的函数,你还需要类似以下功能的函数:


  def get_right_word(self,input_word):
        '''
        输入一个单词,返回正确的单词
        :param input_word:
        :return:
        '''
        words = self.get_all_words()#获取所有正确的单词
        right_word = input_word
        min_distance = 99999
        for item in words:
            distance = levenshtein_dp(input_word,item)
            if min_distance >  distance:
                min_distance = distance
                right_word = item
        return right_word

结果前文中的前辍树,你可以很容易实现拼写纠错功能。

下面给出一种最长子串的求法,供参考:

def common_substring_dp(s: str, t: str) -> int:
    m, n = len(s), len(t)
    table = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            table[i][j] = max(table[i - 1][j], table[i][j - 1], int(s[i - 1] == t[j - 1]) + table[i - 1][j - 1])
    return table[-1][-1]

测试

我使用 cet4 词库来测试一下使用莱文斯坦距离和最长公共子串长度获取的正确单词有什么不同,附完整代码如下:

# -*- codeing:utf-8 -*-

def levenshtein_dp(s: str, t: str) -> int:
    '''
    计算莱文斯坦距离(Levenshtein distance),距离越小,说明两个单词越相近,时间复杂度为 O(mxn)
    :param s:
    :param t:
    :return:
    '''
    m, n = len(s), len(t)
    table = [[0] * (n + 1) for _ in range(m + 1)]
    table[0] = [j for j in range(n + 1)]
    # print(table)
    for i in range(m + 1):
        table[i][0] = i
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            table[i][j] = min(1 + table[i - 1][j], 1 + table[i][j - 1],
                              int(s[i - 1] != t[j - 1]) + table[i - 1][j - 1])
    return table[-1][-1]



def common_substring_dp(s: str, t: str) -> int:
    m, n = len(s), len(t)
    table = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            table[i][j] = max(table[i - 1][j], table[i][j - 1], int(s[i - 1] == t[j - 1]) + table[i - 1][j - 1])
    return table[-1][-1]


def get_right_word_from_levenshtein_dp(all_words,input_word):
      '''
      输入一个单词,返回计算莱文斯坦距离最小的单词
      :param input_word:
      :return:
      '''
      words = all_words #获取所有正确的单词
      right_word = input_word
      min_distance = 99999
      for item in words:
          distance = levenshtein_dp(input_word,item)
          if min_distance >  distance:
              min_distance = distance
              right_word = item
      return right_word

def get_right_word_from_common_substring_dp(all_words,input_word):
    '''
    输入一个单词,返回最长公共子串长度最大的单词
    :param input_word:
    :return:
    '''
    words = all_words #获取所有正确的单词
    right_word = input_word
    min_distance = 0
    for item in words:
        distance = common_substring_dp(input_word,item)
        if min_distance < distance:
            min_distance = distance
            right_word = item
    return right_word

if __name__ == '__main__':
    all_words = []
    with open("cet4.txt",encoding="gbk",mode="r") as r:
        for line in r:
            word = line.strip().split(" ")[0]
            if word != '' and len(word) > 2:
                all_words.append(word)

    while True:
        input_word = input("please input a word.(q for quit.): ")
        if input_word == 'q':
            break
        right_word = get_right_word_from_levenshtein_dp(all_words,input_word)
        print("the right word in cet4 is(levenshtein_dp): ",right_word)
        print(levenshtein_dp(right_word,input_word))

        right_word = get_right_word_from_common_substring_dp(all_words,input_word)
        print("the right word in cet4 is(common_substring_dp): ",right_word)
        print(common_substring_dp(right_word,input_word))

运行效果如下:

please input a word.(q for quit.): afection
the right word in cet4 is(levenshtein_dp):  affection
1
the right word in cet4 is(common_substring_dp):  affection
8
please input a word.(q for quit.): advertise
the right word in cet4 is(levenshtein_dp):  adjective
3
the right word in cet4 is(common_substring_dp):  advertisement
9
please input a word.(q for quit.): atmosph
the right word in cet4 is(levenshtein_dp):  almost
3
the right word in cet4 is(common_substring_dp):  atmosphere
7
please input a word.(q for quit.): assembl
the right word in cet4 is(levenshtein_dp):  assemble
1
the right word in cet4 is(common_substring_dp):  assemble
7
please input a word.(q for quit.): 

结论

测试了 4 个错误的单词,有 2 个返回的单词二者返回是一致的,有 2 个返回不一致。

比如:advertise 使用莱文斯坦距离返回正确单词是 adjective
使用最长公共子串长度返回的则是 advertisement,显然返回 advertisement 是输入者较为期望的结果。在某些场景下,莱文斯坦距离更有效。

因此没有一个放置四海而皆准的办法,实际使用中要结合具体需求,比如还可以加入搜索关键词热度等指标加以权衡。

希望本篇文章能让你开发的系统中的输入框更加智能。

(完)

专注有价值的技术分享。欢迎订阅我的微信公众号 somenzz,及时获得更新。

  • Python

    Python 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    536 引用 • 672 回帖
  • 动态规划
    6 引用 • 1 回帖
  • 拼写纠错
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    4 引用 • 55 回帖 • 1 关注
  • sts
    2 引用 • 2 回帖 • 164 关注
  • 深度学习

    深度学习(Deep Learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。

    40 引用 • 40 回帖 • 1 关注
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1083 引用 • 3461 回帖 • 263 关注
  • SVN

    SVN 是 Subversion 的简称,是一个开放源代码的版本控制系统,相较于 RCS、CVS,它采用了分支管理系统,它的设计目标就是取代 CVS。

    29 引用 • 98 回帖 • 688 关注
  • OnlyOffice
    4 引用 • 15 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    2 引用 • 14 回帖 • 1 关注
  • 博客

    记录并分享人生的经历。

    272 引用 • 2386 回帖 • 2 关注
  • Typecho

    Typecho 是一款博客程序,它在 GPLv2 许可证下发行,基于 PHP 构建,可以运行在各种平台上,支持多种数据库(MySQL、PostgreSQL、SQLite)。

    12 引用 • 60 回帖 • 457 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 232 回帖
  • 房星科技

    房星网,我们不和没有钱的程序员谈理想,我们要让程序员又有理想又有钱。我们有雄厚的房地产行业线下资源,遍布昆明全城的 100 家门店、四千地产经纪人是我们坚实的后盾。

    6 引用 • 141 回帖 • 569 关注
  • 大疆创新

    深圳市大疆创新科技有限公司(DJI-Innovations,简称 DJI),成立于 2006 年,是全球领先的无人飞行器控制系统及无人机解决方案的研发和生产商,客户遍布全球 100 多个国家。通过持续的创新,大疆致力于为无人机工业、行业用户以及专业航拍应用提供性能最强、体验最佳的革命性智能飞控产品和解决方案。

    2 引用 • 14 回帖
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    262 引用 • 664 回帖 • 2 关注
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 611 关注
  • App

    App(应用程序,Application 的缩写)一般指手机软件。

    90 引用 • 383 回帖 • 1 关注
  • 自由行
    2 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    138 引用 • 268 回帖 • 147 关注
  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    198 引用 • 120 回帖 • 1 关注
  • 尊园地产

    昆明尊园房地产经纪有限公司,即:Kunming Zunyuan Property Agency Company Limited(简称“尊园地产”)于 2007 年 6 月开始筹备,2007 年 8 月 18 日正式成立,注册资本 200 万元,公司性质为股份经纪有限公司,主营业务为:代租、代售、代办产权过户、办理银行按揭、担保、抵押、评估等。

    1 引用 • 22 回帖 • 703 关注
  • 创业

    你比 99% 的人都优秀么?

    83 引用 • 1398 回帖
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 31 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    58 引用 • 22 回帖 • 1 关注
  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖
  • Bug

    Bug 本意是指臭虫、缺陷、损坏、犯贫、窃听器、小虫等。现在人们把在程序中一些缺陷或问题统称为 bug(漏洞)。

    71 引用 • 1736 回帖 • 7 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    25 引用 • 191 回帖 • 24 关注
  • ActiveMQ

    ActiveMQ 是 Apache 旗下的一款开源消息总线系统,它完整实现了 JMS 规范,是一个企业级的消息中间件。

    19 引用 • 13 回帖 • 643 关注