博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于拼音的搜索
阅读量:6092 次
发布时间:2019-06-20

本文共 9709 字,大约阅读时间需要 32 分钟。

在这次的讨论中,与大家分析一个使用js实现的拼音搜索程序,这个功能,相信在很多项目中都会用到。其实思路和原理应该都差不多,只是在算法上,会有性能之分。在今天要分享的程序中,主要还是讲解一些实现的思路,在算法上,相信还有更多更好的实现,小弟只是在此起个抛砖引玉的作用。希望能有更多的人加入到讨论中来。下面,我们就开始吧。

 

一.概述。

   我们首先来看下程序运行的效果。

这是页面的初始效果。当我们在文本框中输入拼音或者汉字时,会对数据进行筛选,如下:

功能非常简单,下面我们就来分析下,在界面背后,程序到底做了什么?

二.思路

  1. 拼音的数据哪里来的?

    通过上面程序的运行我们可以看到,输入拼音就能显示匹配的汉字,那么,必定会有一个字典数据集合,保存了大部分品应所对应的汉字。就像下面这样:

      

window.PINYIN_DATA = {   a:"啊阿嗄吖锕...",   ai:"哎爱唉艾挨...",   an:"案按安俺暗岸鞍...",   ....}

    就如同上面看到的,这个字段数据中保存了每种拼音对应的汉字,当然,它的数据量取决于你需要保存多少量的汉字与其对应的拼音。因为这个程序是纯js的,所以特别用了一个js文件

来保存上述的数据,名字的话就叫做PinYinData.js,我们使用的数据结构是对象字面量,可以以key/value的方式获取。

    另外,上面页面中显示的学校的数据,同样保存到一个js文件中,叫做StudentData.js,它的数据结构如下:

var StudentData = [    {
"id":1,"name":"北京大学"}, {
"id":1,"name":"北京外国语大学"}, {
"id":1,"name":"北京中央戏剧学院"}, ...];

    可以看到,它的结构稍微和拼音数据的那个结构不一样,使用的是一个数组,然后里面每个元素是一个对象常量。

    了解了数据的来源之后,我们就可以一个一个的流程进行分析了。

  2.程序实现思路整体概述

    在进行每个具体的代码流程讲解前,有必要把一个大概的思路与大家分享,形成一个整体性的认识。

    首先,页面加载的时候,会去读取学校的数据,将学校名字提取多出来,显示在网页上。

    然后,回去读取拼音数据,读取进来后,需要做一个转换的操作,要将{拼音:"汉字串"}这样的数据结构,转换为{"某个汉字",[拼音]},如何转?为什么要转?拼音为什么变为一个数组了?这个,会在接下来的详细分析中介绍。将转换后的结构保存起来。

    最后,需要将所有的学校汉字串对应的拼音串保存起来,就像下面这样:

var hanToPinyin = {    tags: "北京大学  beijingdaxue  beijingdaixue"    content: {id:1,name:"北京大学"}}_cache.push(hanToPinyin);

    做完上面的工作,页面加载的工作就完成了。

    当用户在文本框中输入拼音或者汉字的时候,就会去遍历_cache里面保存的对象的tags属性,看看有没有符合的,有的话,就将content部分取出来,将name显示在页面上。

    以上,就是程序的一个整体,其实功能并不复杂也不多,它牵涉到如下几个文件。

 

    下面我们就来具体分析。

 

三.详细代码分析

  1.如何加载学校的数据并显示。

    (1)首先,学校数据所在的js文件是肯定要导入的,如下:

    在HTML文件中,定义了一个匿名函数,专门用于加载并显示学校数据,它的声明如下:

