【C++項目實戰】校園公告搜索引擎:完整實現與優化指南

52bc67966cad45eda96494d9b411954d.png

🎬?個人主頁:誰在夜里看海.

📖?個人專欄:《C++系列》《Linux系列》《算法系列》

???道阻且長,行則將至


目錄

📚一、項目概述

📖1.項目背景

📖2.主要功能

📖3.界面展示

📚二、技術背景

📖1.技術棧

📖2.核心邏輯

📚三、后端實現

📖1.構建原生數據庫

🔖網頁爬取

🔖數據整理

🔖標簽清洗

🔖保存信息

📖2.構建索引

🔖正排索引

🔖倒排索引

📖3. 編寫查詢程序

🔖對搜索關鍵字分詞

🔖對檢索結果進行去重

🔖對檢索結果排序

🔖序列化與摘要生成

🔖構建JSON結果

📖4.編寫服務器主程序

🔖初始化索引

🔖設置HTTP服務器

🔖處理搜索請求

📚四、前端實現

📖1.頁面結構

📖2.搜索功能

📖3.結果展示

📖4.分頁功能

📚五、完整演示

📖1.初始界面

📖2.無關鍵字輸入

📖3.輸入關鍵字(無返回)

?編輯

📖4.輸入關鍵字(有返回)?

🔖默認排序??編輯

🔖按時間排序

📚六、總結

📖1.優化方向

📖2.源碼及網址


📚一、項目概述

📖1.項目背景

杭州師范大學教務處官網是學校發布公告的重要平臺,旨在為校內師生提供及時的信息服務。然而,目前官網存在以下問題:

① 更新滯后:主頁展示的公告多為舊信息,用戶難以快速獲取最新動態,增加了時間成本。

②?搜索功能不足:官網搜索引擎缺乏按時間排序的功能,這顯然滿足不了用戶的核心需求,因為公告具有時效性。

③ 界面設計欠佳:搜索界面不夠美觀,用戶體驗較差。

基于以上問題,我決定開發一個教務處官網公告的搜索引擎,旨在為校內師生提供一個更高效、更直觀的信息檢索工具,幫助用戶快速獲取最新公告信息,提升使用體驗。?

📖2.主要功能

校園公告搜索引擎是一個專門服務于本校師生的信息檢索平臺,其核心功能是基于教務處官網的公告公文提供關鍵字搜索服務。用戶可以通過在搜索框中輸入關鍵字,快速瀏覽相關公告的摘要信息,并直接點擊鏈接跳轉至學校官網查看完整內容,實現高效便捷的信息獲取。下面是項目的界面展示:

📖3.界面展示

界面設計簡潔直觀,包含以下內容:

① 搜索框:位于頁面頂部顯著位置,支持用戶輸入關鍵字進行公告檢索;

② 按時間排序選項:位于搜索框側邊,提供將搜索結果按發布時間排序的功能。考慮到官網公告的時效性,這個功能是很必要的。

③ 翻頁按鈕:位于頁面底部,方便用戶在搜索結果較多時進行分頁瀏覽。

學校官網也有自己的搜索引擎,但是不具備時間排序的功能,這就有一個問題:用戶想通過關鍵詞搜索到最新的公告,但是服務器返回的結果是默認按照關鍵詞權重(關鍵詞在文章0出現的頻率)進行排序的,用戶并不能立刻得到想要的結果:

這是學校官網的搜索結果:?

這是個人引擎的搜索結果:

由于引擎搜索數據來源全部來自學校官網,數據量其實并不大(從教務處官網爬下來的公告,總共也就兩千多條),所以關鍵字的覆蓋范圍有限,如果用戶輸入了一個不存在的關鍵字,系統會貼心地給出提示,并給出以下選項:

跳轉學校官網:可以直接去學校官網查看最新公告(目前項目還有瑕疵,尚未實現在線更新功能,有待后續開發);

② 訪問博主個人博客:相當于打個廣告吧hh;

查看項目源碼:如果對這個項目感興趣,也可以跳轉查看源碼。

📚二、技術背景

📖1.技術棧

本項目采用以下技術棧:

Boost準標準庫:用于高效的文件操作和字符串處理。

cppjieba分詞庫:實現中文關鍵字的分詞功能,提升搜索準確性。

jsoncpp序列化工具:將搜索結果序列化為JSON格式,便于前后端交互。

httplib服務器庫:快速搭建輕量級HTTP服務器,處理搜索請求與響應。

接下來,詳細介紹項目的具體實現過程。

📖2.核心邏輯

首先我們需要了解搜索引擎的核心邏輯:客戶端發送搜索關鍵字,服務端根據關鍵字檢索匹配對應的結果,并將結果返回給客戶端。搜索結果通常由三部分組成:

① 文檔標題:簡明扼要地概括文檔內容;

?文檔摘要:包含關鍵字的部分內容,幫助用戶快速了解文檔相關性;

③ 文檔URL:提供跳轉鏈接,方便用戶查看完整內容。

