自定義分詞起始規則實現關鍵詞全詞高亮專案實戰(全語種通吃)

語言: CN / TW / HK

本站內容均來自興趣收集,如不慎侵害的您的相關權益,請留言告知,我們將盡快刪除.謝謝.

背景

最近有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>` + 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>` + 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 即可,如果需要特別卷的話,那就用逐詞匹配吧。