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

 

一.概述。

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

使用arcgissdkjava 使用的拼音_数据

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

使用arcgissdkjava 使用的拼音_javascript_02

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

二.思路

  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显示在页面上。

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

<script src="pinyinData.js"></script> <!-- 拼音数据 -->
<script src="studentData.js"></script> <!-- 学校数据 -->
<script src="pinyinEngine.js"></script> <!-- 实现拼音搜索的js文件 -->

    下面我们就来具体分析。

 

三.详细代码分析

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

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

<script src="studentData.js"></script>

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

var loadSchool = function(callback)

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

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

/**
             * 加载学校数据
             */
            var loadSchool = function(callback) {
                txt = [];
                //遍历学校数据
                for (var i in studentsData) {
                    txt.push("<li><a href='javascript:;' id='");
                    txt.push(studentsData[i].id + "'>");
                    txt.push(studentsData[i].name);
                    txt.push("</a></li>");
                    //设置拼音引擎的缓存
                    pinyinEngine.setCache([studentsData[i].name],false,studentsData[i]);
                }
                txt = txt.join("");
                txt = txt === "" ? '<li><div class="tmpl-schoolBox-noContent">此地区暂时没有数据..</div></li>'
                    : 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:汉字与拼音,拼音与拼音之间的分隔符(有些汉字存在一字多读,可能对应多个拼音)

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

 

使用arcgissdkjava 使用的拼音_数据结构与算法_03

使用arcgissdkjava 使用的拼音_数据结构与算法_04

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完成的:

使用arcgissdkjava 使用的拼音_数据结构与算法_03

使用arcgissdkjava 使用的拼音_数据结构与算法_04

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

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