文章目录
前言
嘿,这是我的第二个项目啦,这个项目还是蛮简单的,不会太难!
但是还是要抱以认真的态度去学习!
一、项目背景
我们常用的搜索引擎有 Google、百度、360 等,这些搜索引擎都是超大型超完善的全网搜索,而本项目Boost搜索引擎只是一个非常简单的站内搜索
该项目源自本人从 github 上找到的⼀个开源项目,主要是日常生活中经常使用搜索引擎, 对于搜索引擎技术如何实现的比较感兴趣。顺便也想锻炼⼀下自己的 C++ 工程代码能力,于是就这么自然而然的有了这个项目,嘻嘻
其实在2023年以前Boost是没有官方的搜索引擎的,但是现在已经有了,对于我做的来说肯定没官网的来得好(,不过也还不错
在我的博客里面经常出现的 cplusplus 官网就是一个很经典的站内搜索,当我们想查看 vector 的官方文档时可以直接在搜索框中搜索,就能得到我们想要的信息。
我们可以看看Edge浏览器的搜索大概是个什么样子展示的
可以看到大概分成 url、标题、摘要 等内容,有的还有图片,当然了为了简单我这里再次偷懒了哈哈哈哈,当然了我们还发现当我们的搜索语句中有多个搜索关键词的时候,它是不严格匹配的,意思是搜索关键字有可能已经被切分过了,比如说被拆成了“中南/大学/中南大学/计算机/科学与技术/计算机科学与技术”(当然还有更多的拆词可能)
我们在这里思考一下搜索引擎搜索的宏观原理是什么,具体你可以看看以下由DeepSeek大人生成的原理图
在有了这个认识以后,大家就可以跟我开始做这个项目啦!
二、项目环境和技术栈
- 项目环境:Ubuntu-22.04、vscode、gcc/g++、makefile
- 技术栈:C/C++、C++11、STL、Boost库、JsonCpp、cppjieba、cpp-httplib
可以看到还用到了好多的库,这些我们都可以 git clone 下来
上面技术栈中,你需要注意一下这两个:
- cppjieba 是一个用 C++ 实现的中文分词库,它具有高效、准确、易用等特点;
- cpp-httplib 是一个轻量级、跨平台的 C++ HTTP 库,它以单头文件的形式存在,使用起来非常便捷。
三、正排索引和倒排索引
假设现在有两份文档,文档内依次有这样的信息:
- 文档1:小泓是中南大学的学生
- 文档2:小钱是计算机学院的学生
正排索引:从文档ID找到文档内容(文档中的关键字)。
文档ID | 文档内容 |
---|---|
1 | 小泓是中南大学的学生 |
2 | 小钱是计算机学院的学生 |
现在对两个目标文档进行分词
文档1:小泓、大学、中南大学、学生
文档2:小钱、计算机、计算机学院、学生
倒排索引:根据文档内容分词,整理不重复的关键字,找到对应文档ID的方案
关键字 | 文档ID |
---|---|
小泓 | 文档1 |
大学 | 文档1 |
中南大学 | 文档1 |
学生 | 文档1、文档2 |
小钱 | 文档2 |
计算机 | 文档2 |
计算机学院 | 文档2 |
所以说我们现在就可以想到一个大致的流程:
- 当用户输入学生 -> 倒排索引中查找 -> 提取文档ID -> 根据正排索引 -> 找到文档内容 -> title+desc+url -> 构建响应结果。
但是其实我们可以看到“学生”这个关键字出现在 文档1 和 文档2 ,那具体会怎么显示呢,不急,我们后面会再跟大家说明
四、数据去标签与清洗
首先我们先去Boost官网下载数据源
之后 rz -E 拉取到 Ubuntu 上,再 tar xzf 解压,⽬前我们只需要 boost_1_89_0/doc/html ⽬录下的 html ⽂件,⽤它来进行建⽴索引
好的,接下来我们要做的就是去标签,具体来说,就是对筛选好的 html 文件进行解析(去标签),拆分出标题、内容、网址
我们现在随便打开一个 html 文件,可以看到有价值的部分就是黑色字体,这就是我们去标签后想要提取的内容
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Acknowledgements</title>
<link rel="stylesheet" href="../../../doc/src/boostbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
<link rel="home" href="../index.html" title="The Boost C++ Libraries BoostBook Documentation Subset">
<link rel="up" href="../accumulators.html" title="Chapter 1. Boost.Accumulators">
<link rel="prev" href="user_s_guide.html" title="User's Guide">
<link rel="next" href="reference.html" title="Reference">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
<table cellpadding="2" width="100%"><tr>
<td valign="top"><img alt="Boost C++ Libraries" width="277" height="86" src="../../../boost.png"></td>
<td align="center"><a href="../../../index.html">Home</a></td>
<td align="center"><a href="../../../libs/libraries.htm">Libraries</a></td>
<td align="center"><a href="http://www.boost.org/users/people.html">People</a></td>
<td align="center"><a href="http://www.boost.org/users/faq.html">FAQ</a></td>
<td align="center"><a href="../../../more/index.htm">More</a></td>
</tr></table>
<hr>
<div class="spirit-nav">
<a accesskey="p" href="user_s_guide.html"><img src="../../../doc/src/images/prev.png" alt="Prev"></a><a accesskey="u" href="../accumulators.html"><img src="../../../doc/src/images/up.png" alt="Up"></a><a accesskey="h" href="../index.html"><img src="../../../doc/src/images/home.png" alt="Home"></a><a accesskey="n" href="reference.html"><img src="../../../doc/src/images/next.png" alt="Next"></a>
</div>
<div class="section">
<div class="titlepage"><div><div><h2 class="title" style="clear: both">
<a name="accumulators.acknowledgements"></a><a class="link" href="acknowledgements.html" title="Acknowledgements">Acknowledgements</a>
</h2></div></div></div>
<p>
Boost.Accumulators represents the efforts of many individuals. I would like
to thank Daniel Egloff of <a href="http://www.zkb.com" target="_top">Zürcher Kantonalbank</a>
for helping to conceive the library and realize its implementation. I would
also like to thank David Abrahams and Matthias Troyer for their key contributions
to the design of the library. Many thanks are due to Michael Gauckler and Olivier
Gygi, who, along with Daniel Egloff, implemented many of the statistical accumulators.
</p>
<p>
I would also like to thank Simon West for all his assistance maintaining Boost.Accumulators.
</p>
<p>
Finally, I would like to thank <a href="http://www.zkb.com" target="_top">Zürcher Kantonalbank</a>
for sponsoring the work on Boost.Accumulators and graciously donating it to
the community.
</p>
</div>
<div class="copyright-footer">Copyright © 2005, 2006 Eric Niebler<p>
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at <a href="http://www.boost.org/LICENSE_1_0.txt" target="_top">http://www.boost.org/LICENSE_1_0.txt</a>)
</p>
</div>
<hr>
<div class="spirit-nav">
<a accesskey="p" href="user_s_guide.html"><img src="../../../doc/src/images/prev.png" alt="Prev"></a><a accesskey="u" href="../accumulators.html"><img src="../../../doc/src/images/up.png" alt="Up"></a><a accesskey="h" href="../index.html"><img src="../../../doc/src/images/home.png" alt="Home"></a><a accesskey="n" href="reference.html"><img src="../../../doc/src/images/next.png" alt="Next"></a>
</div>
</body>
</html>
我们在 data 目录 下的 raw_html 目录下 创建有一个 raw.txt 文件,用来存储干净的数据文档,存储的形式大概是怎么样的呢?
- 写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
- 类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n …
- 方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
- 文件内按照 \3 作为分割符,每个文件再按照 \n 进行区分
五、Parser
我们在 boostEngine 目录下创建 parser.cpp 文件开始编写框架
一共分为三步:
- 第⼀步 EnumFile(src_path, &files_list) :递归式的把每个 html ⽂件名带路径,保存到文件 files_list 中,⽅便后期进⾏⼀个⼀个的 ⽂件进⾏读取(预处理
- 第⼆步 ParseHtml(files_list, &results) :按照 files_list 读取每个⽂件,并进⾏解析
- 第三步 SaveHtml(results, output) :把解析完毕的各个⽂件内容,写⼊到 output ,按照 \3 作为每个⽂档的分割符
#include <iostream>
#include <string>
#include <vector>
// 首先我们肯定会读取文件,所以先将 文件的路径名 罗列出来
// 将 数据源的路径 和 清理后干净文档的路径 定义好
const std::string src_path = "data/input"; // 数据源的路径
const std::string output = "data/raw_html/raw.txt"; // 清理后干净文档的路径
// DocInfo --- 文件信息结构体
typedef struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档的内容
std::string url; // 该文档在官网当中的url
}DocInfo_t;
// 命名规则
// const & ---> 输入
// * ---> 输出
// & ---> 输入输出
// 把每个 html 文件名带路径,保存到 files_list 中
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);
// 按照 files_list 读取每个文件的内容,并进行解析
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
// 把解析完毕的各个文件的内容写入到 output
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);
int main()
{
std::vector<std::string> files_list; // 将所有的 html文件名保存在 files_list 中
// 第一步:递归式的把每个 html文件名 带路径,保存到 files_list 中,方便后期进行一个一个的文件读取
// 从 src_path 这个路径中提取 html 文件,将提取出来的文件存放在 string 类型的 files_list 中
if(!EnumFile(src_path, &files_list)) // EnumFile--枚举文件
{
std::cerr << "enum file name error! " << std::endl;
return 1;
}
return 0;
// 第二步:从 files_list 文件中读取每个.html 的内容,并进行解析
std::vector<DocInfo_t> results;
// 从 file_list 中进行解析,将解析出来的内容存放在 DocInfo 类型的 results 中
if(!ParseHtml(files_list, &results)) // ParseHtml--解析html
{
std::cerr << "parse html error! " << std::endl;
return 2;
}
// 第三部:把解析完毕的各个文件的内容写入到 output ,按照 \3 作为每个文档的分隔符
// 将 results 解析好的内容,直接放入 output 路径下
if(!SaveHtml(results, output))// SaveHtml--保存html
{
std::cerr << "save html error! " << std::endl;
return 3;
}
return 0;
}
所以我们发现, parser.cpp 主要完成 枚举文件、解析 html 文件、保存 html 文件 三个工作,然而这三种方法是需要我们使用 Boost库 中的方法的,我们先来安装一下 Boost库
当然我已经安装过了哈,下面是我们编写代码需要用到的 boost库 当中的 filesystem方法,箭头指向的是部分会用到的部分
六、EnumFile
还是先要明确我们使用这个 EnumFile 函数的目的 – 把每个html文件名带路径保存到 file_lists中
// 在原有的基础上添加这个头文件
#include <boost/filesystem.hpp>
//把每个 html文件名 带路径,保存到 files_list 中
bool EnumFile(const std::string& src_path, std::vector<std::string>* files_list)
{
// 简化作用域的书写
// 在这个函数作用域里面,fs就等同于boost::filesystem
namespace fs = boost::filesystem;
fs::path root_path(src_path); // 定义一个path对象,枚举文件就从这个路径下开始
// 判断路径是否存在
if(!fs::exists(root_path))
{
std::cerr << src_path << " not exists" << std::endl;
return false;
}
// 对文件进行递归遍历
fs::recursive_directory_iterator end; // 定义了一个空的迭代器,用来进行判断递归结束 -- 相当于 NULL
for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
// 判断指定路径是不是常规文件,如果指定路径是目录或图片直接跳过
if(!fs::is_regular_file(*iter))
{
continue;
}
// 如果满足了是普通文件,还需满足是 .html 结尾的
// 如果不满足也是需要跳过的
// ---通过 iter 这个迭代器(理解为指针)的一个 path 方法(提取出这个路径)
// ---然后通过 extension() 函数获取到路径的后缀
if(iter->path().extension() != ".html")
{
continue;
}
//std::cout << "debug: " << iter->path().string() << std::endl; // 测试代码
// 走到这里一定是一个合法的路径,以 .html 结尾的普通网页文件
// 将所有带路径的 html 保存在 files_list 中,方便后续进行文本分析
files_list->push_back(iter->path().string());
}
return true;
}
以及修改我们的 makefile
编译后查看一下链接,发现也是成功链接到外部数据库
我们把那句debug测试语句取消注释,再次编译后查看前十个 html 文件,最后发现也是完美成功!
七、ParseHtml
步骤如下:
- 读取刚刚枚举好的文件
- 解析 html 文件中的 title
- 解析 html 文件中的 content
- 解析 html 文件中的路径,构建 url
多说无益,还是先来上一上代码吧!
bool ParseHtml(const std::vector<std::string>& files_list, std::vector<DocInfo_t>* results)
{
for(const std::string& file : files_list)
{
// 1.读取文件,Read()
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
// 2.解析指定的文件,提取title
DocInfo_t doc;
if(!ParseTitle(result, &doc.title))
{
continue;
}
// 3.解析指定的文件,提取content
if(!ParseContent(result, &doc.content))
{
continue;
}
// 4.解析指定的文件路径,构建url
if(!ParseUrl(file, &doc.url))
{
continue;
}
// 到这里,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
// 本质会发生拷贝,效率肯能会比较低,这里我们使用move后的左值变成了右值,去调用push_back的右值引用版本
results->push_back(std::move(doc));
}
return true;
}
ReadFile
根据文件名,从中读取文件内容到 result 中,这就是我们这个函数所要完成的内容,我们把这个函数定义在 util.hpp 文件的类 FileUtil 中
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
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())
{
std::cerr << "open file " << file_path << " error" << std::endl;
return false;
}
std::string line;
// getline的返回值是一个&,while(bool)
// 能结束循环, 本质是因为重载了强制类型转化
while(std::getline(in, line))
{
*out += line;
}
in.close();
return true;
}
};
}
至于说为什么会这样,我们可以考虑看下 DS大人 给出的回复
ParseTitle
- 可以发现 < title > 标题 < /title > 构成的
- find(< title >)就能找到这个标签的左尖括号的位置
- 然后加上 < title > 的长度,此时就指向了标题的起始位置
- 同理,再去找到 < /title > 的左尖括号,最后截取子串;
static bool ParseTitle(const std::string& file,std::string* title)
{
// 查找 <title> 位置
std::size_t begin = file.find("<title>");
if(begin == std::string::npos)
{
return false;
}
// 查找 </title> 位置
std::size_t end = file.find("</title>");
if(end == std::string::npos)
{
return false;
}
// 计算中间的距离,截取中间的内容
begin += std::string("<title>").size();
if(begin > end)
{
return false;
}
*title = file.substr(begin, end - begin);
return true;
}
ParseContent
可以用一个简易的状态机来完成,状态机包括两种状态:LABLE(标签) 和 CONTENT(内容)
- 起始肯定是标签,我们逐个字符进行遍历判断
- 如果遇到 “>” ,表明下一个即将是内容了,我们将状态机置为 CONTENT ,接着将内容保存起来
- 如果此时遇到了 “<” ,表明到了标签了,我们再将状态机置为 LABLE ;
- 不断的循环,知道遍历结束
// 去标签 -- 数据清洗
static bool ParseContent(const std::string& file,std::string* content)
{
// 去标签,基于一个简易的状态机
enum status // 枚举两种状态
{
LABLE, // 标签
CONTENT // 内容
};
enum status s = LABLE; // 刚开始肯定会碰到 "<" 默认状态为 LABLE
for(char c : file)
{
// 检测状态
switch(s)
{
case LABLE:
if(c == '>') s = CONTENT;
break;
case CONTENT:
if(c == '<') s = LABLE;
else
{
// 我们不想保留原始文件中的\n,因为我们想用\n作为html解析之后的文本的分隔符
if(c == '\n') c = ' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
ParseUrl
boost库 在网页上的 url,和我们 下载的文档的路径 是 有对应关系的
而我们恰好已经定义出了两个路径 源数据路径 和 清理后干净文档的路径
- 于是拿出官网的部分地址作头:url_head = “https://www.boost.org/doc/libs/1_89_0/doc/html”
- 再将我们项目的路径 data/input 删除后得到 /accumulators.html;
- 最后在拼接一下,就是真正的官网url
//构建官网url :url_head + url_tail
static bool ParseUrl(const std::string& file_path,std::string* url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_89_0/doc/html";
std::string url_tail = file_path.substr(src_path.size()); // 将data/input截取掉
*url = url_head + url_tail; // 拼接
return true;
}
测试代码如下,我们就只打印一个即可
// for debug
void ShowDoc(const DocInfo_t& doc)
{
std::cout << "title: " << doc.title << std::endl;
std::cout << "content: " << doc.content << std::endl;
std::cout << "url: " << doc.url << std::endl;
}
bool ParseHtml(const std::vector<std::string>& files_list, std::vector<DocInfo_t>* results)
{
for(const std::string& file : files_list)
{
// 1.读取文件,Read()
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
// 2.解析指定的文件,提取title
DocInfo_t doc;
if(!ParseTitle(result, &doc.title))
{
continue;
}
// 3.解析指定的文件,提取content
if(!ParseContent(result, &doc.content))
{
continue;
}
// 4.解析指定的文件路径,构建url
if(!ParseUrl(file, &doc.url))
{
continue;
}
// 到这里,一定是完成了解析任务
// 当前文档的相关结果都保存在了doc里面
// 本质会发生拷贝,效率肯能会比较低
// 这里我们使用move后的左值变成了右值,去调用push_back的右值引用版本
results->push_back(doc);
// for debug -- 在测试的时候,将上面的代码改写为 results->push_back(doc);
ShowDoc(doc);
break; // 只截取一个文件打印
}
return true;
}
那么接下来我们就测试测试吧!
我们再点击这个 url ,发现确实是官网的 html ,没错
总结
大概差不多第一节就到这里,应该说这个还是不太难的,下篇继续!