中文分词在大数据横行的今天是愈来愈有用武之地了。它不只被普遍用于专业的中文搜索引擎中,并且在关键词屏蔽、黑白名单以及文本类似度等方面也能大显身手。中文分词最简单也最经常使用的方式是基于字典查找的方式,经过遍历待分词字符串并在字典中进行查找匹配以达到分词的目的。本文便是采用这种方式。javascript
在本文中,彻底依赖于字典,所以须要准备好字典。通常面对不一样的领域用不一样的字典。好比面向医学的,则字典会添加许多医学术语方面的词。能够很容易的找到经常使用词的字典,好比搜狗输入法自带的字典等。html
中止词不能用于成词。中止词主要包括无心义的字符(如的、地、得)或词。
java
本文因为只是简单的介绍和实现,因此定义好了简单的字典和中止词,以下代码所示:node
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>简单的中文分词</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="简单的中文分词" /> <meta name="description" content="简单的中文分词" /> </head> <body> <script type="text/javascript"> // 字典 var dict = { "家乡" : 1, "松花" : 1, "松花江" : 1, "那里" : 1, "四季" : 1, "四季迷人" : 1, "迷人" : 1, "花香" : 1 }; // 中止词 var stop = { "的" : 1 }; // 待分词的字符串 var words = "个人家乡在松花江边上,那里有四季迷人的花香。"; </script> </body> </html>
dict和stop之因此定义为Object,是由于这样可令查找的时间复杂度为常值O(1)。分词的过程有点像正则表达式的惰性匹配。先从words中读取第一个字符"我"并在dict中和stop中查找,若是是中止词,则丢掉已读取的,而后读取第二个字"的"。若是在dict中,则添加到结果集,而后继续读到下一个,再一样去stop和dict中查找,直处处理完成。代码以下:正则表达式
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>简单的中文分词</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="简单的中文分词" /> <meta name="description" content="简单的中文分词" /> </head> <body> <script type="text/javascript"> // 字典 var dict = { "家乡" : 1, "松花" : 1, "松花江" : 1, "那里" : 1, "四季" : 1, "四季迷人" : 1, "迷人" : 1, "花香" : 1 }; // 中止词 var stop = { "的" : 1 }; // 待分词的字符串 var words = "个人家乡在松花江边上,那里有四季迷人的花香。"; function splitWords(words) { var start = 0, end = words.length - 1, result = []; while (start != end) { var str = []; for (var i = start; i <= end; i++) { var s = words.substring(i, i + 1); // 若是是中止词,则跳过 if (s in stop) { break; } str.push(s); // 若是在字典中,则添加到分词结果集 if (str.join('') in dict) { result.push(str.join('')); } } start++; } return result; } console.group("Base 分词: "); console.log("待分词的字符串: ", words); console.log("分词结果: ", splitWords(words)); console.groupEnd(); </script> </body> </html>
可是想一下,在实际应用中,字典可能包含了足够多的词,并且字典中不少词是有共同前缀的。好比上述代码中的"松花"和"松花江"就有共同的前缀"松花",存储重复的前缀将致使字典占用大量的内存,而这部分实际上是能够优化的。还记得我以前的一篇介绍Trie树的文章吗?若是您忘了,那请看:Python: Trie树实现字典排序 。事实上仍是有不一样之处的,由于以前只是针对26个字母的Trie树。对于须要支持中文的Trie树来讲,若是直接用一个字符(这个字符多是ASCII码字符,也多是中文字符或其它多字节字符)来表示一个节点,则是不可取的。你们知道最经常使用的汉字有将近一万个,若是每个节点都要用一个数组来保存将近一万个子节点,那就太吓人了。因此我这里选择Object的方式来保存,这样的好处是查找时间复杂度为O(1)。但即便这样,这个Object还将容纳将近一万个key,因此我这里将结合另一种方案来实现。数组
JavaScript的内码是Unicode,它用1~2个字节来存储。若是咱们将一个双字节转成UTF8的三个字节(嗯,是的。本文只考虑UTF8的单字节和三字节,由于双字节、四字节、五字节和六字节太少见了),单字节仍是不变,以第一个字节为起始节点,那么节点的子节点数就变成了固定的256个,而后咱们经过起始字节的大小能够知道这是一个单字节或三字节。这种方式有效的节约了内存。接下来是实现代码:大数据
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>简单的中文分词</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="简单的中文分词" /> <meta name="description" content="简单的中文分词" /> </head> <body> <script type="text/javascript"> // 字典 var dict = [ "家乡", "松花", "松花江", "那里", "四季", "四季迷人", "迷人", "花香", "hello", "kitty", "fine" ]; // 中止词 var stop = { "的" : 1 }; // 待分词的字符串 var words = "hello, kitty!个人家乡在松花江边上,那里有四季迷人的花香。fine~"; // Trie树 function Trie() { this.root = new Node(null); } Trie.prototype = { /** * 将Unicode转成UTF8的三字节 */ toBytes : function(word) { var result = []; for (var i = 0; i < word.length; i++) { var code = word.charCodeAt(i); // 单字节 if (code < 0x80) { result.push(code); } else { // 三字节 result = result.concat(this.toUTF8(code)); } } return result; }, toUTF8 : function(c) { // 1110xxxx 10xxxxxx 10xxxxxx // 1110xxxx var byte1 = 0xE0 | ((c >> 12) & 0x0F); // 10xxxxxx var byte2 = 0x80 | ((c >> 6) & 0x3F); // 10xxxxxx var byte3 = 0x80 | (c & 0x3F); return [byte1, byte2, byte3]; }, toUTF16 : function(b1, b2, b3) { // 1110xxxx 10xxxxxx 10xxxxxx var byte1 = (b1 << 4) | ((b2 >> 2) & 0x0F); var byte2 = ((b2 & 0x03) << 6) | (b3 & 0x3F); var utf16 = ((byte1 & 0x00FF) << 8) | byte2 return utf16; }, /** * 添加每一个词到Trie树 */ add : function(word) { var node = this.root, bytes = this.toBytes(word), len = bytes.length; for (var i = 0; i < len; i++) { var c = bytes[i]; // 若是不存在则添加,不然不须要再保存了,由于共用前缀 if (!(c in node.childs)) { node.childs[c] = new Node(c); } node = node.childs[c]; } node.asWord(); // 成词边界 }, /** * 按字节在Trie树中搜索 */ search : function(bytes) { var node = this.root, len = bytes.length, result = []; var word = [], j = 0; for (var i = 0; i < len; i++) { var c = bytes[i], childs = node.childs; if (!(c in childs)) { return result; } if (c < 0x80) { word.push(String.fromCharCode(c)); } else { j++; if (j % 3 == 0) { var b1 = bytes[i - 2]; var b2 = bytes[i - 1]; var b3 = c; word.push(String.fromCharCode(this.toUTF16(b1, b2, b3))); } } // 若是是中止词,则退出 if (word.join('') in stop) { return result; } // 成词 var cnode = childs[c]; if (cnode.isWord()) { result.push(word.join('')); } node = cnode; } return result; }, /** * 分词 */ splitWords : function(words) { // 转换成单字节进行搜索 var bytes = this.toBytes(words); var start = 0, end = bytes.length - 1, result = []; while (start != end) { var word = []; for (var i = start; i <= end; i++) { var b = bytes[i]; // 逐个取出字节 word.push(b); var finds = this.search(word); if (finds !== false && finds.length > 0) { // 若是在字典中,则添加到分词结果集 result = result.concat(finds); break; } } start++; } return result; }, /** * 词始化整棵Trie树 */ init : function(dict) { for (var i = 0; i < dict.length; i++) { this.add(dict[i]); } } }; // 节点 function Node(_byte) { this.childs = {}; // 子节点集合 this._byte = _byte || null; // 此节点上存储的字节 this._isWord = false; // 边界保存,表示是否能够组成一个词 } Node.prototype = { isWord : function() { return this._isWord; }, asWord : function() { this._isWord = true; } }; var trie = new Trie(); trie.init(dict); var result = trie.splitWords(words); console.group("Trie 分词: "); console.log("待分词的字符串: ", words); console.log("分词结果: ", result); console.groupEnd(); </script> </body> </html>
各位看了输出结果后就会发现,这个分词是有问题的,由于明显少了"松花江"和"四季迷人"。拿"四季"和"四季迷人"来讲,"四季"是"四季迷人"的前缀,在经过trie.isWrod()方法来判断是否成词时,一遇到"四季"就成功了,因此"四季迷人"没有机会获得判断,因此咱们须要修改代码,在Node上加一个属性,表示已判断的次数。代码以下:优化
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>简单的中文分词</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="简单的中文分词" /> <meta name="description" content="简单的中文分词" /> </head> <body> <script type="text/javascript"> // 字典 var dict = [ "家乡", "松花", "松花江", "那里", "四季", "四季迷人", "迷人", "花香", "hello", "kitty", "fine" ]; // 中止词 var stop = { "的" : 1 }; // 待分词的字符串 var words = "hello, kitty!个人家乡在松花江边上,那里有四季迷人的花香。fine~"; // Trie树 function Trie() { this.root = new Node(null); } Trie.prototype = { /** * 将Unicode转成UTF8的三字节 */ toBytes : function(word) { var result = []; for (var i = 0; i < word.length; i++) { var code = word.charCodeAt(i); // 单字节 if (code < 0x80) { result.push(code); } else { // 三字节 result = result.concat(this.toUTF8(code)); } } return result; }, toUTF8 : function(c) { // 1110xxxx 10xxxxxx 10xxxxxx // 1110xxxx var byte1 = 0xE0 | ((c >> 12) & 0x0F); // 10xxxxxx var byte2 = 0x80 | ((c >> 6) & 0x3F); // 10xxxxxx var byte3 = 0x80 | (c & 0x3F); return [byte1, byte2, byte3]; }, toUTF16 : function(b1, b2, b3) { // 1110xxxx 10xxxxxx 10xxxxxx var byte1 = (b1 << 4) | ((b2 >> 2) & 0x0F); var byte2 = ((b2 & 0x03) << 6) | (b3 & 0x3F); var utf16 = ((byte1 & 0x00FF) << 8) | byte2 return utf16; }, /** * 添加每一个词到Trie树 */ add : function(word) { var node = this.root, bytes = this.toBytes(word), len = bytes.length; for (var i = 0; i < len; i++) { var c = bytes[i]; // 若是不存在则添加,不然不须要再保存了,由于共用前缀 if (!(c in node.childs)) { node.childs[c] = new Node(c); } node = node.childs[c]; } node.asWord(); // 成词边界 }, /** * 按字节在Trie树中搜索 */ search : function(bytes) { var node = this.root, len = bytes.length, result = []; var word = [], j = 0; for (var i = 0; i < len; i++) { var c = bytes[i], childs = node.childs; if (!(c in childs)) { return result; } if (c < 0x80) { word.push(String.fromCharCode(c)); } else { j++; if (j % 3 == 0) { var b1 = bytes[i - 2]; var b2 = bytes[i - 1]; var b3 = c; word.push(String.fromCharCode(this.toUTF16(b1, b2, b3))); } } // 若是是中止词,则退出 if (word.join('') in stop) { return result; } // 成词 var cnode = childs[c]; if (cnode.isWord()) { cnode.addCount(); // 用于计数判断 result.push(word.join('')); } node = cnode; } return result; }, /** * 分词 */ splitWords : function(words) { // 转换成单字节进行搜索 var bytes = this.toBytes(words); var start = 0, end = bytes.length - 1, result = []; while (start != end) { var word = []; for (var i = start; i <= end; i++) { var b = bytes[i]; // 逐个取出字节 word.push(b); var finds = this.search(word); if (finds !== false && finds.length > 0) { // 若是在字典中,则添加到分词结果集 result = result.concat(finds); } } start++; } return result; }, /** * 词始化整棵Trie树 */ init : function(dict) { for (var i = 0; i < dict.length; i++) { this.add(dict[i]); } } }; // 节点 function Node(_byte) { this.childs = {}; // 子节点集合 this._byte = _byte || null; // 此节点上存储的字节 this._isWord = false; // 边界保存,表示是否能够组成一个词 this._count = 0; } Node.prototype = { isWord : function() { return (this._isWord && (this._count == 0)); }, asWord : function() { this._isWord = true; }, addCount : function() { this._count++; }, getCount : function() { return this._count; } }; var trie = new Trie(); trie.init(dict); var result = trie.splitWords(words); console.group("Trie 分词: "); console.log("待分词的字符串: ", words); console.log("分词结果: ", result); console.groupEnd(); </script> </body> </html>
如今已经能正确的分词了,即便有相同的前缀也没有问题。我上面分词用到的Trie树称为标准Trie树,这种标准Trie树比较直观。对于须要存储中文的Trie树,也有不少是用数组的方式实现的,好比双数组Trie树(Double Array Trie,简称DAT)、三数组Trie树等,有兴趣的朋友能够去了解一下。ui
本文只是简单的实现了中文分词,还有不少不足的地方。好比没有考虑未登陆词的自动成词,人名、岐义等等。但对于通常的如关键词屏蔽和计算文本类似度等应用已经足够了。this