目录
5.编写数据去标签与数据清洗的模块 -- parser.hpp
1.项目相关背景
- boost的官网是没有站内搜索的,需要我们自己做一个
现在市面上已经有很多搜索引擎,比如百度,搜狗360等等这些公司已经做好的搜索引擎。事实上这些公司写出来的项目都是非常大型的项目,其中的技术门槛非常高。我们自己实现一个完整的搜索引擎是不可能的。但是我们可以写一个简单的搜索引擎,也就是站内搜索,也就是我们今天要写的项目内容。
像百度这种全网型搜索是非常大型的,它需要将全网的信息搜集起来,并将它们保存起来建立相关的索引模块,并且还要根据优先度对他们进行排序,这是一个非常大的工作。
那么站内搜索:它的搜索方式是更垂直的,站内的各个信息相关性更强。
2.搜索引擎的相关宏观原理
前端通过客户端浏览器通过GET方法向后端服务器进行http请求,通过输入关键词/短语分词来进行查找。后端模块首先将官网html文档导入到磁盘中,方便我们进行去标签的清晰标签工作,里面只保存我们需要让用户看到的标题,摘要以及url;然后将清洗好的数据建立正派索引和倒排索引。再其次进行搜索功能建立,对输入的关键词进行分词,然后查找倒排找到文档 id,然后通过他们不同的权重进行从高到低的排列,最后通过httplib将后端代码和前端交互。
3.搜索引擎技术栈和项目环境
技术栈:C/C++,C++11,STL库,标准库Boost(主要处理一些文件),jsoncpp(序列化反序列化),cppjieba(搜索内容分词)cpphttp-lib(帮我们写http),html5,css,js,jQuery,Ajax.
项目环境:Centos7云服务器,vim/gcc(g++),vs2013/vscode(前端)
4.正排索引 && 倒排索引--搜索引擎原理
假设这里有两个文档:
- 文档1:雷军买了四斤小米
- 文档2:雷军发布了小米手机
正排索引:就是从文档ID找到文档内容(找到文档关键字)
文档ID | 文档内容 |
1 | 雷军买了四斤小米 |
2 | 雷军发布了小米手机 |
我们再来看一个概念:
分词:对目标文档进行分词(为了方便建立倒排索引和查找)
- 对文档1进行分词:雷军买了四斤小米:雷军/买/四斤/小米/四斤小米(四斤小米可以作为一个分词整体)
- 对文档2进行分词:雷军发布了小米手机:雷军/发布/小米/手机/小米手机
停止词:了,的,吗,a,the,一般我们再分词的时候可以不考虑。
倒排索引:根据文档内容,分词,整理不重复的各个关键字,找到对应联系文档ID的方案(也就是根据分词找到文档ID)
关键字 (具有唯一性:两个文档都出现,但是我们只保留一份) |
文档ID,weight() |
雷军 | 文档1,文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1,文档2 |
四斤小米 | 文档1 |
发布 | 文档2 |
手机 | 文档2 |
小米手机 | 文档2 |
注意:文档ID中可以包括weight权重,这样就形成了竞价排名,对权重高的排名在前面,显示在前面。
就比如说你要搜索小米,那么那个文档搜索出来的小米权重高呢,哪个就排在前面
模拟一次查找的过程:
用户输入:小米->倒排索引中查找->提取处文档ID(1,2)->根据正排索引->找到文档的内容->将title标题+conent(desc)文档描述+url网址信息=文档结果进行摘要->构建相应结果
5.编写数据去标签与数据清洗的模块 -- parser.hpp
boost官网:Boost C++ Libraries
因为boost网站没有站内搜索引擎,所以我们要写一个。boost官网中已经给我们提供了字典排序,我们将它下载下来,下载最新的即可。下载好后我们在云服务器上创建一个目录boost_searcher,并将我们刚刚下载好的文件导入进去,通过rz -E +文件即可导入。
上传好之后通过tar xzf boost_1_78_0.tar.gz解压,在boost_searcher中形成了一个目录,这个目录中保存了所有boost内容。
实际上我们用到的boost库中的文件内容,大部分来自于doc下的html文件,html目录中的文件就是各种boost组件对应的组件内容。也就是我们只需要html下的文件:
/boost_searcher/boost_1_78_0/doc/html
我们将html中的内容复制到我们新建的目录data/input下,这个input放的就是boost的数据源,也就是我们8000个数据源。这里的内容就可以来构建我们的站内网页信息了。
[wjy@VM-24-9-centos boost_searcher]$ cp -rf boost_1_78_0/doc/html/* data/input/
去标签
在boost_sercher目录下创建parser.cc文件,用来将原始数据 变成 去标签之后的数据
我们随便打开一个文件看一下里面的源文件
[wjy@VM-24-9-centos input]$ nano process.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>Chapter?30.?Boost.Process</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="libraries.html" title="Part?I.?The Boost C++ Libraries (BoostBook Subset)">
<link rel="prev" href="poly_collection/acknowledgments.html" title="Acknowledgments">
<link rel="next" href="boost_process/concepts.html" title="Concepts">
</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>
<>是html的标签,这些标签对我们搜索是没有价值的,我们需要去除标签,一般标签都是成对出现的。比如<td align="center"><a href="../../index.html">Home</a></td>这一行只要在标签里的都是没有用的,有用的只有Home。
所以我们要将数据清洗,清洗后的数据我们需要放在一个文件中,我们将清洗后的数据放在data目录下的raw_html中
[wjy@VM-24-9-centos data]$ mkdir raw_html
[wjy@VM-24-9-centos data]$ ll
total 20
drwxrwxr-x 60 wjy wjy 16384 Jul 30 19:29 input //这里放的是html原始文档
drwxrwxr-x 2 wjy wjy 4096 Jul 30 19:53 raw_html //这里放的是去标签之后干净的文档
查看input目录下,html文档有多少个
[wjy@VM-24-9-centos input]$ ls -Rl | grep -E '*.html' | wc -l
8141
目标:把每个文档都去标签,然后写入到同一个文件中,每个文档内容不需要任何\n换行!文档和文档之间用\n来进行区分。
XXXXXXXXXXX\3YYYYYYYYYYY\3ZZZZZZZZZZZ\3
为什么是\3?在下面的ascii表中我们可以看到,其中有些字符叫做控制字符,是不可显示的;有些字符是打印字符,现在很多文档中字符都是打印字符,其中\3对应的是^C,它是控制字符,是不会显示在文档中。这样就不会污染文档中的显示字符。文档中显示的只有显示字符。当然你也可以用\4,\5这些不可显示字符作为分隔符。
编写parser(将文件去标签)
我么要将input源文件去标签都放到raw_html中,所以要在raw_html目录下创建一个文档raw.txt,专门用来放这8000多个有效字符串。
[wjy@VM-24-9-centos raw_html]$ touch raw.txt
[wjy@VM-24-9-centos raw_html]$ ll
total 0
-rw-rw-r-- 1 wjy wjy 0 Jul 30 20:33 raw.txt
在去标签之前我们需要将input中的原文档读取出来,读取文档之前需要获取文档的路径,所以我们定义一个字符串,因为data/input和perser.cc在一个目录下,所以定义的路径字符串是const std::string src_path="data/input/";有读取文档目录的路径,就有接收文档路径,data/raw_html/raw.txt,将它定义成一个字符串变量output。
下面我们来编写parser.cc的大体框架,我们需要将文档中的一大堆字符串都分成各个文档,分成8000多个文档,放入vector中;然后对每个vector中的string文档进行解析,去标签解析成我们需要的title标题,文档内容content,还有url文档网址,我们将它写成一个结构体。然后将这些解析好的string再放入raw.txt文件下,也即是我们定义的outputstring路径下。
[wjy@VM-24-9-centos boost_searcher]$ cat parser.cc
#include <iostream>
#include <string>
#include <vector>
//读取文件,之前要将所有文件路径罗列出来,要一个一个读
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; //文档的网址
}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> &result,const std::string& output);
int main()
{
//第一步:将源文件中,递归式将每个html文件名带路径,保存到files_list中,方便以后进行一个一个的文件读取
std::vector<std::string> files_list;
if(!EnumFile(src_path,&files_list))
{
std::cerr<<"emnu file name error!" <<std::endl;
return 1;
}
//第二步:按照files_list读取每个文件内容,并进行剖析
std::vector<DocInfo_t> result;
if(!ParseHtml(files_list,&result))
{
std::cerr<<"parse html name error!"<<std::endl;
return 2;
}
//第三步:把剖析完毕的各个文件内容,写入到output中,按照\3作为每个文档的分隔符
if(!SaveHtml(result,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)
{
return true;
}
bool ParseHtml(const std::vector<std::string> files_list,std::vector<DocInfo_t>* results)
{
return true;
}
bool SaveHtml(const std::vector<DocInfo_t> &result,const std::string& output)
{
return true;
}
编写EnumFile函数
因为完成这样的解析动作,需要用到boost库中file_system模块的内容,我们不能直接使用C++,需要加入boost/filesystem.hpp头文件
boost开发库的安装
[wjy@VM-24-9-centos boost_searcher]$ sudo yum install -y boost-devel
这个库是boost的开发库,在这里我们明确一下,我们这里写的项目是对boost手册搜索,搜索时候引擎用的手册是1.78版本,而开发时候用的版本是1.5版本来写代码,二者并不冲突。
在boost官网中查看文档方式:
找到Documention点进去找到1.53版本boost文档,找到Filesystem点进去找到Tutorial,就是boost文档说明,点进去里面随便一个方法就能看到方法合集。
ls -Rl ./data/input查看文档所有路径--遍历所有路径,有可能是.html路径,有可能是img这种目录/图片.png。那么我们要的文件需要有一定规则:1.不是目录,是普通文件。2.需要.html后缀结束
判断路径是否存在:exists(路径)
判断常规文件:is_regular_file(参数:路径)
获取一个路径的后缀:path().extension()
将合法路径转成字符串放到vector中:path().string()
思路:boost库中方法都是包含在头文件boost::filesystem我们需要将传过来的文档参数路径,用boost库中的path方法初始化一下,首先判断这个路径是否存在,如果不存在,就打印错误消息。我们定义一个迭代器来遍历路径文档下的各个路径,那么查找的路径需要遵守两个原则:1.普通路径2..html后缀。如果两项都符合那么插入到输出型参数vector中。
最后我们可以用一条打印语句验证一下我们插入的路径都是合法路径。
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;
for(fs::recursive_directory_iterator iter(root_path);iter!=end;iter++)
{
if(!fs::is_regular_file(*iter))
continue;
if(iter->path().extension()!=".html")
continue;
//走到这,说明是合法路径
std::cout<<"debug"<<iter->path().string()<<std::endl;
files_list->push_back(iter->path().string());
}
return true;
}
makefie文件:
使用了boost文件库,需要链接boost库,boost库不是C++的标准库。
[wjy@VM-24-9-centos boost_searcher]$ cat makefile
cc=g++
parser:parser.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f parser
编译文件make之后,ldd parse可以查看编译文件parser链接了哪些库
[wjy@VM-24-9-centos boost_searcher]$ ldd parser
linux-vdso.so.1 => (0x00007ffc24fb8000)
libboost_system.so.1.53.0 => /lib64/libboost_system.so.1.53.0 (0x00007f87c7bd9000)
libboost_filesystem.so.1.53.0 => /lib64/libboost_filesystem.so.1.53.0 (0x00007f87c79c2000)
libstdc++.so.6 => /home/wjy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f87c7641000)
libm.so.6 => /lib64/libm.so.6 (0x00007f87c733f000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f87c7129000)
libc.so.6 => /lib64/libc.so.6 (0x00007f87c6d5b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f87c7ddd000)
最后可以看到有多少个.html文档,一共是8141个。
[wjy@VM-24-9-centos boost_searcher]$ ./parser | wc -l
8141
最后./parser运行编译文件,可以看到我们提取出来的.html后缀文件有哪些。
当然这只是我们测试,正常项目中不需要写这个debug语句。
编写ParseFile函数
ParseFile函数就是用来解析各个分解出来的文档,将它再分成title,content,url三个部分。那么完成这模块的部分需要经过四个步骤:
1.读取上一个模块vector中的文件的.html文档,将它里面的文档内容放到我们自定义的string类型的result中
2.解析指定文档,提取title
我们随便打开一个html文件看一下里面的内容结构:vim data/input/ratio.html,在文档第五行的位置就是标题,用<titile>标签包含起来,一般在html文档中,只有一个。