jieba源碼研讀筆記(一) - 分詞功能初探

前言

jieba的分詞功能是由jieba這個模組本身及finalseg來完成。
jieba這個模組裡包含了__init__.py__main__.py_compat.pydict.txt四個檔案。
其中__init__.py定義了Tokenizer類別及一些全局函數,用於分詞本身。
__main__.py定義了jieba在命令行裡的使用方式。
_compat.py用於處理Python2/3相容性的問題。
dict.txt則是字典,記錄了各詞的詞頻及詞性。

jieba/init.py

Tokenizer類別

__init__.py這個檔案裡面,定義了一個叫做Tokenizer的類別。
它擁有cut,cut_for_search,tokenize等多種方法,負責了分詞的工作。

class Tokenizer(object):

    def __init__(self, dictionary=DEFAULT_DICT):
        ...

    def __repr__(self):
        return '<Tokenizer dictionary=%r>' % self.dictionary

    def gen_pfdict(self, f):
        ...

    def initialize(self, dictionary=None):
        ...

    def check_initialized(self):
        ...

    def calc(self, sentence, DAG, route):
        ...

    def get_DAG(self, sentence):
        ...

    def __cut_all(self, sentence):
        ...

    def __cut_DAG_NO_HMM(self, sentence):
        ...

    def __cut_DAG(self, sentence):
        ...

    def cut(self, sentence, cut_all=False, HMM=True):
        ...

    def cut_for_search(self, sentence, HMM=True):
        ...

    def lcut(self, *args, **kwargs):
        ...

    def lcut_for_search(self, *args, **kwargs):
        ...

    _lcut = lcut
    _lcut_for_search = lcut_for_search

    def _lcut_no_hmm(self, sentence):
        ...

    def _lcut_all(self, sentence):
        ...

    def _lcut_for_search_no_hmm(self, sentence):
        ...

    def get_dict_file(self):
        ...

    def load_userdict(self, f):
        ...

    def add_word(self, word, freq=None, tag=None):
        ...
    
    def del_word(self, word):
        ...

    def suggest_freq(self, segment, tune=False):
        ...

    def tokenize(self, unicode_sentence, mode="default", HMM=True):
        ...
    
    def set_dictionary(self, dictionary_path):
        ...

Tokenizer相關的全局函數

根據README裡介紹的使用方法,我們可以直接調用jieba.cut來分詞,這是怎麼做到的呢?

在定義好Tokenizer類別後,__init__.py裡建立了一個Tokenizer類別的dt對象。
然後逐一定義全局函數,並將它們指向dt中相對應的函數,如以下代碼所示:

# default Tokenizer instance
dt = Tokenizer()

# global functions
get_FREQ = lambda k, d=None: dt.FREQ.get(k, d)
add_word = dt.add_word
calc = dt.calc
cut = dt.cut
lcut = dt.lcut
cut_for_search = dt.cut_for_search
lcut_for_search = dt.lcut_for_search
del_word = dt.del_word
get_DAG = dt.get_DAG
get_dict_file = dt.get_dict_file
initialize = dt.initialize
load_userdict = dt.load_userdict
set_dictionary = dt.set_dictionary
suggest_freq = dt.suggest_freq
tokenize = dt.tokenize
user_word_tag_tab = dt.user_word_tag_tab


def _lcut_all(s):
    return dt._lcut_all(s)


def _lcut(s):
    return dt._lcut(s)


def _lcut_no_hmm(s):
    return dt._lcut_no_hmm(s)


def _lcut_all(s):
    return dt._lcut_all(s)


def _lcut_for_search(s):
    return dt._lcut_for_search(s)


def _lcut_for_search_no_hmm(s):
    return dt._lcut_for_search_no_hmm(s)

如:cut = dt.cut這一句,它定義了一個全局函數cut,並把它指向dt對象的cut函數。
如此一來,我們就可以不用自己新建一個Tokenizer對象,而是直接使用jieba.cut來分詞。

全局函數_get_abs_path

上述全局函數皆指向dt所擁有的函數。除了這些函數外,還定義一個全局函數_get_abs_path

_get_abs_path = lambda path: os.path.normpath(os.path.join(os.getcwd(), path))

這個函數的參數path是字典的名稱,它的作用是在字典名稱前加上當前路徑,然後把路徑正規化後回傳。

全局函數_replace_file

這個函數的功用是移動(或說重命名)檔案。

if os.name == 'nt':
    from shutil import move as _replace_file
else:
    _replace_file = os.rename

這裡使用if-else的寫法是為了處理重命名函數在不同作業系統上的相容性,確保_replace_file在不同的作業系統上皆能運作。
參考I don’t understand os.name==‘nt’: . what is nt and os.name,代碼第一行的os.name == 'nt'代表當前的作業系統是Windows。

正則表達式

re_userdict = re.compile('^(.+?)( [0-9]+)?( [a-z]+)?$', re.U)

re_eng = re.compile('[a-zA-Z0-9]', re.U)

# \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)
# Adding "-" symbol in re_han_default
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)

這裡定義了數個正則表達式,它們會在分詞及載入字典時發揮作用。
此處定義的正則表達式將會獨立出來,在另外一篇文章中做介紹。

log相關函數

log_console = logging.StreamHandler(sys.stderr)
default_logger = logging.getLogger(__name__)
default_logger.setLevel(logging.DEBUG)
default_logger.addHandler(log_console)

def setLogLevel(log_level):
    global logger
    default_logger.setLevel(log_level)

default_logger如字面上的意思,是這個腳本檔中預設的logger。

並行分詞相關函數

pool = None

def _pcut(sentence, cut_all=False, HMM=True):
    parts = strdecode(sentence).splitlines(True)
    if cut_all:
        result = pool.map(_lcut_all, parts)
    elif HMM:
        result = pool.map(_lcut, parts)
    else:
        result = pool.map(_lcut_no_hmm, parts)
    for r in result:
        for w in r:
            yield w


def _pcut_for_search(sentence, HMM=True):
    parts = strdecode(sentence).splitlines(True)
    if HMM:
        result = pool.map(_lcut_for_search, parts)
    else:
        result = pool.map(_lcut_for_search_no_hmm, parts)
    for r in result:
        for w in r:
            yield w


def enable_parallel(processnum=None):
    """
    Change the module's `cut` and `cut_for_search` functions to the
    parallel version.
    Note that this only works using dt, custom Tokenizer
    instances are not supported.
    """
    global pool, dt, cut, cut_for_search
    from multiprocessing import cpu_count
    if os.name == 'nt':
        raise NotImplementedError(
            "jieba: parallel mode only supports posix system")
    else:
        from multiprocessing import Pool
    dt.check_initialized()
    if processnum is None:
        processnum = cpu_count()
    pool = Pool(processnum)
    cut = _pcut
    cut_for_search = _pcut_for_search


def disable_parallel():
    global pool, dt, cut, cut_for_search
    if pool:
        pool.close()
        pool = None
    cut = dt.cut
    cut_for_search = dt.cut_for_search

參考連結

I don’t understand os.name==‘nt’: . what is nt and os.name

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值