中文和英文不同,英文通常采用空格和标点符号将词隔开,具有天然的分隔符,对英文文本进行相似性分析时,词的获取非常简单。中文虽然句子之间有分隔符,但词与词之间没有分隔符,需要编写专门的分词程序,拆分句子获取单词。
对中文句子进行分词时,有不同的切分方案,对于搜索来说,经常需要同时输出多种粒度的切分结果。例如“世界卫生组织会议”,最粗的切分结果是:“世界卫生组织会议”,稍微细一些的切分结果是“世界/卫生组织/会议”,更细的切分结果是:“世界/卫生/组织/会议”。
分词除了返回词本身,还可以返回词所在的位置,词性信息。
若使用Python编写相似性分析程序,一般使用列表来存储切分好的词,单个分词可以使用类来描述。
分词的存储结构:
# 分词结构
class SegmentWord:
def __init__(self,word,wordAtt,index):
# 分词
self.word = word
# 分词词性
self.wordAtt = wordAtt
# 分词在文本中的位置
self.index = index
最常见的分词方法是基于词典匹配,词典存储已经确定的中文单词,对中文语句进行分词时,从语句的指定位置向后查找在词典中匹配的最长词,也就是最大长度查找方法。例如:词典中包括8个词语:{大,大学,大学生,生活,中,中心,心}。句子“大学生活动中心落成了”从位置0开始最长的词是“大学生”。有时候,也需要找出待切分句子指定位置开始所有的词。例如,句子“大学生活动中心落成了”从位置0开始所有的词是{大,大学,大学生}。
中文分词实际使用的词典规模往往在几十万词以上,采用逐个匹配词典中的词,其查找效率是非常低下的,为了保证分词效率,需要选择一个好的查找词典算法。
词典存储结构一般采用Trie树。Trie树通常又称为字典树、单词查找树或前缀树,是一种用于快速检索的多叉树结构。如下图所示:
上图是存储了三个词的Trie树,Trie树的一个节点只保留一个字符,如果一个单词比一个字符长,则包含第一个字符的节点记录指向下一个字符的节点的属性,以此类推,构成一个层次结构的树,树的第一层包括所有单词的第一个字符,树的第二层包括所有单词的第二个字符,第三层包括所有单词的……。Trie树的最大高度是词典中最长单词的长度。
使用Python实现Trie树,需要一个树类和一个节点类,然后用逐个增加单词到Trie树的方法构建词典树。
节点类代码:
# 节点类
class TrieNode(object):
def __init__(self):
# 字典的key为字符
# 字典的value为TrieNode
self.child = {}
# 标志位,为1表示单词结束
self.flag = None
Trie树类
# 树类
class Trie(object):
def __init__(self):
self.root = TrieNode()
# 添加单词
def add(self, words):
curNode = self.root
for word in words:
if curNode.child.get(word) is None:
nextNode = TrieNode()
curNode.child[word] = nextNode
curNode = curNode.child[word]
curNode.flag = 1
# 精准查找
def search_exact(self, words):
curNode = self.root
for word in words:
if curNode.child.get(word) is None:
return False
else:
print(word)
curNode = curNode.child[word]
if curNode.flag == 1:
return True
# 模糊查找
def search_fuzzy(self, sentence):
curNode = self.root
for word in sentence:
if curNode.child.get(word) is None:
return False
else:
print(word)
if curNode.child[word].flag == 1:
return True
else:
curNode = curNode.child[word]
return True
# 输出单词
def print_words(self):
rootnode = self.root
for key in rootnode.child.keys():
print(key)
self.print_sub_words(rootnode.child[key])
# 输出子节点字符
def print_sub_words(self,node):
for key in node.child.keys():
print(key)
self.print_sub_words(node.child[key])
构建Trie词典树,需要读取词典文件,将词典文件的所有单词添加到Trie词典树,词典文件存储格式一般是可方便查看和编辑的文本文件格式,最基本的词典的文本文件格式就是每行一个词,从文本文件读入词典的代码如下:
# 读取词典文件
def read_text(trie):
filename = "sample.txt"
try:
#使用r模式打开文本文件
fp = open(filename,"r",encoding='utf-8')
print("%s 文件打开成功" % filename)
done = False
#使用while循环读取文本文件所有行数
while not done:
#按行读取文件内容
aline = fp.readline()
if(aline != ""):
# 过滤换行符
words = aline.replace('\r','').replace('\n','').replace('\t','')
# 添加单词到Trie树
trie.add(words)
else:
done = True
#关闭文件对象
fp.close()
except IOError:
print("文件打开失败,%s文件不存在" % filename)
sample.txt和Python程序在同一个目录,sample.txt存储的单词如下:
大
大学
大学生
活动
生活
中
中心
心
存储单词的文本文件也称为词表,分词程序分出来的词都来自词表,词典就是现成的词表,也可以从加工后的语料库得到词表,例如“人民日报语料库”,中文分词最简单的方法是:直接匹配词表,返回词表中最长的词。
正向最大长度匹配法就是从词表中,查找和待匹配串前缀最长匹配的词,如果找到匹配词,则把这个词作为切分词,待匹配串减去该词,如果词典没有匹配上,就按单字切分。
例如:Trie词典树包括如下的8个词语:
大 大学 大学生 活动 生活 中 中心 心
句子“大学生活动中心”,首先会匹配开始的最长词“大学生”,然后再匹配“活动”吗,最后匹配“中心”。
正向最大匹配算法代码如下:
def forward_max_matching(sentence,trie):
words = []
# 句子的字符数
sentence_len = len(sentence)
if sentence_len <= 0:
return
# 起始索引
start = 0
# 终止索引
end = 1
temp = ""
while True:
# 提取分词
word = sentence[start:end]
# 若当前分词匹配成功,添加字符继续匹配
if trie.search_exact(word):
end += 1
temp = word
if end >= sentence_len:
words.append(temp)
break
else:
# 若temp不为空,添加分词到words
if len(temp) > 0:
words.append(temp)
start += len(temp)
temp = ""
# 终止索引加1
end += 1
if end > sentence_len:
break
return words
上述代码是简单的分词程序,实际的分词程序要复杂的多。在编写相似性分析、机器翻译等自然语言处理程序时,我们并不需要专门编写分词程序。
Python分词工具很多,包括盘古分词、Yaha分词、Jieba分词、清华THULAC等,这些分词程序都是开源软件,在许可协议下,可以免费使用这些分词程序。