? ? ?早期在項目中使用ansj分詞,但一直停留在會用,所以我抽空學(xué)習(xí)了一下源碼,確實對分詞的流程和用法有了進一步的理解,在此前我沒有學(xué)過java,所以看代碼的時候很多知識都是請教別人的,所以這里總結(jié)可能廢話比較多,僅僅是個人通過寫來加深理解,希望大家不喜勿噴,總結(jié)如下:
一.內(nèi)容介紹
? ? 在介紹ansj的流程前,咱們先看下該工程的目錄結(jié)構(gòu):其中l(wèi)ibrary文件夾中存放自定義的字典、domian文件夾存放著分詞相關(guān)的實體類,splitword文件夾存放了分詞的類,recognition存放了分詞后結(jié)果的處理類,nature文件夾中存放了詞性表,這些都是我會在接下來的博客中會提到的部分,后面會一一進行解讀。

二.domian模塊
為了理解起來更連貫,我們先去domian 文件夾中學(xué)習(xí)一些基本概念。

接下來我們會介紹domian package下的部分類
1.Nature 詞性實體類


? ? ? 我們可以看到這個類里:
? ? ? ?首先定義了一些成員常量,用于存放詞性的名稱、詞性對照表中的位置、詞性下標(biāo)值以及詞性的頻率,還有一些常量是Nature實體類本身
? ? ? 然后就是構(gòu)造函數(shù),這里構(gòu)造函數(shù)有兩種,第一種是參數(shù)是詞性名稱、詞性表中的位置和詞性下標(biāo)值以及詞性的頻率,另一種是只傳詞性名稱
? ? 我們可以看到Nature這個類的構(gòu)造是通過NatureLibrary.getNature實現(xiàn)的,我們接下來NatureLibrary類的getNature函數(shù)的內(nèi)容
2.NatureLibrary類
因為我們目的是看看Nature類是如何實現(xiàn)的,所以我們先看看getNature這個函數(shù)的作用:

? ? ? 可以看到其傳入的參數(shù)是一個詞性字符串,作用是根據(jù)詞性字符串得到Nature這個類,我們仔細分析一下這個函數(shù)的過程:首先參數(shù)是詞性字符串,然后NATUREMAP這個字典傳入這個key值得到對應(yīng)的value值就是Nature類并賦值給nature變量,后面判斷是否nature是null,如果是,就給這個nature賦一個默認值并更新保存到NATUREMAP字典里,好了,到這里我們能看出來NATUREMAP是(key:詞性字符串,value:Nature實體類),那么搞清楚Nature的關(guān)鍵就在NATUREMAP字典上,下面我們來看一下這個字典的初始化和賦值過程:

? ? ? 可以看出,這里是初始化了一個空的HashMap<String,Nature>賦值給NATUREMAP變量,下面我們看下這個字典是如何被賦值的:
? ? ?我們按ctrl+F搜尋這個變量,發(fā)現(xiàn)其在init函數(shù)里進行了賦值,那么我們來解讀一下這個init函數(shù):

? ? ?首先這個函數(shù)的返回值是void即沒有返回值,上面是init函數(shù)的前半部分,作用是給NATUREMAP這個變量賦值,這部分結(jié)構(gòu)是首先定義了幾個變量,然后就是一個try...catch的代碼塊,主要調(diào)用這個MystaticValue這個類的getNatureMapReader函數(shù)得到了reader變量,那么這個getNatureMapReader 函數(shù)做了什么呢,返回的又是什么呢?

? ? ? 我們發(fā)現(xiàn),getNatureMapReader函數(shù)里面又調(diào)用了dic目錄下的類 DicReader類的getReader函數(shù)


? ? ?getReader函數(shù)的參數(shù)傳的是一個字符串路徑:"nature/nature.map",點擊這個進去發(fā)現(xiàn)是個詞性表,詞性表的格式如下:

