Boost搜索引擎的实现

Boost搜索引擎项目

1.项目的相关背景

  • 这里是基于boost文档所实现的站内搜索引擎,站内搜索它的数据更垂直,数据量更小
  • 一般搜索引擎搜索出来的相关数据一般含有:网页标签(title),网页内容的概要,即将要跳转的网页URL
  • Boost的官方库中是没有站内搜索功能的

2.搜索引擎的相关宏观原理

3.搜索引擎技术栈和项目环境

  • 技术栈:C/C++,STL,准标准库,Boost库,Jsoncpp(客户端和服务器端进行交互的时候所需要进行序列化和反序列化),cppjieba(给搜索关键字进行分词),cpp-httplib(一个开源的http的开源库,可以直接构建http服务器) html5,css,js,jQuery,Ajax
  • 项目环境:Centos 7云服务器vim/g++/Makefile,vscode/vs2019

4.正排索引vs倒排索引 -搜索引擎具体原理

正排索引:就是从文档ID找到文档内容(文档内的关键字)
文档ID文档内容
1新海诚上映了新电影
2新海诚的新电影是铃芽户缔

对目标文档进行分词(目的:方便建立倒排索引和查找):

  • 文档1:新海诚/上映/了新电影
  • 文档2:新海诚/的新电影/是/铃芽户缔

停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑

倒排索引:根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
关键字(具有唯一性)文档ID,weight(权重高的在前)
新海诚文档1,2
上映文档1
新电影文档1,2
铃芽户缔文档2

这样就可以模拟一次查找的过程:

用户输入:新海诚-->倒排索引中查找-->找到之后提取出文档ID(1,2)-->根据正排索引--->找到文档内容 -->title+conent(desc) +url 文档结果进行摘要--->构建响应结果

5.编写数据去标签与数据清洗的模块 Parser(分析器)

boost官网:https://www.boost.org/
//目前只需要boost_1_81_0/doc/html目录下的html文件,用它来建立索引

在这里插入图片描述
之所以选doc/html文件也是因为官网之中绝大部分使用的都是这里面的文件

理解什么是标签,以及去标签的目标
[xifeng@VM-16-14-centos MyBoostSearch]$ touch parser.cc
//想要做数据处理就需要有原始数据-->去标签之后的数据
//标签也就是<>之中包含的,标签对搜索没有价值,需要被去除
//一般标签都是成对出现的

//html放的是原始文档
[xifeng@VM-16-14-centos data]$ html
[xifeng@VM-16-14-centos data]$ purify_html
//去除掉标签之后的html我们保存在puritf_html中(puritf提纯/去除)
[xifeng@VM-16-14-centos html]$ ls -Rl | grep -E '*.html' | wc -l
8429 //一共有8429个html文件
目标: 把每个文档都去标签,然后写入到同一个文件中,文档内容不需要换行(\n),文档和文档之间用\3区分
类似于:xxxxxxx\3xxxxxxxxx\3xxxxxxxxxxxx\3 但是这种方法虽然可行,可是后续处理的时候会比较麻烦,因为还需要去区分title,content,url的内容
---------------------------------------------------
所以我选择的是这种操作:title\3content\3url \n title\3content\3url \n....
就是同一个文档之间不同的数据之间采用\3来区分,文档文档之间采用\n来区分,这样既可以通过getline获取到一个文档的全部信息,又方便区分文档中的不同信息

之所以选择\3也是有一定的理由的:首先是因为它是控制字符,不会显示到文件中,其次就是\3也有正文结束的意思

编写Parser

  • 首先在purify_html中创建了一个文本文件purify.txt用来保存清洗之后的数据

主要有三大块:

  1. 首先要将所有的文件给拿到,可以将带路径的文件名存放到一个数组中(Enum File通过这个函数来枚举各个文件的路径)

  2. 其次就是对文件进行读取和解析(Parser File通过这个函数来解析)

    • 解析数据要解析成什么样子的呢

    • 可以定义一个结构体

      typedef struct Docinfo
      {
      	std::string title;//标签
          std::string content;//内容
          std::string url;//官网所对应的url
      }Docinfo_t;
      
  3. 最后就是将解析到的数据存放在purity.txt中(Save Data通过这个函数来存储数据)

//代码的大致框架
#include<iostream>
#include<string>
#include<vector>
#include<boost/filesystem.hpp>
const std::string src_path = "./data/html";
const std::string save_path= "./data/purify_html/purify.txt";
typedef struct Docinfo
{
  std::string title;//文档的标签
  std::string content;//文件的内容
  std::string url; //文件对应官网中的url
}Docinfo_t;
//注意:输入型参数我使用:const &
//输出型参数:*
//输入输出型参数:&
bool EnumFile(const std::string& src_path,std::vector<std::string>* file_list);
bool ParserFile(const std::vector<std::string>& file_list,std::vector<Docinfo_t>* data_list);
bool SaveData(const std::string& save_path,const std::vector<Docinfo_t>& data_list);
int main()
{
  //第一步:获取到文件中的所有的html路径,并将其存放到一个数组当中
  std::vector<std::string> file_list;//文件列表
  if(!EnumFile(src_path,&file_list))
  {
    //如果遍历失败,后续就没有意义了,直接退出
    std::cerr<<"EnumFile error"<<std::endl;
    return 1;
  }
  //第二步:就是解析文件
  //这里首先需要定义一个结构体,也就是文档需要解析成什么形式,也就是上面定义的Docinfo_t
  //其次就是需要存放这些数据,还是采用vector
  std::vector<Docinfo_t> data_list;//数据列表
  if(!ParserFile(file_list,&data_list))
  {
    //同样解析失败也就是退出
    std::cerr<<"ParserFile error"<<std::endl;
    return 2;
  }
  //第三步也就是保存数据
  if(!SaveData(save_path,data_list))
  {   
      std::cerr<<"SaveData error"<<std::endl;
      return 3;//保存失败退出码为3
  }
  return 0;
}
boost开发库的安装

sudo yum install -y boost-devel,我安装的是1.53版本的boost库,这意味的是用1.53的库去写代码,我搜索的文档还是1.81的文档,这两个并不冲突