所以實現搜索返回結果,最關鍵的是:如何根據關鍵字匹配返回內容?返回內容從何而來? 我們不能簡單地將客戶端的關鍵字轉發給其他搜索引擎,因為返回結果必須來自本地服務器。因此,我們需要在本地構建一個數據庫,存儲文檔的基本數據單元,即:文檔標題、文檔摘要、文檔URL。

文檔內容又從何而來,市面上主流的搜索引擎(如Google、百度)通過網絡爬蟲不斷從互聯網上抓取網頁內容,?將其轉換為數據單元并存儲在本地數據庫中,從而實現全網搜索。

構建一個全網搜索引擎的成本和資源需求極高,尤其是對于個人開發者來說,無論是數據存儲、計算能力還是網絡帶寬,都遠遠超出了博主現有云服務器的承載能力。然而,實現一個校園網站公告的搜索引擎則是一個更加實際和可行的選擇。校園公告的數據量相對較小,存儲和檢索的開銷也相對較低,完全可以在云服務器的能力范圍內高效運行。這種小而精的項目不僅降低了技術門檻,還能為校園用戶提供切實的便利,是一個理想的學習和實踐目標。

📚三、后端實現

?在掌握了搜索引擎的基本原理和數據結構后,我們開始著手實現后端部分。以下是具體的設計與實現過程:

📖1.構建原生數據庫

🔖網頁爬取

首選,我們需要從教務處官網爬取內容,將結果保存為本地.html文件,這些文件是后續處理的基礎數據源。這里我們使用wget工具進行網頁爬取:

wget是一個強大的命令行工具,用于從網絡上下載文件,具有遞歸下載、斷點續傳、限速等特性,非常適合用于批量下載文件或爬取網站內容。我們在云服務終端輸入以下命令:

wget --recursive --no-clobber --no-parent --convert-links --domains jwc.hznu.edu.cn --directory-prefix=data/source_html -A .shtml https://jwc.hznu.edu.cn/

參數說明:

--recursive:遞歸下載整個網站的內容;

--no-clobber:如果文件已存在,則不會重復下載(避免覆蓋);

--no-parent:不下載父目錄中的內容,僅限當前目錄及子目錄;

--convert-links:將下載的文件中的鏈接轉換為本地鏈接,方便本地查看;

--domains jwc.hznu.edu.cn:限制只下載指定域名下的內容

--directory-prefix=data/source_html:將下載的內容保存到?data/source_html?目錄下;

-A .shtml:僅下載?.shtml?文件。

官網的文件就是shtml格式的,這樣我們就能準確地將所有公告文件爬取到本地,忽略不需要的文件。由于官網中公告文件都保存在“/c”目錄下,我們通過wget工具爬取到本地后轉換成本地鏈接,也是保存在/c目錄下面:

我們可以看到,公告文件是按照時間進行分目有序存儲,我們可以通過find指令查看總文件數:

接下來的步驟,就是把下載下來的2064個shtml文件整理到一起:

🔖數據整理

爬取到的?.html 文件內容較為雜亂,需要進一步整理并提取關鍵信息。為了實現這一目標,首先需要遞歸地遍歷 /c?目錄,獲取全部的?.shtml 文件。雖然可以通過 open 打開文件讀取數據,但遞歸訪問目錄是一個需要解決的問題。這里,我們可以使用 Boost.Filesystem?庫來實現這一功能。

Boost.Filesystem 是一個強大的文件系統操作庫,提供了跨平臺的目錄遍歷、文件操作等功能。然而,Boost 并不是 C++ 官方標準庫,因此需要先下載并安裝到本地才能使用。

下面是Boost::filesystem庫的具體使用過程:

// 借助 Boost 庫遞歸遍歷目錄,匯總 .html 文件
bool Enumfile(const string &src_path, vector<string> *files_list)
{namespace fs = boost::filesystem; // 命名空間別名,簡化代碼fs::path root_path(src_path);    // 將字符串路徑轉換為 boost::filesystem::path 對象// 判斷路徑是否存在if (!fs::exists(root_path)){cerr << src_path << " not exists!" << endl;return false;}// 設置一個空的迭代器,作為結束標志fs::recursive_directory_iterator end;// 遞歸遍歷目錄for (fs::recursive_directory_iterator it(root_path); it != end; ++it){// 如果當前文件不是普通文件,則跳過if (!fs::is_regular_file(*it))continue;// 如果當前文件后綴不是 .html,則跳過if (it->path().extension() != ".html")continue;// 將文件名以字符串形式插入列表files_list->push_back(it->path().string());}return true;
}

如此一來,我們所有的shtml文件內容就以一個個字符串的形式保存在了vector數組中。

🔖標簽清洗

shtml文件中包含了關于整個網頁的內容,但是我們只需要三部分內容:文檔標題、文檔摘要、文檔URL。所以下一步,我們要從shtml源文件中提取出這三要素作為一個數據單元進行存儲,這個步驟就是標簽清洗的過程。

①文檔標題

在html中,<title>定義網頁的標題,但是在官網公告中

將“杭州師范大學教務處”作為網頁的標題,而并不是公告的標題,所以我們需要找到公告標題對應的標簽是什么。在網頁中,我們選中公告標題,選擇網頁檢查,就可以看到源碼中的位置:

