如何实现拼写纠错功能?

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

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

显示正确的提示

显示正确的结果

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

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

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

**编辑距离(莱文斯坦距离)**就是从一个词变成另一个词需要的最小编辑次数。这里的编辑是指删除、替换、或插入。比如 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 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    556 引用 • 675 回帖
  • 动态规划
    6 引用 • 1 回帖
  • 拼写纠错
    1 引用

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • AWS
    11 引用 • 28 回帖 • 10 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    410 引用 • 3588 回帖
  • App

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

    91 引用 • 384 回帖 • 1 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    108 引用 • 295 回帖 • 4 关注
  • 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.

    7 引用 • 69 回帖 • 1 关注
  • 酷鸟浏览器

    安全 · 稳定 · 快速
    为跨境从业人员提供专业的跨境浏览器

    3 引用 • 59 回帖 • 47 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 391 关注
  • Rust

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

    58 引用 • 22 回帖 • 5 关注
  • Hprose

    Hprose 是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。你无需专门学习,只需看上几眼,就能用它轻松构建分布式应用系统。

    9 引用 • 17 回帖 • 621 关注
  • HHKB

    HHKB 是富士通的 Happy Hacking 系列电容键盘。电容键盘即无接点静电电容式键盘(Capacitive Keyboard)。

    5 引用 • 74 回帖 • 495 关注
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    345 引用 • 742 回帖
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 218 关注
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖 • 3 关注
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    31 引用 • 97 回帖
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    6 引用 • 15 回帖 • 31 关注
  • Wide

    Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。

    欢迎访问我们运维的实例: https://wide.b3log.org

    30 引用 • 218 回帖 • 643 关注
  • 外包

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

    26 引用 • 233 回帖 • 3 关注
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 268 关注
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    586 引用 • 3538 回帖
  • PWL

    组织简介

    用爱发电 (Programming With Love) 是一个以开源精神为核心的民间开源爱好者技术组织,“用爱发电”象征开源与贡献精神,加入组织,代表你将遵守组织的“个人开源爱好者”的各项条款。申请加入:用爱发电组织邀请帖
    用爱发电组织官网:https://programmingwithlove.stackoverflow.wiki/

    用爱发电组织的核心驱动力:

    • 遵守开源守则,体现开源&贡献精神:以分享为目的,拒绝非法牟利。
    • 自我保护:使用适当的 License 保护自己的原创作品。
    • 尊重他人:不以各种理由、各种漏洞进行未经允许的抄袭、散播、洩露;以礼相待,尊重所有对社区做出贡献的开发者;通过他人的分享习得知识,要留下足迹,表示感谢。
    • 热爱编程、热爱学习:加入组织,热爱编程是首当其要的。我们欢迎热爱讨论、分享、提问的朋友,也同样欢迎默默成就的朋友。
    • 倾听:正确并恳切对待、处理问题与建议,及时修复开源项目的 Bug ,及时与反馈者沟通。不抬杠、不无视、不辱骂。
    • 平视:不诋毁、轻视、嘲讽其他开发者,主动提出建议、施以帮助,以和谐为本。只要他人肯努力,你也可能会被昔日小看的人所超越,所以请保持谦虚。
    • 乐观且活跃:你的努力决定了你的高度。不要放弃,多年后回头俯瞰,才会发现自己已经成就往日所仰望的水平。积极地将项目开源,帮助他人学习、改进,自己也会获得相应的提升、成就与成就感。
    1 引用 • 487 回帖
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 4 关注
  • 安全

    安全永远都不是一个小问题。

    203 引用 • 818 回帖 • 1 关注
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 544 关注
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    56 引用 • 85 回帖 • 1 关注
  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    199 引用 • 541 回帖 • 1 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    124 引用 • 74 回帖
  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    127 引用 • 169 回帖