EnumFile的实现
  • 首先需要通过boost库了解一些相关的接口,这里对文件的操作使用的是boost库中提供的filesystem(头文件:<boost/filesystem.hpp>)

  • 在官方文档的使用样例中,对于命名空间这一块他使用的是namespace fs = boost::filesystem;,所以在我的代码中也就不全局放开了,与文档保持一致,在使用的时候通过域作用限定符去写,不去无脑放开还是很有好处的,能很大程度的减少接口上的冲突

    //一下是需要了解的部分类或者接口
    //path类里面定义了一个root_path
    path root_path() const;
    Returns: root_name() / root_directory()
    -----------------------------------------
    path operator/ (const path& lhs, const path& rhs);
    Returns: path(lhs) /= rhs.
    // /=是被重载了的,作用:将优选目录分隔符附加到包含的路径名,除非:
    //一个添加的分离器将是多余的,或者会将相对路径更改为绝对路径,或P.Empty()或*p.native()。cbegin()是目录分离器。
    //root_name() 如果包含根名则返回根名
    //root_directory() 如果包含根目录返回根目录 
    ------------------------------------------
    //exists用来判断文件是否存在
    bool exists(const path& p);
    //递归遍历
    Class recursive_directory_iterator
    //类型的对象提供标准库 对目录内容进行合规迭代,包括递归到其子目录
    //进行迭代的时候需要考虑要遍历的文件必须是普通文件,必须是.html的,所以这就需要对文件名进行筛选
    //判断是否是常规文件
    bool is_regular_file(const path& p);
    //用来判断后缀的是path类内部的一个成员函数
    path extension(const path& p) const;
    //可以看一下文档举的例子
    std::cout << path("/foo/bar.txt").extension(); // outputs ".txt"
    //这里可以发现调用返回的是".txt",这样就可以进行判断
    //当判断完之后我们就需要将其push到存放地址的数组中去,path类中提供了一个string方法
    template <class String>
    String string(const codecvt_type& cvt=codecvt()) const;
    //返回的是string类型的路径
    

EnumFile具体的实现:

#Makefile编写时,如果不加 -lboost_system和-lboost_filesystem会报错的
#因为boost不是标准库,第三方库链接时需要指明库名称
cc=g++
parser:parser.cc
  $(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem            .PHONY:clean                                                  
clean:
  rm -rf parser
//作用类似于typedef,跟文档样例保持一致,写代码时最好不要遇到命名空间就全部展开
namespace fs = boost::filesystem;
bool EnumFile(const std::string& src_path,std::vector<std::string>* file_list)
{
  //boost库中定义的成员,简单理解就是将我们传入的字符串转换成boost能够识别的路径
  fs::path root_path(src_path);
  if(!exists(root_path))
  {
    //代表目标文件不存在
    return false;
  }
  //到这里就是已经找到了目标文件,可以进行迭代查找了
  fs::recursive_directory_iterator end;//定义一个结束的迭代器
  //构建一个从root_path开始的迭代器
  for(fs::recursive_directory_iterator it(root_path);it!= end;++it)
  {
    //要查找首先得是一个普通文件
    if(!fs::is_regular_file(*it))
    {
      continue;
    }
    //其次需要的是.html后缀的文件
    if(!(it->path().extension()==".html"))
    {
      continue;
    }
    //到这里就是后缀名为.html的普通文件了,将其存放进vector即可
    //如果直接push_back(*it)是不正确的,因为那个已经不是string类型了
    //我们可以通过boost提供的string接口来将其转化一下
    file_list->push_back((it->path()).string());
  }
  return true;
}
Parser File的实现

总体思路:首先已经拿到了所有的带路径的文件名,接下来要做的就是对这些文件进行遍历,打开每一个文件进行读写,将其解析成Docinfo_t这样的结构将其插入到vector中

打开对应的文件
  • 首先需要了解几个相关函数,在c++中对于文件操作的头文件是#include<fstream>
  • ifstream是输入流,其构造函数explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in); 注意一下ios和ios_base没有区别,都可以用
  • open和close分别对应着打开文件和关闭文件;void open (const char* filename, ios_base::openmode mode = ios_base::in); void close();
  • is_open是判断文件是否被打开bool is_open();
  • getline是按行读取,需要将读到的字符拼接成一个长字符串istream& getline (istream& is, string& str);
//以下是具体代码
//这里对c++提供的接口进行了简单的封装
bool Open(const std::string &file_path, std::string *out)
{
     std::ifstream in(file_path, std::ios_base::in);
     if (!in.is_open()) // 如果文件没有被打开则返回false
     {
        // 文件如果打开失败打印失败的文件名
        std::cerr << "Open file error:" << file_path << std::endl;
        return false;
     }
    //文件能够被成功打开
    std::string str;
    //理解getline读取到文件结尾:getline的返回值是一个&,while判断的是一个bool类型,本质就是返回的类型中重载了强制类型转换
    while(std::getline(in,str))//按行读,最后拼接在一起
   	{
       *out +=str;
    }
    in.close();
   	return true;
}
提取title

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADIrHGnD-1679760493442)(C:/Users/yangyr0206/AppData/Roaming/Typora/typora-user-images/image-20230314143742011.png)]

  • 通过STL容器string提供的find函数可以去查找字符串"<title>"和"</titile>"

    size_t find (const string& str, size_t pos = 0) const;
    //如果找不到就会返回std::npos;
    //找到就返回找到的第一个字符对应的位置下标,比如<title>找到就返回<的下标
    
  • 通过substr去获取中间的片段

    string substr (size_t pos = 0, size_t len = npos) const;
    //pos是从什么位置开始,len是取多长
    
//实现
bool ParserTitle(const std::string &result, std::string *title)
{
  // 要提取的就是<title></title>之间的数据
  size_t begin = result.find("<title>");
  size_t end = result.find("</title>");
  if (begin == std::string::npos || end == std::string::npos || begin > end)
  {
    std::cerr << "find error" << std::endl;
    return false;
  }
  // 两个都找到了,且begin < end
  begin += 7;//为了跳过<title>
  *title = result.substr(begin, end - begin); // 左闭右开区间
  return true;
}
提取content,本质就是去标签

在进行遍历时,无论是单标签还是双标签,只要碰到了>,就意味着,当前的标签被处理完毕;只要读到了<意味着新的标签开始了

//实现
bool ParserContent(const std::string &result, std::string *content)
{
  enum Status // 状态机的状态码
  {
    START, // 开始
    OVER   // 结束
  };
  // 去标签,当遇到>我们认为一个标签结束了,遇到<表示一个标签刚刚开始
  // 由此可以设置一个简单的状态机
  enum Status st = START; // 默认是标签的开始
  for (char c : result)
  {
    switch (st)
    {
    case START:
      if (c == '>')
        st = OVER;
      break;
    case OVER:
      if (c == '<')
        st = START;
      else // 正文内容
      {
        if (c == '\n')
          c = ' ';
        content->push_back(c);
      }
      break;
    default:
      break;
    }
  }
  return true;
}
构建url

boost库的官方文档,和我们下载下来的文档,是有路径的对应关系的