原來標題被定義為了<h1>標簽,也就是一級標題,所以我們在源文件中,只需要定位到<h1>標簽就可以找到文檔標題了。

②文檔正文

文檔正文內容位于標簽<div class=\"sp-content\">下,我們同樣定位到該標簽處,將后續標簽清洗,保留正文部分。

標簽清洗的核心邏輯是:在遍歷?HTML?內容時,忽略掉所有被?<?和?>?包圍的部分(即標簽),而保留未被?<>?包圍的部分(即正文內容)。為了實現這一邏輯,我們可以通過定義一個狀態機來實現。

狀態機的設計如下:

①?初始狀態為?TAG,因為遍歷通常從?<?開始;

②?當遇到?>?時,狀態從?TAG?切換到?CONTENT,表示接下來是正文部分;

③?當再次遇到?<?時,狀態從?CONTENT?切換回?TAG,表示接下來的內容是標簽;

④?重復上述過程,直到遍歷完整個 HTML 內容。

static bool ParseContent(const string &file, string *content)
{size_t begin = file.find("<div class=\"sp-content\">");if(begin == string::npos){return false;}begin += string("<div class=\"sp-content\">").size();size_t end = file.find("<div class=\"foter\">");if(end == string::npos){return false;}// 使用狀態機,去除標簽enum status{LABLE,CONTENT};enum status s = LABLE;for(size_t i = begin; i < end; ++i){// cout << "curent status: " << s << endl;switch(s){case LABLE:if(file[i] == '>'){s = CONTENT;}break;case CONTENT:if(file[i] == '<'){s = LABLE;}else{char tmp = file[i];if(tmp == '\n') tmp = ' ';content->push_back(tmp);}break;default:break;}}return true;
}

③文檔URL

在 HTML 源碼中,通常不會直接包含網頁的完整 URL 信息,因此我們需要通過其他方式推斷出 URL。網頁在網站中的存儲通常遵循一定的路徑規則。以教務處官網為例,所有公告網頁都存儲在 /c 目錄下。當我們使用 wget 工具將這些網頁下載到本地時,文件的路徑結構與官網保持一致,即在本地也保留了 /c 目錄下的相對路徑。基于這一特性,我們可以通過以下步驟獲取網頁的完整 URL:即將官網的基礎路徑與本地文件的相對路徑拼接,就得到了完整的URL

🔖保存信息

我們將提取出的文檔標題文檔摘要文檔URL這三個關鍵信息存儲在一個 DocInfo 結構體中,作為基本的數據單元,然后將這些數據單元按行寫入到一個文本文件(?raw.txt)中,其中每個數據單元內部的字段之間用特殊分隔符(如 \3)分隔,不同數據單元之間用換行符 \n 分隔。這個 raw.txt 文件不僅實現了數據的持久化存儲,還為后續索引構建和搜索功能提供基礎數據源

下面是構建原生數據庫的核心代碼片段:

const string src_path = "data/source_html/test";
const string output = "data/raw_data/test.txt";typedef struct DocInfo{string title;   // 文檔標題string content; // 文檔的內容string url;     // 文檔的url
}DocInfo_t;// const & 輸入
//       * 輸出
//       & 輸入輸出
bool Enumfile (const string &src_path, vector<string> *files_list);
bool ParseHtml (const vector<string> &files_list, vector<DocInfo_t> *results);
bool SaveHtml (const vector<DocInfo_t> &results, const string &output);int main()
{// 1.遍歷指定目錄,將html文件匯總在列表里vector<string> files_list;if(!Enumfile(src_path, &files_list)){cerr << "Enum file name error!" << endl;return 1;}// 2.將列表中的每個文件進行解析,提取關鍵數據vector<DocInfo_t> results;if(!ParseHtml(files_list, &results)){cerr << "Parse html error!" << endl;return 2;}// 3.將解析后的數據保存到指定文件中if(!SaveHtml(results, output)){cerr << "Save html error!" << endl;return 3;}return 0;
}

📖2.構建索引

在數據庫建立完成后,我們可以編寫程序處理搜索關鍵字并返回相關內容:首先接收用戶輸入的關鍵字,然后在數據庫中檢索 titlecontent 字段包含該關鍵字的文檔;由于一個關鍵字可能匹配多個文檔,而數據庫未對結果排序,我們需要將這些文檔提取到 vector 容器中,按相關性或其他規則進行排序,最后將排序后的文檔作為搜索結果返回給用戶。

所以第一步我們需要建立的是通過文檔ID索引文檔信息的正排索引

🔖正排索引
    // 正排索引數據節點struct DocInfo{string title;    // 文檔標題string content;  // 文檔去標簽后的內容string url;      // 官網的網址string time;     // 文檔時間uint64_t doc_id; // 文檔ID};// 正排索引通過數組實現,下標天然為文檔IDvector<DocInfo> forward_index;

