纯前端实现地址分词,模糊匹配

关于地址分词的一点思路,一些主要代码的简要说明

本人的思路是,解析的结果存储在一个类似树状的结构中,就和DOM节点类似,用parent字段指向父级,用children字段指向子级

准备工作

CityModel 类

先构建出一个 CityModel 类 用来表示树的每一个节点 具体属性可参考下面

class CityModel{
    constructor(option={}){
        //编码
        this.code=option.code;
        //重点子城市
        this.childrenNode=[];
        //父级
        this.parent=option.parent;
        //层
        this.level=option.level;
        //权重
        this.weights=option.weights;
        //值
        this.value=option.value;
        //父节点code
        this.parentCode=option.parentCode;
        //匹配前的字符
        this.matchText=option.matchText;
        //匹配后的字符
        this.matchedText=option.matchedText;
        //匹配到的字符
        this.matchToText=option.matchToText;
        //当前处理序列
        this.dealIndex=0;
    }
}

解析过程

创建根节点

第一步 先创建一个根节点 然后以此节点向子级查找

createRoot(matchText){
    const root={
        parentCode:null,
        level:0,
        code:ROOTCODE,
        matchText:this.filterInfo(matchText),
        weights:100
    };
    return new CityModel(root)
}

// 这个函数主要是把字符串可能存在的前缀给他过滤掉,因为前缀会影响子节点的权重信息
filterInfo(){
    let matchText=this.matchText||'';
    let reg=/地址[::]+.*$/g;
    let matchResults=matchText.match(reg);
    if(matchResults){
        return matchResults[0].replace(/^地址[::]+/,'')
    }
    return matchText
}

第二步 开始匹配

const root= createRoot(matchText);
root.match();

整体思路就是,匹配的同时,创建子节点。直至匹配结束,从上至下的匹配。

match 函数是匹配规则的核心了 下面来解析一下match函数

match(){
    //先把对应层级的正则提取出来,主要是用于将单位给去掉,比如第一级别的省,第二级别的市,因为客户传入的字符串可能不含这个词
    let reg=orderRegExp[this.level];
    //当前要匹配的词
    let matchText=this.matchText;
    //如果为空,说明后面没有需要解析的字符了,直接返回即可
    if(!matchText){
        return this.checkWeight();
    }
    let parent=this.parent;
    //获取当前节点,所有的子节点文本,如果获取河南省下面所有的市
    let childrenValues=this.getChildrenValues();
    if(childrenValues){
        //当有子节点文本的时候
        //将所有的子节点文本全部汇总成一个字符串,我是为了偷懒
        let libraryStr=childrenValues.join(',');
        if(reg){
            //将当前层级的单位去掉
            libraryStr=libraryStr.replace(reg,'');
        }
        //再转换成数组,上面几步都是为了去掉单位的,可以直接用for循环,也是一样的
        const _library=libraryStr.split(',');
        //判断子节点文本数量长度,如果为1,则说明,类似直辖市的感觉,上海-上海市
        if(_library.length===1){
            //判断当前子节点文本是不是在要匹配的字符串中的位置是否在前面 比如 上海 在中国上海市青浦区徐泾镇的位置。
            //位置不能太靠后,否则会失去参考价值,比如 南阳邓州团结路xxx号重庆火锅 这种情况不能去找重庆,因为重庆没有南阳市
            if(!(new RegExp(_library[0]).test(matchText)&&matchText.indexOf(_library[0])<=3)){
                //如上海市青浦区,第一次匹配上海的时候,匹配后的字符串就变成了市青浦区,此时再匹配上海市就匹配不到了,可以去父级去匹配
                if(parent&&new RegExp(_library[0]).test(parent.matchText)){
                    matchText=this.matchText=this.parent.matchText;
                }
            }
            //根据子节点文本创建子节点,权重和父级权重保持一致
            this.createChildNode(_library[0],this.weights,!new RegExp(_library[0]).test(matchText))
        }else {
            //如果子节点文本数量不为1,说明子节点文本数量大于1,循环一下
            for (let i=0;i<_library.length;i++){
                if(new RegExp(_library[i]).test(matchText)){
                    // 如果当前子节点文本是在要匹配的字符串中,根据位置生成权重信息
                    let _index=matchText.indexOf(_library[i]);
                    let weights=100-_index;
                    if(_index>3){
                        weights=50;
                    }
                    //根据子节点文本生成一个真正的子节点
                    let currentNode=this.createChildNode(_library[i],weights);
                    //如果子节点文本所在的索引小于等于3,说明在前面,则执行父节点加权操作
                    if(currentNode&&_index<=3){
                        currentNode.weightsAdd();
                    }
                }else if(parent&&new RegExp(_library[i]).test(parent.matchText)){
                    //如果子节点文本在父级字符串中找到了,也会有一定可能性,但是只是将当前权重增加,不增加父级权重
                    let _index=parent.matchText.indexOf(_library[i]);
                    let weights=50-_index;
                    this.createChildNode(_library[i],weights);
                }else {
                    //如果没找到,则将权重设为1,虽然没有找到,说不定只是省略了
                    //如 河南省邓州市,当匹配南阳的时候,因为省略了,但是当匹配邓州的时候,邓州的权重为100,则会增加间接增加南阳的权重
                    this.createChildNode(_library[i],1,true)
                }
            }
        }
        //排个序,权重大的在前面,优先进行子匹配
        this.childrenNodeSort();
        const childrenNode=this.childrenNode[0];
        if(childrenNode&&childrenNode.weights<=1&&this.weights<=1){
            //如果当子没有匹配到,自己也没匹配到,要么是这条路走不通了,要么是中间跨了两级都省略了,则直接关闭当前路径,走下一条路
            this.close();
            this.emitNext();
        }else {
            //如果有子,则尝试匹配第一个子,进行递归匹配
            this.childrenNode[0].match();
        }
    }else if(typeof this.getChildren() ==='undefined'){
        //当子节点为undefined的时候,地址库不可能一下子全部加载,所以这里主要是为动态加载做的
        if(this.level>=stopLevel||index.emit('noChildren',this,this.getTopId())===0){
            this.checkWeight();
        }
    }else {
        //当没有子节点的时候
        this.checkWeight();
    }
}