官网的URL样例:https://www.boost.org/doc/libs/1_81_0/doc/html/accumulators.html
下载下来的URL样例:boost_1_81_0/doc/html/accumulators.html
我项目中的URL样例:MyBoostSearch/data/html/accumulators.html
//本质就是我将下载下来doc/html/* cp data/html/
----------------------------------------------------------------
url_head = "https://www.boost.org/doc/libs/1_81_0/doc/html";
url_tail = [data/html](delete) /accumulators.html--->"/accumulators.html";
url = url_head + url_tail;//相当于形成了一个官网链接
const std::string head_url = "https://www.boost.org/doc/libs/1_81_0/doc/html";
bool ParserUrl(const std::string &head_url, const std::string &file, std::string *url)
{
  // 就是进行平接只需要把我的路径中的./data/html-->也就是之前定义的src_path去除与head_url拼接即可
  std::string tail_url = file.substr(src_path.size());
  *url = head_url + tail_url;
  return true;
}
Parser File实现代码
bool ParserFile(const std::vector<std::string> &file_list, std::vector<Docinfo_t> *data_list)
{
  for (const std::string &file : file_list)
  {
    // 1.打开文件
    // result(结果)用来存放文件读出来的数据
    std::string result;
    if (!Tool::Open(file, &result))
    {
      continue;
    }
    // 2.解析成Docinfo_t类型的数据
    // 提取标签,提取内容,拼接url
    Docinfo_t doc;
    // 获取标签
    if (!ParserTitle(result, &doc.title))
    {
      continue;
    }
    // 获取内容
    if (!ParserContent(result, &doc.content))
    {
      continue;
    }
    const std::string head_url = "https://www.boost.org/doc/libs/1_81_0/doc/html";
    // 拼接url
    if (!ParserUrl(head_url, file, &doc.url))
    {
      continue;
    }
    //move函数主要作用就是数据的移动,也就是说moved-from对象处于有效但未指定的状态。这意味着,在这样的操作之后,移出对象的值只应被销毁或分配一个新值;否则,访问它会生成未指定的值。在这里就是doc里面的数据就不再是有效数据了
    //如果不加move会发生拷贝,而一个网页数据有的还是挺大的,拷贝的话需要浪费很多时间
    data_list->push_back(std::move(doc));
  }
  return true;
}
Save Data的实现

就是将前面得到的data_list里面的数据全部按照制定的规则存放到purify.txt文件中即可

bool SaveData(const std::string &save_path, const std::vector<Docinfo_t> &data_list)
{
  std::ofstream out(save_path, std::ios_base::out | std::ios_base::binary);
  //要实现的就是同一网页的title,content,url通过\3分隔,不同网页通过\n分隔
  //在文件中'\3'是以^C的形式体现的
  for(auto & data : data_list)
  {
    //打开文件,以二进制的形式写入
    if(!out.is_open())
    {
      std::cerr<<"open savefail error"<<std::endl;
      return false;
    }
    std::string str;
    str+=data.title;
    str+='\3';
    str+=data.content;
    str+='\3';
    str+=data.url;
    str+='\n';
    out.write(str.c_str(),str.size());//读哪里,读多长
  }
  out.close();
  return true;
}
//之后查看save_path路径下的文件 cat purify.txt | wc -l ---->8429行也就是说里面有8429个文件

6.编写建立索引的模块 Index

  • 首先需要建立index.hpp文件,在其中定义一个数据的数据结构Docinfo

  • 需要建立倒排索引的节点(文档id,权重,关键字)

  • 正排索引的数据结构选择数组,这样的话当知道文档id的话就可以根据下表直接找到(时间复杂度就为O(1))

  • 倒排索引一定会存在一个关键字和多个文档id(一个)有关联,所以倒排索引天然的就适合使用unordered_map

  • 字符串切分虽然用stl容器提供的接口也能够去写,可是比较繁琐,可以使用boost库中的split(<boost/algorithm/string.hpp>)

    boost::split(type,str,boost::is_any_of("\3"),boost::token_compress_on)
    //第一个参数type就是一个用来存放切分后数据的数据结构
    //str就是要切分的字符串
    //boost::is_any_of()里面设置的是分隔符
    //boost::tocken_compress_on:将连续多个分隔符压缩成一个,默认没打开,一般用的时候打开
    

Index的基本代码结构

// 这里是构建索引模块
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
namespace MyIndex
{
  // 文档信息
  struct Docinfo
  {
    std::string title;
    std::string content;
    std::string url;
    uint64_t file_id; // 因为有正排和倒排索引,所以文档id是不可或缺的
  };
  // 倒排元素
  struct InvertedElement
  {
    std::string key_word;
    uint64_t file_id;
    int weight; // 权重,之后显示的先后顺序需要与权重挂钩
  };
  //重命名为倒排拉链
  typedef std::vector<InvertedElement> InvertedList;
  class Index
  {
  private:
    // 正排索引的数据结构
    // 因为正排索引需要的是根据文档id去找文档内容,文档id可以当做数组下标能够实现O(1)的查找
    std::vector<Docinfo> forward_index;
    // 倒排索引的数据结构
    // 根据关键字去找文件id,这天然就是一对多(一对一)的关系
    // 所以用unordered_map最为合适
    std::unordered_map<std::string, InvertedList> inverted_index;
  public:
    Index();
    ~Index();
  public:
    // 获取正排-->返回的是文档信息
    Docinfo *GetForward(const uint64_t &id)
    {
      //这类比较简单就直接写了
      if(id > forward_index.size())
      {
        //id越界
        std::cerr<<"id cross the border error"<<std::endl;
        return nullptr;
      }
      return &forward_index[id];
    }
    // 获取倒排 -->返回的是倒排拉链,也就是一个关键字对应的一组文件id
    InvertedList* GetInverted(const std::string &key_word)
    {
      auto it = inverted_index.find(key_word);
      if(it==inverted_index.end())
      {
        std::cerr<<"not found"<<std::endl;
        return nullptr;
      }
      return &(it->second);
    }
    // 构建索引 --->根据的就是解析后的./data/purify_html/purify.txt里面的内容
    //const std::string target_path = "./data/purify_html/purify.txt"; // 目标文件的路径
    bool BuildIndex(const std::string &input)//在这里面去构建对应的索引模块
    {
      return true;
    }
  };
};

接下来就是对上述函数的实现

Build Index的实现
  • 要构建索引就需要数据,而这数据就是Parser解析出来的存放在./data/purify_html/purify.txt里面的内容

  • 所以理所应当的在Build Index中需要进行文件操作

     bool BuildIndex(const std::string &input)
        {
          // 之前是二进制形式写,这里也是二进制形式读
          std::ifstream in(input.c_str(), std::ios_base::in | std::ios_base::binary);
          if (!in.is_open())
          {
            std::cerr << "open error" << std::endl;
            return false;
          }
          std::string line;
          while (std::getline(in, line))
          {
            // 构建正排-->返回的是Docinfo然后需要通过返回值去构建倒排索引
            Docinfo *doc = BuildForward(line);
            if (doc == nullptr)
            {
              std::cerr << "BuildForward error" << std::endl;
              continue; // 一个文档的正排构建失败就没有必要再去构建倒排了
            }
            // 构建倒排
            BuildInverted(*doc);
          }
          in.close();
          return true;
        }
    