? ? ? 第一列表示詞性對照表的位置,第二列表示詞性下標(biāo)值、第三列表示詞性名、第四列表示詞性的頻率。
? ? ? 那么這個getReader函數(shù)傳這個詞性表路徑進去具體做了什么呢?接著往下看:
? ? ? 我們進去DicReader類看一下里面的內(nèi)容,發(fā)現(xiàn)這個類主要是用來加載詞典的類,這個getReader 函數(shù)的作用主要是加載nature/nature.map 這個詞性文件中的內(nèi)容,讀取所有的詞性字符串和位置以及詞頻作為返回值,getResourceAsStream是java的一個獲取輸入流的函數(shù),通過傳入文件地址得到了InputStream,這個InputStream 的作用是打開了文件的鏈接,獲取一個流,是字節(jié)流即0-1取值的,字節(jié)流可以用于讀所有文件,然后傳到InputStreamReader函數(shù)使用utf-8編碼創(chuàng)造一個BufferedReader字符流,字符流用于讀文本文件,BufferedReader?這個類有readLine方法,然后我們就可以一行一行讀了,InputStream沒有readLine方法,其實這里的getReader函數(shù)其實就是實現(xiàn)了從文件到字符流的封裝
? ? ? 到此我們得理一下,也就是看了這么多,就是為了弄清楚MyStaticValue.getNatureMapReader()函數(shù)的作用:就是讀取了詞性表的內(nèi)容,返回了一個BufferedReader字節(jié)流,那么我們接著回來看init函數(shù)中g(shù)etNatureMapReader函數(shù)后面的內(nèi)容:
? ? ? 后面其實就是調(diào)用reader的readerLine函數(shù)一行一行讀取詞性表中的內(nèi)容,把詞性表中的詞性字符串賦值給NATUREMAP字典的key值,把詞性表中的詞性的位置、下標(biāo)值以及詞頻賦值給Nature類的成員常量,然后把得到的Nature類實例賦值給NATUREMAP字典的value,還有就是這里在讀取每一行的時候都把最大的詞性下表值賦值給maxLength變量,因此最后這個maxLength變量是詞性表中的詞性下標(biāo)值最大的那個值,這個變量后面能用到。到此,我們已經(jīng)解讀了NatureLibrary.getNature函數(shù)的過程,也明白了Nature這個實體類的構(gòu)造過程。
? ? ?到這里我們基本上已經(jīng)把Nature這個類解讀完了,但是我們不妨再看看NatureLibrary這個類:
? ? ?我們進入NatureLibrary這個類,發(fā)現(xiàn)并沒有構(gòu)造函數(shù),而是有一個靜態(tài)代碼塊,對于靜態(tài)代碼塊里面的內(nèi)容是在類初始化的時候初始化一次的,以static關(guān)鍵詞開頭,發(fā)現(xiàn)里面是一個init()函數(shù)

? ? ?然后我們進入這個函數(shù),這個函數(shù)就是剛剛我們解讀的init函數(shù),剛剛的init函數(shù)其實我們只解讀了上半部分,我們接著看看下半部分:

? ? 上半部分是加載詞性表,下半部分是加載詞性關(guān)系,流程和加載詞性表類似,調(diào)用了MystaticValue這個類的getNatureTableReader函數(shù):

加載詞性表和詞性關(guān)聯(lián)表的時候主要用了MystaticValue這個類
? ? 該類中幾乎所有變化、方法均是靜態(tài)的。包括以ResourceBundle.getBundle("library")獲取library.properties配置文件,讀取用戶詞典路徑、歧義詞典路徑、是否用戶辭典不加載相同的詞isSkipUserDefine、isRealName。
? ? ?并讀取resources目錄下的company、person、newword、nature(詞性表、詞性關(guān)聯(lián)表)等文件夾中的數(shù)據(jù)
? ? ?及resources目錄bigramdict.dic(bi-gram模型)、英文詞典englishLibrary.dic、數(shù)字詞典numberLibrary.dic,以及加載crf模型
? ? ? 這個getNatureTableReader函數(shù)同樣調(diào)用了DicReader.getReader函數(shù),只不過此時傳入的參數(shù)字符串是"nature/nature.table",這是詞性關(guān)聯(lián)表,我們看看這個表的內(nèi)容

? ? ?看到這里,是不是暈了,我也暈了,這個詞性關(guān)聯(lián)表的內(nèi)容是什么意思呢,好,我們接著往下看吧:
? ? ? 知道了getNatureTableReader函數(shù)的作用,接著看如何存儲加載進來的詞性關(guān)聯(lián)表的:首先初始化一個空的二維int數(shù)組給NATURETABLE變量,行和列的長度是init上半部分得到的變量maxLength+1,這就是我們上面解釋的變量,是詞性表中第二列所有值中的最大值,第二列含義是詞性下標(biāo)值(取值是0-49),也就是nature.map中有50個詞性,行數(shù)(50行)等同于nature.map中的行數(shù),并且與nature.map相對應(yīng),即每行表示的詞性同nature.map中的詞性。每行中有50個列,即構(gòu)成50*50的矩陣
? ? ?那么這個變量NATURETABLE 的每一行存儲的就是詞性關(guān)聯(lián)表中的一行,由此,加載了整個詞性關(guān)聯(lián)表的數(shù)據(jù),其實目前我也不知道這個變量里的值是什么意思,那么我們就接著看一下NatureLibrary這個類后面的部分,說不定后面有用到這個函數(shù):

