zoukankan      html  css  js  c++  java
  • Xapian+SCWS+TinyXML2构建简易搜索引擎——2

    时间关系,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] = '';
                    printf("WORD: %.*s ", cur->len, title+cur->off);
                    printf("  %s",dividedword);
                    cur = cur->next;
                }
                printf("
    ");
                scws_free_result(res);
            }
            scws_free(s);
            titleElement = titleElement->NextSiblingElement( "doc" );
        }
        delete doc; doc = 0;
        clock_t deleteTime = clock();
        printf( "Test file '%s' loaded. ErrorID=%d
    ", "2013.q1.new.txt", errorID );
        //if ( !errorID ) {
            printf( "Load time=%u
    ",   (unsigned)(loadTime - startTime) );
            printf( "Delete time=%u
    ", (unsigned)(deleteTime - loadTime) );
            printf( "Total time=%u
    ",  (unsigned)(deleteTime - startTime) );
        //}
        
    
        return 0;
    }

     =================  9.16更新  =====================

    1. 问题:Makefile ...2 ... 遗漏分隔符...停止

    出现这种情况是由于Makefile文件下g++命令前只能有tab,不能有空格,否则会报错,可以直接在终端运行编译命令而不用make,或者修改Makefile文件

     2. 发现原来仁慈的老师还是给我们留了一个大坑的QAQ

    在老师给的2013.q1.new.txt中搜索关键字:meta name="description" content="网友曝光肯德基后厨机密你是更担心还是更放心?

    结果会跳转到第201条新闻,我们发现在content标签下居然还藏着一个meta标签,这样会导致我们使用content标签的GetText()方法报错!

    解决办法:

      1. 你可以直接把这个meta标签给删掉= =···。

      2. 你可以每次在使用getText()方法前调用NoChildren()方法进行判断,如果有孩子节点则返回false,如果没有孩子节点则返回true。

    =================== 整体解决方案 ===================

    因为这个不是我的小学期所以我没有整个完成下来。感谢李结文同学将做完后的结果拿给我,我截取部分思路放送QUQ

    首先确保你的2013.q1.new.txt文件按照上面的步骤完成了修补,此代码是按照解决方法1修改txt文件了的。

    1.载入目标文件,要求你的test_xml.cpp文件所在目录下有2013.q1.new.txt文件,或者你使用绝对路径指定也行。

    doc.LoadFile( "2013.q1.new.txt" );

    2.获得doc标签

    XMLElement* docElement = doc.FirstChildElement( "doc" );

    3.获得doc标签下的title,url,content内容

    const char* title = docElement->FirstChildElement( "title" )->GetText();    //获取title内容,同理获得url和content
    const char* url = docElement->FirstChildElement( "url" )->GetText();
    const char* content = docElement->FirstChildElement( "content" )->GetText();

    这里需要注意,如果按照上面的解决方法2的话,需要在content标签的GetText方法前加入判断

    4.将content放入xapian中document的data中

    Xapian::Document newdocument;
    newdocument.set_data(content);

    5.设置特殊搜索:url搜索

    indexer.set_document(newdocument); 
    indexer.index_text(url, 1, "url"); 

    在indexer的index_text方法中,第三个参数就代表你设置的特殊标签,搜索时如果也加上此标签,则就可以针对此标签进行查找。

    6.设置分词参数

    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_set_ignore(s,1);//忽略标点

    前三行都是实例程序给出的,这里重点强调一下最后一行,使用这个方法可以忽略标点,是一个需要注意的地方。

    7.对content进行分词并放入xapian数据库document对应的term中

    while (cur != NULL)
    {
        string temp=content;
        string temp_data=temp.substr(int(cur->off),int(cur->len));
    indexer.index_text(temp_data);    cur = cur->next; }

    8.对title进行分词并设置特殊搜索:title搜索

    while (res = cur = scws_get_result(s))
    {
                while (cur != NULL)
                {
                        string temp1=title;
                        string temp_data1=temp1.substr(int(cur->off),int(cur->len));
                        cout<<i++<<temp_data1<<endl;
                        indexer.index_text(temp_data1, 1, "T");
    
                        cur = cur->next;
                }
                ......
    }

    省略号的地方根据自己需要补充吧。

    9.获取下一个doc标签

    docElement = docElement->NextSiblingElement( "doc" );//获取下一个doc标签

    以上基本就是完整的过程,然后通过一个while循环包起来,就可以循环读出整个文件。

    之后可以自己新建一个文件,打开xapian的数据库,定义一些和用户交互的语句。然后完成一个搜索引擎~

    再次感谢28班单身男神李结文~男神QQ: 233320331

    xapian学习推荐:http://www.coder4.com/archives/2218

  • 相关阅读:
    JsCV Core v0.2发布 & Javascript图像处理系列目录
    Javascript图像处理
    SOE开发之IMapServerSourceDescription
    SOE之JSONObject
    SOE开发之adddynamiclayertomapserver
    JavaScript 的 async/await
    ArcGIS Engine开发系列:将地图导出为图片的两种方法(ZZ)
    原型链
    更优美的javaScript(2)
    CSS选择器
  • 原文地址:https://www.cnblogs.com/xiaoxiaff/p/3962206.html
Copyright © 2011-2022 走看看