本文源自道招网的# 自定义分词起始规则实现关键词全词高亮项目实战(全语种通吃)
背景
最近有BU给我们这边提了一个需求,希望我们能改进现有的内容关键词匹配功能,希望能支持英文的全词匹配。
目前前端页面是会对后台配置的关键词进行高亮显示的,只不过算是模糊匹配了,也就是说如果关键词配的是book
,内容中的booked
中的book
也会高亮,而这并不是BU希望的。
现状
我看了下原来高亮功能的具体实现
export function escapeHtml(text) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, function(m) {
return map[m];
});
}
// 搜索html关键字并高亮
export function htmlKeyWordHighlight(parentNode, keyWards, color = 'yellow') {
if (keyWards === void 0 || !parentNode) return;
for (var i = 0; i < parentNode.childNodes.length; i++) {
var child = parentNode.childNodes[i];
if (child.nodeType == 3 && child.data.indexOf(keyWards) != -1) {
var newChild = document.createElement('span');
var tagStripper = new RegExp(keyWards, 'g');
newChild.innerHTML = escapeHtml(child.data).replace(
tagStripper,
`<span style="background: ${color};">` + keyWards + '</span>',
);
parentNode.replaceChild(newChild, child);
} else {
htmlKeyWordHighlight(child, keyWards, color);
}
}
}
打出这keyWards的我猜测用的编辑器多半是vscode或者是个心态特别好的老哥,但凡是idea系列的那波浪线就容易让人有强迫症。
用法大致就是这样htmlKeyWordHighlight(document.body, "book","#FFB10A")
,这样就会把body
上所有包含book
的字符串高亮起来了。
方法里面执行的是字符串的replace操作,以book
的替换为例,实际执行的是"A guest who booked xxx".replace(/book/g, "***")
操作,此时是不会顾及是否是全词匹配的,只要匹配上都会替换的。
常规解决方案——正则表达式\b
既然之前用的是正则表达式,我们优先考虑能不能优化下正则表达式来完成需求。
如果只是想简单的应对英文的话,我们用上正则表达式的元字符\b
就行,它代表着单词的开头或结尾,也就是单词的分界处。 更精确的说法,\b
匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在) \w
,我们可以简单的理解\b
等识别出一个分词的开始和结束。
很符合英文单词全词匹配,可以测试一下。 "A guest who booked xxx".replace(/\bbook\b/g, "***")
。
带不带\b
效果很明显,客户提的需求也就算满足了。
终极解决方案——逐词匹配
如果想对中文等非英文语种进行类似的分词用\b
就不行了,我们也没法更换\b
的识别机制。
我们试着自己实现下,对潜在的目标文本进行逐词匹配就行。
记得首先要确定潜在目标文本,缩小逐词匹配的范围。
export function htmlKeyWordHighlight(parentNode, keyword, color = 'yellow') {
if (keyword === void 0 || !parentNode) {
return;
}
for (let i = 0; i < parentNode.childNodes.length; i++) {
let child = parentNode.childNodes[i];
if (child.nodeType === 3 && child.data.indexOf(keyword) !== -1) {
let newChild = document.createElement('span');
newChild.innerHTML = keyWordPreciseReplacer(escapeHtml(child.data), keyword,
`<span style="background: ${color};">` + keyword + '</span>'
);
parentNode.replaceChild(newChild, child);
} else {
htmlKeyWordHighlight(child, keyword, color);
}
}
}
/**
* 根据分词规则精准替换关键词
* @param keyword
* @param target
* @param replaceText
* @returns {*}
*/
function keyWordPreciseReplacer(keyword, target, replaceText) {
function isOver(str) {
// 根据常用分词标点符号和空格进行分词
const regStr = '[。!!??,,\.\s()()]';
return new RegExp(regStr).test(str);
}
let index = 0;
let targetIndex = 0;
const result = [];
const text = keyword + ' '; // 结尾添加一个空格方便isOver判断
for (let i = 0; i < text.length; i++) {
const str = text[i];
if (isOver(str)) { // text新的分词开始
if (targetIndex === target.length) { // target也刚好全匹配
result.push([index, i - target.length]);
index = i;
}
targetIndex = 0; // 重新计数
} else if (str === target[targetIndex]) {
targetIndex++; // 继续匹配
} else {
targetIndex = -1; // 本轮分词已没戏,等待下轮分词
}
}
result.push([index])
return result.reduce((acc, curr) => acc + keyword.slice(...curr) + (curr.length > 1 ? replaceText : ''), '');
}
注释写的算比较详细了。
isOver
里面判断的分词的依据可能会有遗漏,后续可能动态调整,建议写到配置文件里面。
现在可没有用\b
了哦。
现在我们搞个中文测试下,我们把关键词设置为我
和后续
,把分词的正则表达式里面加入司会
,即const regStr = '[。!!??,,\.\s()()司会]';
。
预期的效果是:我们
里面的我
不需要高亮,而我司
的我
需要高亮,同时后续
也需要高亮,因为司
和会
代表分词结束。
高亮结果符合预期,后期无非是遗漏了分词符号(比如、——),需要改下配置来调整正则即可。
总结
如果场景较为单一,仅需要支持英文的话,直接用\b
即可,如果需要特别卷的话,那就用逐词匹配吧。