var loadSchool = function(callback)

    它接收一个函数作为参数,这个callback的作用就是当我们把学校数据都提取出来并形成一个HTML串后,用来将这个HTML串赋值给某个div的innerHTML属性。

    接下来,看看这个函数的主体,代码如下:

        /**             * 加载学校数据             */            var loadSchool = function(callback) {                txt = [];                //遍历学校数据                for (var i in studentsData) {                    txt.push("
  • "); txt.push(studentsData[i].name); txt.push("
  • "); //设置拼音引擎的缓存 pinyinEngine.setCache([studentsData[i].name],false,studentsData[i]); } txt = txt.join(""); txt = txt === "" ? '
  • 此地区暂时没有数据..
  • ' : txt; callback(txt); }

        代码pinyinEngine.setCache我们先忽略掉。在这里,最关键的就是那个for循环。也许有人会问,那个suidentData变量哪来的呢?回忆前面学校数据的定义:

    var StudentData = [    {
    "id":1,"name":"北京大学"}, {
    "id":1,"name":"北京外国语大学"}, {
    "id":1,"name":"北京中央戏剧学院"}, ...];

        它作为全局变量被定义在了studentData.js文件中。我们在这里遍历它,分别取出id和name部分,并拼上<li><a>的HTML元素。另外,在这里,我们拼接字符串使用了txt[]数组,在js中,如果要拼接很多字符串,使用数组比直接+=效率要高。

        拼好以后,使用数组的join("")方法,将之转换为一个字符串,然后调用callback对这个HTML串进行处理。在这里,只是简要的显示在页面中,调用如下:

    /**             * 加载学校数据             * @parame {String} 包含学校数据的HTML代码             */            loadSchool(function(html) {                $unisContent.innerHTML = html;            });

        $unisContent是一个div元素。

        嗯,这一步完成了,而且也非常容易,接着,再来看看加载拼音数据并转换结构的代码。

      (2)加载拼音数据并转换结构

        在上面的分析中,我们忽略掉的那行代码,就是实现这个功能的。

    //设置拼音引擎的缓存                    pinyinEngine.setCache([studentsData[i].name],false,studentsData[i]);

        pinyinEngine这个对象在pinyinEngine这个文件中。定义如下:

    var pinyinEngine = function(my) {   .......}(pinyinEngine || {})

        使用这种方式在一个文件里定义一个对象,可以防止其他文件覆盖掉你的定义。比如,你在一个HTML文件中引用了两个外部js文件,其中后面的那个定义了一个对象,与前面那个js文件里定义的对象名字一样,后面的会覆盖掉前面的。使用这种方式定义,就不会。在多人开发时,推荐使用这种方式,特别是一个对象由不同人实现,又放在不同文件中。或者是引入了两个版本不同的库等等。

        对象pinyinEngine的结构如下:

           首先,它定义了两个变量:

    //缓存所有的学校名称以及对应的拼音    var _cache = [];    //保存历史筛选的记录    var _history = {};

        其中_history用于保存历史搜索记录,以加快搜索速度。

        两个内部需要使用到的函数

    /** * 将拼音对应的汉字串转换为每个汉字对应的拼音* @return {Array} 转换后的结果*/function convertHanToPinyin() {        .....            }/*** 利用笛卡尔乘机处理一字多读的情况* @parame {Array} 作为被乘数的拼音* @parame {Array} 作为乘数的拼音* @return 返回乘积的结果*/function product(arr1, arr2, sp) {    ....}

        以及四个可供外部调用的函数

    /*** 根据关键字对数据进行筛选* @param {String} 关键字* @param {function} 一个回调函数,用于操作每次筛选的结果* @return {Array} 筛选的返回结果*/my.search = function(keyword, callback) {    ......}/*** 将可供查询的内容设置到缓存中* @parame {Array} 一个包含汉字的数组* @parame {Any} 一个结构为id/content的对象*/my.setCache = function(tags, single, content, sp) {    ......}/*** 重置缓存和历史记录*/my.resetCache = function() {    ......}/*** 将指定的汉字转换为拼音* @parame {String} 汉字串* @parame {boolean} 是否只提取拼音的第一个字母* @parame {String} 提取的内容的分隔符*/my.toPinyin = function(text, single, sp) {    ......}

        首先关注的就是页面加载时调用的函数 - setCache,这个函数实现了两个功能,分别是转换结构和缓存所有学校名对应的拼音。通过上面的注释可知道,它有四个参数,分别是:

          tags:包含汉字串的数字(在这里,就是学校名字,每取出一个学校名,就传给这个参数)

          single:是否只取出每个汉字对应的拼音的首个字母

          content:一个学校的数据结构{id...,name:....}

          sp:汉字与拼音,拼音与拼音之间的分隔符(有些汉字存在一字多读,可能对应多个拼音)

        弄清楚几个参数的作用后,就要深入它的代码了。如下:

     

    View Code
    var keys = tags,            strKeys = "";            //循环遍历每个汉字串,取得汉字串的拼音        for (var i = 0, imax = tags.length; i < imax; i++) {            keys.push(my.toPinyin(tags[i], single, sp || "\u0001"));        }        //将汉字串和对应的拼音展开为字符串        strKeys = keys.join(sp || "\u0001");                var obj = {            tags: strKeys,            content: content        }        _cache.push(obj);

        关键是那个循环,它遍历tags数组中每个汉字串,然后调用my.toPinyin函数获得这个汉字串对应的拼音,并加入keys数组。my.toPinyin这个函数等下再来分析。最后,把上述字符串放到keys数组中,然后把keys数组展开成字符串,保存到对象obj中,然后把obj保存到_cache缓存中。例如,假如我们向该函数传入["北京大学"]这样一个汉字串,通过for循环后,则会得到如下一个字符串:

    "北京大学   beijingdaxue   beijingdaixue"

        注意,"大"有两种读法,所以得到了两个拼音字符串。然后把这个字符串假如keys数组,因为值传递了一个汉字串,循环执行一次,然后把keys数组转换成字符串。使用如下形式保存下来

    var obj = {    tags:"北京大学   beijingdaxue   beijingdaixue",    content:{id:1,name:"北京大学"}}

        最后,把该obj保存到缓存中。有多少个学校名字,就会得到多少个这种汉字对应拼音的串,所以,_cache就保存了所有的学校汉字和拼音串。我们搜索时,就到这个缓存中来搜索,找到匹配的就可以了。

        那么,my.toPinyin这个函数,是如何分析出汉字串所对应的拼音的呢?

           首先,在toPnyin函数中,需要转换拼音数据的结构,也就是说,把如下的结构:

    window.PINYIN_DATA = {    a:"啊阿嗄锕吖",    .....}

        转换成如下的结构:

    var cache = {    啊:[a],    阿:[a],    ...    大:[da,dai],    ....}

        这个功能,是由内部使用的函数convertHanToPinyin完成的:

    View Code
    /**     * 将拼音对应的汉字串转换为每个汉字对应的拼音     * @return {Array} 转换后的结果     */    function convertHanToPinyin() {        //获取拼音汉字表        datas = window.PINYIN_DATA || {};        var cache = {};        for (var i in datas) {            var hans = datas[i];//获得该拼音下对应的所有汉字            var han = "";            //遍历汉字串的每个汉字            for (var j = 0, max = hans.length; j < max; j++) {                han = hans.charAt(j);                if (!cache[han]) {                    cache[han] = [];                }                //保存该汉字对应的拼音                cache[han].push(i);            }        }        return cache;    }

        它首先获得拼音字典,然后遍历里面的每个拼音,取出每个拼音对应的汉字串。在第二个循环里面,取出这个汉字串里面的每个汉字,以这个汉字作为cache的key值,value值则是这个汉字的拼音。这样,当循环执行完毕后,cache就是程序需要的转换后的结构了。这样转换后,就能找到某个汉字对应的拼音。当然,也可以不进行转换,直接在拼音数据中查找。那就需要找到汉字所在的汉字串,然后取出这个汉字串所对应的拼音。

        将汉字拼音的数据结构转换完成后,就是找出给出的汉字串对应的拼音了。在toPinyin中有如下代码:

       

    View Code
    if (len === 0) return text;        else if (len === 1) {
    //如果只有一个汉字 py = cache[text]; if (single) return (py && py[0] ? py[0] : text); return py || [text]; } else { //多个汉字 for (var i = 0; i < len; i++) { py = cache[text.charAt(i)];//取得该汉字对应的拼音 if (py) { //如果存在对应的拼音 //是否只提取单个字符 pys[pys.length] = single ? py[0] : py; } else { pys[pys.length] = single ? text.charAt[i] : [text.charAt[i]]; } } //如果只返回汉字对应的第一个拼音字母,则不需要处理一字多读的情况 if (single) { return sp == null ? pys : pys.join(sp || ""); } //处理一字多读的情况 var arr1 = pys[0];//第一个拼音 var tmpArr = []; for (var k = 1, kmax = pys.length; k < kmax; k++) { tmpArr = product(arr1, pys[k], sp); arr1 = tmpArr.array; } return (sp == null ? arr1 : tmpArr.string); }

        len就是给出的汉字串的长度。我们一步步看。第一个if就不用说了,汉字串中没有汉字,那就什么都不做,原样返回。第二个if,如果汉字串中只有一个汉字,那么就以这个汉字为key,到转换后的汉字拼音结构中去找(在回顾下上面给出的数据结构,一个汉字对应它的拼音),single是表示是否返回拼音的首字母。else才是关键的代码了。它包括两部分:

        1.查找汉字串中每个汉字的拼音

        2.处理一字多读

         第1步应该都没什么问题,只是多了个循环。关键是如何处理一字多读的呢。程序使用的是笛卡尔乘积的方式,把原理说下:

             假设汉字对应的拼音已经提取出来,其结构如下:

         [[bei],[jing],[da,dai],[xue]]

         它是一个数组结构,在出现一字多读的情况下,会出现一个嵌套的数组。程序的笛卡尔按照如下方式处理:

           1.取出数组中第一个和第二个,即[bei],[jing]

           2.将两个元素拼成一个元素的数组返回,即[beijing]

           3.重复第一步,这时候,参数变成[beijing],[da,tai]

            4.因为第二个参数是一个有两个元素的数组,按照笛卡尔乘机,会变成如下数组返回:[beijingda,beijingtai]

           5.重复第一步,这时候,参数变成[beijingda,beijingdai],[xue]

           6.同样按照笛卡尔乘机,会变成如下数组返回:[beijingdaxue,beijingtaixue]。并结束处理。明白了这个过程,大家再结合给出的代码仔细走一遍流程,就会明白了。

        要注意,这个toPinyin函数会被调用多次,具体多少次,取决于有多少个学校名字,因为它需要找到所有的学校名字对应的拼音,最后会把学校名字连同对应的拼音一起存放到cache中。当程序进行搜索时,则是直接到这个cache中去搜索。代码如下:

    View Code
    /**     * 根据关键字对数据进行筛选     * @param {String} 关键字     * @param {function} 一个回调函数,用于操作每次筛选的结果     * @return {Array} 筛选的返回结果     */    my.search = function(keyword, callback) {        var cache = _cache;        var history = _history;        var values = [],            number = 0;        //如果这次所搜的关键词在上次已经搜索过,则只需在历史记录中搜索        if (history.keyword && history.keyword === keyword) {            cache = history.value;        }        //在cache中进行筛选        for (var i = 0, max = cache.length; i < max; i++) {            if (cache[i].tags.indexOf(keyword) !== -1) {                number++;                values.push(cache[i]);                callback(cache[i].content);            }        }                _history = {            keyword: keyword,            value: values,            count: number        }                return values;    }

        keyword就是用户在界面上输入的拼音或者汉字。程序会首先到历史记录中搜索,如果在历史记录中有,则不会再去整个缓存中搜索了。这段代码不复杂,就交给大家自己分析了。

        大家可以自己尝试做一个输入简体转换成繁体的程序,其原理应该是一样的。好了,今天就到这里吧。谢谢大叫,完整的源代码在这里。

     

     

     

     

    转载于:https://www.cnblogs.com/donet_code/archive/2013/01/23/javascript.html

    你可能感兴趣的文章
    vim进阶:better,faster and stronger
    查看>>
    jQuery.fn.extend jQuery.extend插件机制 tab切换
    查看>>
    算法问题——递归算法
    查看>>
    Vue.js递归组件实现动态树形菜单
    查看>>
    ajax、post、get实例
    查看>>
    centos 7 安装mariadb
    查看>>
    后台项目的控制器继承
    查看>>
    首尾相连的二维数组最大子数组求和
    查看>>
    第一阶段冲刺报告(二)
    查看>>
    java.lang.StackOverflowError 异常
    查看>>
    apt-fast
    查看>>
    列车调度 manage
    查看>>
    Linux获取当前用户信息函数
    查看>>
    vim map nmap(转)
    查看>>
    vuex非父子组件间改值
    查看>>
    计时器(何雨柔201521123040)
    查看>>
    JavaScript的Tab切换
    查看>>
    iOS开发小技巧--富文本字典集合中的Key都是OC中的常量字符串
    查看>>
    构建之法读书笔记之四
    查看>>
    JNDI数据源配置
    查看>>