? ? ?到了這里,是不是明白了,原來這個詞性關(guān)聯(lián)表中的值是兩個詞性之間的頻率,也就是說詞性關(guān)聯(lián)表中的每個(i,j)位置的數(shù)值表示從前一個詞的詞性i變化到下一個詞的詞性j的發(fā)生頻次。用在詞性標(biāo)注工具類NatureRecognition中
? ? ? 到此,我們可以知道NatureLibrary這個類初始化時會讀取nature這個文件中nature.map和nature.table兩個表的內(nèi)容,并分別用Nature實體類和NATURETABLE變量存儲
接著往后面看:

? ? ?這個getTwoTermFreq函數(shù)是獲取兩個Term之間的頻率,這里的Term類是分詞的結(jié)果中的每個詞的信息會保存為該類,我們發(fā)現(xiàn)在在這個類里有:TermNatures詞性列表類

這個類是由很多TermNature類組成的:

好,下面我們就先從TermNature類開始介紹
3.TermNature類


? ? ? ?這個類里面定義了兩個成員變量nature和frequency,還有TermNature實例常量,可以看出是由TermNature類初始化的一個實例,那么我們看構(gòu)造函數(shù),參數(shù)是詞性字符串和詞頻,調(diào)用NatureLibrary.getNature函數(shù)得到的Nature實體類再賦值給成員變量nature,以及直接將參數(shù)frequency參數(shù)賦值給了成員變量frequency,由此可知:這個TermNature實體類是存儲詞的Nature詞性類和詞頻的,至于這個詞頻和Nature里面的詞頻有什么關(guān)系,代表什么意思目前為止我也不清楚,我們接著往后看。
下面我們再接著看TermNatures實體類
4.TermNatures類

這里首先是初始化了一些TermNatures類實例,有兩種初始化方式,主要是參數(shù)不同:



? ? ? ? 第一種構(gòu)造函數(shù)的參數(shù)是TermNature類,函數(shù)的內(nèi)容是:第一行對成員變量termNatures初始化為一個含有一個元素的TermNature數(shù)組,然后將參數(shù)termNature即TermNature類賦值給TermNatures的成員變量termNatures,然后將TermNature類的成員變量nature賦值給TermNatures類的成員變量nature,這樣完成了一個TermNatures類的初始化,也就是說這里TermNatures類在初始化的成員變量都是來源于TermNature類;
第二種構(gòu)造函數(shù)的參數(shù)比第一種多了兩個參數(shù)分別是:


可以看出,這里只是比第一個多初始化了TermNatures類的兩個參數(shù):allFreq和id,這里的參數(shù)allFreq先是賦值給了TermNature的成員變量frequency即詞頻,再賦值給了TermNatures類的參數(shù)id即詞的id,這里可以看出來TermNatures類初始化的時也是含有一個元素的TermNature數(shù)組
第三種構(gòu)造函數(shù)的參數(shù)是一個TermNature數(shù)組和詞的ID

? ? ? 這是當(dāng)一個詞不止一個詞性的情況,此時從for循環(huán)可以看出,對于這個詞的最終的詞性決定是選擇最大頻率的那個詞性的,這個頻率可能是分詞的文章中計算出來的,很可能是這個詞在文章中的頻率或者別的,并且最終把這個TermNature數(shù)組對應(yīng)的元素賦值給termNature這個變量,并把這個變量的成員變量Nature賦值給TermNatures這個類的成員變量nature
? ? ? 綜上可以看出,這個TermNatures類里面有三種構(gòu)造函數(shù),傳的參數(shù)分別是TermNature 數(shù)組和TermNature類兩種,這是當(dāng)一個詞很多詞性和只有一個詞性時的情況,因此這個TermNatures這個類的作用主要是存儲指定ID的詞的詞性,并決定最后的詞性,即每一個term都擁有一個詞性集合
介紹完TermNature類和TermNatures類后,下面介紹Term類
5.Term 類
? ? ? 分詞之后的每個詞會保存為該類,它會保存詞的原本名字、位置、詞性列表(一個詞可能有多種詞性,于是是詞性列表)、分數(shù)(我也不懂分數(shù)是什么鬼)、下一個Term、是否為一個新詞等