構建正排索引的過程是通過文檔 ID 索引文檔信息。在 vector 容器中,下標天然可以作為文檔 ID,而文檔信息結構包括【title、content、URL、ID】。我們可以使用 std::ifstream 創建一個讀取流,將文檔內容寫入流中,并通過 std::getline 方法循環讀取,每次讀取的恰好是一個文檔(文檔之間用 \n 分隔)。

讀取到的文檔是一個字符串,信息段之間用 \3 分隔,因此需要對字符串進行分割。我們可以手動編寫分割代碼(遍歷字符串,遇到 \3 時分割),也可以使用 boost::split 方法,它能夠根據指定字符分割字符串,并將結果存儲到 vector 數組中。分割完成后,將數據段組合成 DocInfo 結構,并存儲到正排索引的 vector 容器中。

下面是構建正排索引的代碼實現:

        // 創建正排索引DocInfo *BuildForwardInfo(const string &line){// 1.對字符串進行切分:title、content、urlvector<string> results;const string sep = "\3";ns_util::StringUtil::CutString(line, &results, sep);if(results.size() < 3){return nullptr;}// 2.字符串填充到DocInfo結構中DocInfo doc;doc.title = results[0];doc.content = results[1];doc.url = results[2];doc.doc_id = forward_index.size();// 從URL中提取時間doc.time = ExtractTimeFromUrl(doc.url);// 3.插入到正排索引的vector中forward_index.push_back(move(doc));return &forward_index.back();}
🔖倒排索引
    // 倒排索引數據節點struct InvertedElem{uint64_t doc_id; // 文檔IDstring word;     // 關鍵字int weight;      // 關鍵字的權重};// 倒排索引通過鍵值對實現,一個關鍵字映射一個/多個倒排拉鏈結構unordered_map<string, InvertedList> inverted_index;

為了通過關鍵字獲取文檔信息,我們需要構建倒排索引。倒排索引是一種映射關系,通過關鍵字映射到文檔信息,文檔信息結構包括【ID、word關鍵字、weight權重】。其中,文檔 ID 用于索引正排容器以獲取更詳細的文檔信息,而 weight 權重則用于文檔的排序。由于關鍵字在每個文檔中出現的頻率不同,我們需要將檢索到的文檔按照關鍵字出現頻率從高到低排列返回

因此,我們需要構建倒排索引結構,這就要求我們將所有關鍵字列舉出來。關鍵字是從文檔的 titlecontent 中提取的,因此在構建每一個正排索引時,可以同時構建該文檔的所有關鍵字的倒排索引。那么,關鍵字的提取規則是什么呢?

我們可以使用 cppjieba 分詞工具,其中的 jieba::for_search 方法專門用于搜索關鍵字的分詞。由于關鍵字在 titlecontent 中出現的權重不同,我們需要定義兩個 vector 容器,分別存儲 titlecontent 的關鍵字分詞結果,并分別統計關鍵字出現的次數。最后,按照特定算法計算關鍵字的權重,從而完成倒排索引的構建。

下面是構建倒排索引的代碼實現:

        // 創建倒排索引bool BuildInvertedIndex(const DocInfo &doc){struct word_cnt{int title_cnt;int content_cnt;word_cnt(): title_cnt(0), content_cnt(0) {};};unordered_map<string, word_cnt> word_map;// 對標題進行分詞vector<string> title_words;ns_util::JiebaUtil::CutString(doc.title, &title_words);// 統計標題中關鍵字的頻次for(auto &it : title_words){word_map[it].title_cnt++;}// 對正文進行分詞vector<string> content_words;ns_util::JiebaUtil::CutString(doc.content, &content_words);// 統計正文中關鍵字的頻次for(auto &it : content_words){word_map[it].content_cnt++;}constexpr int X = 10;  // 定義常量 Xconstexpr int Y = 1;   // 定義常量 Y// 統計關鍵字及其權重,插入InvertedList倒排拉鏈中for(auto &it : word_map){InvertedElem word_elem;word_elem.doc_id = doc.doc_id; // 文檔IDword_elem.word = it.first; // 關鍵字word_elem.weight = it.second.title_cnt * X + it.second.content_cnt * Y; // 計算權重(簡易版)InvertedList &inverted_list = inverted_index[it.first];inverted_list.push_back(move(word_elem));}return true;}

因此,構建索引的步驟如下:循環讀取數據庫,提取每個文檔并構建對應的正排索引,然后根據正排索引中的 titlecontent 提取全部關鍵字,構建倒排索引

至此,我們已經可以正式編寫執行查詢流程的程序了。?

📖3. 編寫查詢程序

🔖對搜索關鍵字分詞

用戶輸入的關鍵字并不能直接用于索引搜索,而是需要先進行分詞處理。我們可以使用 jieba 分詞工具對關鍵字進行切分,然后將分詞結果放入倒排索引中進行檢索。

這里存在一個問題:同一個文檔可能會被多次返回。例如,文檔內容為“小明來了北京”,用戶搜索的關鍵字也是“小明來了北京”,分詞結果為“小明/來了/北京”,這三個詞可能分別檢索到同一個文檔。如果不對這種情況進行去重處理,搜索結果中就會出現重復的文檔。

