Boost搜索引擎
1. 項目的相關(guān)背景
研發(fā)搜索引擎的公司,如百度、搜狗、360搜索,還有各大網(wǎng)站各種客戶端也提供搜索功能
為什么選擇實(shí)現(xiàn)Boost搜索引擎
1)因為Boost官方網(wǎng)站是沒有搜索功能的,所以我們可以為Boost實(shí)現(xiàn)一個站內(nèi)搜索引擎,雖然官方提供了boost相關(guān)的一些方法,標(biāo)準(zhǔn)庫中的一些接口,但是我們想看到官方文檔成本比較高,所以我們可以自己做一個站內(nèi)搜索
2)自行實(shí)現(xiàn)一個全網(wǎng)搜索引擎難度極大,是十分困難的,但是實(shí)現(xiàn)站內(nèi)搜索,也就是只搜索網(wǎng)站內(nèi)的內(nèi)容,這樣搜索的內(nèi)容更垂直(即:搜索的內(nèi)容有很強(qiáng)的相關(guān)性 ),數(shù)據(jù)量更小,也可以達(dá)到以小見大的效果,
對于搜索結(jié)果,基本包含三個部分:網(wǎng)頁標(biāo)題,網(wǎng)頁內(nèi)容摘要,目標(biāo)網(wǎng)頁地址
我們還可以發(fā)現(xiàn),有時候我們可以搜到一個廣告推銷,本質(zhì)上,廣告是搜索引擎的一種盈利的方式,每一個客戶在用搜索引擎的時候,都要搜索關(guān)鍵字,所以這些搜索引擎可以出售這些關(guān)鍵字, 誰給的錢越多,誰的搜索結(jié)果就越靠前
2.搜索引擎的相關(guān)宏觀原理
程序跑起來,一定是在內(nèi)存當(dāng)中跑
1)還沒有進(jìn)行搜索之前,首先需要在全網(wǎng)當(dāng)中抓取網(wǎng)頁,假設(shè)放在data目錄下,里面包含了抓取的所有的網(wǎng)頁信息
2)把網(wǎng)頁抓取下來之后,對內(nèi)容進(jìn)行去標(biāo)簽,數(shù)據(jù)清理, 只保留網(wǎng)頁的內(nèi)容,標(biāo)題,網(wǎng)頁的url
3)searcher建立索引,既然要搜索肯定要建立索引:這個索引作用是為了加速查找
4)開始搜索,服務(wù)器一旦啟動,瀏覽器就需要通過http請求的方式進(jìn)行搜索任務(wù), (本質(zhì)還是提交http請求,然后在我們的服務(wù)端執(zhí)行搜索任務(wù))
3. 相關(guān)技術(shù)棧和項目環(huán)境
技術(shù)棧
- 后端:C/C++,C++11,STL,Boost標(biāo)準(zhǔn)庫,Jsoncpp,cppjieba,cpp-httplib
- jsoncpp:對響應(yīng)的內(nèi)容完成序列化的操作
- cppjieba:對搜索關(guān)鍵字分詞,組合成各種搜索的關(guān)鍵字然后進(jìn)行文檔搜索
- cpp-httplib:開源庫,構(gòu)建http服務(wù)器
- 前端:html5,jQuery,Ajax
項目環(huán)境
Centos7云服務(wù)器,vim/gcc(g++)/Makefile,vscode
4.搜索引擎具體原理-正排索引 && 倒排索引
搜索引擎必然要對內(nèi)容建立索引,才能更快的搜索和返回,有兩種索引:正排索引和倒排索引,以如下內(nèi)容舉例:
有如下兩個文檔,我們對這兩個文檔內(nèi)容建立索引:
文檔1:雷軍買了四斤小米 文檔2:雷軍發(fā)布了小米手機(jī)
正排索引
建立正排索引本質(zhì)就是建立文檔ID和文檔內(nèi)容的對應(yīng)關(guān)系,正排索引就是根據(jù)文檔ID找到文檔內(nèi)容
文檔ID | 文檔內(nèi)容 |
---|---|
1 | 雷軍買了四斤小米 |
2 | 雷軍發(fā)布了小米手機(jī) |
對文檔分詞
拿到文檔首先要對其編號,其次對文檔內(nèi)容進(jìn)行分詞,也就是得到文檔內(nèi)的關(guān)鍵字,為的是建立倒排索引和方便查找
- 對于文檔1:雷軍買了四斤小米 可以分詞為:雷軍/買/了/四斤/小米/四斤小米
- 對于文檔2:雷軍發(fā)布了小米手機(jī) 可以分詞為:雷軍/發(fā)布/了/小米/小米手機(jī)
其中對于“了”、“呢”、“的”、“啊”,這些詞都被稱為停止詞或暫停詞,這些詞對我們建立索引是沒有意義的,一般我們在分詞的時候可以不考慮,因為這些詞的出現(xiàn)頻率太高了,如果保留下來,搜索的時候區(qū)分唯一性的價值不大,會增加建立索引的成本,乃至于增加搜索的成本
倒排索引
倒排索引就是根據(jù)文檔內(nèi)容,進(jìn)行分詞,整理具有唯一性不重復(fù)的關(guān)鍵字,再根據(jù)關(guān)鍵字找到關(guān)聯(lián)文檔ID的方案,簡單來說就是:根據(jù)關(guān)鍵字,找到其在哪些文檔ID出現(xiàn)過
關(guān)鍵字(具有唯一性) | 文檔ID(在哪些文檔出現(xiàn)過) |
---|---|
雷軍 | 1,2 |
買 | 1 |
四斤 | 1 |
小米 | 1,2 |
四斤小米 | 1 |
發(fā)布 | 2 |
小米手機(jī) | 2 |
模擬一次搜索的過程:
用戶輸入:小米 -> 在倒排索引中查找該關(guān)鍵字,提取出文檔ID (1, 2) -> 根據(jù)文檔ID查出正排索引,找到文檔內(nèi)容 ->獲取文檔的標(biāo)題、內(nèi)容、描述、URL -> 對文檔結(jié)果進(jìn)行摘要 -> 構(gòu)建響應(yīng)并返回
- 可能一個關(guān)鍵字出現(xiàn)在多個文檔當(dāng)中,所以我們可以根據(jù)誰的權(quán)重更高,就把誰的文檔放在前面展示
5.數(shù)據(jù)去標(biāo)簽與數(shù)據(jù)清洗的模塊
獲取數(shù)據(jù)源:
boost官網(wǎng): https://www.boost.org/
1.下載搜索的數(shù)據(jù)源: https://www.boost.org/users/history/version_1_78_0.html
2.通過拖拽/re -E
指令,把壓縮包上傳到云服務(wù)器當(dāng)中
3.然后進(jìn)行解包: tar xzf 壓縮包名字
實(shí)際上我們查的大部分的手冊,都是在doc目錄下的html
這個就是標(biāo)準(zhǔn)庫對應(yīng)的各種boost組件對應(yīng)的手冊內(nèi)容,就是一個個的網(wǎng)頁信息 ,就是我們的數(shù)據(jù)源
創(chuàng)建一個名字為data的目錄,里面包含一個input目錄,input里面放我們的數(shù)據(jù)源, 把上述的數(shù)據(jù)源拷貝到input里面
目前我們只需要boost_1_78_0/doc/html目錄下的html文件,用它來進(jìn)行建立索引
認(rèn)識標(biāo)簽和去標(biāo)簽
現(xiàn)在我們首先將數(shù)據(jù)源中的各個文檔去標(biāo)簽化,HTML 是標(biāo)簽化語言,所有的語句都被一對標(biāo)簽包裹起來,由左右尖括號括起來的就是標(biāo)簽,對數(shù)據(jù)本身是無意義的,所以我們首先要將其去掉
- 一般標(biāo)簽都是成對出現(xiàn)的,標(biāo)簽中的屬性信息也是不需要的,只有標(biāo)簽內(nèi)的數(shù)據(jù)是有用數(shù)據(jù)
<!--例子-->
<title>Chapter 37. Boost.STLInterfaces</title>
<link rel="stylesheet" href="../../doc/src/boostbook.css" type="text/css">
<td align="center"><a href="../../index.html">Home</a></td>
我們可以創(chuàng)建一個目錄存放清洗干凈的html文檔
其中:input當(dāng)中放的是:原始的html文檔,也就是數(shù)據(jù)源 raw_html/raw.txt當(dāng)中放的是:去標(biāo)簽之后的干凈文檔內(nèi)容
我們可以看一下當(dāng)前有多少個html數(shù)據(jù)源:
我們當(dāng)前的目標(biāo)就是:把每個文檔都進(jìn)行去標(biāo)簽,把清洗后的內(nèi)容寫到同一個文件中, 寫入到文件當(dāng)中,一定要考慮下一次在讀取的時候也要方便操作
- 我們選擇的方案是: html文件內(nèi)部:以\3分割標(biāo)題,內(nèi)容,鏈接 html文檔和html文檔之間以\n區(qū)分
類似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
這樣我們使用getline(ifsream, line)
,一次讀取一行內(nèi)容,相當(dāng)于直接獲取一個html文檔經(jīng)過清洗之后的全部內(nèi)容:title\3content\3url
注意:這里只需要使用兩個\3即可,不需要使用三個\3:title\3content\3url\3 \n,因為兩個分隔符就可以分隔三個東西
問題:為什么使用\3作為分隔符
首先我們要知道,控制字符是不可以顯示的,即下述綠色框框中的(其ascii碼值在0~31,127)的字符,而有一些字符是打印字符,現(xiàn)在的我們的文檔內(nèi)容是屬于打印字符的范疇
我們使用\3作為分隔符,因為\3是控制字符是控制字符,不顯示,不會污染我們的文檔內(nèi)容,當(dāng)然,我們也可以使用\4之類的不可顯字符作為分割字符
注意: \3,在文檔當(dāng)中是以^c 即出現(xiàn)
關(guān)于參數(shù)的說明
const & 表示是輸入型參數(shù)
& 表示是輸入輸出型參數(shù)
* 表示是輸出型參數(shù)
編寫parser.cc
基本框架
主要過程:
1.獲取所有的帶路徑的文件名數(shù)據(jù)源html,進(jìn)行解析,解析成:title,content,url的形式
2.把解析后的數(shù)據(jù)放到清洗后數(shù)據(jù)存放位置
//首先我們涉及到讀取文件的動作,我們先把將要讀取的文件的路徑定義出來,方便我們進(jìn)行讀取
const std::string src_path = "data/input"; // html網(wǎng)頁數(shù)據(jù)源路徑,input下面放的是所有的html網(wǎng)頁
const std::string output = "data/raw_html/raw.txt"; // 文檔數(shù)據(jù)清洗之后的保存路徑
// 解析html文檔,每一個html文檔都被拆成下面的樣子:
typedef struct DocInfo_t
{
std::string title; // 文檔標(biāo)題
std::string content; // 文檔內(nèi)容
std::string url; // 該文檔在官網(wǎng)中的url
}DocInfo_t;
//先聲明
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文件
//第一步:遞歸式的把src_path路徑下的每個html文件名帶路徑,保存到files_list中
//為什么這樣做? 方便后期進(jìn)行一個一個的文件進(jìn)行讀取
if(!EnumFile(src_path, &files_list)) //枚舉所有帶路徑的文件名
{
std::cerr << "enum file name error!" << std::endl;
return 1;
}
//第二步:文件內(nèi)容的解析
std::vector<DocInfo_t> results;//存放每個文檔解析完的內(nèi)容-DocInfo_t結(jié)構(gòu)
if(!ParseHtml(files_list, &results)){
//本質(zhì)是讀取files_list路徑的文件內(nèi)容并且解析
std::cerr << "parse html error" << std::endl;
return 2;
}
//第三步: 把解析完畢的各個文件內(nèi)容寫入到output路徑對應(yīng)的文件
if(!SaveHtml(results, output)){
std::cerr << "sava html error" << std::endl;
return 3;
}
return 0;
}
枚舉帶路徑的html文件
C++對文件系統(tǒng)的支持并不是很好,所以我們需要使用的是boost庫的file system模塊
#include <boost/filesystem.hpp> //引入boost庫的file system模塊
首先,我們需要在centos下安裝在boost庫 sudo yum install -y boost-devel
- 這里需要注意的是:
我們進(jìn)行搜索的是1.78版本的官方手冊, 而我們用的boost庫的版本是1.53 來寫代碼,這個是兩碼事
不建議直接將命名空間展開,防止命名沖突 ,減少沖突概率
第一步: 使用boost::filesystem命名空間下的path類型定義一個路徑對象,并使用我們的參數(shù)路徑進(jìn)行初始化,需要判斷當(dāng)前路徑釋放是存在的
第二步:定義一個空迭代器,然后遍歷該路徑的文件,如果該文件不是普通文件就不處理,如果是普通文件還要判斷后綴是不是以html結(jié)尾,
//第一個參數(shù):所有文件保存的路徑 第二個參數(shù):輸出型參數(shù),枚舉的文件名保存的位置
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
namespace fs = boost::filesystem;
fs::path root_path(src_path);//定義一個path類型的對象,遍歷的時候就從這個路徑下開始
//判斷root_path這個路徑是否存在,如果不存在,就沒有必要再往后走了
if(!fs::exists(root_path))
{
//注意:這里不能直接打印root_path,因為它是用boost庫的filesystem定義的對象,是一個對象??!
std::cerr << src_path << " not exists" << std::endl;
return false;
}
//對文件遞歸遍歷,定義一個空的迭代器,用來進(jìn)行判斷遞歸結(jié)束
fs::recursive_directory_iterator end;//迭代器是模擬指針的行為,這個空迭代器可以認(rèn)為是nullptr
//從root_path開始遍歷 iter也是迭代器對象,用root_path構(gòu)造它
for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
//對文件進(jìn)行篩選,判斷文件是否是普通文件:html都是普通文件
if(!fs::is_regular_file(*iter))
{
continue;//不是普通文件就不處理
}
//這個普通文件還必須以html結(jié)尾,判斷文件后綴
//iter->path():返回當(dāng)前迭代器所在的路徑 extension方法:提取路徑的后綴
if(iter->path().extension() != ".html"){
//判斷 文件路徑名的后綴是否符合要求
continue;
}
//測試: 來到這里,說明當(dāng)前的路徑一定是一個合法的,以.html結(jié)束的普通網(wǎng)頁文件
//std::cout << "debug: " << iter->path().string() << std::endl;
//iter->path()得到的還是路徑對象, string():將對象所對應(yīng)的路徑以字符串的形式呈現(xiàn)出來
files_list->push_back(iter->path().string()); //將所有帶路徑的html保存在files_list,方便后續(xù)進(jìn)行文本分析
}
return true;
}
解析html文件
大致框架
先讀取文件的所有內(nèi)容,再依次解析文件的 title、content、url,解析成功后拷貝至解析結(jié)果數(shù)組中
//第一個參數(shù):保存所有帶路徑的html文件 第二個參數(shù):輸出型參數(shù),解析結(jié)果數(shù)組
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
//遍歷所有html文件路徑
for(const std::string &file : files_list)
{
//1. 讀取文件,file是帶路徑的html文件,打開它然后把它的內(nèi)容全部讀取出來
std::string result; //把讀取到的內(nèi)容放到result當(dāng)中
if(!ns_util::FileUtil::ReadFile(file, &result)){
continue;//讀取失敗,說明文件打開失敗,不管它
}
//此時result當(dāng)中保存的就是網(wǎng)頁的內(nèi)容
DocInfo_t doc;
//2. 解析指定的文件,提取title
if(!ParseTitle(result, &doc.title)){
continue;
}
//3. 解析指定的文件,提取content,本質(zhì)就是去標(biāo)簽,只保留網(wǎng)頁的內(nèi)容
if(!ParseContent(result, &doc.content)){
continue;
}
//4. 解析指定的文件路徑,構(gòu)建url
if(!ParseUrl(file, &doc.url)){
continue;
}
//來到這里,一定是完成了解析任務(wù),當(dāng)前文檔的相關(guān)內(nèi)容:title,content,url都保存在了doc結(jié)構(gòu)體里面
//results->push_back(doc) //細(xì)節(jié):push_back,本質(zhì)會發(fā)生拷貝,效率可能會比較低,所以直接移動
results->push_back(std::move(doc)); //doc是臨時對象,相當(dāng)于是資源轉(zhuǎn)移
}
return true;
}
注意:上述提取title和content, 傳入的第一個參數(shù)是當(dāng)前獲得的html文件的內(nèi)容,而構(gòu)建ur傳入的第一個參數(shù)則是當(dāng)前html文件的路徑,下面詳解!
讀取html文件
我們可以準(zhǔn)備一個Util.hpp文件,存放我們的工具類,并且把內(nèi)容都放在ns_util命名空間下面,所以我們可以單獨(dú)把讀取文件的函數(shù)寫在一個類里面:
我們可以直接使用ifstream流進(jìn)行讀取,使用getline一次讀取一行內(nèi)容,放在out里面,最后記得關(guān)閉文件流?。?!
class FileUtil
{
public:
//第一個參數(shù):該文件的路徑 第二個參數(shù):輸出型參數(shù),讀到的文件內(nèi)容返回出去
static bool ReadFile(const std::string &file_path, std::string *out)
{
std::ifstream in(file_path, std::ios::in); //in表示讀取
if(!in.is_open()) //判斷是否打開成功
{
std::cerr << "open file " << file_path << " error" << std::endl;
return false;
}
//文件讀取
std::string line; //line保存的就是讀取到的一行內(nèi)容
while(std::getline(in, line))
{
*out += line;
}
in.close(); //關(guān)閉文件
return true;
}
};
問題:如何理解getline讀取到文件結(jié)束呢?
getline的返回值是一個流的引用,while循環(huán)判斷是一個bool類型 ,本質(zhì)是因為返回的對象當(dāng)中重載了強(qiáng)制類型轉(zhuǎn)化,意思就是判斷對象的真假,這個對象里面就做了重載,重載強(qiáng)轉(zhuǎn)之后得到的就是bool值
下面三個:提取title,content,構(gòu)建url的三個函數(shù)都設(shè)置為static靜態(tài)函數(shù),我們只想在本源文件中有效
提取title
一般在html網(wǎng)頁里面只有一對title,我們要做的就說提取這對標(biāo)簽里面的內(nèi)容
其實(shí)很簡單,只需要找到<title>
和</title>
位置即可, begin指向第一個<位置,end指向第二個<位置,然后begin跳過<title>
個長度,來到標(biāo)題的正文位置, 此時[begin,end)就是標(biāo)題的內(nèi)容
//第一個參數(shù):file里面就是網(wǎng)頁內(nèi)容 第二個參數(shù):輸出型參數(shù),存放當(dāng)前網(wǎng)頁的標(biāo)簽內(nèi)容
static bool ParseTitle(const std::string &file, std::string *title)//解析網(wǎng)頁的標(biāo)題內(nèi)容
{
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移動到標(biāo)題內(nèi)容的起始位置
begin += std::string("<title>").size();
if(begin > end){
return false;
}
*title = file.substr(begin, end - begin);//[begin,end)
return true;
}
提取content
本質(zhì)是進(jìn)行去標(biāo)簽,在遍歷整個html文件內(nèi)容的時候,只要碰到>
就意味著當(dāng)前標(biāo)簽被處理完畢,只要碰到<
就意味著即將處理新標(biāo)簽, 所以可以用枚舉類型描述這2種狀態(tài),條件就緒更改狀態(tài),遇到內(nèi)容時就插入到對應(yīng)字符串中
//去標(biāo)簽(數(shù)據(jù)清洗)操作
static bool ParseContent(const std::string &file, std::string *content)
{
//基于一個簡易的狀態(tài)機(jī)
enum status{
//枚舉兩種狀態(tài)
LABLE,//正在讀的是標(biāo)簽里面的內(nèi)容
CONTENT//現(xiàn)在讀取的是正常文本內(nèi)容
};
enum status s = LABLE;//剛開始一定是標(biāo)簽
//file:就是整個html文件的內(nèi)容(網(wǎng)頁的內(nèi)容), 按字符讀取
for( char c : file)//這里沒有加引用!??!
{
switch(s)
{
//此時是標(biāo)簽狀態(tài),說明該字符可能是標(biāo)簽的一部分,如果是,則忽略,繼續(xù)下次循環(huán)讀取
//但是需要注意:如果當(dāng)前字符是右標(biāo)簽,說明下一次就是正常的文本內(nèi)容
case LABLE:
if(c == '>') s = CONTENT; //碰到右標(biāo)簽 ->標(biāo)簽狀態(tài)結(jié)束
break;
//當(dāng)前是文本狀態(tài),說明該字符可能是文本的一部分,如果是則插入到輸出型參數(shù)當(dāng)中
//但是需要注意:如果當(dāng)前字符是左標(biāo)簽,說明下一次就是讀取標(biāo)簽的內(nèi)容
case CONTENT:
if(c == '<')
s = LABLE;//碰到左標(biāo)簽 ->內(nèi)容讀取完了,變換狀態(tài)
else //意味著此時是內(nèi)容的狀態(tài),并且不是左標(biāo)簽
{
//小細(xì)節(jié):我們不想保留原始文件中的\n,因為我們想用\n作為html解析之后文本的分隔符
//所以我們把\n換成空格!所以這也是為什么不加引用的原因!否則就對源html文件做修改了
if(c == '\n') c = ' ';
content->push_back(c);//插入當(dāng)前字符
}
break;
default:
break;
}
}
return true;
}
構(gòu)建url
需要注意:boost庫的官方文檔,和我們下載下來的文檔的路徑,是有對應(yīng)關(guān)系的
官網(wǎng)URL樣例: https://www.boost.org/doc/libs/1_78_0/doc/html/accumulators.html
我們下載下來的url樣例: boost_1_78_0/doc/html/accumulators.html
我們拷貝到我們項目中的樣例: data/input/accumulators.html
所以:我們只需要構(gòu)造url的頭部為:url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html"
然后url的尾部為: url_tail = /accumulators.html
即可,也就是把我們項目當(dāng)中的樣例前面的 data/input
刪掉
url = url_head + url_tail ; 相當(dāng)于形成了一個官網(wǎng)鏈接
//第一個參數(shù):文件在當(dāng)前項目的路徑 第二個參數(shù):輸出型參數(shù)
static bool ParseUrl(const std::string &file_path, std::string *url) //構(gòu)建官網(wǎng)鏈接
{
std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html";//前綴
//最初我們最開始已經(jīng)定義了路徑src_path = "data/input" 放的是所有的html數(shù)據(jù)源
//file_path就相當(dāng)于:data/input/accumulators.html 然后把前面的內(nèi)容data/input去掉
std::string url_tail = file_path.substr(src_path.size());
//構(gòu)成官網(wǎng)鏈接
*url = url_head + url_tail;
return true;
}
寫法2: 不使用src_path
bool ParserUrl(const std::string& file, std::string* url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_80_0/doc/html/"; // 構(gòu)建前綴,注意這里html后面加了個/
int begin = file.rfind('/');//注意是從后往前找/
if (begin == std::string::npos) {
std::cout << "file suffix find error" << std::endl;
return false;
}
//[begin+1,file的結(jié)束位置]就是后綴
std::string url_tail(file, begin + 1);
*url = url_head + url_tail;
return true;
}
數(shù)據(jù)保存
把解析完畢的各個文件內(nèi)容寫入到output路徑對應(yīng)的文件, 以\3為文檔內(nèi)容之間的區(qū)分, 文檔與文檔之間用\n區(qū)分
title\3content\3url \n title\3content\3url \n title\3content\3url \n
這樣我們使用getline
一次讀取一行能夠直接獲得一個html文檔的屬性信息
//第一個參數(shù):經(jīng)過清洗之后的html文件屬性信息數(shù)組 第二個參數(shù):保存解析完畢的文件內(nèi)容
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3' //分隔符
//這里按照二進(jìn)制方式進(jìn)行寫入,但是用文本寫入也是可以的,但是這里因為有\(zhòng)3
//二進(jìn)制寫的特點(diǎn)是:寫入的是什么,文檔里保存的就是什么,程序不會給我們做自動轉(zhuǎn)化
std::ofstream out(output, std::ios::out | std::ios::binary);//因為是要輸出->所以是out,以二進(jìn)制形式寫
if(!out.is_open()){
//打開文件失敗
std::cerr << "open " << output << " failed!" << std::endl;
return false;
}
//遍歷數(shù)組
for(auto &item : results) //item就是DocInfo_t結(jié)構(gòu),放的是一個html網(wǎng)頁的標(biāo)題,內(nèi)容,鏈接
{
//文檔內(nèi)的分隔符是\3 文檔間的分割方是\n
std::string out_string;
out_string = item.title;//標(biāo)題
out_string += SEP;// \3
out_string += item.content;//內(nèi)容
out_string += SEP;// \3
out_string += item.url; //鏈接
out_string += '\n'; //一個文檔和一個文檔的分割符是\n
//第一個參數(shù):要寫入的數(shù)據(jù), 第二個參數(shù):寫入的字節(jié)數(shù)
out.write(out_string.c_str(), out_string.size());//把字符串的內(nèi)容寫到文件
/*
std::string out_string(item._title + SEP + item._content + SEP + item._url + '\n');
out.write(out_string.c_str(), out_string.size());
*/
}
out.close();//記得關(guān)閉流!
return true;
}
用于debug函數(shù)
//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;
}
可以在解析完html文件之后,調(diào)用該函數(shù)調(diào)試!由于內(nèi)容過多,所以可以選擇輸出一個之后就break退出循環(huán)
運(yùn)行結(jié)果:
我們之前看到的是,data/input目錄下一共有8141個html文件, 現(xiàn)在處理之后一共有8141行,符合我們的預(yù)期!raw.txt文件內(nèi)部的每一行都是一個網(wǎng)頁對應(yīng)的內(nèi)容:標(biāo)題\3內(nèi)容\3網(wǎng)頁鏈接 \n
\3對應(yīng)的值就是^c ,屬性之間的分隔符是\3
顯示為^C
,文檔之間的分隔符是\n
不顯示,并且我們可以賦值網(wǎng)址到瀏覽器搜索,看是否正確
可以直接 cat raw.txt | head -1
//查看前面的幾行/后面的幾行進(jìn)行驗證
如果url出現(xiàn)問題,調(diào)試,輸出結(jié)果看是否符合預(yù)期,
例如:可能是src_path的路徑為:data/input/
導(dǎo)致的錯誤,因為我們的url_head最后面的html是沒有加/的 此時我們對data/input/accumulators.html截取尾部獲得的就是accumulators.html,所以拼接起來就是:https://www.boost.org/doc/libs/1_78_0/doc/htmlaccumulators.html
解決辦法: src_path后面的路徑不加/ ,直接就是data/input ,或者url_head的html后面加個/,也就是https://www.boost.org/doc/libs/1_78_0/doc/html/文章來源:http://www.zghlxwxcb.cn/news/detail-438571.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-438571.html
匯總:parser.cc
#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp> //引入boost庫
#include "util.hpp"
//首先我們涉及到讀取文件的動作,我們先把將要讀取的文件的路徑定義出來,方便我們進(jìn)行讀取
const std::string src_path = "data/input"; // html網(wǎng)頁數(shù)據(jù)源路徑,input下面放的是所有的html網(wǎng)頁
const std::string output = "data/raw_html/raw.txt"; // 文檔數(shù)據(jù)清洗之后的保存路徑
// 解析html文檔,每一個html文檔都被拆成下面的樣子:
typedef struct DocInfo_t
{
std::string title; // 文檔標(biāo)題
std::string content; // 文檔內(nèi)容
std::string url; // 該文檔在官網(wǎng)中的url
}DocInfo_t;
//先聲明
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文件
//第一步:遞歸式的把src_path路徑下的每個html文件名帶路徑,保存到files_list中
//為什么這樣做? 方便后期進(jìn)行一個一個的文件進(jìn)行讀取
if(!EnumFile(src_path, &files_list)) //枚舉所有帶路徑的文件名
{
std::cerr << "enum file name error!" << std::endl;
return 1;
}
//第二步:文件內(nèi)容的解析
std::vector<DocInfo_t> results;//存放每個文檔解析完的內(nèi)容-DocInfo_t結(jié)構(gòu)
if(!ParseHtml(files_list, &results)){
//本質(zhì)是讀取files_list路徑的文件內(nèi)容并且解析
std::cerr << "parse html error" << std::endl;
return 2;
}
//第三步: 把解析完畢的各個文件內(nèi)容寫入到output路徑對應(yīng)的文件
if(!SaveHtml(results, output)){
std::cerr << "sava html error" << std::endl;
return 3;
}
return 0;
}
//第一個參數(shù):所有文件保存的路徑 第二個參數(shù):輸出型參數(shù),枚舉的文件名保存的位置
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
namespace fs = boost::filesystem;
fs::path root_path(src_path);//定義一個path類型的對象,遍歷的時候就從這個路徑下開始
//判斷root_path這個路徑是否存在,如果不存在,就沒有必要再往后走了
if(!fs::exists(root_path))
{
//注意:這里不能直接打印root_path,因為它是用boost庫的filesystem定義的對象,是一個對象??!
std::cerr << src_path << " not exists" << std::endl;
return false;
}
//對文件遞歸遍歷,定義一個空的迭代器,用來進(jìn)行判斷遞歸結(jié)束
fs::recursive_directory_iterator end;//迭代器是模擬指針的行為,這個空迭代器可以認(rèn)為是nullptr
//從root_path開始遍歷 iter也是迭代器對象,用root_path構(gòu)造它
for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
//對文件進(jìn)行篩選,判斷文件是否是普通文件:html都是普通文件
if(!fs::is_regular_file(*iter))
{
continue;//不是普通文件就不處理
}
//這個普通文件還必須以html結(jié)尾,判斷文件后綴
//iter->path():返回當(dāng)前迭代器所在的路徑 extension方法:提取路徑的后綴
if(iter->path().extension() != ".html"){
//判斷 文件路徑名的后綴是否符合要求
continue;
}
//測試: 來到這里,說明當(dāng)前的路徑一定是一個合法的,以.html結(jié)束的普通網(wǎng)頁文件
//std::cout << "debug: " << iter->path().string() << std::endl;
//iter->path()得到的還是路徑對象, string():將對象所對應(yīng)的路徑以字符串的形式呈現(xiàn)出來
files_list->push_back(iter->path().string()); //將所有帶路徑的html保存在files_list,方便后續(xù)進(jìn)行文本分析
}
return true;
}
//第一個參數(shù):經(jīng)過清洗之后的html文件屬性信息數(shù)組 第二個參數(shù):保存解析完畢的文件內(nèi)容
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3' //分隔符
//這里按照二進(jìn)制方式進(jìn)行寫入,但是用文本寫入也是可以的,但是這里因為有\(zhòng)3
//二進(jìn)制寫的特點(diǎn)是:寫入的是什么,文檔里保存的就是什么,程序不會給我們做自動轉(zhuǎn)化
std::ofstream out(output, std::ios::out | std::ios::binary);//因為是要輸出->所以是out,以二進(jìn)制形式寫
if(!out.is_open()){
//打開文件失敗
std::cerr << "open " << output <
到了這里,關(guān)于【項目】Boost搜索引擎的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!