本专栏内容为:项目专栏
💓博主csdn个人主页:小小unicorn
⏩专栏分类:基于Boots的搜索引擎
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识
目录
- 项目简介:
- 1.项目的相关背景
- 2.Boost库简介:
- 特点
- 3.项目主要模块
- 4.项目功能预览
- 项目文件结构:
- 运行结果:
- 5.项目宏观原理
- 6.搜索引擎技术栈和项目环境
- 7.正排索引,倒排索引---搜索引擎具体原理
- 正排索引:
- 倒排索引
- 模拟一次查找的过程
- 8.编写数据去标签与数据清洗的模块Parser
- 准备工作
- 编写parser
- 第一步:我们首先得先把所有文件名列出来,方便后期读取:
- 第二步:按照```files_list```中读取每个文件的内容,并进行解析
- 第三步:把解析完的各个文件内容,写入到``output``中,按照``\3``作为每个文档的分割符
- Parser.cc
- ``EnumFile``编写
- ``ParseHtml``编写
- ``ParseTitle``编写
- ``ParseContent``编写
- ``ParseUrl``编写
- ``SaveHtml``编写
- 遗留小问题
- 9.建立索引模块Index
- 基本框架:
- 根据doc_id找到文档内容:
- 根据关键字string,获得倒排拉链:
- 根据去标签,格式化之后的文档,构建正排和倒排索引:
- 正排索引:
- 倒排索引:
- jieba库的安装:
- 引入jieba库:
- 编写倒排索引:
- 10.编写搜索引擎模块Searcher
- 基本结构:
- 编写单例index
- 编写查找代码:
- 编写测试代码:
- 获取摘要内容:
- 综合调试
- 11.编写 http_server 模块
- 12.编写项目前端模块
- 13.结果演示:
- 14.结项与总结
- 项目扩展方向
项目简介:
本项目是一款基于 Boost
文档的站内搜索引擎。用户只需输入查询关键词,即可快速检索到相关的 Boost
在线文档,并提供相关网页链接,为用户提供了便捷的检索服务。
首先,我们对离线版本的 HTML
文档进行解析,并将解析结果整理为一个行文本文件。然后,读取并处理好的文本文件,进行分词、权重计算等操作,同时在内存中构建正排索引和倒排索引。接下来,对用户查询词进行分词和触发,根据相关度对结果进行排序。最后,将结果以 JSON
格式进行包装,并序列化为字符串返回。所有这些功能通过HTTP
服务器搭载搜索页面,为外部用户提供服务。
1.项目的相关背景
在介绍此项目之前,我们先介绍一下项目背景:
首先我们知道目前主流的开发出搜索引擎有好多:
公司:百度,搜狗,360搜索,头条新闻客户端
但是像这些公司开发出来的搜索引擎,基本上都是全网搜索,我们基本上是搞不定的。虽然我们全网搜索搞不定,但是站内搜索我们可以尝试做一做
站内搜索:搜索的数据更垂直,数据量更小
垂直的意思就是数据是由相关性的!比如搜索c++
就会出来c++
相关的,另一个数据量更小!!
举个例子:
我们在cplusplus
查找某一个库函数,实现的这个查找就是一个站内查找!
另外,我们的boost
库官网是没有像cplusplus
这样实现的一个查找功能,因此就
需要我们自己做一个!
这是我们的Boost官网:
Boost C++
再下来,我们可以看一下市面上的一些搜索引擎,以百度为例:
我们在里面查找一个东西,比如大学生:
实际上我们用搜索引擎出来最后出来的结果分三块,网页标题,摘要以及即将要跳转到下一个的链接。其实基本上商业就是以链接的方式盈利的!
就比如输入个头疼,我们可以看到类似于这样的广告,而广告就是获取盈利的其中一种方式!
2.Boost库简介:
Boost
是一个由C++
社区开发和维护的开源库集合,它提供了许多功能强大且高质量的 C++
库,可以扩展 C++
标准库的功能。Boost 库包含了许多与 C++
标准库相似的组件,同时还提供了一些独特的功能和工具,比如 Boost.Asio
用于网络编程、Boost.Thread
用于线程操作、Boost.Filesystem
用于文件系统操作等等。
Boost
库的目标是通过提供高质量、可移植、且兼容标准的 C++
库来增强 C++
的功能和性能。它的发展历史可以追溯到 1998 年,是一个长期积累和发展的项目。由于 Boost
的质量和广泛使用,很多 Boost
库最终被纳入了 C++
标准库,比如智能指针、函数对象、元编程库等。
特点
-
高质量:
Boost
库经过严格的测试和审核,确保了高质量的代码和可靠的功能。 -
可移植性:
Boost
库可以在各种平台上使用,包括Windows、Linux、Mac
等,为C++
程序的跨平台开发提供了便利。 -
兼容性:
Boost
库与C++
标准库兼容,并且遵循C++
的最佳实践,可以与现有的C++
代码很好地集成和使用。 -
功能丰富:
Boost
库涵盖了很多领域,包括但不限于网络编程、多线程、文件系统、智能指针、正则表达式、容器和算法等,为C++
开发者提供了丰富的工具和组件。
总的来说,Boost 库是C++
社区中非常重要的一个开源项目,为 C++
程序员提供了丰富的工具和功能,有助于提高代码的质量、可移植性和性能。
3.项目主要模块
-
网页内容获取,数据预处理模块
这个模块代码的主要思路是从指定目录中枚举所有
HTML
文件,解析出每个文件的标题、内容和URL
,并将这些信息以特定的格式保存到一个文本文件中。我们首先使用boost::filesystem
库递归遍历目录,收集所有HTML
文件的路径,然后逐个读取文件内容,通过自定义的解析函数提取所需信息。解析出的文档数据随后被写入到一个输出文件中,使用特定分隔符来区分不同文档的记录。我们在执行过程中进行了详尽的错误检查和处理,确保了其健壮性。 -
建立正排索引和倒排索引,项目核心模块
这个模块我们定义了一个名为
Index
的C++
类,用于构建和维护一个文档索引系统。该系统采用单例模式确保只有一个索引实例,并使用正排索引和倒排索引来快速检索文档。正排索引存储了文档的基本信息,如标题、内容和URL
,而倒排索引则根据关键词将文档分组。类中提供了构建索引、获取文档信息和获取倒排列表的方法。构建索引的过程涉及读取处理过的数据文件,解析文档数据,并根据文档内容构建索引。此外,我们还实现了简单的进度显示功能。整个索引系统的构建旨在提高文档检索的效率和准确性。 -
编写 http_server 模块,进行网络开放
这个模块是一个基于
C++
的简单搜索引擎服务程序,我们整合了HTTP
服务器和搜索功能。我们首先初始化一个搜索器对象,使用预先处理好的文档数据(来自第一个模块)来建立索引。然后,我们设置了一个HTTP
服务器,并为服务器定义了一个GET
请求的处理函数,该函数响应客户端对"/s"
路径的访问。
当客户端发送包含搜索关键字的请求时,服务器会验证请求中是否包含关键字参数。如果没有关键字,服务器会返回错误信息。如果有关键字,服务器将调用搜索器的搜索方法,传入关键字,并获取搜索结果,这些结果被封装为JSON
字符串返回给客户端。
4.项目功能预览
项目文件结构:
运行结果:
打开任意浏览器, 在网址搜索栏搜索URL:[主机IP]:8081
如下图所示:
在搜索框里面搜索Boost
库内的关键词,如下图我们搜索filesystem
得到的结果
5.项目宏观原理
首先分为客户端和服务端,我们客户端就以浏览器为例,我们的服务器肯定会提供很多服务,我们就以搜索引擎为例,它肯定会提供一个searcher
,当然在搜索的时候,我们肯定是获取到网页的信息,不停的抓取网页,最后放在我们的磁盘里,而抓取网页可以用爬虫来处理,而我们拿到网页信息后,就比如放在我们磁盘下data
目录下,而当我们拿到所有的html
文件的时候,要先进行预处理,例如去标签操作和数据清理的操作,当然也会做建立索引的操作,而建立索引可以加快我们的查找工作。
准备工作做完后,我们就可以通过http
请求的方式进行搜索任务,通过get
方法来上传我们的搜索的关键字,服务端收到请求后,就会通过检索索引得到相关的html
信息最后在通过拼接多个网页的title+desc+url
,构建一个新的网页,最后返回给我们的用户。
而我们要实现的就是红色框起来的这一部分。至于爬虫因为法律原因,我们就不实现了。感兴趣的可以自己实现爬虫程序。
6.搜索引擎技术栈和项目环境
- 技术栈:
c/c++
c++11
STL
准标准库Boost
Jsoncpp
cppjieba
cpp-httplib
html5
css
js
jQuery
Ajax
- 项目环境:
Centos7
云服务器vim/gcc(g++)/Makefile
vs2019 or vscode
7.正排索引,倒排索引—搜索引擎具体原理
我们先来介绍一下搜索引擎的具体原理:
正排索引:
看下面这个:
- 文档1:张三买了两个小米手机
- 文档2:张三发布了小米手机
文档ID | 文档内容 |
---|---|
1 | 张三买了两个小米手机 |
2 | 张三发布了小米手机 |
而我们的正排索引就是从文档ID找到我们的文档内容(也可以说是文档内的关键字)
当然我们还可以对我们的文档内容进行分词(而分词的目的是:方便建立倒排索引和查找)
- 文档1:
张三买了两个小米手机
分词后:张三/买/两个/小米/手机
- 文档2:
张三发布了小米手机
分词后:张三/发布/小米/手机
这样就可以分词好了,那么什么是分词呢,分词其实就是将我们的暂停词去掉,例如了,的,吗,a ,the
等,因为这些在我们分词的时候是可以不用去考虑的。
倒排索引
还是刚才的例子,我们看下面这个表格:通过分词处理后:
关键字(具有唯一性) | 文档ID,Weight(权重) |
---|---|
张三 | 文档1,文档2 |
买 | 文档2 |
两个 | 文档1 |
小米 | 文档1,文档2 |
手机 | 文档1,文档2 |
发布 | 文档2 |
小米手机 | 文档1,文档2 |
而我们的倒排索引:就是根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
模拟一次查找的过程
接下来我们可以模拟一次查找的过程:
我们用户输入:小米-> 倒排索引中查找-> 提取出文档ID(1,2)
-> 根据正排索引-> 找到文档的内容-> title+conent(desc)+url
文档结果进行摘要-> 构建响应结果
不难发现,在查找过程中,我们是正排索引和倒排索引一起用的。
8.编写数据去标签与数据清洗的模块Parser
准备工作
既然是基于Boost
库的,那我们首先需要数据源,我们要先下一个boost
库的资源。
我么进入官网,点击 Documentation
找到后我们进行下载压缩包,最后上传到我们的服务器:
rz -E
上传成功后会显示这样
然后我们将压缩包进行解压:
tar xzf boost_1_86_0.tar.gz
进入到我们库中,查看一下信息:
当然我们的Boost
库压缩包可以删除掉。而对于我们来说,我们是只需要该目录下/doc/html目录下的html文件
,用它来进行索引.
接下来我们需要新建一个目录data
,在该目录下在新建一个子目录,Input
目录用来存放我们的html
文件。
然后将我们的库里面的文件拷贝在我们的Input
文件中:
[xiaoxiaounicorn@VM-16-4-centos search-engine-project]$ cp -rf boost_1_86_0/doc/html/* data/Input/
我们可以看一下里面有多少个html
文件:
一共有8540个。
接下来我们讲解一下什么是去标签操作:
我们可以随便打开一个html
文件:
在html
文件中,用<>
扩起来的就是html
的标签,而这个标签对我们搜索是没有价值的,我们只需要标签里面的信息,因此我们就需要去掉这些标签,而且一般标签都是成对出现的,当然也会有单标签!
既然要处理我们去标签操作,清洗之后结果放哪呢?我们在我们的data目录下,继续新建一个raw_html
目录,用来存放区标签之后的干净文档:
而我么的目标就是把每个文档都去标签,然后写入到同一个文件中!每个文档内容不需要热合的\n
~ 而我们的文档和文档之间用\3
区分
类似于这样:
而为什么是\3
,是因为在Ascll中:
在控制字符里,他是不回显的也就是不显示的。所以我们采取以\3
的方式,当然我们也可以采取\4
\5
之类的因人而异。
编写parser
接下来我们就可以编写parser
模块了:
首先将我们的目录先保存起来,存放的是所有的html
网页
//是一个目录,下面放的是所有的html网页
const std::string src_path="data/Input";
const std::string output="data/raw_html/raw.txt";
接下来就是我们的对文件进行读–去标签—写的操作啦
第一步:我们首先得先把所有文件名列出来,方便后期读取:
std::vector<std::string> files_list;
//第一步:递归式的吧每个html文件名带路径,保存到files_list中,方便后期进行一个个的文件读取
if(!EnumFile(src_path,&files_list))
{
std::cerr<<"enum file name error!"<<std::endl;
return 1;
}
第二步:按照files_list
中读取每个文件的内容,并进行解析
std::vector<DocInfo_t> results;
if(!ParseHtml(files_list,&results))
{
std::cerr<<"Parse Html error!"<<std::endl;
}
注意:我们在解析网页的时候,要分三块,因此我们可以就字段定义在结构体里,像这样:
typedef struct DocInfo{
std::string title; //文档的标题
std::string content; //文档的内容
std::string url; //该文档在官网中的url
}DocInfo_t;
第三步:把解析完的各个文件内容,写入到output
中,按照\3
作为每个文档的分割符
//第三步:把解析完的各个文件内容,写入到output中,按照\3作为每个文档的分割符
if(!SaveHtml(results,output))
{
std::cerr<<"save html error"<<std::endl;
}
那么数据清洗模块我们的基本框架已经出来了:
Parser.cc
#include<iostream>
#include<string>
#include<vector>
//是一个目录,下面放的是所有的html网页
const std::string src_path="data/Input";
const std::string output="data/raw_html/raw.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> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list,std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results,const std::string &output);
int main()
{
std::vector<std::string> files_list;
//第一步:递归式的吧每个html文件名带路径,保存到files_list中,方便后期进行一个个的文件读取
if(!EnumFile(src_path,&files_list))
{
std::cerr<<"enum file name error!"<<std::endl;
return 1;
}
//第二步:按照files_list中读取每个文件的内容,并进行解析
std::vector<DocInfo_t> results;
if(!ParseHtml(files_list,&results))
{
std::cerr<<"Parse Html error!"<<std::endl;
return 2;
}
//第三步:把解析完的各个文件内容,写入到output中,按照\3作为每个文档的分割符
if(!SaveHtml(results,output))
{
std::cerr<<"save html error"<<std::endl;
return 3;
}
return 0;
}
这里说明一下:
const &
:表示输入*
:表示输出&
:表示输入输出
接下来我们就将三个子模块一个一个实现即可。
EnumFile
编写
首先C++
中STL
对文件操作的维护不是很好,所以我们可以引入Boost
库:
[xiaoxiaounicorn@VM-16-4-centos search-engine-project]$ sudo yum install -y boost-devel
这里我们安装的是开发库:
我们编码用的是boost
库的1.53版本。注意虽然我们的编码是1.53版本的我们的手册是1.86版本的,但互不影响!
搜索filesystem
:
点击Turorial
,教程可以看到里面的示例:
点进去后随便点一个函数就会进入到我们的filesystem
的库的所有函数,这里我们可能会用到其中的path
,还有迭代器之类的。
代码:
首先我们得定义一个对象:
//定义对象
namespace fs = boots::filesytem;
fs::path root_path(src_path);
判断完后我们得检查一下路径是否存在,路径不存在,那我们肯定也就没有必要往下走了,这里我们可以用exists
函数来进行判断:
具体代码:
// 判断路径是否存在,不存在,就没有必要再往后走了
if (!fs::exists(root_path))
{
std::cerr << src_path << "not exists" << std::endl;
return false;
}
在读文件时,我们可以用boots
库里面的一个迭代器,通过迭代器进行访问:
recursive_directory_iterator
这个迭代器为递归式目录迭代器,他可以通过递归的方式进行遍历。
在一开始我们先建立一个空的迭代器,用来进行判断我们的递归结果。
// 定义一个空的迭代器,用来进行判断递归结果
fs::recursive_directory_iterator end;
好,在遍历之前,我们还得先处理一下,因为在遍历过程中,它会将我们所有的文件都遍历出来,而我们仅仅只需要的是html
文件就可以了,我们可以用我们库中自带的一个函数is_regular_file
来进行判断它是否为常规文件。
// 判断文件是否是普通文件,html都是普通文件
if (!fs::is_regular_file(*iter))
{
continue;
}
是常规文件后,我们还得在判断文件的路径名的后缀是否符合我们的要求,也就是html
的形式:
if (iter->path().extension() != ".html")
{ // 判断文件路径名的后缀是否符合要求
continue;
}
既然要检查路径,那么我们要返回我们的路径,这里我们用到了path
这个类里面的extension
方法:此方法可以得到迭代器的路径:
走到这,说明我们的路径一定是合法的,并且是以html
为后缀的一个普通网页文件。
接下来我们要将带路径的html
文件保存在files_list
中,方便后续查找!
// 当前的路径一定是一个合法的,以.html结束的普通网页文件
files_list->push_back(iter->path().string()); // 将所有带路径的html保存在files_list,方便后续进行文本分析
这里我们强调一下,我们在插入时,直接插入我们的迭代器肯定是不现实的,这里我们在介绍一下,在path
这个类中,我们有一个函数string
,他可以将我们的路径以字符串的形式输出~!!
完整代码:
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
//定义对象
namespace fs = boots::filesytem;
fs::path root_path(src_path);
// 判断路径是否存在,不存在,就没有必要再往后走了
if (!fs::exists(root_path))
{
std::cerr << src_path << "not exists" << std::endl;
return false;
}
// 定义一个空的迭代器,用来进行判断递归结果
fs::recursive_directory_iterator end;
for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
// 判断文件是否是普通文件,html都是普通文件
if (!fs::is_regular_file(*iter))
{
continue;
}
if (iter->path().extension() != ".html")
{ // 判断文件路径名的后缀是否符合要求
continue;
}
// std::cout << "debug: " << iter->path().string() << std::endl;
// 当前的路径一定是一个合法的,以.html结束的普通网页文件
files_list->push_back(iter->path().string()); // 将所有带路径的html保存在files_list,方便后续进行文本分析
}
return true;
}
接下里我们可以测试一下我们的代码,因为现在只是实现了读的操作,所以我们将我们最后的路径信息打印出来即可。
首先编写我们的Makefile
文件:
cc=g++
parser:parser.cc
$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
.PHONY:clean
clean:
rm -f parser
注意:因为boost
库是一个准标准库,所以我们要加上我们的boost
库依赖。
运行完后我们会看到所有的html
文件都被我们递归式读出来了。
到这我们的第一步已经搞定了,接下来就是解析我们的文件。
ParseHtml
编写
在解析我们文件我们首先要读取我们的文件,我们可以再新建一个util.hpp
文件,此文件用于存放我们所实现的一些工具,也就是工具集。
util.hpp
:
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
namespace ns_util
{
class FileUtil
{
public:
static bool ReadFile(const std::string &file_path, std::string *out)
{
std::ifstream in(file_pat, std::ios::in);
if (!in.is_open())
{
std::cerr << "open file" << file_path << "error" << std::endl;
return false;
}
std::string line;
// 如何理解getline读取到文件结束呢?
// getline的返回值是一个&,while(bool),本质是因为重载了强制类型转换
while (std::getline(in, line))
{
*out += line;
}
in.close();
return true;
}
}
}
当然,读取文件我们要用到c++
中的IO
流.
所以我们解析首要的工作如下:
//1.读取文件,Read();
std::string result;
if(!ns_util::FileUtil::ReadFile(file,&result))
{
continue;
}
读取文件后,我们就需要进行解析了,而之前我们介绍过,网页会分为三个字段,所以我们解析也分三步:
第一步:提取
title
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result,&doc.title))
{
continue;
}
第二步:提取
content
//3.解析指定的文件,提取content,其实本质就是去标签操作!
if(!ParseContent(result,&doc.content))
{
continue;
}
第三步:构建
url
//4.解析指定的文件路径,构建url
if(!ParserUrl(file,&doc.url))
{
continue;
}
走到这,就说明我们的解析基本上就完成了并且当前文档的相关结果都保存在了doc
里面:
//走到这,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
result->push_back(doc);
当然,其实现在还有一个小bug
,因为插入本质还是会发生拷贝的,效率比较低,在后续实现会进一步完善。
到这我们ParseHtml
的基本框架已经出来了,接下来我们实现其中的功能即可。
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;
}
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result,&doc.title))
{
continue;
}
//3.解析指定的文件,提取content,其实本质就是去标签操作!
if(!ParseContent(result,&doc.content))
{
continue;
}
//4.解析指定的文件路径,构建url
if(!ParserUrl(file,&doc.url))
{
continue;
}
//走到这,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
result->push_back(doc);//当然,现在还有一个小bug,因为插入本质还是会发生拷贝,效率比较低
}
return true;
}
ParseTitle
编写
我们先随便打开一个网页,要提取标题,先找到标题这一行,内容提取,我们只要title
里面的,我们只要算好< title>
标签的位置即可,注意是左开右闭区间。
//提取title
static bool ParseTitle(const std::string &file, std::string *title)
{
std::size_t begin = file.find("<title>");
if (begin == std::string::npos)
{
return false;
}
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
编写
提取content
并不是说把网页里面的内容全都拿出来,我们只要其中的有效部分,所以其本质就是去标签的操作。
而我们可以基于一个简易的状态机来进行去标签的操作。
而当我们进行遍历的时候,只要碰到了>
,就意味着,当前的标签处理完了,而只要碰到了<
就意味着新的标签开始了。
// 提取content->不是把网页里面的内容全都拿出来,而是去标签操作
static bool ParseContent(const std::string &file, std::string *content)
{
// 去标签,我们做一个简易的状态机、
enum status
{
LABLE,
CONTENT
};
// 默认刚开始的状态是标签
enum status s = 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库的官网文档,和我们下载下来的文档是有路径的对应的关系的!
我们打开官网:
这是官网路径:
https://www.boost.org/doc/libs/1_86_0/doc/html/accumulators.html
而这个页面在我们下载下来的库里是这样的:(我们下载下来的url示例:)
/home/xiaoxiaounicorn/boost_1_86_0/doc/html/accumulators.html
而我们拷贝到我们项目中的示例:
data/input/accumulators.html
而我们拼接url
时可以这样:
通过这个拼接,就可以得到我们的URL
;
//提取url
static bool ParseUrl(const std::string &file_path, std::string *url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_86_0/doc/html/";
std::string url_tail = file_path.substr(src_path.size());
*url = url_head + url_tail;
return true;
}
我们可以进行一下简单的测试:
我们将我们解析出来的title
,content
以及我们的url
分别打印出来。
可以看到我们的解析基本上是没有什么太大问题的。
SaveHtml
编写
当我们结束了读文件,解析文件的操作,接下来我们就需要将我们解析的文件写入到文件中。
在写文件中,我们之前的版本1是:
文档和文档之间用\3
隔开,也就是这样:
xxxxxxx\3yyyyyyyy\3zzzzzz\3
而这次我们写的时候,我们在写入文件的时候,一定要考虑下一次在读取的时候,也要方便操作!所以我们title
提取完用\3
隔开,content
提取完用\3
隔开,url
提取完用\3
隔开,到这说明我们的一个文档已经弄完了,然后换行即可,依次内推,像这样:
title\3content\3 url \n title\3 content \3 url \n
而我们这样做还有一个目的,那就是方便我们的getline(ifstream,line)
函数可以直接获取到我们的文档的全部内容!title\3content\3url
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3'
// 按照二进制方式进行写入
std::ofstream out(output, std::ios::out | std::ios::binary);
if (!out.is_open())
{
std::cerr << "open " << output << " failed!" << std::endl;
return false;
}
// 就可以进行文件内容的写入了
for (auto &item : results)
{
std::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;
}
我们打开data目录下的txt文件:
查看一下里面:
这个txt
文件里面就是我们存放的干净的html
文档。
到这里我们的第一个模块就结束了!
遗留小问题
接下来我们接一个小插曲,将之前的遗留问题处理一下:
在我们最后解析完成后要将结果返回出去的时候:
//走到这,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
result->push_back(doc);
而我们采取的是直接插入的方式,但是这个doc对象是我们定义出来的,难免会发生拷贝,效率低下的问题。有什么解决办法吗?
这里我们介绍一下C++
中的move
函数。
move
函数是发生移动,而并不是拷贝
所以我们可以将我们的语句进行修改:
result->push_back(std::move(doc));
完整parser.cc
#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include "util.hpp"
// 是一个目录,下面放的是所有的html网页
const std::string src_path = "data/input";
const std::string output = "data/raw_html/raw.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> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);
int main()
{
std::vector<std::string> files_list;
// 第一步:递归式的吧每个html文件名带路径,保存到files_list中,方便后期进行一个个的文件读取
if (!EnumFile(src_path, &files_list))
{
std::cerr << "enum file name error!" << std::endl;
return 1;
}
// 第二步:按照files_list中读取每个文件的内容,并进行解析
std::vector<DocInfo_t> results;
if (!ParseHtml(files_list, &results))
{
std::cerr << "Parse Html error!" << std::endl;
return 2;
}
// 第三步:把解析完的各个文件内容,写入到output中,按照\3作为每个文档的分割符
if (!SaveHtml(results, output))
{
std::cerr << "save html error" << std::endl;
return 3;
}
return 0;
}
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
// 定义对象
namespace fs = boost::filesystem;
fs::path root_path(src_path);
// 判断路径是否存在,不存在,就没有必要再往后走了
if (!fs::exists(root_path))
{
std::cerr << src_path << "not exists" << std::endl;
return false;
}
// 定义一个空的迭代器,用来进行判断递归结果
fs::recursive_directory_iterator end;
int jishuqi = 0;
for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
// 判断文件是否是普通文件,html都是普通文件
if (!fs::is_regular_file(*iter))
{
continue;
}
if (iter->path().extension() != ".html")
{ // 判断文件路径名的后缀是否符合要求
continue;
}
// 调试:
// std::cout << "debug 读取文件" << std::endl;
// std::cout << "debug: " << iter->path().string() << std::endl;
// 当前的路径一定是一个合法的,以.html结束的普通网页文件
files_list->push_back(iter->path().string()); // 将所有带路径的html保存在files_list,方便后续进行文本分析
jishuqi++;
}
std::cout << jishuqi << std::endl;
return true;
}
// 提取title
static bool ParseTitle(const std::string &file, std::string *title)
{
std::size_t begin = file.find("<title>");
if (begin == std::string::npos)
{
return false;
}
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;
}
// 提取content->不是把网页里面的内容全都拿出来,而是去标签操作
static bool ParseContent(const std::string &file, std::string *content)
{
// 去标签,我们做一个简易的状态机、
enum status
{
LABLE,
CONTENT
};
// 默认刚开始的状态是标签
enum status s = 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;
}
// 解析url
static bool ParseUrl(const std::string &file_path, std::string *url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_86_0/doc/html/";
std::string url_tail = file_path.substr(src_path.size());
*url = url_head + url_tail;
return true;
}
// for debug
static 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;
}
DocInfo_t doc;
// 2.解析指定的文件,提取title
if (!ParseTitle(result, &doc.title))
{
continue;
}
std::cout << "title: " << doc.title << std::endl;
// 3.解析指定的文件,提取content,其实本质就是去标签操作!
if (!ParseContent(result, &doc.content))
{
continue;
}
std::cout << "content: " << doc.content << std::endl;
// 4.解析指定的文件路径,构建url
if (!ParseUrl(file, &doc.url))
{
continue;
}
std::cout << "url: " << doc.url << std::endl;
// 走到这,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
results->push_back(std::move(doc)); // 当然,现在还有一个小bug,因为插入本质还是会发生拷贝,效率比较低->使用move
}
return true;
}
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3'
// 按照二进制方式进行写入
std::ofstream out(output, std::ios::out | std::ios::binary);
if (!out.is_open())
{
std::cerr << "open " << output << " failed!" << std::endl;
return false;
}
// 就可以进行文件内容的写入了
for (auto &item : results)
{
std::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;
}
9.建立索引模块Index
我们新建index.hpp
文件:我们先拉大体框架:
构建索引,肯定还是需要用到我们之前的文档,首先定义ocInfo
结构体:
struct DocInfo{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string url; // 官网文档url
uint16_t doc_id; // 文档id,暂时先不过多考虑
};
因为涉及到我们之前讲过的正排索引和倒排索引,所以我们在原始基础上右多加了一个id
字段。但我们目前可以不做多的考虑、
接下来在我们的index
类中就要实现我们的倒排与正排。
首先定义正排,我们知道正排索引的时候,是根据id找到对应的文档,那么我们正排索引的数据结构可以用vectot数组来进行,因为数组的下标天然就是文档id.
// 正排索引的数据结构用数组,数组下标天然是文档的ID
std::vector<DocInfo> forward_index; // 正排索引
而倒排,我们知道是根据关键字
来找对应文档信息,所以我们可以首先定义一个结构体用来存放我们倒排索引中的相关字段:
struct InvertedElem{
uint16_t doc_id;
std::string word;
int weight; // 权重
};
其中就包含我们的文档ID
,关键字以及权重,加上权重的目的就是,在一个关键字查找中可能会对应多个。
那么所以我们倒排索引的数据结构可以选择哈希:
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, std::vector<InvertedElem>> inverted_index;
而我们可以把std::vector<InvertedElem>
单另拿出来,定义成这样:
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
而这个我们称为倒排拉链,那么我们的倒排索引就变成了是关键字与我们倒排拉链的映射关系。
那么接下来就可以构建我们的索引了,首先是正排:
我们要根据id来找到我们的文档内容:
// 根据doc_id找到文档内容
DocInfo *GetForwardIndex(uint64_t doc_id){
return nullptr;
}
其实这个函数实现很简单。我们后面会实现
倒排就是根据关键字来找到我们的倒排拉链:
// 根据关键字string,获得倒排拉链
InvertedList &GetInvertedList(const std::string &word){
return nullptr;
}
接下来,我们就可以根据去标签,格式化之后的文档,构建正排索引和倒排索引:
// 根据去标签,格式化之后的文档,构建正派和倒排索引
// data/raw_html/raw.txt
bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{
return true;
}
我们之前将我们的干净文档写在了我们data
目录下,raw_html
目录下中的raw.txt
文件。
也就是将我们之前paser处理过后的数据交付给我。
到这,我们的基本框架就已经出来了,接下来就是实现他们了。
基本框架:
#progma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
namespace ns_index
{
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string url; // 官网文档url
uint16_t doc_id; // 文档id,暂时先不过多考虑
};
struct InvertedElem
{
uint16_t doc_id;
std::string word;
int weight; // 权重
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
// 正排索引的数据结构用数组,数组下标天然是文档的ID
std::vector<DocInfo> forward_index; // 正排索引
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index;
public:
Index() {}
~Index() {}
public:
// 根据doc_id找到文档内容
DocInfo *GetForwardIndex(uint64_t doc_id)
{
return nullptr;
}
// 根据关键字string,获得倒排拉链
InvertedList &GetInvertedList(const std::string &word)
{
return nullptr;
}
// 根据去标签,格式化之后的文档,构建正派和倒排索引
// data/raw_html/raw.txt
bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{
return true;
}
}
}
根据doc_id找到文档内容:
其实要实现这个模块很简单,我们是用数组来描述我们的正排,那么我们只要doc_id只要比我们的正排下标大,那肯定就说明不在这个范围内,反之就是找到了,直接返回即可。
// 根据doc_id找到文档内容
DocInfo *GetForwardIndex(uint64_t doc_id)
{
if(doc_id>=forward_index.size())
{
std::cerr<<"doc_id out range,error!"<<std::endl;
return nullptr;
}
//找到了,直接返回即可
return &forward_index(doc_id);
}
根据关键字string,获得倒排拉链:
要获取倒排拉链,其实也简单,我们根据映射关系即可进行获取。
// 根据关键字string,获得倒排拉链
InvertedList *GetInvertedList(const std::string &word)
{
auto iter=inverted_index.find(word);
if(iter==inverted_index.end())
{
std::cerr<<word<<"have no InvertedList"<<std::endl;
return nullptr;
}
//获取到了,直接返回即可
return &(iter->second);
}
根据去标签,格式化之后的文档,构建正排和倒排索引:
构建索引是一个复杂的动作,所以我们可以先拉一下基本结构:
既然要读取文件,那肯定就要涉及到我们的文件操作了,
std::ifstream in(input, std::ios::in | std::ios::binary);
if (!in.is_open())
{
std::cerr << "sorry " << input << "open error" << std::endl;
return false;
}
读取成功后,那我们就需要获取里面的内容,获取内容我们直接用我们的getline函数:
std::string line;
while (std::getline(in, line)){
DocInfo *doc = BuildForwardIndex(line);
if (nullptr == doc)
{
std::cerr << "build " << line << "open error" << std::endl; // for debug
continue;
}
BuildInvertedIndex(*doc);
}
我们在不停的读取的同时,每读取到我们就先进行正排索引,随后进行倒排索引。
我们将我们的正排索引和倒排索引定义成私有:
private:
DocInfo *BuildForwardIndex(const std::string &line)
{
}
bool BuildInvertedIndex(const DocInfo &doc)
{
}
接下来就是实现我们的正排和倒排索引:
正排索引:
我们先构建正排索引的结构:
首先我们得解析我们的line
,并进行字符串切分:
也就是把我们刚才获取到的Line
分成3个string
,title
,content
,url
在进行字符串切分时,我们可以用我们C++中的容器,而这里我们介绍一个新的方法:
我们可以使用boost库中的split函数,该函数可以将我们的字符串进行切分。该函数定义:
boost::split(type, select_list, boost::is_any_of(","), boost::token_compress_on);
type
:类型是std::vectorstd::string,用于存放切割之后的字符串
select_list
:传入的字符串,可以为空
boost::is_any_of(“,”)
:设定切割符为,(逗号)
boost::token_compress_on
:将连续多个分隔符当一个,默认没有打开,当用的时候一般是要打开的。
我们在当前目录下新建一个test
目录,用于进行测试:
在我们的测试文件中编写一下测试案例,来测试我们的字符串切分功能:
#include<iostream>
#include<vector>
#include<string>
#include<boost/algorithm/string.hpp>
int main()
{
std::vector<std::string> result;
std::string target="aaaaaa\3bbbbbbbb\3cccccc";
boost::split(result,target,boost::is_any_of("\3"));
return 0;
}
我们运行一下:
我们可以看到我们的字符串被成功切分了,那至于第四个参数,我们看下面这个例子:
#include <iostream>
#include <vector>
#include <string>
#include <boost/algorithm/string.hpp>
int main()
{
std::vector<std::string> result;
std::string target = "aaaaaa\3\3\3\3\3\3\3bbbbbbbb\3cccccc";
boost::split(result, target, boost::is_any_of("\3"));
for (auto &s : result)
{
std::cout << s << std::endl;
}
return 0;
}
如果在示例中,有很多个\3
分隔符号,我们先将第四个参数调整成关闭状态:
boost::split(result, target, boost::is_any_of("\3"), boost::token_compress_off);
我们看一下运行结果:
他会是这样的,也就是说我们的分隔符之间是有空格的,那我们再换成打开状态:
boost::split(result, target, boost::is_any_of("\3"), boost::token_compress_on);
我们看一下结果:
在这里,我们将第四个参数设置成打开状态即可。当然不同的应用场景肯定是不同的。所以我们就用该函数来进行我们的字符串切分。
而我们可以将我们的字符串切分操作写在我们的工具集当中:
util.hpp
class StringUtil
{
public:
static void Split(const std::string &target,std::vector<std::string> *out,const std::string &sep)
{
//boost split;
boost::split(*out,target,boost::is_any_of(esp),boost::token_compress_on);
}
};
那么我们的字符串切分操作就搞定了:
//1.解析line 字符串切分
//line-> 3个string title content url
std::vector<std::string> results;
const std::string sep="\3";
ns_util::StringUtil::Split(line,&results,sep);
if(results.size()!=3)
{
return nullptr;
}
接下来,我们进行第二步:将我们的字符串填充到我们的DocInfo
中:
//2.字符串进行填充到DocInfo中
DocInfo doc;
doc.title=results[0];
doc.content=results[1];
doc.url=results[2];
doc.doc_id=forward_index.size();
第三步,就是插入到正排索引的vector
//3.插入到正排索引的vector
forward_index.push_back(std::move(doc));//doc.html
return &forward_index.back();
当然在插入时为了提高效率,我们可以使用move
的方法。
而我们的id是在插入前就进行更新,这样做的目的是:先进行保存id
,在插入,对应的id就是当前doc
在vector
中的下标!
走到这,我们正排索引就肝完了,完整代码:
DocInfo *BuildForwardIndex(const std::string &line)
{
//1.解析line 字符串切分
//line-> 3个string title content url
std::vector<std::string> results;
const std::string sep="\3";//分隔符
ns_util::StringUtil::Split(line,&results,sep);//字符串切分
if(results.size()!=3)
{
return nullptr;
}
//2.字符串进行填充到DocInfo中
DocInfo doc;
doc.title=results[0];//title
doc.content=results[1];//content
doc.url=results[2];//url
doc.doc_id=forward_index.size();//先进行保存id,在插入,对应的id就是当前doc在vector中的下标!
//3.插入到正排索引的vector
forward_index.push_back(std::move(doc));//doc.html文件内容
return &forward_index.back();
}
倒排索引:
在建立倒排索引之前,我们先简单的讲解一下大致原理,我们首先看一下我们倒排的数据结构:
struct InvertedElem
{
uint16_t doc_id;
std::string word;
int weight; // 权重
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index;
//而我们拿到的文档内容:
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string url; // 官网文档url
uint16_t doc_id; // 文档id,暂时先不过多考虑
};
就假设我们拿到的文档为:
title
:吃葡萄
content
:吃葡萄不吐葡萄皮
url
:http://xxxx
doc_id
:123
而我们根据文档,形成一个或者多个InvertedElem
(倒排拉链)
因为我们是一个一个带飞对文档进行处理的,一个文档会包含多个“词”,都对应到当前的doc_id
那么首先我们就需要对我们的title
和content
进行分词,统计这个词出现了多少次:
title
:吃/葡萄(title_word
)
content
:吃/葡萄/不吐/葡萄皮(content_word
)
分词做好后,我们就要考虑相关性,也就是说我们出现的词在文档中的相关性,而这个相关性的维度就有很多,并且其中也是个复杂的东西,这里我们用词频来作为我们的相关性:
那么第二步我们就得做一下词频统计:(伪代码)
struct word_cnt{
title_cnt;
content_cnt;
}
unordered_map<std::string, word_cnt> word_cnt;
for &word : title_word{
word_cnt[word].title_cnt++; //吃(1)/葡萄(1)/吃葡萄(1)
}
for &word : content_word {
word_cnt[word].content_cnt++; //吃(1)/葡萄(1)/不吐(1)/葡萄⽪(1)
}
知道了在文档中,标题和内容每个词出现的次数后,接下来我们可以自己定义相关性:
for &word : word_cnt{
//具体⼀个词和123⽂档的对应关系,当有多个不同的词,指向同⼀个⽂档的时候,此时该优先显⽰谁??相关性!
struct InvertedElem elem;
elem.doc_id = 123;
elem.word = word.first;
elem.weight = 10*word.second.title_cnt + word.second.content_cnt ; //相关性,我们这⾥拍着脑⻔写了
inverted_index[word.first].push_back(elem);
}
而其中我们应该如何进行分词,这里我们可以用第三方库jieba
:
核心工具,我们用jieba
分词:
jieba库的安装:
首先我们点击这个链接:
获取链接
然后先克隆到我们的test
目录下:
我们tree.
一下:
我们重点关注的是include
里面的文件即可。那么怎么使用呢?
我们先打开当前目录下test
目录里面的demo
文件:
我们可以看到测试demo
:
我们要快速将这个用起来,我们将jieba
的test
目录下的demo
文件拷贝到我们的test目录下:
[xiaoxiaounicorn@VM-16-4-centos test]$ cp demo.cpp ../../
像这样:
然后打开我们的demo
文件:
我们可以看到这个demo
文件包含一个头文件,底下路径就是对应的分词词库,因为你分词肯定要告诉分词的标准以及依据,那么我们现在test
的目录下就需要让他找到我们的词库以及对应头文件,我们可以使用软连接的方式。
我们先看一下词库的路径:
接下来,我们使用软连接的方式:
同理要让我们的头文件找到,也可以建立一个软连接:
然后将我们的demo
文件进行修改,修改我们头文件的路径以及词库的路径:
#include "jieba/cppjieba/Jieba.hpp"
#include<vector>
#include<string>
#include<iostream>
using namespace std;
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";
接下来我们跑一下:
我们会发现并不是头文件找不到,而是说这个limonp
文件找不到,这里我们说一下,jieba
这个库呢有一个坑,如果你想正常使用的话,你需要找到deps
目录下,将这个文件下的limonp
目录无脑拷贝到我们的include
底下的jieba
文件下:
[xiaoxiaounicorn@VM-16-4-centos cppjieba]$ cp deps/limonp include/cppjieba -rf
[xiaoxiaounicorn@VM-16-4-centos cppjieba]$ ls include/cppjieba/
[xiaoxiaounicorn@VM-16-4-centos cppjieba]$ ls include/cppjieba/limonp
这样他就可以找到了log
文件啦。
接下来我们在运行一下:
[xiaoxiaounicorn@VM-16-4-centos test]$ g++ demo.cpp -std=c++11
这就是我们分词后的结果。我们可以看到分词的类型有很多种,而我们重点用的是其中的这一种:
然后其他的我们就可以不要了,只保留一个:
#include "jieba/cppjieba/Jieba.hpp"
#include <iostream>
#include <vector>
#include <string>
using namespace std;
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);
vector<string> words;
vector<cppjieba::Word> jiebawords;
string s;
s = "小明硕士毕业于中国科学院计算所,后在日本京都大学深造";
cout << s << endl;
cout << "[demo] CutForSearch" << endl;
jieba.CutForSearch(s, words);
cout << limonp::Join(words.begin(), words.end(), "/") << endl;
return EXIT_SUCCESS;
}
我们在运行一下:
到这里我们的jieba
就可以使用起来了。
总结一下:
引入jieba库:
刚才我们使用了jieba
库,接下来沃我们可以将我们的Jieba
引入到我们的项目中
我们首先将我们刚下的test
目录下的cppjieba
文件拷贝到我们的家目录中:
然后我们可以建立软链接:
这里我们可以说明一下,要是想删除我们的软链接,可以用我们的unlink
,不是说rm
不可以,我们不建议用rm
unlink dict
首先在我们的util
工具集里引入我们的jieba
:
#include "cppjieba/Jieba.hpp"
接下来我们在我们的工具集里新建一个类JiebarUtil
:
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 JiebarUtil
{
private:
static cppjieba::Jieba jieba;
public:
static void CutString(const std::string &src, std::vector<std::string> *out)
{
jieba.CutForSearch(src, *out);
}
};
cppjieba::Jieba JiebarUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
编写倒排索引:
bool BuildInvertedIndex(const DocInfo &doc)
{
// DocInfo(title,content,url)
// word->倒排拉链
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt() : title_cnt(0), content_cnt(0) {}
};
std::unordered_map<std::string, word_cnt> word_map; // 用来暂存词频的映射表
// 对标题进行分词
std::vector<std::string> title_words;
ns_util::JiebarUtil::CutString(doc.title, &title_words);
// 对标题进行词频统计
for (std::string s : title_words)
{
boost::to_lower(s); // 需要统一转化成小写
word_map[s].title_cnt++; // 如果存在就获取,如果不存在就新建
}
// 对文档内容进行分词
std::vector<std::string> content_word;
ns_util::JiebarUtil::CutString(doc.content, &content_word);
// 对内容进行词频统计
for (std::string s : content_word)
{
boost::to_lower(s);
word_map[s].content_cnt++;
}
#define X 10
#define Y 1
for (auto &word_pair : word_map)
{
InvertedElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt; // 相关性
InvertedList &invertes_list = inverted_index[word_pair.first];
invertes_list.push_back(std::move(item));
}
return true;
}
10.编写搜索引擎模块Searcher
首先我们在当前目录下新建一个Searcher.hpp
文件:
还是跟之前一样,我们先拉基本结构:
基本结构:
首先我们得初始化我们的引擎:
void InitSearcher(const std::string &input)
{
//1.获取或者创建index对象
//2.根据index 对象建立索引
}
那肯定要获取我们刚才写的index
对象,而获取Index
对象我们可以采取单例模式,然后根据Index
对象建立索引。
当然我们的Search
肯定也是要提供搜索服务的。我们现在百度上搜索下面这个内容:
其实从结果我们也可以看到,在百度这个搜索引擎中,他其实对我们输入的内容也是做了分词工作的,所以我们搜索的关键字在我们的服务端其实也是要进行分词工作,然后才能进行查找index
!
void Search(const std::string &query,std::string *json_string)//提供搜索服务
{
//1.[分词]:对我们的query进行按照searcher的要求进行分词
//2.[触发]:就是根据分词的各个“词”,按照相关性(weight)降序排序
//3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
//4[构建]:根据查找出来的结果,构建json串,jsoncpp
}
分词完成后,我们可以进行触发工作,怎么理解触发呢?触发我们可以简单理解成就是根据分词中的各个“词”,按照相关性进行一个降序排序。接下来在进行汇总操作,在按照相关性进行排序,最后就是构建,根据我们查找出来的内容,构建一个json
串,返回给我们的浏览器,而这个json
我们后续也可以使用第三方库!
到这里,我们的基本框架其实已经就出来了:
Searcher.hpp
#include"index.hpp"
namespace ns_searchcher{
class Searchcher{
private:
ns_index::Index *index;//供系统进行查找的索引
public:
Searchcher(){}
~Searchcher(){}
public:
void InitSearcher(const std::string &input)
{
//1.获取或者创建index对象
//2.根据index 对象建立索引
}
//query :搜索关键字
//json_string :返回给用户浏览器的搜索结果
void Search(const std::string &query,std::string *json_string)//提供搜索服务
{
//1.[分词]:对我们的query进行按照searcher的要求进行分词
//2.[触发]:就是根据分词的各个“词”,按照相关性(weight)降序排序
//3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
//4[构建]:根据查找出来的结果,构建json串,jsoncpp
}
};
}
编写单例index
接下来我们将编写我们的index
单例:
index.hpp
private:
Index(){}
Index(const Index&)=delete;
Index& opeartor=(const Index&)=delete;
static Index* instance;
public:
static Index* GetInstance()
{
if(nullptr==instance)
{
instance=new Index();
}
return instance;
}
但很显然这个版本的单例是不安全的,所以我们得进行加锁操作,这里我们直接使用C++
中的mutex
来进行加锁操作:
index.hpp
private:
Index(){}
Index(const Index&)=delete;
Index& opeartor=(const Index&)=delete;
static Index* instance;
static std::mutex mtx; // 进行上锁
public:
static Index *GetInstance()
{
if (nullptr == instance)
{
mtx.lock(); // 加锁
if (nullptr == instance)
{
instance = new Index();
}
mtx.unlock(); // 解锁
}
return instance;
}
构建好后,我们就可以在我们的searcher
中进行获取:
void InitSearcher(const std::string &input)
{
//1.获取或者创建index对象
index=ns_index::Index::GetInstance();
//2.根据index 对象建立索引
index->BuildIndex(input);
}
编写查找代码:
接下来编写我们的查找代码:
首先我们进行分词操作:
// 1.[分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::JiebarUtil::CutString(query, &words);
接下来我们既然要触发,那一定要各种各样的词,同时肯定也要进行转化、因为当时进行Index
查找时,建立Index
是忽略大小写的,所以搜索,关键字也需要进行转化:
那现在就是要查了,那么怎么差呢,首先搜索的应该先是正排呢还是倒排呢?我们正排是根据id查找文档信息,倒排是根据关键字进行查找文档对应ID
,换言之必须先查倒排,那既然要查倒排,那肯定需要获取倒排拉链。
for (std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
// 如果没有倒排,当然没有倒排肯定也就没有正排
if (nullptr == inverted_list)
{
continue;
}
}
当然,在进行获取倒排拉链的时候,是需要进行检查一下的,万一给的这个关键字就没有倒排拉链,那当然,没有倒排拉链肯定也就没有正排了。
走到这,就说明我们获取到了倒排拉链,但是这里就需要注意一下了:我们在这里获取的时候,可能会发生很多拉链,那么在这,我们其实就可以先保存一下:
// 存放所有词的倒排拉链
ns_index::InvertedList inverted_list_all;
保存好后,我们就需要将我们的结果,批量化合并到一起,这里我们可以用我们的inset
函数:
// 容器批量化插入 倒排拉链就合并在一起了
inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
但其实这里还是不完美的,有没有可能我搜索过程中,可能会截出多个词,会映射出多个文档,进而->提取出重复的html
文档。
什么意思?举个例子:
当我们搜索雷军小米,那么分词后就会变成雷军,小米进而查倒排,就会获取到两个倒排拉链,两个倒排拉链就会对应两个正排,也就是会对应两个重复的文档1和文档2,而最后就会对应两个html
。
这个问题,我们后面会在进行解决。
接下来就是合并排序了,合并我们也直接用库函数里面的sort
即可。在写排序时,我们可以写一个lambda表达式
,进行降序:
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
std::sort(inverted_list_all.begin(), inverted_list_all.end(),
[](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
{
return e1.weight > e2.weight;
});
这里我们得补充一点,为什么>是降序呢,这里告诉大家一个诀窍,我们将>画一条横线,不就是降序嘛。
接下来就是我们的第四步骤:进行构建,首先走到这就说明我们能拿到id
了,那么现在肯定就需要差正排的操作了,
for (auto &item : inverted_list_all)
{
ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id);
if (nullptr == doc)
{
continue;
}
}
走到这,我们就可以拿到doc
的所有信息,接下来就是我们需要通过jsoncpp
完成序列化&&反序列化.将信息以json串的形式返回出去。所以我们得先安装第三方库json
:
安装json
库:
[xiaoxiaounicorn@VM-16-4-centos search-engine-project]$ sudo yum install -y jsoncpp-devel --disablerepo=centos-sclo-sclo
安装完后,我们可以在我们的test
测试目录下,先测试一下:
test.cc
#include <iostream>
#include <vector>
#include <string>
#include <boost/algorithm/string.hpp>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
Json::Value item1;
item1["key1"]="Value11";
item1["key2"]="Value22";
Json::Value item2;
item2["key1"]="Value1";
item2["key2"]="Value2";
root.append(item1);
root.append(item2);
Json::StyledWriter writer;
std::string s=writer.write(root);
std::cout<< s<< std::endl;
// std::vector<std::string> result;
// std::string target = "aaaaaa\3\3\3\3\3\3\3bbbbbbbb\3cccccc";
// boost::split(result, target, boost::is_any_of("\3"), boost::token_compress_on);
// for (auto &s : result)
// {
// std::cout << s << std::endl;
// }
// return 0;
}
我们运行一下:
[xiaoxiaounicorn@VM-16-4-centos test]$ g++ test.cc -l jsoncpp
其实我们可以理解我们返回给浏览器的就是这样的一个数据(像上方结果那样):可能会觉得这样有点丑,我们可以改变一下其中的参数:
当然感觉这个不舒服,我们可以调整成这样的:
#include <iostream>
#include <vector>
#include <string>
#include <boost/algorithm/string.hpp>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
Json::Value item1;
item1["key1"] = "Value11";
item1["key2"] = "Value22";
Json::Value item2;
item2["key1"] = "Value1";
item2["key2"] = "Value2";
root.append(item1);
root.append(item2);
// Json::StyledWriter writer;
// 当然我们也可以调整一下,用FastWriter,更快:
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
// std::vector<std::string> result;
// std::string target = "aaaaaa\3\3\3\3\3\3\3bbbbbbbb\3cccccc";
// boost::split(result, target, boost::is_any_of("\3"), boost::token_compress_on);
// for (auto &s : result)
// {
// std::cout << s << std::endl;
// }
// return 0;
}
运行结果:
当然我们还是选择第一种,因为第一种方便我们调试。测试完我们的json
没问题后,我们就可以继续将我们的第四步构建写完了:
首先在search.hpp
中引入我们的json
:
#include <jsoncpp/json/json.h>
编写构建代码:
// 4[构建]:根据查找出来的结果,构建json串,jsoncpp ---通过jsoncpp完成序列化&&反序列化
Json::Value root;
for (auto &item : inverted_list_all)
{
ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id);
if (nullptr == doc)
{
continue;
}
Json::Value elem;
elem["title"] = doc->title;
elem["desc"] = doc->content; // 注意这里content是文档去标签之后的结果,但不是我们想要的,我们要的只是其中的一部分
elem["url"] = doc->url;
root.append(elem);
}
Json::StyledWriter writer;
*json_string = writer.write(root);
走到这,我们的搜索结果就大功告成了,接下来我们新建一个server.cc
文件来进行测试我们的搜索结果:
编写测试代码:
首先编写我们的server.cc
文件,这里我们只是先简单测试一下,后面我们还会在做优化:
#include "search.hpp"
#include <iostream>
#include <string>
const std::string input = "data/raw_html/raw.txt";
int main()
{
// for test
ns_searcher::Searcher *search = new ns_searcher::Searcher();
search->InitSearcher(input);
std::string query;
std::string json_string;
while (true)
{
std::cout << "Plesase Enter Your Search Query:";
std::cin >> query;
search->Search(query, &json_string);
std::cout << json_string << std::endl;
}
return 0;
}
然后修改一下我们的makefile
:
PARSER=parser
SEARCHSERVER=search_server
cc=g++
.PHONY:all
all:$(PARSER) $(SEARCHSERVER)
$(PARSER):parser.cc
$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
$(SEARCHSERVER):server.cc
$(cc) -o $@ $^ -l jsoncpp -std=c++11
.PHONY:clean
clean:
rm -f parser search_server
然后可以在我们的获取单例和建立索引的过程加上我们的调试信息:这样就更有利于我们进行查看:
Search.hpp
public:
void InitSearcher(const std::string &input)
{
// 1.获取或者创建index对象
index = ns_index::Index::GetInstance();
// for debug
std::cout << "获取index单例成功...." << std::endl;
// 2.根据index 对象建立索引
index->BuildIndex(input);
std::cout << "建立正排和倒排索引成功...." << std::endl;
}
index.hpp
// 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())
{
std::cerr << "sorry " << input << "open error" << std::endl;
return false;
}
std::string line;
// for debug
// 定义一个计数器:
// 查看一下建立索引的过程:
int count = 0;
while (std::getline(in, line))
{
DocInfo *doc = BuildForwardIndex(line);
if (nullptr == doc)
{
std::cerr << "build " << line << "open error" << std::endl; // for debug
continue;
}
BuildInvertedIndex(*doc);
count++;
if (count % 50 == 0)
{
std::cout << "当前已经建立的索引文档:" << count << std::endl;
}
}
return true;
}
接下来我们调试运行一下:
我们输入filesystem
:
我们是可以看到我们的json
信息的,这里我们可以打开其中的一个url
:
查找我们的filesystem
,这里确实能看到,说明基本是没问题的。
但是这个文档太长了,不好看,显然不是我们所需要的,那么我们就可以在search
中实现一个获取摘要的过程。
有人可能就说了,摘要不简简单单,我们提取文档的前100个字节,但是这个有个弊端,我们看下面例子:
那就是其实在我们的搜索引擎中,摘要部分其实也是会包含我们的搜索关键字的信息的。
那么实现摘要就肯定还得需要暴露出我们的与搜索关键字相关的内容。
获取摘要内容:
有了刚才的铺垫,我们这样实现,找到word在html文档中的首次出现,然后往前找50
这里我们重点讲一下更新位置这一部分:
我们让当前位置减去我们的50个字节,如果大于0,就说明是够的,那么我们就可以直接更新start
的位置。同理:让当前位置加上我们的100个字节,如果小于我们的size-1
,就说明后面也是够的,那么我们就可以直接更新end
的位置.
要是这两个情况都不满足,那么我们就用我们start和end的起始位置即可。
std::string GetDesc(const std::string &html_content, const std::string &word)
{
// 找到word在html_content中的首次出现,然后往前找50字节,(如果没有,我们就从begin开始)往后找100字节,(如果没有,到end就可以)
// 截取出这部分内容
const std::size_t prev_step = 50;
const std::size_t next_step = 100;
// 1.找到首次出现
std::size_t pos = html_content.find(word);
if (pos == std::string::npos)
{
return "None1"; // 这种情况是不可能存在的
}
// 2.获取start end
std::size_t start = 0;
std::size_t end = html_content.size() - 1;
// 如果之前有50+字符,就重新调整开始位置
if (pos - prev_step > start)
start = pos - prev_step;
if (pos + next_step < end)
end = pos + next_step;
// 3.截取子串,return
if (start >= end)
return "None2";
return html_content.substr(start, end - start);
}
接下来我们测试一下:
但其实这还是有一点小问题的:
既然出现了这个情况,那肯定我们的代码或多或少都是有问题的,我们将在调试中解决。
综合调试
为了后续将我们的服务端代码分离开,我们将刚才的server.cc测试文件重新命名为debug.cc
文件,修改我们的makefile
文件:
PARSER=parser
DEBUG=debug
cc=g++
.PHONY:all
all:$(PARSER) $(DEBUG)
$(PARSER):parser.cc
$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
$(DEBUG):debug.cc
$(cc) -o $@ $^ -l jsoncpp -std=c++11
.PHONY:clean
clean:
rm -f $(PARSER) $(DEBUG)
重新编译一下:
其实在我们刚才的提取摘要的过程中,类型不应该用size_t
,而应该用int
整形,不然会有坑:
std::string GetDesc(const std::string &html_content, const std::string &word)
{
// 找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
// 截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
// 1. 找到首次出现
auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y)
{ return (std::tolower(x) == std::tolower(y)); });
if (iter == html_content.end())
{
return "None1";
}
int pos = std::distance(html_content.begin(), iter);
// 2. 获取start,end , std::size_t 无符号整数
int start = 0;
int end = html_content.size() - 1;
// 如果之前有50+字符,就更新开始位置
if (pos > start + prev_step)
start = pos - prev_step;
if (pos < end - next_step)
end = pos + next_step;
// 3. 截取子串,return
if (start >= end)
return "None2";
std::string desc = html_content.substr(start, end - start);
desc += "...";
return desc;
}
11.编写 http_server 模块
升级g++
百度搜索:scl gcc devsettool升级gcc
//安装scl
$ sudo yum install centos-release-scl scl-utils-build
//安装新版本gcc,这里也可以把7换成8或者9,我用的是9,也可以都安装
$ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
$ ls /opt/rh/
//启动: 细节,命令行启动只能在本会话有效
$ scl enable devtoolset-7 bash
$ gcc -v
//可选:如果想每次登陆的时候,都是较新的gcc,需要把上面的命令添加到你的~/.bash_profile中
$ cat ~/.bash_profile
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
#添加下面的命令,每次启动的时候,都会执行这个scl命令
scl enable devtoolset-7 bash
安装cpp-httplib
创建软连接:
接下来就是编写我们的http_server
模块:
本模块我们整合了HTTP
服务器和搜索功能。我们首先初始化一个搜索器对象,使用预先处理好的文档数据(来自第一个模块)来建立索引。然后,我们设置了一个HTTP
服务器,并为服务器定义了一个GET
请求的处理函数,该函数响应客户端对"/s"
路径的访问。
当客户端发送包含搜索关键字的请求时,服务器会验证请求中是否包含关键字参数。如果没有关键字,服务器会返回错误信息。如果有关键字,服务器将调用搜索器的搜索方法,传入关键字,并获取搜索结果,这些结果被封装为JSON
字符串返回给客户端。
#include "cpp-httplib/httplib.h"
#include "search.hpp"
// 定义输入文件的路径和Web服务器的根目录
const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";
// 主函数,程序的入口点
int main()
{
// 创建Searcher类的实例并初始化
ns_searcher::Searcher search;
search.InitSearcher(input);
// 创建httplib::Server类的实例,用于处理HTTP请求
httplib::Server svr;
// 设置服务器的根目录为root_path
svr.set_base_dir(root_path.c_str());
// 为服务器添加一个GET请求的处理函数
svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp)
{
// 检查请求中是否包含搜索关键字参数"word"
if (!req.has_param("word"))
{
// 如果没有搜索关键字,返回错误信息
rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
return;
}
// 从请求中获取搜索关键字
std::string word = req.get_param_value("word");
// 记录日志,输出用户搜索的关键字
LOG(NORMAL, "用户搜索的: " + word);
// 定义用于存储搜索结果的字符串
std::string json_string;
// 调用search实例的Search方法进行搜索,并将结果存储在json_string中
search.Search(word, &json_string);
// 设置响应内容为搜索结果,并指定内容类型为JSON
rsp.set_content(json_string, "application/json"); });
// 记录日志,表示服务器启动成功
LOG(NORMAL, "服务器启动成功...");
// 服务器开始监听所有IP的8081端口
svr.listen("0.0.0.0", 8081);
// 返回0,表示程序正常结束
return 0;
}
代码解释:
- 配置文件路径和服务器根目录:
- 定义了两个常量,
input
用于指定搜索引擎的数据输入文件路径,root_path
用于指定Web服务器的根目录。
- 初始化搜索组件:
- 在
main
函数中,创建了一个ns_searcher::Searcher
类的实例,并调用其InitSearcher
方法来初始化搜索索引。这通常涉及加载数据文件和构建搜索所需的数据结构。
- 设置
HTTP
服务器:
- 使用
httplib::Server
创建一个HTTP
服务器实例。 - 通过
set_base_dir
方法设置服务器的根目录,这样服务器就可以提供静态文件服务。 - 为服务器添加一个
GET
请求的处理函数,这个函数对应于URL路径/s
。
- 处理搜索请求:
- 在
GET
请求处理函数中,首先检查请求是否包含名为word
的查询参数,这是用户输入的搜索关键字。 - 如果没有搜索关键字,服务器返回一个错误信息。
- 如果有搜索关键字,服务器记录日志并提取关键字。
- 调用
Searcher
实例的Search
方法,传入关键字,并获取搜索结果。 - 将搜索结果存储在一个
JSON
格式的字符串中,准备发送给客户端。
- 发送搜索结果:
- 设置
HTTP
响应的内容和类型为JSON
格式,然后将搜索结果字符串发送给客户端。
- 启动服务器:
- 记录日志表示服务器已成功启动。
- 调用
listen
方法使服务器开始监听指定端口(8081
)上的所有IP地址,准备接收客户端的请求。
整体来说,http_server
展示了如何使用C++
和现代库来创建一个简单的Web
搜索引擎。它处理HTTP
请求,执行搜索查询,并将结果以JSON
格式返回给用户。这种设计模式适用于创建轻量级的搜索引擎或其他Web
服务应用程序。
12.编写项目前端模块
在我们的根目录下的(wwroot)
的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: #CCC;
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: #4e6ef2;
}
.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;
}
.container .result .item i {
/* 设置为块级元素,单独站一行 */
display: block;
/* 取消斜体风格 */
font-style: normal;
color: green;
}
</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 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 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>
13.结果演示:
14.结项与总结
到这里我们的搜索引擎到这就结项了,历经一个月,我们完成了此项目,最后以一句话结尾吧:别抱怨努力的苦,那是你去看世界的路
项目扩展方向
- 建⽴整站搜索
- 设计⼀个在线更新的⽅案,信号,爬⾍,完成整个服务器的设计
- 不使⽤组件,⽽是⾃⼰设计⼀下对应的各种⽅案(有时间,有精⼒)
- 在我们的搜索引擎中,添加竞价排名(强烈推荐)
- 热次统计,智能显⽰搜索关键词(字典树,优先级队列)(⽐较推荐)
- 设置登陆注册,引⼊对mysql的使⽤(⽐较推荐的)
项目源码:搜索引擎
涉及到项目里面的各种文档都已经上传到资源当中,感兴趣的可以下载获取。