正排索引函数的实现
  • 需要使用到之前提到的boost库中提供的split函数

    void SlicingString(const std::string& line,std::vector<std::string>*result, const std::string& separator)
            {
                //可以通过stl提供的容器接口来实现,可是实现起来比较繁琐
                //所以这里直接使用boost库中的<boost/algorithm/string.hpp> split
                boost::split(*result,line,boost::is_any_of(separator),boost::token_compress_on);
                //*result 存放切分的数据的结构vector<string>
                //line 要切分的数据
                //is_any_of() 构建切分符
                //token_compress_on将连续的分隔符压缩成一个分隔符(默认关闭,需要手动打开)
            }
    
Docinfo *BuildForward(const std::string &line)
    {
      std::vector<std::string> result;
      const std::string separator = "\3";
      MyTool::StringTool::SlicingString(line, &result, separator);
      if (result.size() != 3)
      {
        // 如果切分后的数据没有三部分则切分出错
        return nullptr; // 切分失败返回空
      }
      // 将切分的数据填充到doc中
      Docinfo doc;
      doc.title = result[0];
      doc.content = result[1];
      doc.url = result[2];
      doc.file_id = forward_index.size(); // 先保存在push,这样可以让id和数组下标对应,比如一开始什么都没有size=0,文档id=0,当这个doc push进去之后它所在的下标也是0
      // 将doc 插入到正排索引的vector中
      forward_index.push_back(std::move(doc));// 通过拷贝效率太低,直接move可以提升效率
      return &forward_index.back();
    }
倒排索引的原理
//原理:就是需要构建出多个这样的结构,之前已经拿到了Docinfo的数据  
struct InvertedElement
  {
    std::string key_word;
    uint64_t doc_id;
    int weight; // 权重,之后显示的先后顺序需要与权重挂钩
  };
//倒排拉链
typedef std::vector<InvertedElement> InvertedList;
//倒排索引一定是一个key_word和一组(一个)InvertedElement对应
//通过正排索引可以拿到的内容
struct Docinfo
{
    std::string title;
    std::string content;
    std::string url;
    uint64_t file_id;
};
//举例文档:
title:新电影铃芽户缔
content:新海诚出了一部新的电影名叫铃芽户缔
url:http://XXX 
doc_id: 324
根据文档内容形成一个或多个InvertendElement(倒排拉链)
因为是一个一个的对文档进行处理的,一个文档会包含多个词,都对应到当前的doc_id
1. 需要对title和content进行分词  --这里使用jieba分词-->CutForSearch(s,words)
   titile:新/电影/新电影/铃芽/户缔/铃芽户缔 title_words
   content:新/海诚/新海诚/出/一部/新/电影/电影名/叫/铃芽/户缔/铃芽户缔 content_words
   词和文档的相关性(相关性的实现并不是一个简单的技术,这里用最简单的方式去实现)
2. 词频统计
   这里我实现相关性的方案就是根据词频来设置,同时我认为在标题中出现的词相关性会更高一些,在内容中出现相关性低一些
   struct word_count{
       int title_count;//标题中key_word出现的次数
       int content_count;///内容中key_word出现的次数
   };
   //建立关键字和出现次数的映射关系
   unordered_map<std::string,word_count> words_count;
	for &word : title_word{
        word_count[word].title_count++;
    }
	for &word : content_word{
        word_count[word].content_count++;
    }
知道了文档内容中词的出现次数
3. 自定义相关性
    for &word : words_count{
        InvertendElement elem;
        //因为处理的是同一个文档的内容,所以文档的id是可知的
        elem.doc_id = 324; 
        elem.key_word = word.first;
        elem.weight = 10*word.second.title.count + 1* word.second.content_count;
        inverted_index[word.first].push_back(elem);
    }
//s要切部分
//words切后存储的结构
//string s;
//boost::to_lower(s)将字符串转化成小写
//c也提供了一些函数接口:toupper()//转换为大写,tolower()//转换为小写
//c++提供的接口可以用:transform(s.begin(),s.end(),s.begin(),::toupper);
//几个参数简单理解就是从哪开始,到哪结束,结果储存到哪,转成大写还是小写
jieba分词的使用
  • 获取链接:git clone https://gitcode.net/mirrors/yangyiwu/cppjieba.git

  • 如何使用:需要的是include/cppjieba/Jieba.hpp

  • 文件中有个test/demo.cpp的文件里面是jieba分词的用法

  • 如果想要正确编过,需要将deps/limonp里面的所有文件都拷贝进include/cppjieba中,我这里使用切词的函数是里面的:CutForSearch(s,words) ---->s表示要切分的数据,words表示存放的数据

    //在下载后的cppjieba文件中,有一个test文件里面有个demo.cpp文件,里面记录了各种接口函数的用法以及样例
    //接下来来看看CutForSearch(s,words)函数在文档中的用法
    #include "../include/cppjieba/Jieba.hpp"
    #include<iostream>
    #include<string>
    #include<vector>
    using namespace std;
    //这些const定义的是词库的所在路径,自己使用的时候需要注意调整这些路径
    const char* const DICT_PATH = "../dict/jieba.dict.utf8";
    const char* const HMM_PATH = "../dict/hmm_model.utf8";
    const char* const USER_DICT_PATH = "../dict/user.dict.utf8";
    const char* const IDF_PATH = "../dict/idf.utf8";
    const char* const STOP_WORD_PATH = "../dict/stop_words.utf8";
    int main(int argc, char** argv) {
      cppjieba::Jieba jieba(DICT_PATH,
            HMM_PATH,
            USER_DICT_PATH,
            IDF_PATH,
            STOP_WORD_PATH);
      string s;
      vector<string> words;
      s = "小明硕士毕业于中国科学院计算所,后在日本京都大学深造";
      cout << s << endl;
      cout << "[demo] CutForSearch" << endl;
      jieba.CutForSearch(s, words);
      cout << limonp::Join(words.begin(), words.end(), "/") << endl;
      return 0;
    }
    //我这里采用ln -s软链接的方式去调整路径
    //结果:
    //小明硕士毕业于中国科学院计算所,后在日本京都大学深造
    //[demo] CutForSearch
    ///小明/硕士/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造
    
    #include "./cppjieba/Jieba.hpp" //我在当前目录设置了一个cppjieba的软连接
    //lrwxrwxrwx cppjieba -> data/cppjieba/include/cppjieba
        const char *const DICT_PATH = "./dict/jieba.dict.utf8";
        const char *const HMM_PATH = "./dict/hmm_model.utf8";
        const char *const USER_DICT_PATH = "./dict/user.dict.utf8";
        const char *const IDF_PATH = "./dict/idf.utf8";
        const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";
        class Cppjieba
        {
        private:
            static cppjieba::Jieba jieba;//要定一个全局的静态成员变量,这样不需要每一次调用CutForSearch都去先创建一个jieba对象,大大的节省了时间
            //因为jieba分词在index建立索引中会非常频繁的使用,所以一旦在函数里面定义jieba对象就会让整个程序变的很慢
        public:
            static void CutForSearch(const std::string &s, std::vector<std::string> *words)
            {
                jieba.CutForSearch(s, *words);
            }
        };
        cppjieba::Jieba Cppjieba::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
    