? ? ? 下面可以看出來:這是toAnalysis.parse(分詞函數(shù))分詞完以后的結(jié)果,存放到了list[Term]里面,可以看出來每個詞存為一個Term類



? ? ?Term類為DAG的節(jié)點,字段包括:offe首字符在句子中的位置、name為詞,next具有相同首字符的節(jié)點、from前驅(qū)節(jié)點、score打分。
? ? ? 既然這個Term類是分詞的結(jié)果類,那么我們先看一下分詞的過程,畢竟我們的目的就是為了能夠運用分詞的功能

? ? ? 由上可知:這個Graph類的構(gòu)造函數(shù)參數(shù)是一個字符串,然后將·這個字符串轉(zhuǎn)化為字符數(shù)組賦值給Graph的成員變量chars,再初始化一個Term類空數(shù)組(長度是參數(shù)字符串長度+1)給graph的成員變量terms,再初始化了一個Term類實例給Graph類的成員變量end,這個end代表當(dāng)前Term的起始位置,即是傳進來的str的長度,即最后一個,然后初始化一個Term實例賦值給root變量表示graph圖的第一個Term,這個root說明首字符在句中的位置是-1,對應(yīng)的這里的Term類構(gòu)造函數(shù)如下:


? ? ? 可以看到:這里的第一個參數(shù)十個詞性字符串,大家還有沒有印象這個E和B是詞性表nature.map的第一行和第二行的第三列即詞性字符串,第二個參數(shù)是offe在Term類里注釋是當(dāng)前詞的起始位置,第三個參數(shù)是詞性列表,判斷這個item即AnsjItem類的成員變量termNatures是否是null,如果不是,就把這個termNatures賦值給Term類的成員變量termNatures,接著判斷是否termNatures類的成員變量nature是否是null,如果不是就把nature賦值給Term類的成員變量nature,也就是這個Term類包含了當(dāng)前詞的名字name,起始位置offe,所有的詞性列表termNatures以及最終在詞性列表中按照詞頻最高的決定當(dāng)前詞的詞性nature。
下面我們稍微了解一下這個AnsjItem類:它是Item的子類,Item類里面方法接班都是抽象的,在AnsjItem類里得到了實現(xiàn):




這個方法傳的參數(shù)是split字符串?dāng)?shù)組,第一個元素是index,即詞性ID,第二個元素是name,即詞性字符串,第三個第四個元素是base,check,第五個元素是status即當(dāng)前詞的狀態(tài),status的數(shù)值具有如下含義:
1對應(yīng)的詞性為null,name不能單獨成詞,應(yīng)繼續(xù),比如“振臂一”;
2表示name既可單獨成詞,也可與其他字符組成新詞,比如詞“印度”;
3表示詞結(jié)束,name成詞不再繼續(xù),比如詞“捅婁子”;
4表示英文字母(包括全角)+字符',共計105(26*4+1)個字符;
5表示數(shù)字(包括全角)+小數(shù)點,共有21(10*2+1)個字符.
那么當(dāng)status的值大于1即說明name可單獨成詞時,將split的第五個元素和index傳入TermNature類的SetNatureStrToArray方法,我們接下來看看這個方法:

這里我們可以看出:這個詞性字符串傳進去以后按逗號分割成一個字符串?dāng)?shù)組,然后每個字符串元素是“詞性字符串=詞頻”形式,將這個詞性字符串和詞頻傳入TermNature構(gòu)造函數(shù)得到TermNature實例,作為一個TermNature數(shù)組保存
從這里可以看出,Term類的成員變量termNatures即當(dāng)前詞的詞性列表是來源于AnsjItem類的initValue方法中對termNatures成員變量進行了賦值,賦值是將可以獨立成詞的name對應(yīng)的詞性列表通過處理傳到了TermNatures類中,成了一個TermNatures類實例,但是此時我們還不知道這個split的來源是什么?以及這里的base和check具體是什么我們也不知道,只能接著往后看:
接著分詞函數(shù)往下看:



這個AMBIGUITY是個HashMap,this.ambiguityForest是通過AmbiguityLibrary類的get函數(shù)獲得的Forest類,這個get函數(shù)內(nèi)部判斷了這個AMBIGUITY是否有DEFAULT key值,如果沒有就返回null,否則就返回default key對應(yīng)的value值