我們通過 JiebaUtil::CutString 方法對 query 進行分詞,并將分詞結果存儲在 words 中:

    class StringUtil{public:static void CutString(const string &target, vector<string> *out, const string &sep){boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on); // 壓縮重復字符}};
🔖對檢索結果進行去重

為了解決重復文檔的問題,我們使用哈希表 inverted_map,通過文檔 ID 映射倒排索引的方式完成去重操作。對于檢索到的重復文檔,將其權重累加起來。由于這些文檔是由不同關鍵字檢索到的,還需要將這些關鍵字保存起來。為此,我們定義了 InvertedElemPrint 結構,用于存儲文檔 ID、關鍵字列表和權重。

我們遍歷每個分詞結果,從倒排索引中獲取相關文檔,并將其合并到 inverted_map 中:

unordered_map<uint64_t, InvertedElemPrint> inverted_map;
for(string word : words) {boost::to_lower(word);ns_index::InvertedList *word_list = index->GetInvertedIndex(word);if(nullptr == word_list) continue;for(const auto &elem: *word_list) {auto &item = inverted_map[elem.doc_id];item.doc_id = elem.doc_id;item.words.push_back(elem.word);item.weight += elem.weight;}
}
🔖對檢索結果排序

在得到去重后的倒排索引集合后,需要按照權重 weight 對結果進行降序排列。我們使用 std::sort 函數實現這一排序操作,確保最相關的文檔排在前面。

我們將 inverted_map 中的數據移動到 gather 中,并按權重排序

vector<InvertedElemPrint> gather;
for(const auto &item : inverted_map) {gather.push_back(move(item.second));
}
sort(gather.begin(), gather.end(), [](const InvertedElemPrint &e1, const InvertedElemPrint &e2) {return e1.weight > e2.weight;
});
🔖序列化與摘要生成

整理后的索引結果無法直接通過網絡傳輸,需要以序列化和反序列化的方式進行處理。我們使用 Json 作為通用的序列化工具,構建 json 字符串返回給用戶。但還有一個問題:索引中提取的是文檔的全部正文內容,如果直接將全部內容返回并顯示在用戶的搜索界面上,顯然不夠友好。因此,我們需要對正文部分生成摘要,便于用戶快速了解文檔內容并決定是否跳轉查看詳情。

摘要內容最好包含用戶搜索的關鍵字。我們通過 GetDigest 方法生成摘要:在 content 中查找第一個關鍵字的位置,然后取關鍵字前 50 字節和后 100 字節作為摘要內容。

string GetDigest(const string &content, const string &key) {const int prev_chars = 50;const int next_chars = 100;auto itea = search(content.begin(), content.end(), key.begin(), key.end(), [](char a, char b) {return (tolower(a) == tolower(b));});if(content.end() == itea) return "未找到關鍵詞";// 計算字符位置并生成摘要string utf8_content = content;vector<size_t> char_positions;size_t byte_pos = 0;while (byte_pos < utf8_content.size()) {char_positions.push_back(byte_pos);unsigned char c = utf8_content[byte_pos];if (c < 0x80) byte_pos += 1;else if (c < 0xE0) byte_pos += 2;else if (c < 0xF0) byte_pos += 3;else byte_pos += 4;}int char_pos = distance(content.begin(), itea);int char_index = 0;for (size_t i = 0; i < char_positions.size(); i++) {if (char_positions[i] >= (size_t)char_pos) {char_index = i;break;}}int start_char = max(0, char_index - prev_chars);int end_char = min((int)char_positions.size() - 1, char_index + next_chars);size_t start_byte = char_positions[start_char];size_t end_byte = (end_char + 1 < char_positions.size()) ? char_positions[end_char + 1] : utf8_content.size();string digest = utf8_content.substr(start_byte, end_byte - start_byte);bool has_more_at_start = (start_char > 0);bool has_more_at_end = (end_char < (int)char_positions.size() - 1);if (has_more_at_start) digest = "..." + digest;if (has_more_at_end) digest = digest + "...";return digest;
}
🔖構建JSON結果

最后,我們將排序后的結果構建為 json 字符串返回給用戶。

void BuildJsonResult(const vector<InvertedElemPrint> &gather, string *json_string) {Json::Value root;for(auto &item : gather) {ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id);if(nullptr == doc) continue;Json::Value elem;elem["title"] = doc->title;elem["digest"] = GetDigest(doc->content, item.words[0]);elem["url"] = doc->url;elem["id"] = doc->doc_id;elem["weight"] = item.weight;elem["time"] = doc->time;root.append(elem);}Json::StyledWriter writer;*json_string = writer.write(root);
}

📖4.編寫服務器主程序

我們使用 httplib 庫實現了一個簡單的 HTTP 服務器,用于處理用戶的搜索請求并返回結果。httplib 是一個輕量級的 C++ HTTP 庫,易于集成和使用。?

🔖初始化索引

在程序啟動時,我們首先需要初始化搜索引擎的索引。通過調用 ns_sercher::Sercher 類的 InitIndex 方法,從指定的數據文件 data/raw_data/raw.txt 中加載數據并構建正排索引和倒排索引。

