一、简介这次分析的是ICTCLAS中的
//Generate Word according the segmentation route
bool CSegment::GenerateWord(int **nSegRoute, int nIndex)

本来这个函数没有必要详细分析,但是我注意到中科院论文中并没有描述这个函数、而Sinboy和吕震宇也基本上跳过这个函数不讲了,所以这个函数还没有有人详细的分析过呢。在这里,我具体的分析一下这个函数,另外,也提出一些问题供打算重写ICTCLAS的朋友来考虑。
二、功能介绍这个函数虽然叫做GenerateWord,但是事实上并不仅仅是生成词,准确说,它大部分的工作不是为了将已经计算好的结果以词的形式显示出来,而是处理各种格式的数字。
数字实际上属于Named Entity的一种,属于未登录词识别的一部分。按理说应该像ICTCLAS处理人名、地名的方法一样,利用隐马,利用概率来处理。不过可惜的是,作者在对于数字的处理上,没有能够使用概率的优势来排除歧义,而采取了另一种方式,规则,来进行数字的合并和切分。在稍后,我们会讲述ICTCLAS在数字处理上出现的一些问题。我们先看看它所处理的规则:
- 合并所有的数字
- 针对 [数字][-—][数字] 的形式把减号从中分离出来。
- 合并:[数字]([月日时分秒]|月份)
- 合并:[数字][年]
- 处理时间格式:.*[点]$
- 处理数字格式:[∶·././]$
GenerateWord()对满足上述条件的字符串进行拆分和合并的操作,这就是GenerateWord()的主要工作。
三、代码分析
//Generate Word according the segmentation route
bool CSegment::GenerateWord(int **nSegRoute, int nIndex)

