对接Hanlp的最佳实践(仅为本公司的对接示例)
一、Hanlp的背景介绍
- github地址: https://github.com/hankcs/HanLP
- 官网:https://www.hanlp.com/
- 作用:分词。这里举个例子,“傻逼”,从汉字分词的角度,是会分割为“傻”“逼”两个字,而不会是一个词。这也使得我们需要自定义词典,后文将着重讲我们公司是怎么使用的。
二、目标
- 高性能是本系统设计的一个关键目标,所以不能每次检测的时候,都读取数据库(无论是mysql还是redis)。
- 业务上,要求敏感词的动态调整,及时性是一个基本要求。自定义的敏感词和hanlp分词的自定义词典,做到实时同步。
- 不用重启应用。添加或删除了敏感词,都是更改txt文件的内容,然而在hanlp中无法生效,只有重启应用。所以,我们需要调用hanlp的api接口,做到动态地增删.txt.bin内容,做到无需重启应用。
- 敏感词的操作,如果是在业务高峰期,触发jvm内存所带来的首次慢的问题。可以采用临时内存区间,对接数据库中的敏感词库,待加载完成后,然后让原内存失效,启用新内存。
三、敏感词服务的设计思路

- 应用启动的时候,将自定义的敏感词词库,加载到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;

- 兜底操作,会读取环境变量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;
}