ns_sercher::Sercher serch;
serch.InitIndex(input);
🔖設置HTTP服務器

我們使用 httplib創建一個 HTTP 服務器,并設置服務器的根目錄為 ./wwwroot。該目錄用于存放靜態資源文件(如 HTML、CSS、JavaScript 等),供客戶端訪問。

httplib::Server svr;
svr.set_base_dir(root_path.c_str());
🔖處理搜索請求

我們為服務器定義了一個 /search 路由,用于處理用戶的搜索請求。該路由通過 GET 方法接收用戶輸入的關鍵字,并根據請求參數執行不同的搜索邏輯。

根據請求參數 time_priority 的值,決定是按時間排序還是按權重排序

if (time_priority){cout << "按時間排序" << endl;serch.TimePrioritySerch(word, &json_string);
} else {cout << "按權重排序" << endl;serch.CommonSerch(word, &json_string);
}

如果搜索關鍵詞為空,返回全部文檔的時間排序結果(便于瀏覽最新公告):

// 檢查輸入是否為空或僅包含空格
if (word.empty() || word.find_first_not_of(' ') == string::npos) {cout << "返回所有文檔信息" << endl;serch.GetAllDocuments(&json_string);resp.set_content(json_string, "application/json; charset=utf-8");return;
}

如果搜索關鍵字不存在,則返回空結果和廣告信息:

if (json_string.empty()) {json_string = R"({"results": [], "ads": [{"text": "進入校園官網:", "url": "https://jwc.hznu.edu.cn/", "linkText": "杭州師范大學教務處"},{"text": "分享學習筆記,記錄生活點滴,歡迎訪問我的博客:", "url": "https://kanhai-night.blog.csdn.net", "linkText": "Kanhai's 技術博客"},{"text": "本項目已開源:", "url": "https://gitee.com/HZNUYuwen/Linux_gitee/tree/master/HZNUSercher", "linkText": "查看項目源碼"}]})";
}

📚四、前端實現

前端頁面實現了搜索功能的核心交互邏輯,包括關鍵字輸入、搜索請求、結果展示和分頁瀏覽。以下是對主要功能的介紹:

📖1.頁面結構

頁面分為以下幾個部分:

① 搜索框:用戶輸入關鍵字,并選擇是否按時間排序。

② 搜索結果區域:動態展示搜索結果的標題、摘要和鏈接。

③ 分頁控件:支持上一頁和下一頁的翻頁操作。

<div class="container initial-state"><div class="search"><input type="text" value="請輸入搜索關鍵字"><div class="search-options"><label><input type="checkbox" id="time-priority"> 按時間先后</label></div><button onclick="Search()">搜索一下</button></div><div class="result hidden"><!-- 動態生成網頁內容 --></div><div class="pagination hidden"><button onclick="prevPage()">上一頁</button><span id="page-info"></span><button onclick="nextPage()">下一頁</button></div>
</div>

📖2.搜索功能

通過 Search 函數發起搜索請求,將用戶輸入的關鍵字發送到后端,并動態更新搜索結果:

function Search() {currentQuery = $(".container .search input").val().trim(); // 獲取關鍵字let timePriority = $("#time-priority").is(":checked"); // 是否按時間排序$.ajax({type: "GET",url: "/search?word=" + currentQuery + "&time_priority=" + timePriority,success: function(data) {searchResults = data; // 保存搜索結果currentPage = 0; // 重置頁碼BuildHtml(); // 渲染結果setResultState(); // 切換到結果狀態}});
}

📖3.結果展示

通過 BuildHtml 函數動態生成搜索結果,并支持關鍵字高亮顯示:

function BuildHtml() {let result_lable = $(".container .result");result_lable.empty(); // 清空之前的結果let start = currentPage * resultsPerPage;let end = start + resultsPerPage;let pageResults = searchResults.slice(start, end); // 獲取當前頁結果for (let elem of pageResults) {let highlightedTitle = highlightKeyword(elem.title, currentQuery); // 高亮標題let highlightedDigest = highlightKeyword(elem.digest, currentQuery); // 高亮摘要result_lable.append(`<div class="item"><a href="${elem.url}" target="_blank">${highlightedTitle}</a><p>${highlightedDigest}</p><i>${elem.url}</i><span style="display: block; color: #888; font-size: 12px; margin-top: 5px;">${elem.time ? "發布時間: " + elem.time : ""}</span></div>`);}
}

📖4.分頁功能

通過 prevPagenextPage 函數實現分頁瀏覽:

