目录
- 一、对数据源的数据清洗(去标签)操作:parse.cc
- 二、根据去标签之后的干净的数据构建正排和倒排索引:index.hpp
- 三、提供搜索功能:searcher.hpp
- 四、放公用方法的头文件(包括boost库中的一些方法以及jieba分词的方法):util.hpp
- 五、引入http_server
- 5.1 httplib.h
- 5.2 http_server.cc
- 5.3 debug.cc
- 六、log.hpp
- 七、makefile
- 八、wwwroot:俗称web根目录
- 九、gitee源码分享
- 十、部署到Linux服务器上的指令
- 十一、项目的扩展方向
一、对数据源的数据清洗(去标签)操作:parse.cc
数据清洗(去标签)的本质就是把实现下载下来的数据源,把数据源中的标签去除,保留网页中真正的有效数据。详细实现见以下代码及注释:
#include <iostream>
#include <vector>
#include <string>
#include <boost/filesystem.hpp>
#include "util.hpp"
using std::cerr;
using std::cout;
using std::endl;
using std::string;
using std::vector;
// //是一个目录,下面放的是所有的html网页
// const string& src_path="data/input";
// //所有的文档去标签之后的内容全部以'\3'作为分隔符,全部都放到这个文件中
// const string& output="data/raw_html/raw.txt";
// typedef struct DocInfo
// {
// string title; //文档的标题
// string content; //文档的内容
// string url; //该文档在官网中的url
// }DocInfo_t;
// bool EnumFile(const string& src_path,vector<string>* files_list)
// {
// namespace fs=boost::filesystem;
// fs::path root_path(src_path);
// //判断路径是否存在,如果不存在就直接返回false了
// if(!fs::exists(root_path))
// {
// cerr<<"EnumFile is no exists..."<<endl;
// return false;
// }
// //定义一个空的迭代器,用来判断递归的结束
// fs::recursive_directory_iterator end;
// for(fs::recursive_directory_iterator iter(root_path);iter!=end;iter++)
// {
// //判断文件是否是普通文件,.html都是普通文件
// if(!fs::is_regular(*iter))
// {
// continue;
// }
// //判断后缀是否为.html
// if(iter->path().extension()!=".html")
// {
// continue;
// }
// //来到这里说明当前的文件一定是一个合法的,后缀是.html的普通网页文件
// files_list->push_back(iter->path().string());
// }
// return true;
// }
// static bool ParseTitle(const string& result,string* title)
// {
// size_t begin=result.find("<title>");
// if(begin==string::npos)
// {
// return false;
// }
// size_t end=result.find("</title>");
// if(end==string::npos)
// {
// return false;
// }
// begin+=string("<title>").size();
// *title=result.substr(begin,end-begin);
// // cout<<(*title)<<endl;
// return true;
// }
// bool ParseContent(const string& result,string* content)
// {
// //用一个简易的状态机去标签
// enum status
// {
// LABEL,
// CONTENT
// };
// enum status s=LABEL;
// for(const char& ch:result)
// {
// if(ch=='<')
// {
// s=LABEL;
// continue;
// }
// else if(ch=='>')
// {
// s=CONTENT;
// continue;
// }
// else if(s==LABEL)
// {
// continue;
// }
// else if(s==CONTENT)
// {
// if(ch=='\n')
// {
// continue;
// }
// *content+=ch;
// }
// }
// return true;
// }
// static bool ParseUrl(const string& file_path,string* url)
// {
// string url_head="https://www.boost.org/doc/libs/1_84_0/doc/html";
// string url_tail=file_path.substr(src_path.size());
// *url=url_head+url_tail;
// return true;
// }
// static void ShowDoc(const DocInfo_t& doc)
// {
// cout<<"title : "<<doc.title<<endl;
// cout<<"content : "<<doc.content<<endl;
// cout<<"url : "<<doc.url<<endl;
// }
// bool ParseHtml(const vector<string>& files_list,vector<DocInfo>* results)
// {
// for(const string& file:files_list)
// {
// //1.读取文件,Read()
// string result;
// if(!ns_util::FileUtil::ReadFile(file,&result))
// {
// continue;
// }
// DocInfo_t doc;
// //2.解析指定的文件,提取title
// if(!ParseTitle(result,&doc.title))
// {
// continue;
// }
// //3.解析指定的文件,提取content,也就是去标签
// if(!ParseContent(result,&doc.content))
// {
// continue;
// }
// //4.解析指定的文件路径,构建url
// if(!ParseUrl(file,&doc.url))
// {
// continue;
// }
// //一定要在results->push_back(std::move(doc))之前
// //调用ShowDoc,因为move之后doc中的资源就被转移了
// //ShowDoc(doc);
// //done,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
// results->push_back(std::move(doc));
// //break;
// }
// return true;
// }
// bool SaveHtml(const vector<DocInfo_t>& results,const string& output)
// {
// //按照二进制方式进行写入
// std::ofstream ofs(output,std::ios::out|std::ios::binary);
// if(!ofs.is_open())
// {
// cerr<<"open "<<output<<"failed..."<<endl;
// return false;
// }
// #define SEP '\3'
// //进行文件内容的写入
// for(const auto& doc:results)
// {
// string out_string;
// out_string=doc.title;
// out_string+=SEP;
// out_string+=doc.content;
// out_string+=SEP;
// out_string+=doc.url;
// out_string+='\n';
// ofs.write(out_string.c_str(),out_string.size());
// }
// ofs.close();
// return true;
// }
// int main()
// {
// vector<string> files_list;
// //第一步:递归式地把每个html文件名(带路径),保存到files_list
// //中,方便后期进行一个一个文件的读取
// if(!EnumFile(src_path,&files_list))
// {
// perror("enum file name failed...");
// return 1;
// }
// //第二步:按照files_list读取每个文件的内容,并进行解析
// vector<DocInfo_t> results;
// if(!ParseHtml(files_list,&results))
// {
// perror("ParseHtml failed...");
// return 2;
// }
// //cout<<"分析文件内容成功"<<endl;
// //第三步:把解析完毕的各个文件的内容,写入到output,按照\3作为每个文档的分隔符
// if(!SaveHtml(results,output))
// {
// perror("SaveHtml failed...");
// return 3;
// }
// return 0;
// }
// 需要被数据清洗的源文件的文件路径
const string src_file = "data/input";
// 数据清洗(去标签)之后的干净的文件的保存位置
const string output = "data/raw_html/raw.txt";
typedef struct DocInfo
{
string title; // 文档的标题
string content; // 文档的内容
string url; // 文档的官网url
} DocInfo_t;
// 枚举所有的文件名
//如果想建立整站搜索,那就把需要数据清洗的源文件的路径全部传到
//EnumFileName函数中,即vector<string> src_files,然后for循环
//遍历每个目录下的.html文件并建立索引即可
static bool EnumFileName(const string &src_file, vector<string> *files_list)
{
// 定义一个命名空间,缩写
namespace fs = boost::filesystem;
// 利用src_file的文件路径创建一个用于迭代遍历的路径
fs::path root_path(src_file);
// 判断资源的路径是否存在,如果src_file,即放文件名的目录:"data/input"都
// 不存在,那么就没有必要继续枚举了
if (!fs::exists(root_path))
{
perror("root_path is not exists");
return false;
}
// 迭代器,类似于nullptr
fs::recursive_directory_iterator end;
for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
// 判断该文件名是否为常规文件,如果不是常规文件也就不可能是.html文件
// 那么就跳过该文件
if (!fs::is_regular_file(*iter))
{
continue;
}
// 走到这里说明该文件一定是常规文件
// 再判断该文件是否为.html文件
if (iter->path().extension() != ".html")
{
continue;
}
// 当前文件一定是一个.html文件,插入并保存
files_list->push_back(iter->path().string());
}
return true;
}
// 解析标题,把html文件的title找出来
static bool ParseTitle(const string &file, string *doc_title)
{
// 查找前标签
auto begin = file.find("<title>");
if (begin == string::npos)
{
return false;
}
// 查找后标签
auto end = file.find("</title>");
if (end == string::npos)
{
return false;
}
begin += string("<title>").size();
if (begin > end)
{
return false;
}
// 提取标签中间的标题内容
*doc_title = file.substr(begin, end - begin);
// cout<<(*doc_title)<<endl;
return true;
}
// 解析内容
static bool ParseContent(const string &file, string *doc_content)
{
// 状态机,表示两个状态,一个是LABEL(标签),一个是CONTENT(内容)
enum status
{
LABEL,
CONTENT
};
// 文件内容的开始一定是标签,所以默认的状态设置为LABEL(标签)
enum status s = LABEL;
for (auto ch : file)
{
switch (s)
{
case LABEL:
{
// 如果标签状态的同时ch又是'>'的话,说明LABEL(标签)的状态已经结束了,
// 接下来的状态就是CONTENT(内容)状态,所以设置为CONTENT状态
if (ch == '>')
{
s = CONTENT;
}
break;
}
case CONTENT:
{
// 如果是CONTENT状态的同时ch为'<',说明新的标签开始了,所以把状态设置为LABEL
if (ch == '<')
{
s = LABEL;
}
else // 走到这里说明是文件的内容,直接插入即可
{
//文件内容不想要'\n',所以把'\n'变成' '再插入
if (ch == '\n')
{
ch = ' ';
}
doc_content->push_back(ch);
}
break;
}
default:
{
break;
}
}
}
return true;
}
// 解析并拼接url
static bool ParseUrl(const string &file_path, string *doc_url)
{
// 官网的url的头部
string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";
// 把我们下载到file_path路径下的文件的路径进行切分,即只要文件名,不要本地的路径
// 切分出来的文件名再和url_head拼接就能得到该文件的一个完整的官网url
string url_tail = file_path.substr(src_file.size());
*doc_url = url_head + url_tail;
return true;
}
void ShowDoc(const DocInfo_t &doc)
{
cout << "title : " << doc.title << endl;
cout << "content : " << doc.content << endl;
cout << "url : " << doc.url << endl;
}
static bool ParseHtml(const vector<string> &files_list, vector<DocInfo_t> *results)
{
//遍历所有的.html文件
for (const auto &file : files_list)
{
string result;
//读取文件的内容,放到result中
ns_util::FileUtil::ReadFile(file, &result);
//用读到的内容构建一个文档的DocInfo结构体
DocInfo_t doc;
// 1、解析网页标题
if (!ParseTitle(result, &doc.title))
{
continue;
}
// 2、解析网页内容
if (!ParseContent(result, &doc.content))
{
continue;
}
// 3、解析网页的官网url
if (!ParseUrl(file, &doc.url))
{
continue;
}
// // for debug
// ShowDoc(doc);
// break;
//把构建好的DocInfo_t结构体插入到文档的vector中
results->push_back(std::move(doc));
}
return true;
}
static bool SaveHtml(const string &output, const vector<DocInfo_t> &results)
{
// 定义title、content和url的分隔符
#define SEP '\3'
// 以写的形式打开文件
// 按照二进制的方式写入
std::ofstream out(output, std::ios::out | std::ios::binary);
if (!out.is_open())
{
std::cerr << "open " << output << " failed..." << endl;
return false;
}
//把格式化的结构体的内容以\3作为title,content和url的分隔符,
//\n作为文档和文档的分隔符,构建成一个一个的字符串,然后写入到
//指定路径的文件中去
for (const auto &item : results)
{
string out_string;
out_string += item.title;
out_string += SEP;
out_string += item.content;
out_string += SEP;
out_string += item.url;
out_string += '\n';
out.write(out_string.c_str(), out_string.size());
}
// 关闭文件
out.close();
return true;
}
int main()
{
// 1、枚举所有的.html的文件名
vector<string> files_list;
if (!EnumFileName(src_file, &files_list))
{
perror("EnumFileName failed");
return 1;
}
// 2、读取所有的.html文件并解析出title,content,url(去标签)
vector<DocInfo_t> results;
if (!ParseHtml(files_list, &results))
{
perror("ParseHtml failed");
return 2;
}
// 3、把解析出来的结果全部写进output路径下,title/content/url以'\3'作为分隔符,
// 文件与文件之间以'\n'作为分隔符
if (!SaveHtml(output, results))
{
perror("SaveHtml failed");
return 3;
}
return 0;
}
二、根据去标签之后的干净的数据构建正排和倒排索引:index.hpp
正排索引是根据id找到对应的文档的一种索引形式。
倒排索引是根据关键字找到对应的文档id的一种索引形式。
例如:
我们的搜索引擎都是通过关键字搜索内容的,所以我们平时使用搜索引擎搜索东西的时候,服务器后台都是先通过关键字查找自己的倒排索引,得到对应的doc_id,再通过doc_id查正排索引,获取到文档的内容,合并并放回给用户的。
那么正排索引和倒排索引到底是怎么建立的呢?见以下代码及注释:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include "util.hpp"
#include <mutex>
#include "log.hpp"
namespace ns_index
{
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档的内容
std::string url; // 文档的官网url
uint16_t doc_id; // 文档的ID
};
//每一个关键字对应的节点结构体,即倒排拉链的节点
struct InvertedElem
{
std::string word; // 关键字
uint64_t doc_id; // 文档的ID
int weight; // 权重
};
//倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
// 正排索引
std::vector<DocInfo> forward_index;
// 倒排拉链
// 一个关键字和一组InveredElem对应
std::unordered_map<std::string, InvertedList> inverted_index;
static Index *inst;//设置成单例模式
static std::mutex mtx;//互斥锁
private:
Index()
{
}
Index(const Index &) = delete;
Index &operator=(const Index &) = delete;
public:
//获取单例对象的指针
static Index *GetIndex()
{
if (nullptr == inst)
{
mtx.lock();
if (nullptr == inst)
{
inst = new Index;
}
mtx.unlock();
}
return inst;
}
~Index()
{
}
// 根据doc_id,找到文档内容,正排索引
DocInfo *GetForwardIndex(uint16_t doc_id)
{
// 先判断doc_id是否越界
if (doc_id >= forward_index.size())
{
perror("doc_id out of range");
return nullptr;
}
// 直接返回对应下标的DocInfo_t即可
return &forward_index[doc_id];
}
// 根据关键字word,获取倒排拉链
InvertedList *GetInvertedList(const std::string &word)
{
// 查找倒排拉链
auto ret = inverted_index.find(word);
if (ret == inverted_index.end())
{
perror("InvertedList is not found");
return nullptr;
}
// 返回找到的倒排拉链
return &(ret->second);
}
// 建立索引
// 根据去标签,格式化之后的文档内容,构建正排和倒排索引
// input: data/raw_html/raw.txt
bool BuildIndex(const std::string &input) // Parse处理完的数据交给我建立索引
{
//以二进制读文件的形式打开文件
std::ifstream in(input, std::ios::in | std::ios::binary);
if (!in.is_open())
{
perror("open file failed");
return false;
}
std::string line;
int count = 0;
//因为文档和文档之间的分隔符是'\n',所以按行读取即可
while (std::getline(in, line))
{
// 用读取到的整个文档的内容建立正排索引,并把该构建的
//结构体的指针进行返回
DocInfo *pdoc = BuildForWardIndex(line);
if (nullptr == pdoc)
{
perror("BuildForWardIndex failed");
continue;
}
//利用正排索引的结构体内容,
// 建立倒排索引
bool ret = BuildInvertedIndex(*pdoc);
if (!ret)
{
perror("BuildInvertedIndex failed");
continue;
}
//for debug
count++;
if (count % 50 == 0)
{
//std::cout << "当前已经建立到索引:" << count << std::endl;
LOG(NORMAL,"当前已经建立到索引:"+std::to_string(count));
}
}
return true;
}
private:
// 建立正排索引
DocInfo *BuildForWardIndex(const std::string &line)
{
// 1、解析line,字符串切分
std::vector<std::string> results;
const std::string sep = "\3";
ns_util::StringUtil::Split(line, &results, sep);
if (results.size() != 3)
{
perror("ns_util::StringUtils::CutString failed");
return nullptr;
}
// 2、构建DocInfo
DocInfo doc;
doc.title = results[0];
doc.content = results[1];
doc.url = results[2];
doc.doc_id = forward_index.size();
// 3、插入到正排索引的vector
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
//进行词频统计的结构体
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt()
: title_cnt(0), content_cnt(0)
{
}
};
// 建立倒排索引
bool BuildInvertedIndex(const DocInfo &doc)
{
std::unordered_map<std::string, word_cnt> word_map;
// 1、对标题进行分词,jieba分词
std::vector<std::string> title_words;
ns_util::JiebaUtils::CutString(doc.title, &title_words);
// 对标题进行词频统计
for (auto s : title_words)
{
// 进行小写转换,即忽略大小写
boost::to_lower(s);
word_map[s].title_cnt++;
}
// 2、对文档内容进行分词,jieba分词
std::vector<std::string> content_words;
ns_util::JiebaUtils::CutString(doc.content, &content_words);
// 对内容进行词频统计
for (auto s : content_words)
{
// 进行小写转换,即忽略word的大小写
boost::to_lower(s);
word_map[s].content_cnt++;
}
const int X = 10;
const int Y = 1;
// 构建倒排索引
for (const auto &word_pair : word_map)
{
// 构建倒排拉链的节点
InvertedElem elem;
//节点的doc_id一定和文档的doc_id是一样的
elem.doc_id = doc.doc_id;
elem.word = word_pair.first;//关键字
elem.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt;//词频累加
// 获取该关键字对应的倒排拉链
InvertedList &inverted_list = inverted_index[elem.word];
// 在该倒排拉链中插入节点
inverted_list.push_back(std::move(elem));
}
return true;
}
};
//静态成员初始化
Index *Index::inst = nullptr;
std::mutex Index::mtx;
}
三、提供搜索功能:searcher.hpp
这个就是根据关键字查正排索引和倒排索引的.hpp文件:
#pragma once
#include "util.hpp"
#include "index.hpp"
#include <algorithm>
#include <jsoncpp/json/json.h>
#include "log.hpp"
namespace ns_searcher
{
class Searcher
{
public:
Searcher()
{
}
~Searcher()
{
}
//这个结构体是和InvertedElem结构体对应的,因为分词的时候
//每一个关键字都必须对应一个倒排拉链,所以同一个搜索内容
//中的经过分词的关键字可能对应的是同一个文档,
//例如:"我要去上学了",可以分词成"我/要/去/上学/去上学",
//这样的话"我/要/去/上学/去上学"的每个词可能都对应同一个文档,
//所以这句搜索内容得到了5个同样的文档,这对用户来说就不太好,
//所以我们在返回的时候应该将同样的文档的东西进行合并,即关键字
//作为一个vector保存,权重(weight)就累加即可
struct InvertedElemPrint
{
uint64_t doc_id; //文档的doc_id
std::vector<std::string> words; //搜索内容在文档中的关键字
int weight; //权重
InvertedElemPrint()
: doc_id(0), weight(0)
{
}
};
void InitSearcher(const std::string &input)
{
index = ns_index::Index::GetIndex();
if (nullptr == index)
{
perror("GetIndex failed");
return;
}
//std::cout << "获取index单例成功..." << std::endl;
LOG(NORMAL,"获取index单例成功...");
//建立索引
index->BuildIndex(input);
//std::cout << "建立正排和倒排索引完成" << std::endl;
LOG(NORMAL,"建立正排和倒排索引完成...");
}
std::string GetDesc(const std::string &html_content, const std::string &word)
{
//不能直接用find查找,因为建立的索引中的文档内容全部转变为了
//小写,但是html_content是文档的原始内容,是存在大小写的
// int pos = html_content.find(word);
// if (pos == std::string::npos)
// {
// std::cout<<"1111111111111111"<<std::endl;
// return "None";
// }
//利用迭代器查找,可以在比较的之前把ch1和ch2变成小写再比较
auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(),
[](char ch1, char ch2)
{
return std::tolower(ch1) == std::tolower(ch2);
});
if (iter == html_content.end())
{
return "None";
}
//注意不要用size_t
int pos = iter - html_content.begin();
int start = 0;
int end = html_content.size() - 1;
int prev_step = 50;
int next_step = 100;
if (pos - prev_step > start)
{
start = pos - prev_step;
}
if (pos + next_step < end)
{
end = pos + next_step;
}
if (start >= end)
{
// std::cout<<"2222222222222222222"<<std::endl;
return "None";
}
// std::cout<<"len : "<<end-start<<std::endl;
// std::cout<<"start : "<<start<<std::endl;
// std::cout<<"end : "<<end<<std::endl;
// std::cout<<"len : "<<end-start<<std::endl;
std::string desc = html_content.substr(start, end - start);
desc += "...";
return desc;
// return "None";
}
void Search(const std::string &query, std::string *json_string)
{
std::vector<std::string> words;
// jieba分词
ns_util::JiebaUtils::CutString(query, &words);
//对于同一个doc_id,所以的内容合并成一个InvertedElemPrint
std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;
std::vector<InvertedElemPrint> inverted_list_all;
// for (auto word : words)
// {
// boost::to_lower(word);
// ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
// if (nullptr == inverted_list)
// {
// continue;
// }
// inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
// }
for (auto word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (nullptr == inverted_list)
{
continue;
}
//倒排拉链的所有doc_id相同的InvertedElem节点的
//合并成一个InvertedElemPrint节点
for (const auto &iter : (*inverted_list))
{
auto& elem=tokens_map[iter.doc_id];
elem.doc_id = iter.doc_id;
elem.weight += iter.weight;
elem.words.push_back(iter.word);
}
}
//最后再遍历tokens_map,把所以的InvertedElemPrint节点
//保存起来
for (auto &elem : tokens_map)
{
inverted_list_all.push_back(std::move(elem.second));
}
//对所有的InvertedElemPrint节点进行降序排序
std::sort(inverted_list_all.begin(), inverted_list_all.end(),
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2)
{
return e1.weight > e2.weight;
});
// std::cout<<"111111111111111111111111111"<<std::endl;
//利用jsoncpp进行序列和反序列化
//万能对象
Json::Value root;
for (auto &item : inverted_list_all)
{
const ns_index::DocInfo *pdoc = index->GetForwardIndex(item.doc_id);
if (nullptr == pdoc)
{
continue;
}
//万能对象
Json::Value elem;
elem["title"] = pdoc->title;
// std::cout << "111111111111111111111111111" << std::endl;
elem["desc"] = GetDesc(pdoc->content, item.words[0]);
elem["url"] = pdoc->url;
// // for debue for delete
// elem["weight"] = item.weight;
// elem["doc_id"] = (int)item.doc_id;
//把elem追加到root对象中,即嵌套万能对象
root.append(elem);
}
//序列化
Json::FastWriter writer;
*json_string = writer.write(root);
// std::cout<<"111111111111111111111111111"<<std::endl;
}
private:
// 供系统进行查找的索引
ns_index::Index *index;
};
}
四、放公用方法的头文件(包括boost库中的一些方法以及jieba分词的方法):util.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <mutex>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include "log.hpp"
#include "cppjieba/Jieba.hpp"
// namespace ns_util
// {
// class FileUtil
// {
// public:
// static bool ReadFile(const std::string& file_path,std::string* out)
// {
// std::ifstream in(file_path,std::ios_base::in);
// if(!in.is_open())
// {
// std::cerr<<"open file"<<file_path<<"failed"<<std::endl;
// return false;
// }
// std::string line;
// //如何理解getline读取到文件结束呢??
// //getline的返回值是一个引用,while(bool),
// //本质是因为重载了强制类型转化
// while(getline(in,line))
// {
// *out+=line;
// }
// in.close();
// return true;
// }
// };
// }
namespace ns_util
{
class FileUtil
{
public:
static bool ReadFile(const std::string &file_path, std::string *out)
{
// 读取文件,以读的方式打开文件
std::ifstream in(file_path, std::ios::in);
if (!in.is_open())
{
perror("open file failed");
return false;
}
std::string line;
// getline返回值是一个istream,但是重载了operator bool,所以可以作为while循环的
// 结束判断条件
while (std::getline(in, line))
{
(*out) += line;
}
// 关闭文件
in.close();
return true;
}
};
class StringUtil
{
public:
// 字符串切分
static void Split(const std::string &line, std::vector<std::string> *results, const std::string sep)
{
// 以\3为分隔符,切分line字符串,切分得到的结果放到results中,token_compress_on表示如果存在
// 多个分割符就压缩成一个
boost::split(*results, line, boost::is_any_of("\3"), boost::token_compress_on);
}
};
// jieba分词的词库的路径
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 JiebaUtils
// {
// private:
// static cppjieba::Jieba jieba;
// public:
// static void CutString(const std::string src,std::vector<std::string>* out)
// {
// //把src字符串进行jieba分词,分词的结果放到*out中
// jieba.CutForSearch(src,*out);
// }
// };
// //用jieba分词的词库初始化jieba对象
// cppjieba::Jieba JiebaUtils::jieba(DICT_PATH,
// HMM_PATH,
// USER_DICT_PATH,
// IDF_PATH,
// STOP_WORD_PATH);
class JiebaUtils
{
private:
// 创建一个jieba对象
cppjieba::Jieba jieba;
//存放暂停词的哈希表
std::unordered_map<std::string, bool> stop_words;
static JiebaUtils *instance;
private:
JiebaUtils()
: jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH)
{
}
JiebaUtils(const JiebaUtils&)=delete;
JiebaUtils& operator=(const JiebaUtils&)=delete;
public:
static JiebaUtils* GetInstance()
{
static std::mutex mtx;
if(instance==nullptr)
{
mtx.lock();
if(instance==nullptr)
{
instance=new JiebaUtils();
instance->InitJiebaUtils();
}
mtx.unlock();
}
return instance;
}
//读取存放暂停词的文件,
//初始化暂停词哈希表
void InitJiebaUtils()
{
// 打开存放暂停词的文件
std::ifstream in(STOP_WORD_PATH);
if (!in.is_open())
{
perror("open STOP_WORD_PATH file failed");
return;
}
std::string line;
// 读取暂停词并存放到哈希表中
while (std::getline(in, line))
{
stop_words[line] = true;
}
//关闭文件
in.close();
}
void CutStringHelper(const std::string src, std::vector<std::string> *out)
{
//jieba分词
jieba.CutForSearch(src, *out);
//遍历查询,看看jieba分词分出来的结果中存不存在
//暂停词,如果存在,即把它删除掉
for (std::vector<std::string>::iterator iter = out->begin(); iter != out->end();)
{
auto ret = stop_words.find(*iter);
if (ret != stop_words.end())
{
//用iter接收erase的结果是为了
//防止迭代器失效的问题
iter = out->erase(iter);
}
else
{
iter++;
}
}
}
public:
static void CutString(const std::string src, std::vector<std::string> *out)
{
// 把src字符串进行jieba分词,分词的结果放到*out中
ns_util::JiebaUtils::GetInstance()->CutStringHelper(src, out);
}
};
JiebaUtils *JiebaUtils::instance = nullptr;
}
五、引入http_server
http_server是直接从第三方库引入的:
5.1 httplib.h
这个httplib.h是从gitee上的开源代码中引入的,由于代码太多,所以大家可以从我的gitee上获取:
第三方库中引入的httplib.h
5.2 http_server.cc
提供网络服务的代码:
#include <iostream>
#include "httplib.h"
#include "searcher.hpp"
#include "log.hpp"
//web根目录
const std::string& rootdir="./wwwroot";
//经过数据清洗(去标签)之后的干净的文档内容的路径
const std::string input="./data/raw_html/raw.txt";
int main()
{
//创建并启动http服务器
httplib::Server svr;
//设置web根目录
svr.set_base_dir(rootdir.c_str());
ns_searcher::Searcher search;
//用经过数据清洗(去标签)之后的干净的文档内容初始化search服务
search.InitSearcher(input);
//注册一个方法
//在浏览器通过ip:port/s?word=...的形式访问我们的http服务器
svr.Get("/s",[&search](const httplib::Request& req,httplib::Response& res) {
if(!req.has_param("word"))//如果没有搜索关键字,则直接返回一条日志信息
{
res.set_content("必须要有搜索关键字...","text/plain;charset=UTF-8");
return;
}
std::string json_string;
//提取关键字
std::string ret=req.get_param_value("word");
LOG(NORMAL,"用户在搜索:"+ret);
//进行搜索服务,把搜索之后得到的结果放到json_string中
search.Search(ret,&json_string);
//返回内容给浏览器,然后浏览器显示出来的响应
res.set_content(json_string,"application/json");
});
//启动服务器
svr.listen("0.0.0.0",8081);
return 0;
}
5.3 debug.cc
这是用来调试代码,如果程序出现bug,就用这个文件进行调试,修复好bug之后再用http_server.cc进行发布。
#include <iostream>
#include "index.hpp"
#include "searcher.hpp"
const std::string input="data/raw_html/raw.txt";
//测试搜索服务
int main()
{
//创建搜素对象
ns_searcher::Searcher* search=new ns_searcher::Searcher;
search->InitSearcher(input);//初始化
std::string query;
std::string ret;
while(true)
{
std::cout<<"Please Enter: ";
std::getline(std::cin,query);
search->Search(query,&ret);
std::cout<<ret<<std::endl;
//std::cout<<query<<std::endl;
}
delete search;
return 0;
}
六、log.hpp
用来打印调试日志的方法。
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#define NORMAL 1
#define WARNING 2
#define DEBUG 3
#define FATAL 4
#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)
//打印日志信息
void log(std::string level, std::string message, std::string file, int line)
{
std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" \
<< "[" << message << "]" << "[" << file << " : " << line << "]" \
<< std::endl;
}
七、makefile
.PHONY:all
all:parser debug http_server
parser:parser.cc
g++ -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem -ljsoncpp
debug:debug.cc
g++ -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem -ljsoncpp
http_server:http_server.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -f parser debug http_server
八、wwwroot:俗称web根目录
在wwwroot目录下建立一个index.html文件,这个就是搜索时返回的网页的内容。
所有的网页资源都放在该目录下,这个就是涉及到前端的知识了,这里就不过多地叙述了,直接见代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<title>boost 搜索引擎</title>
<style>
/* 去掉网页中的所有的默认内外边距,html的盒子模型 */
* {
/* 设置外边距 */
margin: 0;
/* 设置内边距 */
padding: 0;
}
/* 将我们的body内的内容100%和html的呈现吻合 */
html,
body {
height: 100%;
}
/* 类选择器.container */
.container {
/* 设置div的宽度 */
width: 800px;
/* 通过设置外边距达到居中对齐的目的 */
margin: 0px auto;
/* 设置外边距的上边距,保持元素和网页的上部距离 */
margin-top: 15px;
}
/* 复合选择器,选中container 下的 search */
.container .search {
/* 宽度与父标签保持一致 */
width: 100%;
/* 高度设置为52px */
height: 52px;
}
/* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*/
/* input在进行高度设置的时候,没有考虑边框的问题 */
.container .search input {
/* 设置left浮动 */
float: left;
width: 600px;
height: 50px;
/* 设置边框属性:边框的宽度,样式,颜色 */
border: 1px solid black;
/* 去掉input输入框的有边框 */
border-right: none;
/* 设置内边距,默认文字不要和左侧边框紧挨着 */
padding-left: 10px;
/* 设置input内部的字体的颜色和样式 */
color: #383131;
font-size: 14px;
}
/* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/
.container .search button {
/* 设置left浮动 */
float: left;
width: 150px;
height: 52px;
/* 设置button的背景颜色,#4e6ef2 */
background-color: #4e6ef2;
/* 设置button中的字体颜色 */
color: #FFF;
/* 设置字体的大小 */
font-size: 19px;
font-family:Georgia, 'Times New Roman', Times, serif;
}
.container .result {
width: 100%;
}
.container .result .item {
margin-top: 15px;
}
.container .result .item a {
/* 设置为块级元素,单独站一行 */
display: block;
/* a标签的下划线去掉 */
text-decoration: none;
/* 设置a标签中的文字的字体大小 */
font-size: 20px;
/* 设置字体的颜色 */
color: #0529ba;
}
.container .result .item a:hover {
text-decoration: underline;
}
.container .result .item p {
margin-top: 5px;
font-size: 16px;
font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
color: #810b0b;
}
.container .result .item i{
/* 设置为块级元素,单独站一行 */
display: block;
/* 取消斜体风格 */
font-style: normal;
color: rgb(4, 120, 4);
}
</style>
</head>
<body>
<div class="container">
<div class="search">
<input type="text" value="请输入搜索关键字">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result">
<!-- 动态生成网页内容 -->
<!-- <div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div> -->
</div>
</div>
<script>
function Search(){
// 是浏览器的一个弹出框
// alert("hello js!");
// 1. 提取数据, $可以理解成就是JQuery的别称
let query = $(".container .search input").val();
console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据
//2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: function(data){
console.log(data);
BuildHtml(data);
}
});
}
function BuildHtml(data){
// 获取html中的result标签
let result_lable = $(".container .result");
// 清空历史搜索结果
result_lable.empty();
for( let elem of data){
// console.log(elem.title);
// console.log(elem.url);
let a_lable = $("<a>", {
text: elem.title,
href: elem.url,
// 跳转到新的页面
target: "_blank"
});
let p_lable = $("<p>", {
text: elem.desc
});
let i_lable = $("<i>", {
text: elem.url
});
let div_lable = $("<div>", {
class: "item"
});
a_lable.appendTo(div_lable);
p_lable.appendTo(div_lable);
i_lable.appendTo(div_lable);
div_lable.appendTo(result_lable);
}
}
</script>
</body>
</html>
除了以上这些关键的文件之外,还有一些需要下载下来的文件,例如boost的提供搜索的资源的文件等,哪些文件就可以到我的gitee上直接下载或者复制即可。
九、gitee源码分享
boost搜索引擎的项目源代码分享
十、部署到Linux服务器上的指令
nohup ./http_server > log.txt 2>&1 &
十一、项目的扩展方向
虽然我们的项目叫做boost搜索引擎,但是它不仅仅叫做boost搜索引擎,它叫什么完全取决于这个搜索引擎的数据源是什么,如果数据源是百度的所有的资源,那么就可以叫做百度搜索引擎。
项目的扩展方向有以下几个,可以尝试做一下:
- 建立整站搜索,可以把boost库中所有版本的资源都下载下来,添加到我们的服务器上。
- 不使用组件,而是自己设计一下对应的各种方案(例如不使用http服务器,jieba分词,jsoncpp等第三方库)
- 在我们的搜索引擎中,添加竞价排名(强烈推荐),例如可以在我们搜索某个关键字的时候把我们的博客链接推送出去。
- 热词统计,智能显示搜索关键词(字典树,优先级队列等)(比较推荐)
- 设置登陆注册,引入对mysql的使用(比较推荐)
以上就是boost搜索引擎项目的全部内容了,你做出来了吗?如果感觉到有所帮助,那就点亮一个小心心,点点关注呗,我们下期见!!!