match说明

关于匹配函数 match 中出现的一些重要的函数说明

createChildNode

关于createChildNode函数的说明,通过子节点文本创建子节点

createChildNode(matchToText,weights,noMatch=false){
    let level=this.level+1;
    let parent=this;
    //通过文本超找完整文本
    //例如 匹配到了南阳,根据字典,找出完整的字符南阳市
    let fullInfo=this.getFullInfoByValue(matchToText);
    //判断完整的字符是否在当前匹配的字符中,如果是,则匹配到的字符按照完整的来
    //例如 匹配到了南阳,实际字符串为南阳市邓州市 则直接说明匹配到了南阳市,将邓州市作为子匹配
    if(new RegExp(fullInfo.value).test(this.matchText)){
        matchToText=fullInfo.value;
    }
    
    //将matchText用matchToText分割,后面的部分用于匹配
    //比如 南阳邓州都司  matchToText为南阳,则当前生成的节点的matchText为邓州都司
    //后续匹配南阳的子节点的时候,会将南阳所有的子节点分别去匹配邓州都司。以此类推
    let matchTexts=this.matchText.split(matchToText);
    let matchText=this.matchText;
    if(matchTexts.length>1){
        matchText=matchTexts.slice(1).join(matchToText);
    }
    weights=weights||0;
    this.matchedText=matchText;
    if(noMatch){
        //如果没有匹配到,则生成的节点的matchToText为null
        matchToText=null;
    }
    if(fullInfo){
        //将完整的code,value取出来
        let {code,value}=fullInfo;
        //根据已有的信息生成一个子节点
        let cityNode=new CityModel({
            parentCode:parent.code,
            code:code,
            weights,
            matchToText,
            matchText,
            level,
            parent,
            value
        });
        //将当前子节点推入childrenNode中
        this.childrenNode.push(cityNode);
        return cityNode
    }
    return false;
}

checkWeight

关于checkWeight的说明,校验权重,判断是否已经退出递归

checkWeight(){
    if(this.weights<=97){
        //小于97则认为当前分支正确性不高
        this.close();
        return this.emitNext();
    }else {
        //大于97则认为当前分支是正确的,可以结束了
        return this.end();
    }
}

emitNext

emitNext 指向器 自动前往下一个节点进行匹配

emitNext(){
    //获取父级节点
    const parent=this.parent;
    if(parent){
        //获取处理位置
        let dealIndex=parent.dealIndex;
        //获取父节点的子节点数量,也就是兄弟节点的长度
        let length=parent.childrenNode.length;
        if((dealIndex+2>length||length===0)){
            //如果长度===0或者位置已经是最后一个了,则执行当前,否则执行父节点的 emitNext
            if(parent.weights>=97&&parent.level>=0){
                //如果父节点权重大于97,并且层级大于0
                //排个序,权重大在前
                parent.childrenNodeSort();
                let child=parent.childrenNode[0];
                if(child&&child.weights>=97){
                    //第一个子节点权重如果大于97,直接使用第一个子节点结束,否则父节点结束
                    child.end()
                }else {
                    parent.end();
                }
                return true
            }
            return parent.emitNext();
        }else {
            // 将光标移动至下一个,然后进行匹配
            parent.dealIndex++;
            parent.childrenNode[parent.dealIndex].match();
        }
    }
    return false
}

因个人能力有限,以上内容多有疏漏,欢迎大家指正

完整代码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值