IK分词器访问远程词典功能实现

IK Analyzer 介绍

IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出了3个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的 IKAnalyzer3.0则发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。

IK Analyzer 2012特性:

  1. 采用了特有的“正向迭代最细粒度切分算法“,支持细粒度和智能分词两种切分模式;
  2. 2012版本的智能分词模式支持简单的分词排歧义处理和数量词合并输出。
  3. 采用了多子处理器分析模式,支持:英文字母、数字、中文词汇等分词处理,兼容韩文、日文字符
  4. 优化的词典存储,更小的内存占用。支持用户词典扩展定义。特别的,在2012版本,词典支持中文,英文,数字混合词语。

介绍了Ik分词器基本情况之后,接下来分析分析源码。从github上将IK分词器源码下载下来https://github.com/wks/ik-analyzer,这个是官方 ik 分词器的一个fork, 原项目地址为 https://code.google.com/p/ik-analyzer

  • 将工程文件夹打开之后是下图的结构


    项目目录.png
  • pom.xml中项目依赖
<dependencies>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${lucene.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.solr</groupId>
            <artifactId>solr-core</artifactId>
            <version>${lucene.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

可以看出,IK分词器实现依赖了lucene及solr,其实solr只是作为Solr分词器工厂实现的依赖,与IK分词器关系不大。

配置文件分析

image.png

IKAnalyzer.cfg.xml为IK分词器的配置文件,main2012.dic文件为主词典,quantifier.dic文件为量词词典,stopword.dic文件为停用词词典,ext.dic文件为扩展词词典。

  • IKAnalyzer.cfg.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
<properties>  
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">ext.dic;</entry> 
    
    <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">stopword.dic;</entry> 
</properties>

源码分析

目录结构.png
  • 目录解释

cfg:配置管理类接口和实现
core:分词器上下文,字符集工具,中文-日韩文子分词器,中文数量词子分词器,IK分词歧义裁决器,IK分词器主类,子分词器接口,英文字符及阿拉伯数字子分词器,IK词元对象,IK分词器专用的Lexem快速排序集合
dic:词典管理类,词典树,词典匹配命中类
lucene:IK分词器的Lucene Analyzer接口实现,IK分词器 Lucene Tokenizer适配器类
query:IK简易查询表达式解析,SWMC算法
sample:IK分词器使用demo
solr:Solr分词器工厂实现

由于今天主题是实现IK分词器访问远程词典的功能实现,故IK具体分词算法今天不分析,只分析新功能实现。
要实现IK分词器远程访问词典,首先要了解IK分词器如何进行分词的。先从一个demo开始。

public Map<String, Integer> getParticipleByStr(String content, boolean useSmart) {
        Map<String, Integer> res=new HashMap<>();
        try {
            byte[] bt = content.getBytes();// str
            InputStream ip = new ByteArrayInputStream(bt);
            Reader read = new InputStreamReader(ip);
            IKSegmenter iks = new IKSegmenter(read, useSmart);
            Lexeme t;
            while ((t = iks.next()) != null) {
                String tmpKey=t.getLexemeText();
                if(res.containsKey(tmpKey)){
                    Integer val= res.get(tmpKey)+1;
                    res.replace(tmpKey,val);
                }else{
                    res.put(tmpKey,1);
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return res;
    }

该方法是根据传入一段文本,根据useSmart值来决定是否启用智能分词。从demo可以看出,真正进行分词的是IKSegmenter iks = new IKSegmenter(read, useSmart);这一行代码。IKSegmenter 类是IK分词器主类,是一个单例类,构造该类的时候有两个参数,一个是传入文本的字节流Reader对象,一个是是否启用智能分词。构造方法如下:

public IKSegmenter(Reader input , boolean useSmart){
        this.input = input;
        this.cfg = DefaultConfig.getInstance();
        this.cfg.setUseSmart(useSmart);
        this.init();
    }

DefaultConfig类实现了Configuration接口。也是个单例类,类中有获取主词典路径,量词词典路径,本地扩展词典路径,停用词典路径等方法。DefaultConfig构造方法源码如下:

     /*
     * 初始化配置文件
     */
    private DefaultConfig(){        
        props = new Properties();
        InputStream input = this.getClass().getClassLoader().getResourceAsStream(FILE_NAME);
        if(input != null){
            try {
                props.loadFromXML(input);
            } catch (InvalidPropertiesFormatException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

IKSegmenter构造方法中有个this.init()方法,该方法代码如下:

   private void init(){
       //初始化词典单例
       Dictionary.initial(this.cfg);
       //初始化分词上下文
       this.context = new AnalyzeContext(this.cfg);
       //加载子分词器
       this.segmenters = this.loadSegmenters();
       //加载歧义裁决器
       this.arbitrator = new IKArbitrator();
   }

故,IkSegment初始化的时候要初始化词典Dictionary,该类为词典管理类,同样是单例模式,同时该类中有加载各种词典的方法及个别分词算法使用的方法。

  • Dictionary构造方法
private Dictionary(Configuration cfg){
        this.cfg = cfg;
        this.loadMainDict();
        this.loadStopWordDict();
        this.loadQuantifierDict();
    }

通过构造方法可以看出,该类初始化的时候就加载了主词典,停用词典,量词词典。先看一个loadMainDict()方法代码:

    private void loadMainDict(){
        //建立一个主词典实例
        _MainDict = new DictSegment((char)0);
        //读取主词典文件
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(cfg.getMainDictionary());
        if(is == null){
            throw new RuntimeException("Main Dictionary not found!!!");
        }
        
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(is , "UTF-8"), 512);
            String theWord = null;
            do {
                theWord = br.readLine();
                if (theWord != null && !"".equals(theWord.trim())) {
                    _MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());
                }
            } while (theWord != null);
            
        } catch (IOException ioe) {
            System.err.println("Main Dictionary loading exception.");
            ioe.printStackTrace();
            
        }finally{
            try {
                if(is != null){
                    is.close();
                    is = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //加载扩展词典
        this.loadExtDict();
    }   

通过InputStream is = this.getClass().getClassLoader().getResourceAsStream(cfg.getMainDictionary());可以看出,加载的词典文件必须存放于类根目录才行,即工程resources文件夹下,这样的功能也限制了词典的动态扩展性。

如何实现远程访问扩展词典?

  1. 首先在IKAnalyzer.cfg.xml文件中添加远程访问词典路径,要发送http请求访问。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">ext.dic</entry>
    <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">stopword.dic</entry>
    <!--用户可以在这里配置远程扩展字典 -->
    <entry key="remote_ext_dict">http://192.168.70.33:8080/tag.dic</entry>
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
  1. 在DefaultConfig类中添加获取远程访问词典路径的方法
private final static  String REMOTE_EXT_DICT = "remote_ext_dict";

private final static  String REMOTE_EXT_STOP = "remote_ext_stopwords";

/**
     * 获取远程扩展词典配置路径
     *
     * @return List&lt;String&gt;
     */
    public List<String> getRemoteExtDictionarys() {
        List<String> remoteExtDictFiles = new ArrayList<String>(2);
        String remoteExtDictCfg = props.getProperty(REMOTE_EXT_DICT);
        if (remoteExtDictCfg != null) {

            String[] filePaths = remoteExtDictCfg.split(";");
            for (String filePath : filePaths) {
                if (filePath != null && !"".equals(filePath.trim())) {
                    remoteExtDictFiles.add(filePath);
                }
            }
        }
        return remoteExtDictFiles;
    }

/**
     * 获取远程停用词典配置路径
     * @return List&lt;String&gt;
     */
    public List<String> getRemoteExtStopWordDictionarys() {
        List<String> remoteExtStopWordDictFiles = new ArrayList<String>(2);
        String remoteExtStopWordDictCfg = props.getProperty(REMOTE_EXT_STOP);
        if (remoteExtStopWordDictCfg != null) {

            String[] filePaths = remoteExtStopWordDictCfg.split(";");
            for (String filePath : filePaths) {
                if (filePath != null && !"".equals(filePath.trim())) {
                    remoteExtStopWordDictFiles.add(filePath);

                }
            }
        }
        return remoteExtStopWordDictFiles;
    }
  1. 在Dictionary初始化initial(Configuration cfg)方法中添加一个定时发送head请求的单线程的线程池
private static ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
/**
     * 词典初始化
     * 由于IK Analyzer的词典采用Dictionary类的静态方法进行词典初始化
     * 只有当Dictionary类被实际调用时,才会开始载入词典,
     * 这将延长首次分词操作的时间
     * 该方法提供了一个在应用加载阶段就初始化字典的手段
     *
     * @return Dictionary
     * @param cfg a {@link Configuration} object.
     */
    public static Dictionary initial(Configuration cfg){
        if (singleton == null) {
            synchronized (Dictionary.class) {
                if (singleton == null) {
                    singleton = new Dictionary(cfg);
                    singleton.loadMainDict();
                    singleton.loadQuantifierDict();
                    singleton.loadStopWordDict();

                    if(cfg.isEnableRemoteDict()){
                        // 建立监控线程
                        for (String location : cfg.getRemoteExtDictionarys()) {
                            // 10毫秒是初始延迟可以修改的 60是间隔时间 单位毫秒
                            pool.scheduleAtFixedRate(new Monitor(location), 0, 10, TimeUnit.SECONDS);
                        }
                        for (String location : cfg.getRemoteExtStopWordDictionarys()) {
                            pool.scheduleAtFixedRate(new Monitor(location), 0, 10, TimeUnit.SECONDS);
                        }
                    }

                    return singleton;
                }
            }
        }
        return singleton;
    }

该定时执行的线程池内有个Monitor类,该类实现了Runnable接口,同时该类有个远程访问的路径,线程池会定时访问该路径,如果head请求返回的内容有变化就设置last_modified和eTags,如果没变化,则不做任何动作。代码如下:

public class Monitor implements Runnable {
    private static CloseableHttpClient httpclient = HttpClients.createDefault();
    /*
     * 上次更改时间
     */
    private String last_modified;
    /*
     * 资源属性
     */
    private String eTags;

    /*
     * 请求地址
     */
    private String location;

    public Monitor(String location) {
        this.location = location;
        this.last_modified = null;
        this.eTags = null;
    }
    /**
     * 监控流程:
     *  ①向词库服务器发送Head请求
     *  ②从响应中获取Last-Modify、ETags字段值,判断是否变化
     *  ③如果未变化,休眠1min,返回第①步
     *  ④如果有变化,重新加载词典
     *  ⑤休眠1min,返回第①步
     */

    public void run() {
        //超时设置
        RequestConfig rc = RequestConfig.custom().setConnectionRequestTimeout(10*1000)
                .setConnectTimeout(10*1000).setSocketTimeout(15*1000).build();

        HttpHead head = new HttpHead(location);
        head.setConfig(rc);

        //设置请求头
        if (last_modified != null) {
            head.setHeader("If-Modified-Since", last_modified);
        }
        if (eTags != null) {
            head.setHeader("If-None-Match", eTags);
        }
        CloseableHttpResponse response = null;
        try {
            response = httpclient.execute(head);
            //返回200 才做操作
            if(response.getStatusLine().getStatusCode()==200){

                if (((response.getLastHeader("Last-Modified")!=null) && !response.getLastHeader("Last-Modified").getValue().equalsIgnoreCase(last_modified))
                        ||((response.getLastHeader("ETag")!=null) && !response.getLastHeader("ETag").getValue().equalsIgnoreCase(eTags))) {

                    // 远程词库有更新,需要重新加载词典,并修改last_modified,eTags
                    Dictionary.getSingleton().reLoadMainDict();
                    last_modified = response.getLastHeader("Last-Modified")==null?null:response.getLastHeader("Last-Modified").getValue();
                    eTags = response.getLastHeader("ETag")==null?null:response.getLastHeader("ETag").getValue();
                }
            }else if (response.getStatusLine().getStatusCode()==304) {
                //没有修改,不做操作
                //noop
            }else{
            }
        } catch (Exception e) {
        }finally{
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
            }
        }
    }
}

为什么会发送Head请求?因为head请求并不返回消息体,特别适用在优先的速度和带宽下来检查资源的有效性,检查超链接的有效性,检查请求资源是否被修改。
4.将远程扩展词典文件置于tomca/webapps/ROOT文件夹下面,启动远程tomcat。
5.启用新IK分词器
功能实现!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355