总篇107篇 2020年第31篇
背景
文章板块是汽车之家海外站(yesauto.com)的重要组成部分,在产生自发流量和整站SEO方面作用明显。为方便读者,提升阅读体验,同时让汽车内容与汽车销售产生更直接的关联,即提升留资转化率,需要更有效的方式将文章内容与经销商库存直接关联起来。
因为汽车评测文章内容中包含很多品牌、车系等信息,直接把品牌、车系变成热点,配置相关超链接,这样用户点击时,能直接跳转到该品牌的库存列表页面。将直接产生导流效果,也符合用户的期待。
显然,随着网站输出的文章内容越来越多,如果编辑采用手动方式的把文章内容与品牌车系部分替换成对应相关的链接,会花费很多不必要的时间开销。为了让编辑更专注在提升内容的品质上,最终采用软件方式,笔者开发出扫描文章动态增加超链接功能。
匹配的内容是到车系级别,词库的数据是通过经销商库存接口返回的数据来构建词库。英文词库大概在330+个单词词组14000+个字符,德文词库大概是280+单词词组6500+个字符。英国文章平均字符数大概5000+个字符,德国文章平均字符数也大概5000+个字符。同时,需要解决以下两方面的技术问题:
一是最好不改变原文内容,主要原因是创作者可能会编辑修改,加入链接后会让创作者感到意外;
二是库存列表本身也是动态的,可能今天某个车系、车型有库存,则加链接是合适的,但明天没有了,就不应该加链接,换句话说,链接能根据库存情况,在用户阅读文章那一刻动态添加。
基于以上的考虑,简单地循环关键词,遍历文章替换的方案在时间复杂度显然不可行。本文介绍的算法能在时间复杂度上很好地满足以上两点需求。采用开源的HanLP系统进行构建词库和匹配内容。
HanLP简介
Aho-Corasick算法
Trie树
Trie树也称前缀树。即将所有模式串构建为一颗字典树,同时将终止状态绑定外部value。Trie树利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。Trie树的基本实现有两种,array和linked-list(都以bachelor、baby、badge、jar举例)。
Double-ArrayTrie
![33509d325039edd3fe181c2443a72e95.png](https://i-blog.csdnimg.cn/blog_migrate/83bda3d1879fbc7f16892d489a03337d.png)
tail of string [b1..bh] has no common prefix and the corresponding state is m: base[m] < 0; p = -base[m], tail[p] = b1, tail[p+1] = b2, ..., tail[p+h-1] = bh;
那么,用DAT检索词badge的过程如下:
// root -> b base[1] + 'b' = 4 + 3 = 7 // root -> b -> a base[7] + 'a' = 1 + 2 = 3 // root -> b -> a -> d base[3] + 'd' = 1 + 5 = 6 // badge# base[6] = -12 tail[12..14] = 'ge#' 应用
主要流程如下:
<dependency> <groupId>com.hankcsgroupId> <artifactId>hanlpartifactId> <version>portable-1.7.7version> dependency>
构建词库
业务需求方面,主要是要根据文章内容中品牌或者车系的字符来进行匹配,并且生成的指向经销商的库存页面的超链接。例如:Land Rover跳转到https://example.com/land-rover,所以构建的词库,会存储品牌、品牌-车系的两种数据结构进行存储。在HanLP中,DoubleArrayTrie是有序的TreeMap构建,key存储词库,value就用来存储业务要求跳转的后缀。返回的业务数据是可预见的,所以相应的做了转换。
/** * 构建词库 */ TreeMap<String, String> treeThesaurus = new TreeMap<String, String>(); resultMap.getResult().get("params").forEach(o -> { if ("makeList".equals(o.get("paramname"))) { o.get("options").forEach(d-> { //构建品牌 String sourceBrandValue = d.get("value").toString().trim(); String brandValue = replaceStockWord(sourceBrandValue.replace(" ","-")); treeThesaurus.put(sourceBrandValue.toUpperCase(), brandValue); List<Map> options = (List<Map>) d.get("options"); if (!CollectionUtils.isEmpty(options)) { options.forEach(v-> { //构建品牌--》车系 String svalue = v.get("value").toString().trim(); String value = brandValue+"/"+replaceStockWord(svalue.replace(" ","-")); treeThesaurus.put((sourceBrandValue+" "+svalue).toUpperCase(), value.trim()); } }); } }); } });
匹配文章
String cleanArticle = Jsoup.clean(content, Whitelist.none());
剔除完之后的字符串和用构建好的词库再去调用了HanLP的parseText方法。
parseText需要实现一个命中字符串后处理的接口。
public interface IHit<V> { /** * @param begin 模式串在母文本中的起始位置 * @param end 模式串在母文本中的终止位置 * @param value 模式串对应的值 */ void hit(int begin, int end, V value); }
这里需要记录命中的位置,去做相同位置字符串的最长匹配。
这里我采用了另外个TreeMap来进行剔除。
并且使用命中的最起始字符的下表来做key,value使用构建BuildThesaurus实体,主要是用来存储命中begin、end和词库拼接好的value。
为后面进行替换的这步操作路标记好起止位置和需替换字符:
BuildThesaurus实体结构,begin存匹配起始字符下表、end存匹配字符结束下表、sourceStr存储字典库的value:
public class BuildThesaurus { private Integer begin; private Integer end; private String sourceStr; //get set..... }
实现IHIT接口,找到起始位置相同且长度最长的字符串:
BuildThesaurusConfiguration.getAhoCorasickDoubleArrayTrie(region).parseText(s.toUpperCase(), new AhoCorasickDoubleArrayTrie.IHit() { @Override public void hit(int begin, int end, String value) { BuildThesaurus bt = new BuildThesaurus(begin, end, value); if (null != buildThesaurusMap.putIfAbsent(begin, bt)) { if (bt.getSourceStr().length() > buildThesaurusMap.get(begin).getSourceStr().length()) { buildThesaurusMap.put(begin, bt); } } } });
build
ThesaurusMap存了相同最长匹配的字符串和此字符串,在除去html标签下的字符串的首位下标,为下一步原文替换做准备。
判断单词
private static boolean judgingWord (int begin, int end, int wordsLength, String ownText) { //判断是否为是句子第一个单词 if (begin == 0 && end != wordsLength) { .... return true; } //判断是否为是句子结尾单词 if (end == wordsLength && begin != 0) { .... return true; } //略 }
替换
判断句中有可以替换的单词之后,开始按照之前匹配 最后把超链接拼接字符串。这里注意需要记录替换前后字符变化长度的差值。每次替换之后,和最开始进行匹配存储的字符串字符下标会对应不上,所以得全局记录替换前后字符串差值。在下次循环替换的时候,才能找到真正需要替换的字符起始位置。
总结
本文主要贴合工作中遇到的业务需求,使用的HanLP在海外业务中的一些实际应用。目前海外业务主要是以英德两文为主,所以HanLP另外的一些强大的功能例如:分词、命名实体识别、篇章理解、依存句法分析等等应用不上。所以只是介绍目前在使用的功能,如有更好的解决方案或者建议的部分,欢迎指出。
参考文献
《Aho–Corasick算法》-----维基百科
http://www.hankcs.com/program/algorithm/aho-corasick-double-array-trie.html
https://www.cnblogs.com/en-heng/p/6265256.html