function prevPage() {if (currentPage > 0) {currentPage--;BuildHtml(); // 更新結果}
}function nextPage() {if ((currentPage + 1) * resultsPerPage < searchResults.length) {currentPage++;BuildHtml(); // 更新結果}
}

📚五、完整演示

下面是《校園公告搜索引擎》項目功能的完整演示:

📖1.初始界面

📖2.無關鍵字輸入

當無關鍵字輸入時,返回用戶的結果是經過時間排序的全部文檔內容

📖3.輸入關鍵字(無返回)

📖4.輸入關鍵字(有返回)?

🔖默認排序?
🔖按時間排序

📚六、總結

在這里就不對項目本身過多贅述了,下面說一下項目的不足與優化方向:

📖1.優化方向

① 在線更新:目前項目尚未實現在線更新功能,獲取的官網公告數據截至 2025年3月14日,最新的官網公告未能同步到搜索引擎。未來可以引入定時任務或實時爬蟲機制,確保數據及時更新。

② 熱詞統計:在搜索時,如果能智能顯示熱門搜索關鍵詞,可以進一步提升用戶體驗。

③ 登錄系統:由于該搜索引擎主要服務于本校師生,可以增加登錄認證功能。

④ 響應速度:目前服務器的響應速度還有提升空間。可以通過優化索引結構、引入緩存機制等。

⑤ 擴大搜索范圍:除了教務處官網,未來可以引入其他學校官網(如學院、圖書館、招生辦等)的數據作為搜索對象。

⑥ 引入域名:目前項目通過 IP 地址和端口號訪問服務器,這種方式不夠直觀且不利于記憶。

📖2.源碼及網址

這里給出項目源碼以及訪問網址:

項目源碼

校園公告搜索引擎


以上就是【校園公告搜索引擎】的全部內容,歡迎指正~?

碼文不易,還請多多關注支持,這是我持續創作的最大動力!?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/897879.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/897879.shtml
英文地址,請注明出處:http://en.pswp.cn/news/897879.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

代理(Delegate)、閉包(Closure)、Notification(通知中心) 和 swift_event_bus適用場景和工作方式

在 Swift 開發中&#xff0c;在 Swift 開發中&#xff0c;代理&#xff08;Delegate&#xff09;、閉包&#xff08;Closure&#xff09;、Notification&#xff08;通知中心&#xff09; 和 swift_event_bus 主要用于 組件之間的通信&#xff0c;但它們的適用場景和工作方式有…

設計模式--單例模式(Singleton)【Go】

引言 在設計模式中&#xff0c;單例模式&#xff08;Singleton Pattern&#xff09;是一種非常常見且實用的模式。它的核心思想是確保一個類只有一個實例&#xff0c;并提供一個全局訪問點。這種模式在需要全局唯一對象的場景中非常有用&#xff0c;比如配置管理、日志記錄、數…

MySQL數據庫復制

文章目錄 MySQL數據庫復制一、復制的原理二、復制的搭建1.編輯配置文件2.在主庫上創建復制的用戶3.獲取主庫的備份4.基于從庫的恢復5.建立主從復制6.開啟主從復制7.查看主從復制狀態 MySQL數據庫復制 MySQL作為非常流行的數據庫&#xff0c;支撐它如此出彩的因素主要有兩個&am…

Sourcetree——使用.gitignore忽略文件或者文件夾

一、為何需要文件忽略機制&#xff1f; 1.1 為什么要會略&#xff1f; 對于開發者而言&#xff0c;明智地選擇忽略某些文件類型&#xff0c;能帶來三大核心優勢&#xff1a; 倉庫純凈性&#xff1a;避免二進制文件、編譯產物等污染代碼庫 安全防護&#xff1a;防止敏感信息&…

基于yolov8+streamlit實現目標檢測系統帶漂亮登錄界面

【項目介紹】 基于YOLOv8和Streamlit實現的目標檢測系統&#xff0c;結合了YOLOv8先進的目標檢測能力與Streamlit快速構建交互式Web應用的優勢&#xff0c;為用戶提供了一個功能強大且操作簡便的目標檢測平臺。該系統不僅具備高精度的目標檢測功能&#xff0c;還擁有一個漂亮且…

分享vue好用的pdf 工具實測

vue3-pdf-app&#xff1a; 帶大綱&#xff0c;帶分頁&#xff0c;帶縮放&#xff0c;帶全屏&#xff0c;帶打印&#xff0c;帶下載&#xff0c;帶旋轉 下載依賴&#xff1a; yarn add vue3-pdf-appornpm install vue3-pdf-app 配置類&#xff1a; 創建文件 pdfConfig.ts /…

基于微信小程序開發的寵物領養平臺——代碼解讀

項目前端 一、項目的技術架構概況 一句話概括&#xff1a;該項目是基于微信小程序開發的寵物領養平臺&#xff0c;采用原生小程序框架進行用戶界面的構建&#xff0c;使用 wx.request 進行 API 請求&#xff0c;并通過 getApp() 和本地存儲來管理全局狀態和用戶信息。 一&am…

最完美的WPF無邊框設計!

常規的無邊框方法設計 常規的WPF無邊框設計方法都是通過AllowsTransparency="True"和WindowStyle=“None”,并且使用WindowChrome樣式來實現,但是這樣會有問題就是,窗體最大化的時候將底部任務欄給擋住了,另外最大化的時候不能拖動窗體。參考這個大佬的設計@ 若…

【區塊鏈】btc

學習視頻源鏈接&#xff1a; https://www.bilibili.com/video/BV1Vt411X7JF/ 本文是根據肖老師的視頻進行的筆記記錄 一、 cryptographic hash function 1.1. collision resistance抗碰撞性 &#xff1a; collision 指的是hash碰撞 抗碰撞性 (Collision Resistance) 是密碼…

C語言【數據結構】:時間復雜度和空間復雜度.詳解

引言 詳細介紹什么是時間復雜度和空間復雜度。 前言&#xff1a;為什么要學習時間復雜度和空間復雜度 算法在編寫成可執行程序后&#xff0c;運行時需要耗費時間資源和空間(內存)資源。因此衡量一個算法的好壞&#xff0c;一般是從時間和空間兩個維度來衡量的&#xff0c;即時…

QT:文件讀取

問題&#xff1a; 在文件讀取&#xff0c;判斷md5值時&#xff0c;遇到py文件讀取轉String后&#xff0c;再轉byte&#xff0c;md5前后不一致問題。 解決方法&#xff1a; python文件讀取要使用QTextStream&#xff0c;避免\t 、\r、\n的換行符跨平臺問題&#xff08;window…

32單片機——LED

LED原理圖如圖所示&#xff1a; 代碼 DS0和DS1每過500ms一次交替閃爍&#xff0c;實現類似跑馬燈的效果 GPIO輸出配置步驟 &#xff08;1&#xff09;使能對應GPIO時鐘 STM32在使用任何外設之前&#xff0c;我們都要先使能其時鐘&#xff08;下同&#xff09;。本實驗用到…

貪心算法和遺傳算法優劣對比——c#

項目背景&#xff1a;某鋼管廠的鋼筋原材料為 55米&#xff0c;工作需要需切割 40 米&#xff08;1段&#xff09;、11 米&#xff08;15 段&#xff09;等 4 種規格 &#xff0c;現用貪心算法和遺傳算法兩種算法進行計算&#xff1a; 第一局&#xff1a;{ 40, 1 }, { 11, 15…

【Java篇】一法不變,萬象歸一:方法封裝與遞歸的思想之道

文章目錄 Java 方法的使用&#xff1a;從基礎到遞歸的全面解析一、方法的概念及使用1.1 什么是方法 (method)?1.2 方法定義1.3 方法調用的執行過程1.4 實參和形參的關系1.5 沒有返回值的方法 二、方法重載2.1 為什么需要方法重載2.2 方法重載的概念2.2.4 C 和 Java 的比較&…

深入理解 HTML 中的<div>和元素:構建網頁結構與樣式的基石

一、引言 在 HTML 的世界里&#xff0c;<div>和元素雖看似普通&#xff0c;卻扮演著極為關鍵的角色。它們就像網頁搭建過程中的萬能積木&#xff0c;能夠將各種 HTML 元素巧妙地組合起來&#xff0c;無論是構建頁面布局&#xff0c;還是對局部內容進行樣式調整&#xff…

《大語言模型》學習筆記(一)

一、什么是大語言模型 大語言模型是指在海量無標注文本數據上進行預訓練得到的大型預訓練語言模型&#xff0c;例如GPT-3&#xff0c;PaLM和LLaMA。大語言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;是一種基于深度學習的自然語言處理模型&#xff0c;能…

電力行業中分布式能源管理(Distributed Energy Management System, DEMS)的實現

以下是電力行業中分布式能源管理(Distributed Energy Management System, DEMS)的實現方案,涵蓋系統架構、關鍵技術、核心功能及實施路徑,結合典型場景與代碼示例: 一、系統架構設計 采用云-邊-端三層架構,實現分布式能源的高效協同管理: 1. 終端層(感知層) 設備組…

實驗5 邏輯回歸

實驗5 邏輯回歸 【實驗目的】掌握邏輯回歸算法 【實驗內容】處理樣本&#xff0c;使用邏輯回歸算法進行參數估計&#xff0c;并畫出分類邊界 【實驗要求】寫明實驗步驟&#xff0c;必要時補充截圖 1、參照“2.1梯度下降法實現線性邏輯回歸.ipynb”和“2.2 sklearn實現線性邏輯…

思維訓練讓你更高、更強 |【邏輯思維能力】「刷題訓練筆記」假設法模式邏輯訓練題(1-5)

每日一刷 思維訓練讓你更高、更強&#xff01; 題目1 誰在說謊&#xff0c;誰拿走了零錢&#xff1f; 姐姐上街買菜回來后&#xff0c;就隨手把手里的一些零錢放在了抽屜里&#xff0c;可是&#xff0c;等姐姐下午再去拿錢買菜的時候發現抽屜里的零錢沒有了&#xff0c;于是&…

【愚公系列】《高效使用DeepSeek》004-DeepSeek的產品形態和功能詳解

標題詳情作者簡介愚公搬代碼頭銜華為云特約編輯,華為云云享專家,華為開發者專家,華為產品云測專家,CSDN博客專家,CSDN商業化專家,阿里云專家博主,阿里云簽約作者,騰訊云優秀博主,騰訊云內容共創官,掘金優秀博主,亞馬遜技領云博主,51CTO博客專家等。近期榮譽2022年度…