• IK 分词器源码阅读笔记(1)


    1.Hit 类

    这个类只包含几个状态位,用于判断匹配的类型。
    结构很简单
    主要是几个常量:

    //Hit不匹配
    private static final int UNMATCH = 0x00000000;
    //Hit完全匹配
    private static final int MATCH = 0x00000001;
    //Hit前缀匹配
    private static final int PREFIX = 0x00000010;
    默认状态是UNMATCH
    private int hitState = UNMATCH;

    同时还有词段的开始和结束为止

    //词段开始位置
    private int begin;
    //词段的结束位置
    private int end;

    补充一个DictSegment类的对象,存储词典匹配过程中,当前匹配到的词典分支节点

    private DictSegment matchedDictSegment;

    暴露出来的公共方法

    • isMatch判断是否完全匹配
    • isPrefix判断是否是词的前缀
    • isUnmatch判断是否是不匹配
    • 以及对应的set方法

    2.DictSegment 类

    主要功能是存储字典使用。我的理解,这个类就是tire词典数中的每一个节点,
    内置HashMap和Array两个结构,默认有一个数组limit值,为3。也就是说当存储字符不超过3的情况下,使用数组存放,如果超过3个,就用map存放。个人理解使用数组是为了节省存储空间,使用map是为了达到o(1)的查找效率。

    先看一下field:

    • charMap 存储字符用的map
    • ARRAY_LENGTH_LIMIT 数组上界,超过这个大小就是用map结构
    • childrenArray 数组存储结构
    • childrenMap map存储结构
    • nodeChar 当前节点是哪个字
    • storeSize 当前节点存储的segment数
    • nodeState 从根节点到当前节点是不是一个完整的词语,1是完整词语,0不是完整词语。

    接下来就是几个核心的方法及其变种方法:

    第一个核心方法:

    • fillSegment,参数(char[] charArray , int begin , int length , int enabled)
      • charArray就是一个词语转成的char数组
      • begin是当前这个字位于charArrary的index
      • length是后面还有几个字(如果当前字符是词语的最后一个字,length=1)
      • enable是给停用词或者非停用词做标志位是哟,enable=0为停用词,enable=1为非停用词。

    作用是将词语转换成tire词典中的节点。
    代码如下:

          {
                    charMap中没有charArray[begin],没有则添加
                        Character beginChar = new Character(charArray[begin]);
                        Character keyChar = charMap.get(beginChar);
                        //字典中没有该字,则将其添加入字典
                        if(keyChar == null){
                            charMap.put(beginChar, beginChar);
                            keyChar = beginChar;
                        }
                    通过lookforSegment寻找charArray[begin]对应DictSegment的存储,enable表示没有则创建
                    DictSegment ds = lookforSegment(keyChar , enabled);
                    如果找到了,分情况进行处理
                        if(ds != null){
                            //处理keyChar对应的segment
                            if(length > 1){
                                //length大于1,说明当前还不是词语的结尾,需要继续递归,词元并没有完全加入词典树
                                ds.fillSegment(charArray, begin + 1, length - 1 , enabled);
                            }else if (length == 1){
                                //已经是词元的最后一个char,设置当前节点状态为enabled,
                                //enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词
                                ds.nodeState = enabled;
                            }
                        }
                }  


    顺带说一下其中出现的lookforSegment:
    lookforSegment方法,参数(Character keyChar , int create)

      • keyChar 待寻找的char
      • create =1如果没有找到,则创建新的segment ; =0如果没有找到,不创建,返回null

    作用是根据keyChar 找到其对应的DictSegment,如果找不到根据标识位决定是否进行创建。作者本身注释已经很全面了,在这里仅作补充

          private DictSegment lookforSegment(Character keyChar ,  int create){
                    
                    DictSegment ds = null;//用于返回的DictSegment值
    
                    if(this.storeSize <= ARRAY_LENGTH_LIMIT){//没有达到数组最大值,说明使用数组进行存储
                        //获取数组容器,如果数组未创建则创建数组
                        DictSegment[] segmentArray = getChildrenArray();//获取数组对象,如果为空则创建,不为空则直接使用(childrenArray)
                        //搜寻数组
                        DictSegment keySegment = new DictSegment(keyChar);//根据keyChar创建一个DictSegment
                        int position = Arrays.binarySearch(segmentArray, 0 , this.storeSize, keySegment);//二分法查找
                        if(position >= 0){
                            ds = segmentArray[position];//找到则赋值给ds并用于返回值
                        }
                    
                        //遍历数组后没有找到对应的segment
                        if(ds == null && create == 1){//如果没找到,并且需要进行创建
                            ds = keySegment;//将创建好的keySegment赋值给ds
                            if(this.storeSize < ARRAY_LENGTH_LIMIT){//检查是否达到数组最大值
                                //数组容量未满,使用数组存储
                                segmentArray[this.storeSize] = ds;
                                //segment数目+1
                                this.storeSize++;
                                Arrays.sort(segmentArray , 0 , this.storeSize);//排序(用于二分法查找)
                                
                            }else{
                                //数组容量已满,切换Map存储
                                Map<Character , DictSegment> segmentMap = getChildrenMap();//获取Map容器,如果Map未创建,则创建Map
                                //将数组中的segment迁移到Map中
                                migrate(segmentArray ,  segmentMap);
                                //存储新的segment
                                segmentMap.put(keyChar, ds);
                                //segment数目+1 ,  必须在释放数组前执行storeSize++ , 确保极端情况下,不会取到空的数组
                                this.storeSize++;
                                //释放当前的数组引用
                                this.childrenArray = null;
                            }
    
                        }			
                        
                    }else{//在此之前已经达到了数组最大值,从map中查找
                        //获取Map容器,如果Map未创建,则创建Map
                        Map<Character , DictSegment> segmentMap = getChildrenMap();
                        //搜索Map
                        ds = (DictSegment)segmentMap.get(keyChar);
                        if(ds == null && create == 1){//如果没找到,并且需要创建新的segment
                            //构造新的segment
                            ds = new DictSegment(keyChar);
                            segmentMap.put(keyChar , ds);
                            //当前节点存储segment数目+1
                            this.storeSize ++;
                        }
                    }
    
                    return ds;
                }
    

      getChildrenArray,getChildrenMap,migrate三个方法比较简单,前两个是判断一下是否为空,为空则创建,注意下线程安全就好(作者好像没有考虑指令重排的情况),migrate方法就是把数据从array中转存到map中,就不多进行分析了。

    • 接下来是另外一个重头方法match方法:

    match方法参数(char[] charArray , int begin , int length , Hit searchHit)
    前三个参数和fillSegment一模一样,hit也介绍了,就是用于存放匹配结果。
    直接看方法:

    {
                     //searchHit开始可能为空,为空则创建,不为空则将hit设置为unmatch状态。
                    if(searchHit == null){
                        //如果hit为空,新建
                        searchHit= new Hit();
                        //设置hit的其实文本位置
                        searchHit.setBegin(begin);
                    }else{
                        //否则要将HIT状态重置
                        searchHit.setUnmatch();
                    }
                    //设置hit的当前处理位置
                    searchHit.setEnd(begin);
                    
                    Character keyChar = new Character(charArray[begin]);
                    DictSegment ds = null;
                    
                    //引用实例变量为本地变量,避免查询时遇到更新的同步问题
                    DictSegment[] segmentArray = this.childrenArray;
                    Map<Character , DictSegment> segmentMap = this.childrenMap;		
                    
                    //STEP1 在节点中查找keyChar对应的DictSegment
                    if(segmentArray != null){
                        //在数组中查找
                        DictSegment keySegment = new DictSegment(keyChar);
                        int position = Arrays.binarySearch(segmentArray, 0 , this.storeSize , keySegment);
                        if(position >= 0){
                            ds = segmentArray[position];
                        }
    
                    }else if(segmentMap != null){
                        //在map中查找
                        ds = (DictSegment)segmentMap.get(keyChar);
                    }
                    
                    //STEP2 找到DictSegment,判断词的匹配状态,是否继续递归,还是返回结果
                    if(ds != null){			
                        if(length > 1){
                            //词未匹配完,继续往下搜索
                            return ds.match(charArray, begin + 1 , length - 1 , searchHit);
                        }else if (length == 1){
                            
                            //搜索最后一个char
                            if(ds.nodeState == 1){
                                //添加HIT状态为完全匹配
                                searchHit.setMatch();
                            }
                            if(ds.hasNextNode()){
                                //添加HIT状态为前缀匹配
                                searchHit.setPrefix();
                                //记录当前位置的DictSegment
                                searchHit.setMatchedDictSegment(ds);
                            }
                            return searchHit;
                        }
                        
                    }
                    //STEP3 没有找到DictSegment, 将HIT设置为不匹配
                    return searchHit;		
                }   
    

      

    3.Dictionary 类

    一个典型的懒加载单例模式类,不过没有考虑指令重排

    • 先说一下几个主要的field
    • _MainDict 主要词典
    • _StopWordDict 停用词词典
    • _QuantifierDict 量词词典
    • singleton 真正词典实例
    • cfg 配置文件

    还有两个默认Path路径(内置Main和Quantifier词典路径)
    方法也不是太多,很容易理解

    • getInstance 不多说了,获取实例对象
    • addWords 批量增加单词的
    • disableWords 批量过滤的
    • matchInMainDict 主词典是否匹配
    • matchInQuantifierDict 量词词典是否匹配
    • isStopWord 是否是停用词
    • loadMainDict 加载主词典(内置)
    • loadExtDict 加载外部词典
    • loadStopWordDict 加载停用词词典
    • loadQuantifierDict 加载量词词典
    • matchWithHit 从已匹配的Hit中直接取出DictSegment,继续向下匹配

    上述方法基本都是对DictSegment中的match方法进行封装,要么就是直接读取dic,比较容易,就不过多分析了。

    上述3个类了解之后,基本上来说就算是熟悉了字典树的生成(Tire词典),match匹配的过程了。

    4.测试一下基于字典的Dag,并实现分词

    • 一个创建Dag的方法,主要是判断字典中是否包含对应的key,在此之前现根据标点符号进行切分。
        private static Map<Integer,List<Integer>>  getDag(String text){
                Map<Integer,List<Integer>> dagMap = new HashMap<>();
                int length = text.length();
                for (int i = 0; i < length; i++) {
                    List<Integer> list = new ArrayList<>();
                    int k = i;
                    String  subStr;
                    while (k<length ){
                        subStr =text.substring(i,k+1);
                        if (dict.containsKey(subStr)){
                            list.add(k);
                        }
    
                        k++;
                    }
                    if (list.isEmpty())
                        list.add(i);
                    dagMap.put(i,list);
                }
                return dagMap;
            }
    

      

    代码还是很好理解的,接受到字符串。字符串长度从1开始一直到结束,检查字典中是否包含,这个应该输属于最大力度切分了,算法复杂度O(n^2),应该有优化空间吧。

    • dict声明及初始化如下:
            private static Map<String,Integer> dict = new HashMap<>();
             static {
                load("dict",dict);
            }
            private static void load(String fileName,Map<String,Integer> map,boolean flag){
            try {
                    InputStream stream = Main.class.getClassLoader().getResource("com/company/"+fileName).openStream();
                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
                        String line;
                        while ((line=reader.readLine())!=null){
                            String[] arr = line.split(" ");
                            String str=arr[0];
                            Integer num = Integer.parseInt(arr[1]);
                            map.put(str,num);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    • 在Main函数中测试一下:
            public static void main(String[] args) throws IOException {
                String text="一花一世界";
                Map<Integer, List<Integer>> dag = getDag(text);
                System.out.println(dag);
                for (List<Integer> list : dag.values()) {
                    Integer start = list.remove(0);
                    if (list.size() == 0)
                        System.out.println(text.charAt(start));
                    else
                        for (Integer end : list) {
                            System.out.println(text.substring(start,end+1));
                        }
                }
            }
    • 输出结果如下:
    {0=[0], 1=[1], 2=[2, 3], 3=[3, 4], 4=[4]}
    一
    花
    一世
    世界
    界

    最大粒度切分基本就这样,后面还有歧义处理部分。

  • 相关阅读:
    rzc generate exited with code -2147450730.
    c#WebService动态调用
    c#BarTender打印,打印微调
    记一次ios下h5页面图片显示问题
    FID
    RSA密钥对生成,并解析公钥指数和模数
    angularjs-6
    angularjs-5
    angularjs-4
    angularjs-4
  • 原文地址:https://www.cnblogs.com/eviltuzki/p/9267224.html
Copyright © 2020-2023  润新知