diff --git a/CMakeLists.txt b/CMakeLists.txt index 56361fb..121a7f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ ENDIF() ADD_SUBDIRECTORY(src) ADD_SUBDIRECTORY(dict) -if (!${APPLE}) +IF("${CMAKE_SYSTEM}" MATCHES "Linux") ADD_SUBDIRECTORY(script) ADD_SUBDIRECTORY(conf) endif() diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..f548a3f --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,23 @@ +## v2.3.1 + +1. 修复安装时的服务启动问题(不过安装切词服务只是linux下的一个附加功能,不影响核心代码。) + + +## v2.3.0 + +1. 增加`KeywordExtractor.hpp`来进行关键词抽取。 +2. 使用`gtest`来做单元测试。 + +## v2.2.0 + +1. 性能优化,提升切词速度约6倍。 +2. 其他暂时也想不起来了。 + +## v2.1.1 (v2.1.1之前的统统一起写在 v2.1.1里面了) + +1. 完成__最大概率分词算法__和__HMM分词算法__,并且将他们结合起来成效果最好的`MixSegment`。 +2. 进行大量的代码重构,将主要的功能性代码都写成了hpp文件。 +3. 使用`cmake`工具来管理项目。 +4. 使用`Limonp`作为工具函数库,比如日志,字符串操作等常用函数。 +5. 使用`Husky` 搭简易分词服务的服务器框架。 + diff --git a/README.md b/README.md index c7c96c4..87e42d6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ 之所以全写成hpp文件,是因为这样在别的项目需要使用到中文分词功能的时候直接`#include"xx.hpp" `进来就可以使用,无需麻烦的链接。 +实践证明写成hpp使用起来真的很爽,在后面提到的在iOS应用中的使用,和包装成`Node.js`的扩展[NodeJieba]都特别顺利。 + +如果对代码细节感兴趣的请见 [代码详解] + ## 中文编码 现在支持utf8,gbk编码的分词。 @@ -12,8 +16,8 @@ ### 依赖 -* g++ (version >= 4.6); -* cmake (version >= 2.8); +* g++ (version >= 4.6 recommended); +* cmake (version >= 2.8 recommended); ### 下载和安装 @@ -38,6 +42,8 @@ make test ### 启动服务 +因为服务的后台运行需要`start-stop-daemon`,在ubuntu下是自带的。但是在CentOS下就需要自己安装了。 + ``` #Usage: /etc/init.d/cjserver {start|stop|restart|force-reload} #启动 @@ -147,58 +153,42 @@ Full方法切出所有字典里的词语。 Query方法先使用Mix方法切词,对于切出来的较长的词再使用Full方法。 +### 关键词抽取 -## 模块详解 +``` +make && ./test/keyword.demo +``` -本项目主要是如下目录组成: +you will see: -### src +``` +我是蓝翔技工拖拉机学院手扶拖拉机专业的。不用多久,我就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰。 + -> +["CEO:11.7392", "蓝翔:11.7392", "白富美:11.7392", "升职:10.8562", "加薪:10.6426"] +``` -核心目录,包含主要源代码。 +关键词抽取的demo代码请见`test/keyword_demo.cpp` -#### TrieManager模块 -TrieManager.hpp 提供一个单例TrieManager,负责管理trie树。 -通过该单例获取trie树时,会先判断是否已经由该字典文件生成了一颗trie树,如果已有则返回已有的trie树,否则重新创建一颗trie树返回。 -#### Trie树 -Trie.hpp 负责载入词典的trie树,主要供Segment模块使用。 +## 相关应用 -#### Segment模块 - -MPSegment.hpp -(Maximum Probability)最大概率法:负责根据Trie树构建有向无环图和进行动态规划算法,是分词算法的核心。 - -HMMSegment.hpp -是根据HMM模型来进行分词,主要算法思路是根据(B,E,M,S)四个状态来代表每个字的隐藏状态。 -HMM模型由dict/下面的`hmm_model.utf8`提供。 -分词算法即viterbi算法。 - -FullSegment.hpp -枚举句子中所有可能成词的情况,找出字典里存在的即可。 - -#### TransCode模块 - -TransCode.hpp 负责转换编码类型,将utf8和gbk转换成`uint16_t`类型,也负责逆转换。 - -### src/Husky - -提供服务的框架代码, - -详见: https://github.com/aszxqw/husky - -### src/Limonp - -主要是一些工具函数,例如字符串操作等。 -直接include就可以使用。 - -详见: https://github.com/aszxqw/limonp - -## 关于CppJieba的跨语言包装使用 +### 关于CppJieba的跨语言包装使用 收到邮件询问跨语言包装(ios应用开发)使用的问题,这方面我没有相关的经验,建议参考如下python使用cppjieba的项目: -[jannson] 开发的供 python模块调用的项目 [cppjiebapy] , 和相关讨论 [cppjiebapy'discussion] . +[jannson] 开发的供 python模块调用的项目 [cppjiebapy] , 和相关讨论 [cppjiebapy_discussion] . +### NodeJieba + +如果有需要在`node.js`中使用分词,不妨试一下[NodeJieba]。 + +### simhash + +如果有需要在处理中文文档的的相似度计算,不妨试一下[simhash]。 + +## 演示 + +http://cppjieba-webdemo.herokuapp.com/ ## 客服 @@ -206,12 +196,15 @@ TransCode.hpp 负责转换编码类型,将utf8和gbk转换成`uint16_t`类型 ## 鸣谢 -"结巴中文"分词作者: SunJunyi +"结巴"中文分词作者: SunJunyi https://github.com/fxsjy/jieba -顾名思义,之所以叫CppJieba,是参照SunJunyi大神的Jieba分词Python程序写成的,所以饮水思源,再次感谢SunJunyi。 +顾名思义,之所以叫CppJieba,是参照Jieba分词Python程序写成的,所以饮水思源,再次感谢SunJunyi。 [CppJieba]:https://github.com/aszxqw/cppjieba [jannson]:https://github.com/jannson [cppjiebapy]:https://github.com/jannson/cppjiebapy -[cppjiebapy'discussion]:https://github.com/aszxqw/cppjieba/issues/1 +[cppjiebapy_discussion]:https://github.com/aszxqw/cppjieba/issues/1 +[NodeJieba]:https://github.com/aszxqw/nodejieba +[simhash]:https://github.com/aszxqw/simhash +[代码详解]:http://aszxqw.github.io/jekyll/update/2014/02/10/cppjieba-dai-ma-xiang-jie.html diff --git a/script/cjserver b/script/cjserver index 11cf5f2..85666f0 100755 --- a/script/cjserver +++ b/script/cjserver @@ -20,7 +20,7 @@ case "$1" in mkdir -p $RUNDIR touch $PIDFILE chmod 755 $RUNDIR - if start-stop-daemon --start --quiet --umask 007 --pidfile $PIDFILE --exec /bin/bash -- -c "$DAEMON $DAEMON_ARGS >> $LOGFILE 2>&1" + if start-stop-daemon --start --quiet --pidfile $PIDFILE --exec /bin/bash -- -c "$DAEMON $DAEMON_ARGS >> $LOGFILE 2>&1" then echo "$NAME." else diff --git a/src/KeywordExtractor.hpp b/src/KeywordExtractor.hpp index a78ea1f..228df79 100644 --- a/src/KeywordExtractor.hpp +++ b/src/KeywordExtractor.hpp @@ -138,8 +138,11 @@ namespace CppJieba itr ++; } - keywords.resize(MIN(topN, wordmap.size())); - partial_sort_copy(wordmap.begin(), wordmap.end(), keywords.begin(), keywords.end(), _cmp); + keywords.clear(); + std::copy(wordmap.begin(), wordmap.end(), std::inserter(keywords, keywords.begin())); + topN = MIN(topN, keywords.size()); + partial_sort(keywords.begin(), keywords.begin() + topN, keywords.end(), _cmp); + keywords.resize(topN); return true; } private: @@ -153,7 +156,7 @@ namespace CppJieba } private: - static bool _cmp(const pair& lhs, const pair& rhs) + static bool _cmp(const pair& lhs, const pair& rhs) { return lhs.second > rhs.second; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8b35898..faf5003 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,6 @@ SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/test) ADD_EXECUTABLE(segment.demo segment.cpp) +ADD_EXECUTABLE(keyword.demo keyword_demo.cpp) ADD_EXECUTABLE(load_test load_test.cpp) ADD_SUBDIRECTORY(unittest) diff --git a/test/keyword_demo.cpp b/test/keyword_demo.cpp new file mode 100644 index 0000000..9e0d9eb --- /dev/null +++ b/test/keyword_demo.cpp @@ -0,0 +1,13 @@ +#include "../src/KeywordExtractor.hpp" +using namespace CppJieba; + +int main(int argc, char ** argv) +{ + KeywordExtractor extractor("../dict/jieba.dict.utf8", "../dict/hmm_model.utf8", "../dict/idf.utf8"); + string s("我是蓝翔技工拖拉机学院手扶拖拉机专业的。不用多久,我就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰。"); + vector > wordweights; + size_t topN = 5; + extractor.extract(s, wordweights, topN); + cout<< s << "\n -> \n" << wordweights << endl; + return EXIT_SUCCESS; +} diff --git a/test/unittest/TKeywordExtractor.cpp b/test/unittest/TKeywordExtractor.cpp index 8a84985..3b9c184 100644 --- a/test/unittest/TKeywordExtractor.cpp +++ b/test/unittest/TKeywordExtractor.cpp @@ -3,63 +3,16 @@ using namespace CppJieba; -const char* KEYWORD_EXT_TEST_SENTENCE = "我来自北京邮电大学。 学号123456"; + TEST(KeywordExtractorTest, Test1) { KeywordExtractor extractor("../dict/jieba.dict.utf8", "../dict/hmm_model.utf8", "../dict/idf.utf8"); - const char* res[] = {"学号", "北京邮电大学"}; - vector words; - ASSERT_TRUE(extractor); - ASSERT_TRUE(extractor.extract(KEYWORD_EXT_TEST_SENTENCE, words, 2)); - ASSERT_EQ(words, vector(res, res + sizeof(res)/sizeof(res[0]))); + string s("我是蓝翔技工拖拉机学院手扶拖拉机专业的。不用多久,我就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰。"); + string res; + vector > wordweights; + size_t topN = 5; + extractor.extract(s, wordweights, topN); + res << wordweights; + ASSERT_EQ(res, "[\"CEO:11.7392\", \"蓝翔:11.7392\", \"白富美:11.7392\", \"升职:10.8562\", \"加薪:10.6426\"]"); } - -TEST(KeywordExtractorTest, Test2) -{ - KeywordExtractor extractor("../dict/jieba.dict.utf8", "../dict/hmm_model.utf8", "../dict/idf.utf8"); - const char* res[] = {"学号", "北京邮电大学", "123456", "来自"}; - vector words; - ASSERT_TRUE(extractor); - ASSERT_TRUE(extractor.extract(KEYWORD_EXT_TEST_SENTENCE, words, 9)); - ASSERT_EQ(words, vector(res, res + sizeof(res)/sizeof(res[0]))); -} - - -TEST(KeywordExtractorTest, Test3) -{ - ifstream ifs("../test/testdata/weicheng.utf8"); - ASSERT_TRUE(!!ifs); - string str((istreambuf_iterator(ifs)), (istreambuf_iterator())); - KeywordExtractor extractor("../dict/jieba.dict.utf8", "../dict/hmm_model.utf8", "../dict/idf.utf8"); - const char* res[] = {"柔嘉", "小姐", "孙小姐", "方鸿渐", "鸿渐"}; - const char* res2 = "[\"柔嘉:5611.34\", \"小姐:4268.75\", \"孙小姐:3789.41\", \"方鸿渐:3030.35\", \"鸿渐:2552.93\"]"; - vector keywords; - string resStr; - vector > keywords2; - extractor.extract(str, keywords, 5); - extractor.extract(str, keywords2, 5); - ASSERT_EQ(keywords, vector(res, res + sizeof(res)/sizeof(res[0]))); - resStr << keywords2; - ASSERT_EQ(res2, resStr); - -} - -//TEST(KeywordExtractorTest, Test4) -//{ -// ifstream ifs("../test/testdata/weicheng.utf8"); -// ASSERT_TRUE(!!ifs); -// string str((istreambuf_iterator(ifs)), (istreambuf_iterator())); -// KeywordExtractor extractor("../dict/jieba.dict.utf8", "../dict/hmm_model.utf8", "../dict/idf.utf8"); -// //const char* res[] = {"小姐", "孙小姐", "方鸿渐", "自己", "没有"}; -// vector > keywords; -// extractor.extract(str, keywords, 5); -// //print(keywords); -// string res; -// res << keywords; -// print(keywords); -// print(__LINE__); -// exit(1); -// ASSERT_EQ(res, "[\"小姐:4268.75\", \"孙小姐:3789.41\", \"方鸿渐:3030.35\", \"自己:2300.54\", \"没有:2104.27\"]"); -// -//}