倒排索引的实现
bool BuildInverted(const Docinfo &doc)
{
#define T_weight 10
#define C_weight 1
    struct word_count{
        int title_cnt;//标题key_word出现次数
        int content_cnt;//内容key_word出现次数
      };
      //1.需要对标题和内容进行切分 &&  2.进行词频统计
      std::vector<std::string> title_result;//存放切分后的数据
      std::unordered_map<std::string,word_count> word_cnt;//建立key_word和词频的映射
      MyTool::Cppjieba::CutForSearch(doc.title,&title_result);
      for(auto word : title_result)//遍历标题分词
      {
       	//这里有个细节,Hello hello HELLO这些词是算一个,还是算三个-->通过百度浏览器的搜索结果可以发现搜索时是不区分大小写的
        //这里就定个规定:文档的标题和正文分词全部按照小写来分词,同时用户输入之后也将其转换成小写
        //可以用c提供的toupper()--->转换为大写,tolower()---->转换为小写 /c++ <algorithm>中的transform等等
        //我选择的就是boost库提供的一个接口to_lower()
        boost::to_lower(word);//因为我不想改变doc里面的数据,所以没有用引用
        word_cnt[word].title_cnt++;//unordered_map重载了[],如果key存在时返回的就是value的引用,如果不存在就插入key
      }
      std::vector<std::string> content_result;
      MyTool::Cppjieba::CutForSearch(doc.content,&content_result);
      for(auto word : content_result)
      {
          boost::to_lower(word);
          word_cnt[word].content_cnt++;
      }
      //3.构建相关性
      for(auto& word : word_cnt)
      {
       	//word 在这里是unordered_map<string,vector<word_count>>
        InvertedElement elem;
        //这里处理的都是一个文档的分词,所以id就是doc里面的file_id
        elem.file_id = doc.file_id;
        elem.key_word = word.first;
        elem.weight = T_weight* word.second.title_cnt + C_weight* word.second.content_cnt;//设置相关性
        //unordered_map<string,vector<InvertedList>> inverted_index;
        InvertedList& inverted_list = inverted_index[word.first];
        inverted_list.push_back(std::move(elem));//这里加不加move都还好,里面数据比较小
      }
      return true;
}

7.编写搜索引擎模块Searcher

基本代码结构:

//安装jsoncpp:yum install -y jsoncpp-devel
    void InitSearcher(const std::string &path)
    {
      // 1.构建/获取index对象--->因为对于index,建立完之后主要就是查找,并不会修改里面的内容,所以index只需要一份即可
      // 也就是把index设计成一个单例即可
    }
	//这个就是通过用户的搜索信息去索引中去查找相关文档,并输出序列化之后的字符串
	void Search(const std::string &Inquire, std::string *json_string)
    {
      	// 1.分词:将搜索Inquire进行分词
        // 2.查询:分词之后用关键词去索引表中查找,如果有会拿到倒排拉链InvertedList-->vector<InvertedElement>
        // 3.合并排序:将查找后的结果通过weight权重进行降序排序
        // 4.构建:根据查询结果构建json字符串--->也就是序列化,需要jsoncpp
    }

InitSearcher函数的实现

index改为单例模式
  • 原因:我所实现的搜索引擎只是对boost进行搜索,所以理论上并不需要多份索引,构建索引也需要消耗资源,只需要构建一份之后其他的共用即可,这里就需要将index改为单例,具体改法如下:

    //在index类中设置一个staitc的index对象,和一把锁(解决线程安全问题,如果是多线程不加锁,对单例的获取存在线程安全问题)
      static Index *singleton_index;
      static std::mutex mtx; // 创建一把锁---->#include<mutex>
    //之后就是说构造函数私有化-->必须要有,因为这样才能够在类内new一个index对象
    //禁止构造和拷贝构造函数
      Index(const Index &in) = delete;            // 禁止拷贝构造
      Index &operator=(const Index &in) = delete; // 禁止赋值拷贝
    //之后就是在public中定义一个静态的成员函数
      static Index *GetIndex()// 静态成员函数才能访问静态成员变量
      {
          // 两个if是为了提高效率,多个线程竞争锁也是需要消耗资源的,
         if (singleton_index == nullptr)
         {
           // 不加锁的话多线程情况下不是线程安全的
           mtx.lock(); // 加锁
           if (singleton_index == nullptr)
           {
             singleton_index = new Index;
           }
           mtx.unlock();
         }
         return singleton_index;
      }
    
//构建好单例之后直接使用接口即可
index = MyIndex::Index::GetIndex();
index->BuildIndex(path);

Search函数的实现

struct Compare//定义一个降序的仿函数用于sort的第三个参数
{
   bool operator()(const MyIndex::InvertedElement &e1, const MyIndex::InvertedElement &e2)
   {
      return e1.weight > e2.weight;
   }
};
void Search(const std::string &Inquire, std::string *json_string)
    {
      // 1.分词:将搜索Inquire进行分词
      std::vector<std::string> words;
      MyTool::Cppjieba::CutForSearch(Inquire, &words);
      // 2.查询:分词之后用关键词去索引表中查找,如果有会拿到倒排拉链InvertedList-->vector<InvertedElement>
      MyIndex::InvertedList result;//different
      for (std::string key_word : words)
      {
        boost::to_lower(key_word); // 建立索引的时候默认全是小写,搜索的时候也同样需要
        // 先查倒排,再根据查询后的结果查正排
        MyIndex::InvertedList *inverted_list = index->GetInverted(key_word);
        if (inverted_list == nullptr) // 如果词不存在,就继续下一次查找
        {
          continue;
        }
        // 这个就是通过迭代器将InvertedList里面的InvertedElement全部插入到result中
        result.insert(result.end(), inverted_list->begin(), inverted_list->end());
      }
      // 3.合并排序:将查找后的结果通过weight权重进行降序排序
      std::sort(result.begin(), result.end(), Compare());
      // 4.构建:根据查询结果构建json字符串--->也就是序列化,需要jsoncpp
      Json::Value root; //Value是json里面的万能类
      //json序列化通常使用Wright类(FastWriter,StyledWriter) 反序列化通常是Reader类
      for(auto& elem : result)
      {
        //根据result里面的数据,去正排查找文档并且把查找到的文档序列化
        MyIndex::Docinfo* doc = index->GetForward(elem.file_id);
        Json::Value tmp;
        tmp["title"] = doc->title;//标题
        tmp["desc"] = GetDesc(doc->content, elem.key_word);//描述
        tmp["url"] = doc->url;//要跳转的网页
        root.append(tmp);
      }
      //要注意无论是FastWriter还是StylenWriter它们都是类,而write是它们的成员函数,所以需要先构建一个匿名对象来使用它的成员函数
      //*json_string = Json::FastWriter().write(root);//StyleWriter方便调试,所以先用,后面没有问题之后再用这个
      *json_string = Json::StyledWriter().write(root);//这样用户就获得了通过权重排序之后的文档信息
    }

