美文网首页
对接Hanlp的最佳实践(仅为本公司的对接示例)

对接Hanlp的最佳实践(仅为本公司的对接示例)

作者: 天草二十六_简村人 | 来源:发表于2022-08-23 00:08 被阅读0次

一、Hanlp的背景介绍

  • github地址: https://github.com/hankcs/HanLP
  • 官网:https://www.hanlp.com/
  • 作用:分词。这里举个例子,“傻逼”,从汉字分词的角度,是会分割为“傻”“逼”两个字,而不会是一个词。这也使得我们需要自定义词典,后文将着重讲我们公司是怎么使用的。

二、目标

  • 高性能是本系统设计的一个关键目标,所以不能每次检测的时候,都读取数据库(无论是mysql还是redis)。
  • 业务上,要求敏感词的动态调整,及时性是一个基本要求。自定义的敏感词和hanlp分词的自定义词典,做到实时同步。
  • 不用重启应用。添加或删除了敏感词,都是更改txt文件的内容,然而在hanlp中无法生效,只有重启应用。所以,我们需要调用hanlp的api接口,做到动态地增删.txt.bin内容,做到无需重启应用。
  • 敏感词的操作,如果是在业务高峰期,触发jvm内存所带来的首次慢的问题。可以采用临时内存区间,对接数据库中的敏感词库,待加载完成后,然后让原内存失效,启用新内存。

三、敏感词服务的设计思路

hanlp在敏感词检测服务中的作用.png
  • 应用启动的时候,将自定义的敏感词词库,加载到jvm内存中。提升检测敏感词的性能。hanlp的自定义词典是设计中的核心,把上一步的敏感词词库添加至自定义词典。
  • 维护敏感词的时候,除了更新jvm内存中的敏感词词库外,还需要调用com.hankcs.hanlp.dictionary.CustomDictionary中的api方法(insert()/remove()),以更新自定义词典。

四、关键设计思路

3.1、引入离线包

       <dependency>
            <groupId>com.hankcs</groupId>
            <artifactId>hanlp</artifactId>
            <version>portable-1.8.2</version>
        </dependency>

3.2、引入配置文件hanlp.properties

/**
     * hanlp.properties的路径,一般情况下位于classpath目录中。
     * 但在某些极端情况下(不标准的Java虚拟机,用户缺乏相关知识等),允许将其设为绝对路径
     */
    public static String HANLP_PROPERTIES_PATH;
默认读取classpath目录下的hanlp.properties.png
  • 兜底操作,会读取环境变量HANLP_ROOT,作为root配置的值。

3.3、指定字典所在的根目录(方式一)

只需要修改这一个配置,root=/opt/SensitiveData/data-for-1.7.5/
这种方式,会需要把hanlp.properties放在Jar包外,额外带来了下面的几行代码。

        String hanlp_properties_path = System.getProperty("hanlp_properties_path");
       
        if (null != hanlp_properties_path && !"".equals(hanlp_properties_path.trim())) {
            Predefine.HANLP_PROPERTIES_PATH = hanlp_properties_path;
        }

3.4、指定字典所在的根目录(方式二)

设置环境变量HANLP_ROOT,推荐使用这种方式。

  • 既不用在resources目录,也不能在Jar包外,引入配置文件hanlp.properties

3.5、JVM内存中自定义的敏感词库做到实时刷新

这里的jvm内存数据结构,可以是Set/Map,更推荐caffeine实现。

创建一个标识,用来记录单个jvm节点的内存数据是否需要刷新。
多个Jvm节点,保存到redis的set集合中(值可以是jvm进程号)。在启动的时候,判断当前的进程号是否存在于redis的set集合中,如果不存在,说明需要重新刷新jvm内存中的自定义的敏感词库。

在敏感词维护的时候,新增或删除敏感词,除了操作mysql数据库外,将当前进程号从redis中的set集合移除。做到重新刷新jvm内存中的敏感词库。

3.6、hanlp的自定义词典

  • 错误的配置

CustomDictionaryPath=data/dictionary/custom/CustomDictionary.txt;data/dictionary/custom/自定义.txt;data/dictionary/custom/现代汉语补充词库.txt;data/dictionary/custom/全国地名大全.txt ns;data/dictionary/custom/人名词典.txt;data/dictionary/custom/机构名词典.txt;data/dictionary/custom/上海地名.txt ns;data/dictionary/person/nrf.txt nrf;

因为mainPath=data/dictionary/custom/CustomDictionary.txt,所以 if (loadDat(mainPath, dat)) return true; 所有后面的词典文件不再读取。

  • 正确(默认)的配置
CustomDictionaryPath=data/dictionary/custom/CustomDictionary.txt
  • 总结
    要么不要配置data/dictionary/custom/CustomDictionary.txt,要么就沿用默认。
