最近需要用到中文分词,本来想省事,用python的第三方库结巴分词,但看了下API,计算文本关键词的方法没有没有返回关键字对应的权值,翻了下文档应该是不还不支持,只好继续使用中科院的那套ICTCLAS分词组件,2013最新版已经改名为NLPIR,函数名前缀做了改变,少许函数的参数也有了变化,对于这种没有产品意识的产品来说,我已无力吐槽,我等只能跟着修改中间模块的源代码了。

转成Python调用常用的有三种模式:

python使用ctypes加载so文件:高耦合度,要去so文件里翻函数的链接符号,太繁琐,但也是最直接的方式。

通过swig将c++的声明文件转化为swig的表达方式,再通过g++编译成python可直接import的模块:无奈swig对一些复杂的C++数据结构还支持的不是很好,类似NLPIR中提供的NLPIR_ParagraphProcessA(NLPIR_API const result_t * NLPIR_ParagraphProcessA(const char *sParagraph,int *pResultCount,bool bUserDict=true);)无法很好的被swig转换,按照官网的一些建议折腾半天也没成功,有搞定的同学跟我说声儿。

使用boost.python库将NLPIR的声明文件进行转化,之后用g++编译为python可直接import的模块,由于boost是C++系的,对原始的数据结构兼容性好,本文选用该方法。

本文测试环境:

Ubuntu 10.04 x64 + NLPIR2013

首先安装boost.python开发包:

apt-get install libboost-python-dev

将官方提供的libNLPIR.so,NLPIR.h置于同一目录下,并在该目录下新建名为nlpir.cpp的文件,内容如下:

# Modified by pnig0s @ 20131214#include #include #include #include "NLPIR.h"using namespace boost::python;struct my_result{ int start; int length; std::string sPOS; int iPOS; int word_ID; int word_type; int weight;};bool ict_init(const char* pDirPath, int encode=0){ return NLPIR_Init(pDirPath);} bool ict_exit(){ return NLPIR_Exit();} unsigned int import_dict(const char *sFilename){ return NLPIR_ImportUserDict(sFilename);} const char * process_str(const char *sParag, int bTagged){ return NLPIR_ParagraphProcess(sParag, bTagged);} my_result& copy_result_t(my_result& re, const result_t& t){ re.start = t.start; re.length = t.length; re.sPOS = std::string(&t.sPOS[0]); re.iPOS = t.iPOS; re.word_ID = t.word_ID; re.word_type = t.word_type; re.weight = t.weight; return re;} list process_str_ret_list(const char *sParag){ int pResultCount = 0; const result_t* re = NLPIR_ParagraphProcessA(sParag, &pResultCount); list result; for (int i=0; i("result_t") .def_readonly("start", &my_result::start) .def_readonly("length", &my_result::length) .def_readonly("ipos", &my_result::iPOS) .def_readonly("word_id", &my_result::word_ID) .def_readonly("spos", &my_result::sPOS) .def_readonly("word_type", &my_result::word_type) .def_readonly("weight", &my_result::weight); def("ict_init", ict_init); def("ict_exit", ict_exit); def("import_dict", import_dict); def("process_str", process_str); def("process_str_ret_list", process_str_ret_list); def("process_file", process_file); def("process_str_ret_word_count", process_str_ret_word_count); def("add_user_word", add_user_word); def("get_keywords", get_keywords); def("save_user_dict", save_user_dict); def("del_user_word", del_user_word); def("set_pos_map", set_pos_map); enum_("POSMAP") .value("ICT_SECOND", ICT_SECOND) .value("ICT_FIRST", ICT_FIRST) .value("PKU_SECOND", PKU_SECOND) .value("PKU_FIRST", PKU_FIRST); enum_("CODE") .value("CHAR_GBK_CODE", CHAR_GBK_CODE) .value("CHAR_UTF8_CODE", CHAR_UTF8_CODE) .value("CHAR_BIG5_CODE", CHAR_BIG5_CODE) .value("CHAR_GBK_FANTI_CODE", CHAR_GBK_FANTI_CODE);}
g++ nlpir.cpp -shared -o nlpir.so -I/usr/include/python2.6 -lboost_python -L. -lNLPIR -m64 -DOS_LINUX -fPIC