注意:jsoncpp是第三方库所以使用g++编译的时候需要指定库-ljsoncpp

摘要函数GetDesc的实现
    struct GetCompare//定义了一个仿函数
    {
      bool operator()(int x, int y)
      {
        return std::tolower(x) == std::tolower(y);
      }
    };
    std::string GetDesc(const std::string &content, const std::string &key_word)
    {
      // 获取描述---->这里就简易实现一下
      // 获取第一次出现的key_word的前50个字符,获取key_word的后100个字符,如果不够就从头开始,或者截取到尾部
      const int prev = 50; // 因为size_t是无符号整数,所以为了方便就直接设置为int
      const int next = 100;
      // int pos = content.find(key_word); // 这里查找会出现None1的情况,主要是因为在之前的代码中我对切分后的数据统一to_lower了,但是find函数在查找的时候不会自己进行大小写转化
      // 这里使用C++的<algorithm>提供的算法search
      auto it = std::search(content.begin(), content.end(), key_word.begin(), key_word.end(), GetCompare());
      if (it == content.end())
        return "None"; // 如果到结尾都没有查到就返回None---不会发生
      int pos = std::distance(content.begin(), it);
      int start = 0;
      int end = content.size() - 1;
      // 代表前面有50个字符
      if (pos - prev > start)
        start = pos - prev;
      // 代表后面有100个字符
      if (end - next > pos)
        end = pos + next;
      if (start >= end) // 同理
        return "None";
      std::string desc = content.substr(pos, end - start);
      desc += "......";
      return desc;
    }
  • C++的#include<algorithm>提供的search函数

    template <class ForwardIterator1, class ForwardIterator2>
       ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,ForwardIterator2 first2, ForwardIterator2 last2,BinaryPredicate pred);
    //简单点理解就是1,2两个参数是要查找数据的迭代器:意味着从哪开始查到哪结束比如:string str = "hello word";
    //3,4两个参数是查找目标的迭代器string key = "word";  在str中去查找key
    //第5个参数可以理解为传入查找方法,我这里是传入的一个GetCompare的仿函数
    
  • #include <iterator>提供的函数destance可以用来查看迭代器相比于begin走了多远

    template<class InputIterator>
      typename iterator_traits<InputIterator>::difference_type
        distance (InputIterator first, InputIterator last);
    //用法
      std::list<int> mylist;
      for (int i=0; i<10; i++) mylist.push_back (i*10);
      std::list<int>::iterator first = mylist.begin();
      std::list<int>::iterator last = mylist.end();
      std::cout << "The distance is: " << std::distance(first,last) << '\n';
      return 0;  //result:The distance is 10
    

8.编写http server模块

升级gcc

如果想使用cpp-httplib库
注意:
centos 7 默认的gcc编译器的版本是4.8.5版本的,如果想要使用cpp-httplib是需要更新gcc编译器的,如果用老的会编译不通过,或者运行时报错
更新gcc方法,搜索关键字: scl gcc devsettool升级gcc
1.安装scl源:
    yum install centos-release-scl scl-utils-build
2.安装新版本gcc
    sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
安装好之后,工具集在ls /opt/rh中
//启动新版本的gcc:命令行启动只能在本会话有效
   	scl enable devtoolset-7 bash
//建议将上面的启动命令添加到~/.bash_profile这个登录脚本里面
 # .bash_profile
   
 # Get the aliases and functions
 if [ -f ~/.bashrc ]; then
    . ~/.bashrc
 fi
   
 # User specific environment and startup programs
   
 PATH=$PATH:$HOME/.local/bin:$HOME/bin
  
 export PATH
 #每次启动的时候都会执行scl这个命令
 scl enable devtoolset-7 bash                              

安装cpp-httplib

最新的cpp-httplib在使用的时候,如果gcc不是特别新的话可能会有运行时错误
我这里使用的是cpp-httplib 0.7.15版本
通过gitee搜索cpp-httplib,下载zip文件上传到服务器即可

使用cpp-httplib

  inline bool Request::has_param(const char *key) const {
       return params.find(key) != params.end();//他这里面的params是一个multimap<std::string, std::string>类型
  }
//Server端的使用
//http 
httplib::Server svr;
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
  res.set_content("Hello World!", "text/plain");
});
svr.lesten("0.0.0.0",8080);
#include "./cpp-httplib/httplib.h"//安装的cpp-httplib库的位置
#include "searcher.hpp"
const std::string input = "./data/purify_html/purify.txt";//建立索引的数据源
const std::string root_path = "./wwwroot";//web根目录
int main()
{
    MySearcher::Searcher searcher;
    searcher.InitSearcher(input);
    httplib::Server svr;
    //设置web根目录
    svr.set_base_dir(root_path.c_str());
    //Lambda表达式想要引用外部的对象,需要在[] 中&+对象---->[&searcher]
    svr.Get("/s",[&searcher](const httplib::Request& req, httplib::Response& res){
        //res.set_content("hello word!","text/plain");//里面是页面的显示内容,以文本形式显示
        if(!req.has_param("word"))//has_param--->判断是否有参数
        {
            res.set_content("需要有搜索关键字!", "text/plain; charset=utf-8");
            //设置返回内容"text/plain" 对应的是http中的Content-Type设置charset=utf-8的时候中间不能带空格
            return ;
        }
        std::string key_word = req.get_param_value("word");//获取参数
        std::cout<<"用户在搜索:"<<key_word<<std::endl;
        std::string json_string;
        searcher.Search(key_word,&json_string);
        //到这里就是执行完搜索服务了,需要给用户返回搜索结果---->json的Content-Type是application/json
        res.set_content(json_string,"application/json");
    });
    //将其设置为listen状态
    svr.listen("0.0.0.0",8080);
    return 0;
}

9.编写前端模块

了解vscode

它是一个编辑器

安装插件

1.Chinese(汉化)
2.open in browser//写好的网页可以直接单击右键用浏览器打开
3.Remote -SSH //用来链接Linux
    在命令行输入remote -ssh之后就开始登录了跟xshell的登录是一样的

编写前端代码