/**
     * 加载词典
     *
     * @param mainPath 缓存文件文件名
     * @param path     自定义词典
     * @param isCache  是否缓存结果
     */
    public static boolean loadMainDictionary(String mainPath, String path[], DoubleArrayTrie<CoreDictionary.Attribute> dat, boolean isCache)
    {
        logger.info("自定义词典开始加载:" + mainPath);
        if (loadDat(mainPath, dat)) return true;
        TreeMap<String, CoreDictionary.Attribute> map = new TreeMap<String, CoreDictionary.Attribute>();
        LinkedHashSet<Nature> customNatureCollector = new LinkedHashSet<Nature>();
        try
        {
            //String path[] = HanLP.Config.CustomDictionaryPath;
            for (String p : path)
            {
                Nature defaultNature = Nature.n;
                File file = new File(p);
                String fileName = file.getName();
                int cut = fileName.lastIndexOf(' ');
                if (cut > 0)
                {
                    // 有默认词性
                    String nature = fileName.substring(cut + 1);
                    p = file.getParent() + File.separator + fileName.substring(0, cut);
                    try
                    {
                        defaultNature = LexiconUtility.convertStringToNature(nature, customNatureCollector);
                    }
                    catch (Exception e)
                    {
                        logger.severe("配置文件【" + p + "】写错了!" + e);
                        continue;
                    }
                }
                logger.info("以默认词性[" + defaultNature + "]加载自定义词典" + p + "中……");
                boolean success = load(p, defaultNature, map, customNatureCollector);
                if (!success) logger.warning("失败:" + p);
            }
            if (map.size() == 0)
            {
                logger.warning("没有加载到任何词条");
                map.put(Predefine.TAG_OTHER, null);     // 当作空白占位符
            }
            logger.info("正在构建DoubleArrayTrie……");
            dat.build(map);
            if (isCache)
            {
                // 缓存成dat文件,下次加载会快很多
                logger.info("正在缓存词典为dat文件……");
                // 缓存值文件
                List<CoreDictionary.Attribute> attributeList = new LinkedList<CoreDictionary.Attribute>();
                for (Map.Entry<String, CoreDictionary.Attribute> entry : map.entrySet())
                {
                    attributeList.add(entry.getValue());
                }
                DataOutputStream out = new DataOutputStream(new BufferedOutputStream(IOUtil.newOutputStream(mainPath + Predefine.BIN_EXT)));
                // 缓存用户词性
                if (customNatureCollector.isEmpty()) // 热更新
                {
                    for (int i = Nature.begin.ordinal() + 1; i < Nature.values().length; ++i)
                    {
                        customNatureCollector.add(Nature.values()[i]);
                    }
                }
                IOUtil.writeCustomNature(out, customNatureCollector);
                // 缓存正文
                out.writeInt(attributeList.size());
                for (CoreDictionary.Attribute attribute : attributeList)
                {
                    attribute.save(out);
                }
                dat.save(out);
                out.close();
            }
        }
        catch (FileNotFoundException e)
        {
            logger.severe("自定义词典" + mainPath + "不存在!" + e);
            return false;
        }
        catch (IOException e)
        {
            logger.severe("自定义词典" + mainPath + "读取错误!" + e);
            return false;
        }
        catch (Exception e)
        {
            logger.warning("自定义词典" + mainPath + "缓存失败!\n" + TextUtility.exceptionToString(e));
        }
        return true;
    }

相关文章

  • 对接Hanlp的最佳实践(仅为本公司的对接示例)

    一、Hanlp的背景介绍 github地址: https://github.com/hankcs/HanLP[ht...

  • 恒指期货招商 新华期货代理

    为什么要选择我们? 1、正规平台对接香港期货公司。本公司所有下单对接到香港期货公司,可查询每笔交易电子交割单。 2...

  • 2018-05-23 阿里健康远程会诊熙牛淘宝技术对接

    2018/5/23 阿里健康远程会诊熙牛淘宝技术对接 1、点对点视频对接实现,本地测试成功。后面开发可以参考示例...

  • Guava Cache最佳实践

    项目中经常使用Guava Cache,根据经验总结了一些最佳实践。 示例代码 快速有效的使用示例如下: 最佳实践 ...

  • Conda安装Mgltools

    MGLTools 是Autodock对接程序配套使用的工具. 内含大量对接前处理, 对接结果分析, 对接可视化工具...

  • 前后端接口对接规范(主要前端内容).md

    restful最佳实践--接口规范 为了前后端分工明确,对接流畅,确保可读性和扩展性以及高可用、一致性,特约定下述...

  • 分子对接(二)对接

    1蛋白的pdbqt准备 包括加氢、计算电荷、添加原子类型打开AutoDockTools,菜单栏File->Read...

  • 对接

    对接 coordinate A with B integrate A with B build synergy b...

  • 对接

    对接对于我们每一个人成人来说都不陌生,它可以是物体与物体之间的对接,也可以是事与事之间的对接。当然,在对接当中离不...

  • 对接

    五点半妈妈喊我起床的时候,我甚是疑惑:“起床?”已经到起床的时候了?我睡了吗?我有睡着嘛?(°u°) 」感觉自己好...

网友评论

      本文标题:对接Hanlp的最佳实践(仅为本公司的对接示例)

      本文链接:https://www.haomeiwen.com/subject/bovigrtx.html