jieba源码解析(一):分词之前

简介

总的来说,jieba分词主要是基于统计词典,构造一个前缀词典;然后利用前缀词典对输入句子进行切分,得到所有的切分可能,根据切分位置,构造一个有向无环图;通过动态规划算法,计算得到最大概率路径,也就得到了最终的切分形式。

初始化

jieba采用了延迟加载机制,在import后,不会立刻加载词典文件,在利用jieba.cut或jieba.lcut分词的时候才加载本地词典。如果有必要可以采用下面方式进行手动初始化:

import jieba
jieba.initialize()    # 默认主词典dict.txt
print(jieba.lcut("这是一句测试文本!"))

主词典文件dict.txt根据98年人民日报语料和一些小说的分词结果统计所得,形如:

AT&T 3 nz
B超 3 n

以“词 词频 词性”作为一条记录。
jieba本身是一个类库,其初始化可以指定主词典文件,通过初始化时指定不同字典文件可以达到同时对不同领域分词的目的。假如现在有A和B两个不同领域的文本和词典文件,通过下面方式可以同时做不同的分词:

import jieba
# 不同词典初始化
ajieba = jieba.Tokenizer('adict.txt')
bjieba = jieba.Tokenizer('bdict.txt')
# 分词
awords = ajieba.lcut('a文本')
bwords = bjieba.lcut('b文本')

词典数据

初始化时会读取dict.txt,生成前缀词典。在__init__.py脚本中,可以看出通过下面方式读取并生成词典数据:

def gen_pfdict(f_name):
    lfreq = {}
    ltotal = 0
    f = open(f_name, 'rb')
    for lineno, line in enumerate(f, 1):
        try:
            line = line.strip().decode('utf-8')  # 解析dict.txt
            word, freq = line.split(' ')[:2]     # 获取词和词频
            freq = int(freq)
            lfreq[word] = freq
            ltotal += freq
            for ch in range(len(word)):          # 获取前缀
                wfrag = word[:ch + 1]
                if wfrag not in lfreq:           # 如果某前缀词不在前缀词典中,则将对应词频设置为0
                    lfreq[wfrag] = 0
        except ValueError:
            raise ValueError('invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
    f.close()
    return lfreq, ltotal

lfreq和ltotal分别表示前缀词典和总词频,二者通过marshal模块将数据持久化到本地。假如以上面dict.txt示例中的两个词,构成lfreq,其结果如下:

key value
A 0
AT 0
AT& 0
AT&T 3
B 0
B超 3

附:

1、词典数据格式的修改

根据jieba项目修改日志,可以看到从2014年的0.34版本,jieba就的词典结构就不再使用trie!但可以学习一下trie树的构造方式:

def gen_trie(f_name):
    '''trie树'''
    lfreq = {}
    trie = {}
    ltotal = 0.0
    with open(f_name, 'rb') as f:
        lineno = 0
        for line in f.read().rstrip().decode('utf-8').split('\n'):
            lineno += 1
            try:
                word,freq,_ = line.split(' ')
                freq = float(freq)
                lfreq[word] = freq
                ltotal+=freq
                p = trie
                for c in word:
                    if c not in p:
                        p[c] ={}
                    p = p[c]
                p['']='' # ending flag
            except ValueError:
                raise ValueError
    return trie, lfreq,ltotal

为什么jieba没有使用trie树作为本地词典存储的数据结构?

参考jieba中的issue--不用Trie,减少内存加快速度;优化代码细节 #187,本处直接引用该issue的comment,如下:

对于get_DAG()函数来说,用Trie数据结构,特别是在Python环境,内存使用量过大。经实验,可构造一个前缀集合解决问题。

该集合储存词语及其前缀,如set(['数', '数据', '数据结', '数据结构'])。在句子中按字正向查找词语,在前缀列表中就继续查找,直到不在前缀列表中或超出句子范围。大约比原词库增加40%词条。

该版本通过各项测试,与原版本分词结果相同。

测试:一本5.7M的小说,用默认字典,64位Ubuntu,Python 2.7.6。

Trie:第一次加载2.8秒,缓存加载1.1秒;内存277.4MB,平均速率724kB/s;

前缀字典:第一次加载2.1秒,缓存加载0.4秒;内存99.0MB,平均速率781kB/s;

此方法解决纯Python中Trie空间效率低下的问题。

同时改善了一些代码的细节,遵循PEP8的格式,优化了几个逻辑判断。

2、新的词典

本人结合1998年和2014年人民日报语料重新做了词频统计,语料并不完美,也是废了一番功夫。在分享出的百度云链接里面还有个million_dict.txt是个经过验证的百万级词典,可作为个人自定义词典使用。

posted @ 2019-09-11 17:55  AloisWei  阅读(...)  评论(...编辑  收藏