了解html css js
html: 是网页的骨骼---负责网页结构
css: 负责网页美观
js(javascript):网页的灵魂--网页的动态效果,前后端交互
//我是对着教程:w3cschool这个网站去写的一些前端模块
编写html
  1. div 元素是块级元素,它是可用于组合其他 HTML 元素的容器。div 元素没有特定的含义

  2. input 是设置表单数据

  3. button元素定义可点击的按钮

  4. a标签,href 属性规定链接的目标。开始标签和结束标签之间的文字被作为超级链接来显示。

    <a href="http://www.w3school.com.cn/">Visit W3School</a>
    <!--上面这行代码显示为:Visit W3School-->
    
  5. <p>这个就是一个普通的标签</p>
    <i>这个是斜体标签</i> 
    
编写css

有很多种方法将html和css进行关联起来,这里我就直接采用style将其内联到html中

设置样式的本质:找到要设置的标签,设置它的属性
1.选择特定的标签:类选择器,标签选择器,复合选择器
2.设置指定标签的属性:具体见代码(很多属性也可以去参考已有的网页他们的属性设置)
<style>
      /*去掉网页中的所有默认内外边距*/
       * {
            /*设置外边距*/
            margin: 0;
            /*设置内边距*/
            padding: 0;
         }
        /*设置body内的内容和html的呈现是1:1的*/
        html,
        body {
            height: 100%;
        }
        /*.开头的一般叫类选择器*/
        .container {
            /*设置div的宽度*/
            width: 800px;
            /*通过设置外边距达到居中效果*/
            margin: 0px auto;
            /* 设置外边距的上边距,保持元素和网页的上部距离 */
            margin-top: 15px;
        }
        /* 复合选择器,选择container下的search */
        .container .search {
            /* 宽度与父标签一致 */
            width: 100%;
            /* 高度设置为52xp */
            height: 52px;
        }
        /* 选中input标签 单看input就是标签选择器,不需要加.*/
        .container .search input {
            float: left;
            /*给input和button设置left浮动就可以让两个盒子之间的边距清零就可以拼在一起了*/
            width: 600px;
            height: 50px;
            /* input在设置的时候没有考虑边框的问题,所以同height的情况下button会比input小一点 */
            /* 设置边框宽度(1px),边框的样式(实线) ,边框颜色(black) */
            border: 1px solid black;
            /* 右边框给去除 */
            border-right: none;
            /* 设置内边距,让默认文字不要紧贴左侧边框 */
            padding-left: 10px;
            /* 设置input内部字体的颜色和字体大小、样式*/
            color: #ccc;
            font-size: 17px;
            font-family: Georgia, serif;
        }
        .container .search button {
            float: left;
            width: 150px;
            height: 50px;
            /*设置button的背景颜色,可以通过f12去查看百度或者其他浏览器的颜色设置*/
            background-color: #4e6ef2;
            /*设置button中的字体颜色*/
            color: #FFF;
            /*设置button字体大小*/
            font-size: 20px;
            /* 设置字体样式 */
            font-family: Georgia, serif;
        }
        .container .result {
            width: 100%;
        }
        .container .result .docinfo {
            /*上边距*/
            margin-top: 10px;
        }
        /*a标签的设置,a和i属于行内元素,为了防止显示错误需要加上display:block*/
        .container .result .docinfo a {
            /*设置块级元素,可以独占一行*/
            display: block;
            /*去除a标签的下划线*/
            text-decoration: none;
            /*设置a标签的字体大小,颜色*/
            font-size: 18px;
            color: #4e6ef2;
        }
        /*a标签的光标事件*/
        .container .result .docinfo a:hover
        {
            /*设置鼠标放在a标签上的动态效果*/
            text-decoration: underline;
        }
        /*p标签属性设置*/
        .container .result .docinfo p {
            /*就是让p标签内容与a标签内容有点空位*/
            margin-top: 3px;
            font-size: 15px;
            font-family: Georgia, serif;
        }
        /*i标签属性设置*/
        .container .result .docinfo i {
            /*设置块级元素,可以独占一行*/
            display: block;
            /*取消标签斜体风格*/
            font-style: normal;
            font-size: 13;
            font-family: Georgia, serif;
            color: greenyellow;
        }
</style>
编写js
直接使用原生的js成本比较高,这里我使用的是JQuery
//有点像c++语言和STL库之间的关系
//这里我直接网页引入微软的CDN
<head>
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.9.0.min.js"></script>
</head>

JQuery中会遇到的一些函数或者用法:

  • //这样就可以提取input里面的value数据
    //()里面填写的是要提取谁里面的数据,提input里面的value
    len query = $(".container .search input").val();
    
  • console.log("query = " +query);
    //console 是浏览器的对话框,可以用来查看js数据
    //具体可以通过F12然后选择控制器去查看
    
  • //发送http请求,ajax:属于一个和后端进行数据交互的函数,JQuery中的
    ajax({});//具体的使用见下面的代码
    

还有一些的使用不好单独举例,见下面的代码注释

<!--js代码-->
<script>
    function Search() {
        //是浏览器的弹出框,当点击搜索一下会弹出alert的内容
        //alert("hello js");
        //1.提取数据,$可以理解成JQuery的别称
        //()里面填写的是要提取谁里面的数据,提input里面的value
        let query = $(".container .search input").val();
        console.log("query = " + query);//console 是浏览器的对话框,可以用来查看js数据
        //2.发送http请求,ajax:属于一个和后端进行数据交互的函数
        //type是请求的方法(GET,POSE)这里用GET,url就是自己的url后面加上/s?word=query
        //success:function(data)请求成功返回的参数就存在data中,相当于设置了一个回调
        $.ajax({
            type: "GET",
            url: "/s?word=" + query,
            success: function(data) {
                console.log(data);
                BuildHtml(data);
            }
        });
    }
    function BuildHtml(data) {
        //想重新构建一个网页信息,data是从http_server中返回的json_string
        let result_tag = $(".container .result");//获取网页中的result标签
        result_tag.empty();//清空历史搜索结果
        //类似于c++里面的for(auto e : result)
        for(let elem of data){
            let a_tag = $("<a>", {
                text: elem.title, //text就代表标签内容
                href: elem.url,  //设置跳转网页的url
                target: "_blank" //设置跳转,即跳转到一个新的网页中
            });
            let p_tag = $("<p>", {
                text: elem.desc
            });
            let i_tag = $("<i>", {
                text: elem.url
            });
            let doc_tag = $("<div>", {
                class: "docinfo"
            });
            a_tag.appendTo(doc_tag);//表示a_tag添加到doc_tag中
            p_tag.appendTo(doc_tag);
            i_tag.appendTo(doc_tag);
            doc_tag.appendTo(result_tag);//doc_tag要添加到result标签中
        }
    }
</script>

10.细节优化

文档重复显示问题