...{
unsigned int i=0,k=0;
int j,nStartVertex,nEndVertex,nPOS;
char sAtom[WORD_MAXLENGTH],sNumCandidate[100],sCurWord[100];
ELEMENT_TYPE fValue;
// nSegRoute的是原子位置的数组
// 循环,i初始为0,判断(i)和(i+1)有效,并判断nSegRoute[nIndex][i] < nSegRoute[nIndex][i+1]
// 这里为什么要加一个小于来判断?必然前面的小于后面的。
while(nSegRoute[nIndex][i]!=-1&&nSegRoute[nIndex][i+1]!=-1&&nSegRoute[nIndex][i]<nSegRoute[nIndex][i+1])

...{
nStartVertex=nSegRoute[nIndex][i];
j=nStartVertex;//Set the start vertex
nEndVertex=nSegRoute[nIndex][i+1];//Set the end vertex
nPOS=0;
// 取得该分段(粗分的词)的词性(nPOS)和词频(fValue)
m_graphSeg.m_segGraph.GetElement(nStartVertex,nEndVertex,&fValue,&nPOS);
// 将该分段对应的词保存进sAtom
sAtom[0]=0;
while(j<nEndVertex)

...{//Generate the word according the segmentation route
strcat(sAtom,m_graphSeg.m_sAtom[j]);
j++;
}

// 将sAtom的值赋给sNumCandidate
m_pWordSeg[nIndex][k].sWord[0]=0;//Init the result ending
strcpy(sNumCandidate,sAtom);

// 如果sNumCandidate全是数字的话,进行特殊处理。
// 这里判断了sNumCandidate是不是全是半角数字或者全角数字。
// *需要注意的是*,IsAllChineseNum() 还有部分日期数字的判断功能,不仅仅是全角判断
while(sAtom[0]!=0&&(IsAllNum((unsigned char *)sNumCandidate)||IsAllChineseNum(sNumCandidate)))

...{
//Merge all seperate continue num into one number
//sAtom[0]!=0: add in 2002-5-9
// k? 在遥远的前方,在那函数入口的地方,被初始为0
// 将sNumCandidate对应的词放到结果集m_pWordSeg[nIndex][k].sWord中
strcpy(m_pWordSeg[nIndex][k].sWord,sNumCandidate);
//Save them in the result segmentation

// 开始看下一个分段,将下一段的文字放到sAtom中
i++;//Skip to next atom now
sAtom[0]=0;
// *注意* 这里是[i+1],而不是i,也就是说sAtom里面是下一个词啦。
while(j<nSegRoute[nIndex][i+1])

...{//Generate the word according the segmentation route
strcat(sAtom,m_graphSeg.m_sAtom[j]);
j++;
}
// 将sAtom追加到sNumCandidate中。
// 下一个循环的时候依旧再看一下这个sNumCandidate是否是数字。
// 如果是数字,就替换原有的m_pWordSeg[nIndex][k].sWord
strcat(sNumCandidate,sAtom);
}
// 点评:如果仅仅是为了合并所有的数字,这个循环臃肿了。
// 可以直接循环判断出数字所在的范围,然后一次性的追加即可。
// 合并数字这件事情应该在原子处理的时候进行处理,唯一需要注意的是含有数字的日期的合并放在原子上可能并不合适。
// 至于合并含有除了[0-90-9零-九]以外的字符,比如分之,大写数字,[几数第上成] *,应该放在这里,但不能是原子切分那里。

//
unsigned int nLen=strlen(m_pWordSeg[nIndex][k].sWord);
if(
(nLen==4 && CC_Find("第上成±—+∶·./",m_pWordSeg[nIndex][k].sWord))
||
(nLen==1 && strchr("+-./",m_pWordSeg[nIndex][k].sWord[0]))
)

...{
// 这里的判断很不解。
// 第一条是判断前缀是否是数字的前缀,但是为什么长度是4?如果长度是4的话,永远都无法满足啊?恐怕这里的长度应该是写2的。
// 第二条是判断如果只有一个字符而且还是数字的前缀。
//Only one word
strcpy(sCurWord,m_pWordSeg[nIndex][k].sWord);//Record current word
// i--?为啥让i退一步呢?
// 什么情况能进这个条件判断语句块呢?我们在上面的循环得到了字符的前缀,可是却发现后面的字符不是数字。
// i--,是说既然后面不是数字,那么我们在前面while loop里面的那个i++就盲目了。我们需要退回到这个"第上成"这个字接着考虑其他case。
i--;
}
else if(m_pWordSeg[nIndex][k].sWord[0]==0)//Have never entering the while loop

...{
// 因为当前词不是数字,没能够进入前面的循环。
// 将当前词放入结果,并且记录当前词。
strcpy(m_pWordSeg[nIndex][k].sWord,sAtom);
//Save them in the result segmentation
strcpy(sCurWord,sAtom);//Record current word
}
else

...{
// 到这里就说明进入了前面的while loop,而且不仅仅是一个前缀而已,是个真的数字。
// 真的么?看下面两个if判断的意思,还是可能出现不是数字的,因此还需要i--退一步考虑。
//It is a num
if(
// 看保存在记录里的词是不是"--","—"或者仅仅是一个"-"
strcmp("--",m_pWordSeg[nIndex][k].sWord)==0
||
strcmp("—",m_pWordSeg[nIndex][k].sWord)==0
||
m_pWordSeg[nIndex][k].sWord[0]=='-' && m_pWordSeg[nIndex][k].sWord[1]==0
)//The delimiter "--"

...{
// 设置词性为 'w' : 标点符号。显然也不是数字了,所以i--,退回一个词。
nPOS=30464;//'w'*256;Set the POS with 'w'
i--;//Not num, back to previous word
}
else

...{
//Adding time suffix

char sInitChar[3];
unsigned int nCharIndex=0;
//Get first char
// 取第一个字符。这里是通过判断char是否小于零,从而判断是不是汉字,需不需要追加一个字符的。
sInitChar[nCharIndex]=m_pWordSeg[nIndex][k].sWord[nCharIndex];
if(sInitChar[nCharIndex]<0)

...{
nCharIndex+=1;
sInitChar[nCharIndex]=m_pWordSeg[nIndex][k].sWord[nCharIndex];
}
nCharIndex+=1;
sInitChar[nCharIndex]='';
// 这个长长的判断是干嘛的呢?我改写为缩进格式,更利于逻辑上的理解。
// 其实就是为了把原来的[数字], [-][数字],的分词调整为:[数字]、[-]、[数字]
// 我们看看具体的实现。先进行条件判断:
if(
// 1、只考虑第二个词及其以后的词,因为这里需要考虑前一个词的词性
k > 0
&&
// 2、前一个词的词性是0x6D00('m')数字或0x7400
(
abs(m_pWordSeg[nIndex][k-1].nHandle) == 27904
||
abs(m_pWordSeg[nIndex][k-1].nHandle) == 29696
)
&&
// 3、第一个字符是减号
(
strcmp(sInitChar,"—") == 0
||
sInitChar[0] == '-'
)
// 4、除了第一个字符还有别的字符。呵呵,其实只有负号的已经在前面被过滤了,按理说这里不该再担心这个问题了。
&&
(
strlen(m_pWordSeg[nIndex][k].sWord)>nCharIndex)
)

...{
// 这个条件判断到底是什么条件?下面这个注释注释的好,无非就是针对:
// [数字][-—][数字]
// 的形式把减号从中分离出来。汗一下……
//3-4月
//27904='m'*256
//Split the sInitChar from the original word
strcpy(m_pWordSeg[nIndex][k+1].sWord,m_pWordSeg[nIndex][k].sWord+nCharIndex);
m_pWordSeg[nIndex][k+1].dValue=m_pWordSeg[nIndex][k].dValue;
m_pWordSeg[nIndex][k+1].nHandle=27904;
m_pWordSeg[nIndex][k].sWord[nCharIndex]=0;
m_pWordSeg[nIndex][k].dValue=0;
m_pWordSeg[nIndex][k].nHandle=30464;//'w'*256;
// 将分离出的减号加入优化后的图。
m_graphOptimum.SetElement(
nStartVertex,
nStartVertex+1,
m_pWordSeg[nIndex][k].dValue,
m_pWordSeg[nIndex][k].nHandle,
m_pWordSeg[nIndex][k].sWord
);
nStartVertex+=1;
k+=1;
}

// 取得第k个词的长度。如果进了上述循环,那么nLen长度是第二个[数字]的长度。
nLen=strlen(m_pWordSeg[nIndex][k].sWord);
// 如果sAtom是 [月日时分秒]或者 "月份" 的话。
// 等等~,sAtom和m_pWordSeg[nIndex][k].sWord难道还不一样么?
// 从前面的代码看,sAtom最多也就比sWord多一个减号啊?
// 我们再回去看第一个循环的时候就会注意到,在那里,sAtom被赋予了[i+1]的字符串
// 也就是说sAtom实际上已经是下一个词了。
// 那么重新解释一下下面判断的意思就是:
// [数字]([月日时分秒]|月份)
if(
(
strlen(sAtom) == 2
&&
CC_Find("月日时分秒",sAtom)
)
||
strcmp(sAtom,"月份") == 0
)

...{
// 如果是如下模式:
// [数字]([月日时分秒]|月份)
// 将他们视为同一个词,加入到m_pWordSeg里,
// 并且将sCurWord设置为"未##时",词性为't'
//2001年
// ^--- 啊?兄弟弄错了吧?下一个条件才是年呢。这个是月啊。:)
strcat(m_pWordSeg[nIndex][k].sWord,sAtom);
strcpy(sCurWord,"未##时");
nPOS=-29696;//'t'*256;//Set the POS with 'm'
}
else if(strcmp(sAtom,"年")==0)

...{
// 同上表示的话,应该是这个模式:
// [数字][年]
// 通过IsYearTime确认[数字]是合法的数字
// 如果满足就将年追加其后,将当前词改为特征词"未##时",并且词性改为't'
if(IsYearTime(m_pWordSeg[nIndex][k].sWord))//strncmp(sAtom,"年",2)==0&&

...{//1998年,
strcat(m_pWordSeg[nIndex][k].sWord,sAtom);
strcpy(sCurWord,"未##时");
nPOS=-29696;//Set the POS with 't'
}
else

...{
// 如果不满足,那么这些数字就仅仅是数字
// 将当前词改为特征词"未##数",词性设为'm'--数字。
// 并且因为不是时间,所以就得i--退一步。
strcpy(sCurWord,"未##数");
nPOS=-27904;//Set the POS with 'm'
i--;//Can not be a time word
}
}
else

...{
// 又不是月份,又不是年,那现在看看是不是时分秒。
//早晨/t 五点/t
// .*[点]$
// 看看是不是以"点"结尾的
if(strcmp(m_pWordSeg[nIndex][k].sWord + strlen(m_pWordSeg[nIndex][k].sWord) - 2,"点") == 0)

...{
// 如果是的话就改sCurWord为特征词"未##时",词性为't'
strcpy(sCurWord,"未##时");
nPOS=-29696;//Set the POS with 't'
}
else

...{
// 如果不是以[∶·././]结尾的,那就改sCurWord为特征词"未##数",词性为'm'
if(
!CC_Find("∶·./",m_pWordSeg[nIndex][k].sWord+nLen-2)
&&