? ? ? ?在Analysis.anlysisStr函數(shù)里:首先判斷是否啟用歧義詞典。若是,找出句子中是否包含歧義詞。若不存在,對整個句子調(diào)用Analysis.analysis;若存在,優(yōu)先歧義詞:以歧義詞分隔原句子,根據(jù)歧義分詞數(shù)組中的詞及詞性逐個添加到graph中,并對非歧義詞的部分分別調(diào)用Analysis.analysis。Analysis.analysis的過程為按字從DAT中找,通過GetWordsImpl.allWords()查詢字在DAT中的base、check等獲得狀態(tài)返回單字或詞,調(diào)用graph.addTerm添加節(jié)點到graph的terms數(shù)組中,同時標(biāo)注是否為數(shù)字,英文

這里有三個參數(shù)Graph類、起始位置、結(jié)束位置,然后進入一個for循環(huán),對Graph的成員變量chars從起始位置到結(jié)束位置進行循環(huán),接著進入了一個條件語句,判斷status()函數(shù)的結(jié)果:


這里我們可以看到得到的是一個dat數(shù)組,但是我們并不知道這個dat數(shù)組具體是什么,只知道這個是DoubleArrayTire類的成員變量,因此我們從DAT對象研究:


這個DAT對象是DoubleArrayTire類,是調(diào)用DATDictionary類的loadDAT方法得到的,這個方法里面調(diào)用了DoubleArrayTire類的loadText方法:

這個方法里面?zhèn)髁艘粋€字節(jié)流InputStream字節(jié)流,以及Item的子類,作用是從文本中加載模型,這個InputStream是來自于DicReader.getInputStream(‘core.dic’)得到的,進而調(diào)用getReaourceAsStream函數(shù)讀取core.dic核心字典成一個字節(jié)流傳進loadText函數(shù)

IOUtil.instanceFileIterator函數(shù)將字節(jié)流轉(zhuǎn)化為一個字符流,下面可以看到函數(shù)的實現(xiàn)過程,最后調(diào)用了BufferedReader函數(shù)將輸入的字節(jié)流InputsTream轉(zhuǎn)化為字符流便于后面我們讀取進行處理:



接下來我們接著看loadText函數(shù),得到了文件的字符串迭代器,下面我們要對得到的內(nèi)容進行處理,首先我們先看一下core.dic字典的內(nèi)容:

第一行是這個字典的行數(shù),在loadText函數(shù)里也看到把it的第一行轉(zhuǎn)化為int類型然后賦值給了DoubleArrayTire類的成員變量ArrayLength,然后初始化了一個ArrayLength長度的Term數(shù)組給DoubleArrayTire類的成員變量dat,接下來就對核心字典里的內(nèi)容進行循環(huán)處理了:

這里我們可以看到對每一行temp都按照tap字符進行分割,得到每一字段的值,然后作為參數(shù)傳到initValue函數(shù)里,將initValue的處理結(jié)果item作為dat數(shù)組的每個元素,這里的initValue函數(shù)就是我們前面分析過的,現(xiàn)在是不是清楚了當(dāng)時傳的那個split變量是什么了,其實就是我們這里的核心字典的每一行,結(jié)合initValue函數(shù)里的賦值和處理,我們可以知道:核心詞典共有6列,分別為
index? name? ? base? ? check? status? {詞性->詞頻}
其中,index表示字符串的id(若為單字符,則為其unicode編碼對應(yīng)的整數(shù)值),name為詞,base、check分別為DAT的base數(shù)組、check數(shù)組,status記錄當(dāng)前詞的狀態(tài),最后一列表示詞性集合,對應(yīng)于類org.ansj.domain.AnsjItem中的成員變量termNatures
到此為止,我們知道了這個dat數(shù)組是什么了,她是個Item數(shù)組我們是知道的,里面的內(nèi)容是從核心字典加載的:不過Item的initValue方法是抽象的,子類AnsjItem類對這個方法進行了實現(xiàn)

我們再看這個DAT對象即DoubleArrayTire類在被調(diào)用的時候就進行了初始化,即調(diào)用了loadText函數(shù),也就是會調(diào)用loadText函數(shù),那么就會讀取core.dic核心字典,即我們只要調(diào)用了DATDictionary類的status方法就會自動加載這個核心字典

?我們再看一下這個status這個函數(shù)的結(jié)果:首先從dat數(shù)組中取得c索引位置的Item類,然后判斷是否是null,如果是即這個dat數(shù)組中沒有對應(yīng)的字符串的id,那么返回0,表明這個字符串不是個詞,不是就說明在dat數(shù)組中有,就調(diào)用Item的getStatus方法得到Item的當(dāng)前狀態(tài)status,然后根據(jù)這個值決定后面我們該如何處理
