从0开始的jieba分词源码分析_1_从cut开始


从一个函数入口逐步分析分词的整个过程,最后对关键函数做了简化实现,附在最后供大家参考

分析

寻根

import jieba
jieba.cut("sentence")

查找cut的引用:

dt = Tokenizer()
cut = dt.cut#位于jieba/__init__.py

找到cut()方法,首先里面有很多re开头的变量,这些都是用于文字和字符分割用的re的实例

# \u4E00-\u9FD5a-zA-Z0-9+#&\._ : All non-space characters. Will be handled with re_han
# \r\n|\s : whitespace characters. Will not be handled.
re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%]+)", re.U)
re_skip_default = re.compile("(\r\n|\s)", re.U)
re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
re_skip_cut_all = re.compile("[^a-zA-Z0-9+#\n]", re.U)
    def cut(self, sentence, cut_all=False, HMM=True):
        '''
        The main function that segments an entire sentence that contains
        Chinese characters into seperated words.

        Parameter:
            - sentence: The str(unicode) to be segmented.
            - cut_all: Model type. True for full pattern, False for accurate pattern.
            - HMM: Whether to use the Hidden Markov Model.
        '''
        sentence = strdecode(sentence)

        if cut_all:
            re_han = re_han_cut_all
            re_skip = re_skip_cut_all
        else:
            re_han = re_han_default
            re_skip = re_skip_default
        if cut_all:
            cut_block = self.__cut_all
        elif HMM:
            cut_block = self.__cut_DAG
        else:
            cut_block = self.__cut_DAG_NO_HMM
        blocks = re_han.split(sentence)#将文字和非文字分开成块
        '''
        提供一个切分示例:
        stri = "我去北京玩游戏,去了北京找人"
		re_han_default.split(stri)
		output:
		['', '我去北京玩游戏', ',', '去了北京找人', '']
        '''
        for blk in blocks:
            if not blk:#如果为空
                continue
            if re_han.match(blk):#如果不是符号
                for word in cut_block(blk):
                    yield word
            else:#对字符的处理
                tmp = re_skip.split(blk)
                for x in tmp:
                    if re_skip.match(x):
                        yield x
                    elif not cut_all:
                        for xx in x:
                            yield xx
                    else:
                        yield x

我们知道cut方法返回的是一个迭代器,也就是里面每一个yield返回的值了

可以看到对文字的处理关键在:

 if re_han.match(blk):#如果不是符号
     for word in cut_block(blk):
         yield word

找到cut_block,一共有三种实现,依次看一下

if cut_all:
    cut_block = self.__cut_all
elif HMM:
    cut_block = self.__cut_DAG
else:
    cut_block = self.__cut_DAG_NO_HMM

__cut_all

首先是__cut_all,里面涉及到方法get_DAG(),这个方法其实就是得到文字的有向无环图,这个方法我将其稍加修改,将其中涉及到类的参数作为方法变量传入:

def get_DAG(sentence,FREQ):
	#FREQ是{词:词频}的字典,类似于{'北京大学': 2053, '北': 0, '北京': 0, '北京大': 0, '大学': 20025, '大': 0}
    # 检查系统是否已经初始化
    # DAG存储向无环图的数据,数据结构是dict
    DAG = {}
    N = len(sentence)
    # 依次遍历文本中的每个位置
    for k in range(N):
        tmplist = []
        i = k
        # 位置 k 形成的片段
        frag = sentence[k]
        # 判断片段是否在前缀词典中
        # 如果片段不在前缀词典中,则跳出本循环
        # 也即该片段已经超出统计词典中该词的长度
        while i < N and frag in FREQ:
            # 如果该片段的词频大于0
            # 将该片段加入到有向无环图中
            # 否则,继续循环
            if FREQ[frag]:
                tmplist.append(i)
            # 片段末尾位置加1
            i += 1
            # 新的片段较旧的片段右边新增一个字
            frag = sentence[k:i + 1]
        if not tmplist:
            tmplist.append(k)
        DAG[k] = tmplist
    return DAG#最后返回的DAG的形式类似于
    #{0: [0], 1: [1], 2: [2, 3, 5], 3: [3], 4: [4, 5], 5: [5], 6: [6]}

下面回归__cut_all()get_DAG()方法就在__cut_all()方法的第一句(这个方法我也稍微修改了一下,将类变量提取了出来):

def __cut_all(sentence,FREQ):
    dag = get_DAG(sentence,FREQ)
    print(dag)
    print(iter(dag))
    old_j = -1#旧的位置
    for k, L in iteritems(dag):#iteritems是jieba根据python版本不同写出来的迭代方法,就是迭代dag的key和value
        if len(L) == 1 and k > old_j:#如果节点只有一条边,说明是单字,返回这个字
            yield sentence[k:L[0] + 1]
            old_j = L[0]
        else:
            for j in L:  # 对每个边对应的位置
                # 想象一下,先输出北京,再输出北京大学
                if j > k:  # 如果位置比k大,个人感觉因为是有向无环图图,这个if应该恒为真吧
                    yield sentence[k:j + 1]  # 返回
                    old_j = j

以上就是cut中__cut_all的全部分析,对__cut_DAG的实现,由于涉及的函数过多,因此放到下一篇进行分析

简化实现

将__cut_all的所有涉及的方法抽取为普通方法放到这里,提供了一个示例,可以直接运行,供调试和学习

#cut_all_ana.py
from cut_all_ana import get_DAG,gen_pfdict
import re
re_han = re.compile("([\u4E00-\u9FD5]+)")
re_skip = re.compile("([a-zA-Z0-9]+(?:\.\d+)?%?)")



from math import log
def calc(sentence, DAG, route,FREQ,total):
    N = len(sentence)
    route[N] = (0, 0)
    logtotal = log(total)
    for idx in range(N - 1, -1, -1):
        ini = [(log(FREQ.get(sentence[idx:x + 1]) or 1)#FREQ.get(sentence[idx:x + 1]) or 1 ,如果FREQ有这个单词就用这个单词,如果没有就用1
                    - logtotal
                + route[x + 1][0], x
            ) for x in DAG[idx]]
        print(ini)
        route[idx] = max(ini)#元组的大小比较是按字典序比较,先比较第一个的大小,如果第一个大小相等再比较第二个的大小
def __cut_DAG(sentence,FREQ):
    DAG = get_DAG(sentence)
    route = {}
    calc(sentence, DAG, route)
    x = 0
    buf = ''
    N = len(sentence)
    while x < N:
        y = route[x][1] + 1
        l_word = sentence[x:y]
        if y - x == 1:
            buf += l_word
        else:
            if buf:
                if len(buf) == 1:
                    yield buf
                    buf = ''
                else:
                    if not FREQ.get(buf):
                        recognized = fcut(buf)
                        for t in recognized:
                            yield t
                    else:
                        for elem in buf:
                            yield elem
                    buf = ''
            yield l_word
        x = y

    if buf:
        if len(buf) == 1:
            yield buf
        elif not FREQ.get(buf):
            recognized = fcut.cut(buf)
            for t in recognized:
                yield t
        else:
            for elem in buf:
                yield elem


if __name__ == "__main__":
    k = [
        ["北京大学", 2053],
        ["大学", 20025],
        ["去", 123402],
        ["玩", 4207],
        ["北京", 34488],
        ["北", 17860],
        ["京", 6583],
        ["大", 144099],
        ["学", 17482],

    ]
    ex = "我去北京大学玩"
    freq, total = gen_pfdict(k)
    dag = get_DAG(ex,freq)
    route = {}
    
    calc(ex, dag, route, freq, total)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值