1. 程式人生 > >從0開始的jieba分詞原始碼分析_1_從cut開始

從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)