首先是之前在searcher.hpp中有一处的问题具体暴露出来就是以下问题,当一个文档同时出现几个关键词时,会重复的将其打印出来在这里插入图片描述

接下来就是去优化这段代码,其实也很简单,我这里选择重新定义一个类

struct DedupElem
{
    uint64_t doc_id;
    int wight;//权值进行加和
    vector<string> key_word;//存放一组关键字
};

具体实现见下面代码:

void Search(const std::string &Inquire, std::string *json_string)
    {
      std::vector<std::string> words;
      MyTool::Cppjieba::CutForSearch(Inquire, &words);
//-------------------------begin--------------------------------- 这是需要的两个变量
      std::unordered_map<uint64_t, DedupElem> dedup_map; // 建立一个文档id与Dedupmlem的映射
      std::vector<DedupElem> list_all;//用来存dedup_map中的second
//--------------------------end--------------------------------
      for (std::string key_word : words)
      {
        boost::to_lower(key_word);
        MyIndex::InvertedList *inverted_list = _index->GetInverted(key_word);
        if (inverted_list == nullptr)
        {
          continue;
        }
//------------------------------begin-----------------------------------------这之间的就是优化的代码
        // 优化代码,遍历InvertedList
        for (const auto &elem : *inverted_list)
        {
          auto &item = dedup_map[elem.file_id];    // 如果文档id没有就构建,有了就返回second的引用
          item.doc_id = elem.file_id;              // 构建文档id
          item.weight += elem.weight;              // 将关键字的权重全部相加
          item.key_words.push_back(elem.key_word); // 同一文档的关键字全部插入到vector中
        }
      }
      for (auto &elem : dedup_map)
      {
        list_all.push_back(std::move(elem.second));
      }
//-------------------------------end-------------------------------------------到这结束    
      std::sort(list_all.begin(), list_all.end(), Compare());
      Json::Value root;
      for (auto &elem : list_all)
      {
        // 根据result里面的数据,去正排查找文档并且把查找到的文档序列化
        MyIndex::Docinfo *doc = _index->GetForward(elem.doc_id);
        Json::Value tmp;
        tmp["title"] = doc->title; // 标题
        tmp["desc"] = GetDesc(doc->content, elem.key_words[0]); 
        // 描述--->这里就取vector<string>里面的第一个词作为关键字了
        tmp["url"] = doc->url;     // 要跳转的网页
        root.append(tmp);
      }
      *json_string = Json::FastWriter().write(root);
    }

添加一些日志信息

具体见代码,这个主要目的就是为了看着方便,只要是之前代码用cerr或者cout打印的都可以换成LOG();

#pragma once 
#include<iostream>
#include<string>
#include<cstring>
#include<vector>
#include<ctime>
#define NORMAL 0    //正常
#define WARNING 1   //警告
#define DEBUG 3     //调试
#define CRITICAL 4  //严重错误
#define LOG(STATE,MESSAGE) log(#STATE,MESSAGE,__FILE__,__LINE__)

void log(const std::string &state,const std::string& message,const std::string& file,int line)
{
  //看日志的话一般看处于什么状态是正常还是错误以及输入的信息,其次看什么文件出问题了,在哪一行
  time_t t = time(nullptr);//获取当前的时间戳
  //下面是将时间戳转换成北京时间
  //struct tm *gmtime(const time_t *timep);
  struct tm* my_tm = gmtime(&t);
  //因为有时差,所以小时需要加上8
  my_tm->tm_hour+=8;
  std::string format  = "%Y-%m-%d:%H:%M:%S";
  //size_t strftime(char *s, size_t max, const char *format,const struct tm *tm);
  //char* s 输出型参数
  //size_t max 是s的大小
  //format就是要按照什么形式输出
  char buff[25];
  memset(buff,0,sizeof(buff));
  strftime(buff,sizeof(buff),format.c_str(),my_tm);
  std::cout<<"["<<state<<"]"<<"["<< buff <<"]" <<"[" << message <<"]"<<"["<<file<<"]"<<"["<< line <<"]"<<std::endl;
}

添加去暂停词代码

    const char *const DICT_PATH = "./dict/jieba.dict.utf8";
    const char *const HMM_PATH = "./dict/hmm_model.utf8";
    const char *const USER_DICT_PATH = "./dict/user.dict.utf8";
    const char *const IDF_PATH = "./dict/idf.utf8";
    const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";
    class Cppjieba
    {
    private:
        // 增加一个去暂停词的功能---->这个功能会让创建变的非常的慢
        cppjieba::Jieba jieba;
        std::unordered_map<std::string, bool> stop_word; // 同时这些只需要同时存在一份,所以直接设置为单例
        static Cppjieba *singloten;
    private:
        Cppjieba()
            : jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH){}
        Cppjieba(const Cppjieba &ba) = delete;
        Cppjieba &operator=(const Cppjieba &ba) = delete;
    public:
        static Cppjieba *GetJieba() // 获取单例
        {
            static std::mutex mtx;
            if (singloten == nullptr)
            {
                mtx.lock();
                if (singloten == nullptr)
                {
                    singloten = new Cppjieba();
                    singloten->InitJieba(); // 构建完进行初始化
                }
                mtx.unlock();
            }
            return singloten;
        }
        bool InitJieba()
        {
            std::ifstream in(STOP_WORD_PATH);
            if (!in.is_open())
            {
                std::string st = "open file error:";
                LOG(CRITICAL, st += STOP_WORD_PATH);
                return false;
            }
            std::string line;
            while (std::getline(in, line))
            {
                stop_word[line] = true; // 插入暂停词到stop_word中
            }
            in.close();
            return true;
        }
        void CutForSearchHelper(const std::string &s, std::vector<std::string> *words)
        {
            jieba.CutForSearch(s, *words);
            for (auto item = words->begin(); item != words->end();) // 就是遍历words然后里面的每个元素都去stop_find里面去找
            {
                auto it = stop_word.find(*item);
                if (it != stop_word.end())
                {
                    // item是暂停词需要去除,同时需要考虑迭代器失效问题
                    item = words->erase(item);
                }
                else
                {
                    item++;
                }
            }
        }
    public:
        static void CutForSearch(const std::string &s, std::vector<std::string> *words)
        {
            MyTool::Cppjieba::GetJieba()->CutForSearchHelper(s, words); // 直接调用上面的分词助手
            // 好处就是无需修改其他的代码
        }
    };
    Cppjieba *Cppjieba::singloten = nullptr;
};

11.将其部署到云服务器上

部署

部署其实很简单

将日志信息输出到log.txt中 ,把标准错误重定向到标准输出 最后的&就是以守护进程的方式 ---这样即使关闭xshell,这个服务依旧是存在的
nohup ./Http_Server > log.txt 2>&1 &
如果想要关闭服务,可以通过ps axj | grep Http_Server 查看pid然后通过 kill命令将其关闭
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值