时间关系,linux环境下的配置过程先放着,有时间会一并整理出来···
现在就先从老师给的cygwin环境开始。
开始之前需要先准备以下文件:
1. 老师给的 Cygwin_setup.rar,没有的可以到http://dl.vmall.com/c0cu5yxa2i下载
2. 老师给的cygwin.rar,这里面是老师配好的linux环境打包,若不需要此项的则在linux环境下自行配置,具体配置过程有空整理放上来。
3. 老师给的2013.q1.new.txt测试文档,里面是以xml格式编排好的类似网页的文本,作为我们测试时使用。
任务概要:
Xapian 为核心开发一个搜索程序,以 13 年第一季度的新浪新闻为检索目标(请参加附件文件“2013.q1.new.txt”,XML 格式),要求自行设计文档解析程序、调用 xapian 建索引并实现一般检索、以及一个特殊的修饰符搜索功能(如 url 搜索、标题搜索、时间搜索等),程序运行即查询过程采用 web 界面和名命令行方式均可。
从实验要求上,我们需要从老师给的2013.q1.new.txt测试文件内读出语料,由于测试文件的格式使用xml,所以第一步我们需要使用xml解析程序,将测试文件内的数据解析出来(此解析并非加密解密中的解密,而是因为xml有着良好的数据格式,可以通过特定的解析程序方便的读出各部分信息)。在这里我们使用开源的xml解析程序TinyXML2。
官网 https://github.com/leethomason/tinyxml2
参考文档 http://grinninglizard.com/tinyxml2docs/index.html
解析后我们需要将获得的语料进行分词,分词过后得到的关键字就是我们搜索时使用的关键字。分词我们使用开源的分词组件开源分词组件SCWS。
官网 http://www.xunsearch.com/scws/
参考文档 http://www.xunsearch.com/scws/docs.php
最后我们有了关键字,有了我们的数据,就可以通过xapian建立数据库,将读取到的文档信息和关键字对应起来,当我们需要进行数据检索时则可以利用关键字找到对应的文档信息。
实验步骤:
因为使用到的很多开源库在linux环境下配置比较方便,所以可以使用linux系统安装也可以使用Cygwin,Cygwin是一个在windows平台上运行的类UNIX模拟环境
1.cygwin的安装:
Cygwin是一个在windows平台上运行的类UNIX模拟环境
关于cygwin的安装老师已经给了完整的配置过程在《多媒体搜索》实验课任务指南内,这里就不多余描述了。
安装完成后,请用老师给出的cygwin.rar文件内的Cygwin文件夹替换你安装时选择的cygwin目录文件夹。这样你的Cygwin模拟的linux系统就已经是配置好xapian,SCWS,TinyXML2的系统了(老师这个福利简直了)。接下来你需要做的就是在这个已经完全配置好的系统上做出自己的东西了。
2.编译示例程序
打开Cygwin,你会发现这是一个命令行程序,刚进入的时候你会在你的用户路径下,
Administrator@USER-AF9V7JPDNB ~
这是我刚进入时的用户路径,在这里简单介绍几个命令:
ls 查看目录中的文件
ls -F 查看目录中的文件
ls -l 显示文件和目录的详细资料
cd .. 返回上级目录
cd *** 进入指定目录
cd / 返回系统根目录
现在你需要通过 cd / 命令返回到系统根目录。通过ls命令可以看到当前目录下的文件,也就和你cygwin文件夹下的目录是一样的,有了一个命令行程序,再通过对cygwin文件夹的操作你可以直接对文件进行增删改查,你就相当于有一个完整的可操作的linux系统了。
再通过cd home/Administrator 进入老师给我们准备好的Administrator目录。完整操作命令如下:
Administrator@USER-AF9V7JPDNB ~ $ cd / Administrator@USER-AF9V7JPDNB / $ cd home Administrator@USER-AF9V7JPDNB /home $ cd Administrator Administrator@USER-AF9V7JPDNB /home/Administrator
进入Administrator目录后,在cd test进入test目录,里面就有老师给我们准备好的实例程序了。接下来我们需要对他们进行编译。
用windows资源管理器进入test目录,用记事本打开此目录下的Makefile文件,你会发现这里已经有写好的编译命令:
test: test_scws.cpp test_xapian.cpp test_xml.cpp
g++ -o test_scws -I/usr/local/scws/include -L/usr/local/scws/lib test_scws.cpp -lscws -Wl,--rpath -Wl,/usr/local/scws/lib
g++ -o test_xapian test_xapian.cpp -l xapian
g++ -o test_xml test_xml.cpp tinyxml2.cpp tinyxml2.h
这里编译了三个cpp文件,分别是test_scws.cpp test_xapian.cpp test_xml.cpp
回到cygwin命令行下,输入make,此时Makefile文件内描述的编译命令就会被执行,一键轻松搞定。
目录下将会多出test_scws.exe,test_xapian.exe,test_xml.exe三个文件
此时在cygwin命令行下,输入./test_scws,可以发现这个程序是对例句:“要切分的字符串或文件,如不指定则程序自动读取标准输入,每输入一行执行一次分词”
进行中文分词,可以在windows资源管理器下打开test_scws.exe查看源码,这就是我们之后分词的参考程序。
输入./test_xapian,可以发现这个程序是一个搜索查询程序,打开test_xapian.cpp。可以看出源文件内首先生成“This is No1, bla bla bla”和“This is No2, bla bla bla”两个文档,并用“bla“作为item,然后对"bla"进行查询。
输入./test_xml,会有很多内容,这些内容比较没有规律性,需要参照test_xml.cpp源码阅读,这个程序是TinyXML2的原版示例程序。
3.源码导读
我们的任务需要同时使用到这三个组件,在整合这三个组件之前,先对三个源码进行一定的解读,方便接下来的整合。
test_xml
首先从test_xml开始,打开test_xml程序,这个程序非常冗长,但是对TinyXML有一个非常详细的介绍。针对本次任务内容需要做有针对性的阅读。
首先我们找到main函数的所在,看到main函数中首先建立了一个目录/resources/out/,用windows资源管理器可以看到此目录。
如果执行此test_xml.exe时带了参数,则会将参数读入一个XMLDocument数据结构,若无参数,则往下进入4个example,而后再进入其他实例。
简单起见可以把4个XMLTest(328行)之后的程序都删除,我们只需要看看这4个example就可以对程序有一个大概的了解。
首先看到XMLTest( "Example-1", 0, example_1() );(325行)
这个函数调用了example_1()函数,找到对应位置(91行),这个函数内载入了"resources/dream.xml"文件。这个例程展示了XMLDocument结构对文件的载入方法之一····
再找到example_2()函数(109行),这个函数先定义了一个char数组,然后通过XMLDocument的Parse(const char*)方法将文本放入XMLDocument。
example_3()函数(128行)是比较重点的函数,通过Parse方法传入doc文件后,调用了FirstChildElement("PLAY")方法获得了第一个xml标签,再通过XMLElement的GetText()方法即可获得指定标签内的字符串了。
example_4()主要是对xml标签内的attribute进行获取,本次实验不需要。
看到这里再看看老师给我们的2013.q1.new.txt文件内的数据格式
<doc>
<url>
http://go.rss.sina.com.cn/redirect.php?url=http://sports.sina.com.cn/j/2013-01-01/00316360053.shtml
</url>
<title>
申花新年凌晨宣布签两新人 王长庆携实德小将加盟_国内足球-中超_新浪竞技风暴_新浪网
</title>
<content>
新浪体育讯 北京时间2013年1月1日……
</content>
</doc>
<doc>
<url>
http://go.rss.sina.com.cn/redirect.php?url=http://sports.sina.com.cn/o/2013-01-01/00516360059.shtml
</url>
<title>
林丹跨年盛典星味不输五月天 对阵小选手意外落败_综合体育_新浪竞技风暴_新浪网
</title>
<content>
新浪体育讯 2012年……
</content>
</doc>
因此对于此文件的处理就有了一个大致的思路:
首先使用LoadFile( "yourPath" );载入2013.q1.new.txt,再通过example_3()内的方法获得doc标签,再依次获得doc标签下的url、title、content即可。获取结束后通过XMLEliment的NextSiblingElement( "doc" )方法即可获取下一个doc文件。
Element类的方法参考此网页:http://grinninglizard.com/tinyxml2docs/classtinyxml2_1_1_x_m_l_element.html
NextSiblingElemtn方法在此文档中并没有提及,而是在XMLHandler类下的一个示例程序内才有,因此推荐大家多看看其他类的方法和示例,也许能找到更好的实现方法。
部分代码如下:
doc->LoadFile( "2013.q1.new.txt" ); //获取文件,请先将文件放至test目录下 XMLElement* titleElement = doc->FirstChildElement( "doc" );//获得第一个doc标签 const char* title = titleElement->FirstChildElement( "title" )->GetText(); //获取title内容,同理获得url和content titleElement = titleElement->NextSiblingElement( "doc" );//获取下一个doc标签
以上四行代码只是节选,具体使用时自己加入while循环等实现循环读入。
获得内容的char数组后。我们需要对content内容进行分词,之后要实现title检索,时间检索等还要对title标签和url标签进行分词(url内有时间)。
test_scws
这个例程相对简单,声明一个字符数组test作为我们需要分词的字符串,而后指定字符集等,再通过scws_send_text进行分词,结果通过一个res和cur指针进行读取,在这里注意res和cur为scws_res_t类型,这个类型可以直接和字符型指针进行转换,在之后获取分词,将分词传入xapian中需要转换为字符串。
test_xapian
xapian的内容非常丰富,因此这里提出的仅仅是一些简单的思路,在对xapian有了深入理解后大家可以做出更简单,更高效的程序,这里仅仅作为导读。
源码中首先创建一个可写数据库,创建方式有多种:
Xapian::DB_CREATE_OR_OPEN 打开以便读写,如果不存在则创建。
Xapian::DB_CREATE 总是创建新的database,如果存在则失败。
Xapian::DB_CREATE_OR_OVERWRITE 如果database存在的话则覆盖之,如果不存在则创建。
Xapian::DB_OPEN 打开以便读写,如果不存在则失败。
根据需要自己选择。如果不想每次运行一次程序结果就叠加在一起的话,推荐OVERWRITE方法。
在信息检索(IR)中,我们企图要获取的项称之为“document”,每一个document是被一个terms集合所描述的。
xapian内最重要的就是创建document,document内有term和data两种数据类型,其中term就类似于我们说的关键字,data就是document对应的数据。每次检索时通过term可以搜索到指定的data。
创建Document并设置data和term:
方法1:直接添加
Xapian::Document newdocument1; newdocument1.set_data("This is No1, bla bla bla"); newdocument1.add_term("bla"); database.add_document(newdocument1);
方法2:使用解析器
//创建并添加文档,第二种方式,使用解析器 string content = "This is No2, bla bla bla"; Xapian::Document newdocument2; Xapian::TermGenerator indexer; newdocument2.set_data(content); indexer.set_document(newdocument2); indexer.index_text(content); database.add_document(newdocument2);
推荐使用方法二,因为之后扩展功能内有特殊的修饰符搜索功能(如 url 搜索、标题搜索、时间搜索等),使用解析器可以创建带prefix的term。
创建query
Xapian::Enquire enquire(database); Xapian::Query query("bla"); cout << "Performing query `" << query.get_description() << "'" << endl; enquire.set_query(query); Xapian::MSet matches = enquire.get_mset(0, 10); cout << matches.size() << " results found" << endl;
这里不做太多解释,理解上应该没什么问题。接下来我们主要精力放在3个组件的组合上。
4.整合组件
首先我们以test_xml.cpp为我们主要进行操作的文件,打开并在include中加入
#include <xapian.h> #include <scws/scws.h> #include <stdio.h> #include <string.h>
由于xapian.h和scws/scws.h需要指定路径,因此打开Makefile文件对编译指令进行编辑,将xapian和scws的参数添加到test_xml后,添加后指令如下:
g++ -o test_xml test_xml.cpp tinyxml2.cpp tinyxml2.h -I/usr/local/scws/include -L/usr/local/scws/lib -l xapian -lscws -Wl,--rpath -Wl,/usr/local/scws/lib
接下来就是对test_xml.cpp进行符合要求的修改
可以将不需要的内容都删去,保留main函数,将main函数中
if ( argc > 1 ) {
以后的内容全部删去。
创建XMLDocument变量,通过LoadFile方法读入2013.q1.new.txt。
这里主要创建while循环,循环体内,通过以上test_xml导读内提到的方法依次提取title,url以及content。
如果你需要将完整的内容放入xapian的document中,还需要将提取到的内容进行一个整合。
将提取到的字符串放入分词组件
scws_send_text(s, title, strlen(title));
其中title为从xml获取到的const char*。
从res中不断获取结果,使用
char dividedword[100]={0}; memcpy(dividedword,title+cur->off,cur->len);
分词结果如下:
Name of title (1): 申花新年凌晨宣布签两新人 王长庆携实德小将加盟_国内足球-中超_新浪竞技风暴_新浪网
WORD: 申花 申花WORD: 新年 新年WORD: 凌晨 凌晨WORD: 宣布 宣布WORD: 签 签WORD: 两 两WORD: 新人 新人
WORD: 王 王WORD: 长庆 长庆WORD: 携 携WORD: 实德 实德WORD: 小将 小将WORD: 加盟 加盟
WORD: _ _
WORD: 国内足球 国内足球
WORD: - -
WORD: 中超 中超
WORD: _ _
WORD: 新浪 新浪WORD: 竞技 竞技WORD: 风暴 风暴
WORD: _ _
WORD: 新浪网 新浪网
Name of title (2): 林丹跨年盛典星味不输五月天 对阵小选手意外落败_综合体育_新浪竞技风暴_新浪网
WORD: 林丹跨 林丹跨WORD: 年 年WORD: 盛典 盛典WORD: 星 星WORD: 味 味WORD: 不 不WORD: 输 输WORD: 五月天 五月天
WORD: 对阵 对阵WORD: 小 小WORD: 选手 选手WORD: 意外 意外WORD: 落败 落败
WORD: _ _
WORD: 综合 综合WORD: 体育 体育
WORD: _ _
WORD: 新浪 新浪WORD: 竞技 竞技WORD: 风暴 风暴
WORD: _ _
WORD: 新浪网 新浪网
(以下省略199条数据……因为又输出了一次dividedword字符串,因此输出会重复两次)
实际操作过程中可以将_ |等特殊字符过滤掉。
将结果放入字符数组dividedword中,接下来通过创建xapian的实例,即可完成搜索引擎的开发。
关于指定内容的查询(url查询,title查询等),可以通过解析器中对指定term加入prefix的方式
//prefix插入
termgenerator.index_text(title, 1, 'S')
termgenerator.index_text(description, 1, 'XD')
//prefix查询
queryparser.add_prefix("title", "S") queryparser.add_prefix("description", "XD")
实际操作过程中替换后面的prefix参数即可。
小结
以上内容仅仅是一个初步的入门,有兴趣的同学可以查阅xapian的API对xapian进行深入了解:
http://getting-started-with-xapian.readthedocs.org/en/latest/index.html
而后如果需要对指定网站进行抓取,linux环境下使用wget开源包进行处理。
由于老师不希望我们去参考别人的程序,所以这里就不放上源代码,有问题欢迎联系指出,感激不尽QUQ。
================= 9.14更新 =====================
鉴于很多人对整合步骤有问题,这里放上整合过后的Makefile文件和test_xml文件,这里的test_xml文件只是示范如何循环提取title以及分词。
Makefile:
test: test_scws.cpp test_xapian.cpp test_xml.cpp g++ -o test_xapian test_xapian.cpp -l xapian g++ -o test_xml test_xml.cpp tinyxml2.cpp tinyxml2.h -I/usr/local/scws/include -L/usr/local/scws/lib -l xapian -lscws -Wl,--rpath -Wl,/usr/local/scws/lib
test_xml.cpp:
#if defined( _MSC_VER ) #define _CRT_SECURE_NO_WARNINGS // This test file is not intended to be secure. #endif #include "tinyxml2.h" #include <xapian.h> #include <scws/scws.h> #include <stdio.h> #include <string.h> #include <cstdlib> #include <cstring> #include <ctime> #define SCWS_PREFIX "/usr/local/scws" #if defined( _MSC_VER ) #include <direct.h> // _mkdir #include <crtdbg.h> #define WIN32_LEAN_AND_MEAN #include <windows.h> _CrtMemState startMemState; _CrtMemState endMemState; #elif defined(MINGW32) || defined(__MINGW32__) #include <io.h> // mkdir #else #include <sys/stat.h> // mkdir #endif using namespace tinyxml2; int gPass = 0; int gFail = 0; int main( int argc, const char ** argv ) { #if defined( _MSC_VER ) && defined( DEBUG ) _CrtMemCheckpoint( &startMemState ); #endif #if defined(_MSC_VER) || defined(MINGW32) || defined(__MINGW32__) #if defined __MINGW64_VERSION_MAJOR && defined __MINGW64_VERSION_MINOR //MINGW64: both 32 and 64-bit mkdir( "resources/out/" ); #else _mkdir( "resources/out/" ); #endif #else mkdir( "resources/out/", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); #endif //printf("153"); int i=1; XMLDocument* doc = new XMLDocument(); clock_t startTime = clock(); scws_t s; scws_res_t res, cur; doc->LoadFile( "2013.q1.new.txt" ); clock_t loadTime = clock(); int errorID = doc->ErrorID(); XMLElement* titleElement = doc->FirstChildElement( "doc" ); while(titleElement) { const char* title = titleElement->FirstChildElement( "title" )->GetText(); printf( "Name of play (%d): %s ",i++, title ); if (!(s = scws_new())) { printf("ERROR: cann't init the scws! "); } scws_set_charset(s, "utf8"); scws_set_dict(s, "/usr/local/scws/etc/dict.utf8.xdb", SCWS_XDICT_XDB); scws_set_rule(s, "/usr/local/scws/etc/rules.utf8.ini"); scws_send_text(s, title, strlen(title)); while (res = cur = scws_get_result(s)) { while (cur != NULL) { char dividedword[100]={0}; memcpy(dividedword,title+cur->off,cur->len); dividedword[cur->len] = '