給出一段文本,自動識別出文本中包含的關鍵字信息,關鍵字是庫裏已知的數據,根據類型的不一樣顯示出不一樣的顏色css
1)採用css:文本識別出來後,根據識別出的文本更改對應文本的dom,經過更改css來實現
缺點:比較麻煩,只能標註顏色,不易於維護和拓展
2)採用svg:使用svg繪製文本,繪製顏色標註
優勢:比較靈活,便於後續拓展vue
想象很美好,實現很骨感,代碼實現的過程當中遇到了很多問題,這裏記錄下解決方法。本文主要粘貼核心代碼,不是所有的業務代碼哦~數組
svg是不支持換行顯示的
設計思路:須要自動識別字典文本,使用到文本的下標,根據下標位置來進行保存和標註。所以將每一個文本都單獨設置成一個tspan
,因爲識別的文字包含漢字,英文字母,符號等,因此相應的文字給予對應的長度。獲取svg的最大顯示寬度,當文本的寬度>svg寬度的時候實行自動換行。dom
1.1 文本寬度的設定
不一樣的文本的寬度不同,漢字、符號還好能夠給個統一的設置,可是英文字母,有的寬,有的窄,若是設置成同樣的,顯示會很怪,這裏通過測試,獲取了一個正常顯示的範圍值。(本文項目代碼基於vue.js)
定義常量以下:
svg
// 獲取文本的寬度 getTxtWidth(text) { let smallEnglishRegx = /^[a-z]+$/; // 小寫 let bigEnglishRegx = /^[A-Z]+$/; // 大寫 let numberRegx = /^[0-9]$/; // 數字 let chinaRegx = /[\u4E00-\u9FA5\uF900-\uFA2D]/; // 中文 let unitWidth; if (chinaRegx.test(text)) { unitWidth = this.chWidth; } else if (/\s/.test(text)) { unitWidth = this.spaceWidth; } else if (smallEnglishRegx.test(text)) { unitWidth = this.enWidth; } else if (bigEnglishRegx.test(text)) { unitWidth = this.bigEnWidth; } else if (numberRegx.test(text)) { unitWidth = this.numWidth; } else { unitWidth = this.charWidth; } // 特殊文本的特殊處理 if (this.smallerLetter1.includes(text)) { unitWidth = unitWidth - 1; } if (this.smallerLetter2.includes(text)) { unitWidth = unitWidth - 2; } if (this.smallerLetter4.includes(text)) { unitWidth = unitWidth - 4; } if (this.bigLetter1.includes(text)) { unitWidth = unitWidth + 1; } if (this.bigLetter2.includes(text)) { unitWidth = unitWidth + 2; } if (this.bigLetter4.includes(text)) { unitWidth = unitWidth + 4; } return unitWidth; }
1.2 文本的拆分
默認的換行,須要顯示。超出svg區域的,手動換行。具體實現看代碼,這裏使用的是svg.js庫用於繪製svg圖形。測試
chunkWords() { this.dataChunk = []; let text = this.words; // 按換行符號換行 text = text.replace("↵↵", "\n"); text = text.replace("↵", "\n"); let sentenceArr = text.split("\n"); for (let i = 0, len = sentenceArr.length; i < len; i++) { // 先按空格分開 let wordsArr = sentenceArr[i].split(" "); // 再把每一個字都分了 let wordsArrCopy = []; for (let j = 0, len2 = wordsArr.length; j < len2; j++) { // 判斷是否包含中文,若是包含中文再繼續拆分 let unit = wordsArr[j]; for (let k = 0, len3 = unit.length; k < len3; k++) { let firstword = unit.slice(k, k + 1); // 插入 wordsArrCopy.push(firstword); } // 空格也要加上 wordsArrCopy.push(" "); } sentenceArr[i] = wordsArrCopy; } // 再加上換行符,用於後面的換行,SVG文本不支持本身換行 for (let i = 0, len = sentenceArr.length; i < len; i++) { let item = sentenceArr[i]; let length = item.length; // 判斷最後一個是否是有字 let lastWord = item[length - 1].trim(); // 有字則新增個 if (lastWord) { item[length] = "↵↵"; } else { item[length - 1] = "↵↵"; } } // 對每行再進行拆分,若是大於svg的寬度後再進行換行 // this.dataChunk = [[],[]] this.chunkIndex = 0; for (let i = 0, len = sentenceArr.length; i < len; i++) { // 獲取當前this.dataChunk[index]的總長度,大於等於this.svgX+currentWidth的時候加行; // 每一個span是一個對象,包含一些字段信息。一個對象對應一個tspan let sentence = sentenceArr[i]; // 須要換行的狀況 if (this.chunkIndex > 0 && this.dataChunk[this.chunkIndex].length) { this.chunkIndex++; } for (let j = 0, len2 = sentence.length; j < len2; j++) { let unit2 = sentence[j]; let unitWidth = this.getTxtWidth(unit2); this.insertDataChunk(unit2, unitWidth); } } this.drawText(); }, // 根據分片繪製文本 drawText() { this.textGroup.clear(); let that = this; for (let i = 0, len = this.dataChunk.length; i < len; i++) { let item = this.dataChunk[i]; this.textGroup .text(function(add) { for (let j = 0, len2 = item.length; j < len2; j++) { let unit = item[j]; if (i === 0) { item[j].offset = j; item.allOffset = j; } else { let pos = that.dataChunk[i - 1].allOffset + j + 1; item[j].offset = pos; item.allOffset = pos; } item[j].row = i; item[j].index = j; // 記錄下來 that.textDom[item[j].offset] = add .tspan(unit.text) .attr("x", unit.dx) .attr("y", unit.dy) .data("offset", item[j].offset); } }) .data("row", i); } }
實現出來的效果:
字體
這樣,每一個文字都被拆成一個tspan
幷包含對應的data-offset
屬性了。this
庫裏的數據分不一樣的類型,以數組形式顯示,相似這樣:
spa
根據給的文本,若是包括了數組中的數據,則高亮顯示:設計
// check語句,將句子中已有的實體/關係/操做/屬性識別出來 checkWord() { // 對換行符號進行相同的處理 let words = this.words; words = words.replace("↵↵", "\n"); words = words.replace("↵", "\n"); words = words.split("\n"); this.words = words.join(""); // 識別關係 for (let i = 0, len = this.relationArr.length; i < len; i++) { this.setKnownData(this.relationArr[i], "relation"); } // 識別操做 for (let i = 0, len = this.operateArr.length; i < len; i++) { this.setKnownData(this.operateArr[i], "operate"); } // 識別實體 for (let i = 0, len = this.objectArr.length; i < len; i++) { this.setKnownData(this.objectArr[i], "object"); } // 識別屬性 for (let i = 0, len = this.attrArr.length; i < len; i++) { this.setKnownData(this.attrArr[i], "attr"); } // 根據獲取的數據來渲染高亮片斷 ... }, // 設置已知數據,獲取的數據放到this.result中 setKnownData(item, type, pwords, pindex) { let words = pwords ? pwords : this.words; let index = words.indexOf(item); let stringLen = item.length; pindex = pindex ? pindex : 0; if (index > -1) { // 構造標註須要的數據 let data = { type, word: item, name: item, offset: [index + pindex, index + pindex + stringLen - 1], id: Math.ceil(new Date().getTime() * Math.random() * (index + 1)) }; // 添加到數據中,根據位置信息來判斷 if (this.result[type].length === 0) { this.result[type].push(data); } else { let insertIndex = -1; this.result[type].find((unit, index) => { if (data.offset[0] <= unit.offset[1]) { insertIndex = index; return true; } }); if (insertIndex > -1) { this.result[type].splice(insertIndex, 0, data); } else { this.result[type].push(data); } } // 繼續遍歷,可能會包含多個 let word2 = words.substr(index + stringLen); this.setKnownData(item, type, word2, pindex + index + stringLen); } }
svg.js繪製矩形的方法很簡單,須要肯定的是繪製的矩形的寬高,位置便可,而這些信息根據字符的offset
就能夠算出來。在上面的數據中,咱們在result
中存了一些識別出來的數據。根據這些數據便可繪製不一樣顏色的矩形來了。
... for (let i in this.result) { this.result[i].forEach(item => { this.sureMarkWord(item); }); }
// 肯定標註數據,高亮文本,標註實體 sureMarkWord(data) { // 根據座標獲取字的信息 let start = this.findWord(data.offset[0]); let end = this.findWord(data.offset[1]); if (!start || !end) { return; } let startRow = start.row; let endRow = end.row; let startIndex = start.index; let endIndex = end.index; // 同一行 if (startRow == endRow) { this.singleRowMark(start, end, data, endRow); } else { //1,endRow從起始開始標註 let start_endrow = this.dataChunk[endRow][0]; this.singleRowMark(start_endrow, end, data, endRow); // endRow前面的行所有標註上 for (let i = startRow; i < endRow; i++) { let len = this.dataChunk[i].length; let end_i = this.dataChunk[i][len - 1]; if (i === startRow) { this.singleRowMark(start, end_i, data, startRow, true); } else { // 整行標註 this.singleRowMark( this.dataChunk[i][0], end_i, data, i ); } } } }, // 根據位置選擇文字 findWord(offset) { let result = null; for (let i = 0, len = this.dataChunk.length; i < len; i++) { let item = this.dataChunk[i]; for (let j = 0, len2 = item.length; j < len2; j++) { let unit = item[j]; if (unit.offset === offset) { result = unit; break; } } if (result) { break; } } return result; }, singleRowMark(start, end, data, row) { // 回調繪製chunk的矩形 let width = end.dx + end.width - start.dx; let x = start.dx; let y = start.dy - this.wordHeight + 4; let height = this.wordHeight; // wordHeight是文本的高度,根據字體的大小設置,14px的定義爲17 let { name, type, word, id} = data; // 數據記錄 let obj = { width, height, x, y, type, word, name, id, row, ry: y }; this.drawMarkGroups(obj); }, // 文字底層顏色 drawChunkRect(obj) { let { width, height, x, y, type, id, row, word} = obj; let color; color = this.wordColors[type]; // 根據類型的不一樣設置不一樣的顏色 let obj = {}; // 記錄dom obj.rect = this.rectRows[row] .group() .rect(width, height) .move(x, y) .fill(isTemp ? "none" : color) .attr("id", id) .data("type", obj.type) .data("word", obj.word); this.wordRectDom[id].push(obj); }
至此,實現了劃詞標註的顯示部分