我们只需要最终编译出来的nlpir.so,可以使用python直接import。ictclas一直以来对UTF-8的支持有先天缺陷,最新版虽然可以通过在init的时候指定UTF8_CODE支持UTF8文本的输入,但部分函数在处理UTF8编码的文本时还是存在问题,所以建议统一传入gb2312编码的文本,下面给出个用python封装过的例子:

#!/usr/bin/python# coding:utf-8# pnig0s 2013/12/14 import nlpir as icimport charset as csimport os curr_path = os.path.abspath(os.path.dirname(__file__)) class wordSeg: def __init__(self,userDict='',posMap=0): self.preText = '' self.rawCharset = '' self.resultDict = [] self.resultStr = '' self.Init = ic.ict_init self.Exit = ic.ict_exit self.ImportUserDict = ic.import_dict self.ProcessToList = ic.process_str_ret_list self.ProcessToStr = ic.process_str self.GenKeyWords = ic.get_keywords self.setPosMap = ic.set_pos_map self.ICT_FIRST = ic.POSMAP.ICT_FIRST self.ICT_SECOND = ic.POSMAP.ICT_SECOND self.PKU_FIRST = ic.POSMAP.PKU_FIRST self.PKU_SECOND = ic.POSMAP.PKU_SECOND self.GBK_CODE = ic.CODE.CHAR_GBK_CODE self.UTF8_CODE = ic.CODE.CHAR_UTF8_CODE self.BIG5_CODE = ic.CODE.CHAR_BIG5_CODE self.GBK_FANTI_CODE = ic.CODE.CHAR_GBK_FANTI_CODE self.init(userDict,posMap) def init(self,userDict= '',posMap= 0): if not self.Init(curr_path,self.GBK_CODE): print 'ICTCLAS init failed.' else: if userDict: if not self.ImportUserDict(curr_path+'/'+userDict): print 'Import user dict failed.' else: if posMap: if not self.setPosMap(posMap): print 'Set pos map failed.' def segment(self,rList= True,rKeyWords= True,rPOS= True): ''' 封装了ICTCLAS两种分词模式: rList = True时,返回分词结果的详细数据结构,其中rKeyWords指明以关键词模式返回或普通模式返回 rList = False时,返回所有分词片段组成的字符串,其中rPOS指明是否进行词性标注。 ''' if all((rList,self.preText)): optResult = [] self.preText = self.convertToGB(self.preText) splitResult = self.ProcessToList(self.preText) if rKeyWords: token_str = self.GenKeyWords(self.preText,len(splitResult),True) if token_str: token_list = token_str.split('#')[:-1] if vars().has_key('token_list') and token_list: for token in token_list: token = token.split('/') optResult.append({'spos':token[1], 'weight':float(token[2]), 'word_piece':self.decodeText(token[0])}) else: for rs in splitResult: optResult.append({'spos':rs.spos,'weight':float(0),'word_piece':self.decodeText(self.preText[rs.start:(rs.start+rs.length)])}) self.resultDict = optResult if not rList and self.preText: self.resultStr = self.decodeText(self.ProcessToStr(self.convertToGB(self.preText),1 if rPOS else 0)) def convertToGB(self,Text = ''): '''统一将文本编码转换为ICTCLAS要求的gb2312''' try: charset = cs.check(html=Text) except Exception,e: charset = 'ascii' self.rawCharset = 'gb2312' if charset == 'ascii': rc = Text.encode('gb2312','ignore') else: rc = Text.decode(charset,'ignore').encode('gb2312','ignore') return rc def decodeText(self,Text = ''): '''统一将文本编码转换为操作系统默认的utf8''' if self.rawCharset: return Text.decode(self.rawCharset,'ignore') def test(): ws = wordSeg('',posMap=0) ws.preText = '吃葡萄不吐葡萄皮,不吃葡萄倒吐葡萄皮。' ws.segment(rList=True,rKeyWords=False) for result in ws.resultDict: for key,value in result.iteritems(): print '%s:%s' % (key,value) ws.Exit() if __name__ == '__main__': test()