当前位置: 首页 > news >正文

【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.源码及网址

这里给出项目源码以及访问网址:

项目源码

校园公告搜索引擎


以上就是【校园公告搜索引擎】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力! 

相关文章:

【C++项目实战】校园公告搜索引擎:完整实现与优化指南

&#x1f3ac; 个人主页&#xff1a;谁在夜里看海. &#x1f4d6; 个人专栏&#xff1a;《C系列》《Linux系列》《算法系列》 ⛰️ 道阻且长&#xff0c;行则将至 目录 &#x1f4da;一、项目概述 &#x1f4d6;1.项目背景 &#x1f4d6;2.主要功能 &#x1f4d6;3.界面展…...

C语言每日一练——day_8

引言 针对初学者&#xff0c;每日练习几个题&#xff0c;快速上手C语言。第八天。&#xff08;连续更新中&#xff09; 采用在线OJ的形式 什么是在线OJ&#xff1f; 在线判题系统&#xff08;英语&#xff1a;Online Judge&#xff0c;缩写OJ&#xff09;是一种在编程竞赛中用…...

单片机农业大棚浇花系统

标题:单片机农业大棚浇花系统 内容:1.摘要 本文针对传统农业大棚浇花方式效率低、精准度差的问题&#xff0c;提出了一种基于单片机的农业大棚浇花系统。该系统以单片机为核心控制器&#xff0c;通过土壤湿度传感器实时采集土壤湿度数据&#xff0c;并将数据传输至单片机进行处…...

Kubernetes 单节点集群搭建

Kubernetes 单节点集群搭建教程 本人尝试基于Ubuntu搭建一个单节点K8S集群&#xff0c;其中遇到各种问题&#xff0c;最大的问题就是网络&#xff0c;各种镜像源下载不下来&#xff0c;特此记录&#xff01;注意&#xff1a;文中使用了几个镜像&#xff0c;将看来可能失效导致安…...

windows安装两个或多个JDK,并实现自由切换

我用两个JDK来做演示&#xff0c;分别是JDK8和JDK17(本人已安装JDK8&#xff0c;所以这里只演示JDK17的安装)。 1、下载JDK17安装 Java Downloads | Oracle 2、安装JDK17,这里忽略。直接双击软件&#xff0c;点击下一步就可以。 3、配置环境变量 在系统变量中新建一个CLASSP…...

如何打包数据库mysql数据,并上传到虚拟机上进行部署?

1.连接数据库&#xff0c;使得我们能看到数据库信息&#xff0c;才能进行打包上传 2. 3. 导出结果如下&#xff0c;是xml文件 4.可以查询每个xml文件的属性&#xff0c;确保有大小&#xff0c;这样才是真实导出 5跟着黑马&#xff0c;新建文件夹&#xff0c;并且把对应的东西放…...

fastapi +angular​迷宫求解可跨域

说明&#xff1a;我计划使用fastapi angular&#xff0c;实现​迷宫路径生成与求解 后端功能包括&#xff1a; 1.FastAPI搭建RESTful接口。写两个接口&#xff0c; 1.1生成迷宫&#xff0c; 1.2求解路径 前端功能包括 1.根据给定的长宽值&#xff0c;生成迷宫 2.点击按钮&…...

CobaltStrike详细使用及Linux上线

1、工具准备 cs工具 将teamserver.zip放进服务端给必要文件增加可执行文件( 执行时会有提示 )服务端启动服务监听 sudo ./teamserver <IP地址> <密码> [c2配置文件]客户端直接连接即可端口默认&#xff1a;50050主机&#xff1a;服务端ip地址2、基础配置 启动监听…...

WSL2 Ubuntu安装GCC不同版本

WSL2 Ubuntu安装GCC不同版本 介绍安装gcc 7.1方法 1&#xff1a;通过源码编译安装 GCC 7.1步骤 1&#xff1a;安装编译依赖步骤 2&#xff1a;下载 GCC 7.1 源码步骤 3&#xff1a;配置和编译步骤 4&#xff1a;配置环境变量步骤 5&#xff1a;验证安装 方法 2&#xff1a;通过…...

WPF CommunityToolkit.MVVM库的简单使用

CommunityToolkit.MVVM 是 .NET 社区工具包中的一部分&#xff0c;它为实现 MVVM&#xff08;Model-View-ViewModel&#xff09;模式提供了一系列实用的特性和工具&#xff0c;能帮助开发者更高效地构建 WPF、UWP、MAUI 等应用程序。以下是关于它的详细使用介绍&#xff1a; 1…...

4个 Vue 路由实现的过程

大家好&#xff0c;我是大澈&#xff01;一个喜欢结交朋友、喜欢编程技术和科技前沿的老程序员&#x1f468;&#x1f3fb;‍&#x1f4bb;&#xff0c;关注我&#xff0c;科技未来或许我能帮到你&#xff01; Vue 路由相信朋友们用的都很熟了&#xff0c;但是你知道 Vue 路由…...

Compose 实践与探索十 —— 其他预先处理的 Modifier

1、PointerInputModifier PointerInputModifier 用于定制触摸&#xff08;包括手指、鼠标、悬浮&#xff09;反馈算法&#xff0c;实现手势识别。 1.1 基本用法 最简单的使用方式就是通过 Modifier.clickable() 响应点击事件&#xff1a; Box(Modifier.size(40.dp).backgro…...

基于Python的天气预报数据可视化分析系统-Flask+html

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.8数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 系统登录 可视化界面 天气地图 天气分析 历史天气 用户管理 摘要 本文介绍了基于大数据…...

“消失的中断“

“消失的中断” 1. 前言 在嵌入式开发过程中&#xff0c;中断必不可少。道友们想必也经常因为中断问题头疼不已&#xff0c;今天来说说一个很常见的问题&#xff0c;“消失的中断”。最近项目在使用第三方MCAL的时候&#xff0c;就遇到了I2C中断丢失的问题&#xff0c;排查起…...

对C++面向对象的理解

C的面向对象编程&#xff08;OOP&#xff09;是其核心特性之一&#xff0c;通过类&#xff08;Class&#xff09;和对象&#xff08;Object&#xff09;实现数据和行为的封装&#xff0c;支持继承、多态和抽象等核心概念。以下是关键点解析&#xff1a; 1. 类&#xff08;Class…...

代码随想录-训练营-day52

97. 小明逛公园 (kamacoder.com) #include<iostream> #include<vector> using namespace std; int main(){int n,m,u,v,w;cin>>n>>m;vector<vector<vector<int>>> grid(n1,vector<vector<int>>(n1,vector<int>(n1…...

Java File 类详解

1. 概述 File 类是 Java 提供的用于文件和目录路径名的抽象表示。它能够用于创建、删除、查询文件和目录的信息&#xff0c;但不用于读写文件内容。如果需要对文件进行读写&#xff0c;可以结合 FileReader、FileWriter、BufferedReader 等类来完成。 2. File 类的构造方法 …...

JS实现省份地级市的选择

JS实现省份地级市的选择 效果展示&#xff1a; 代码实现 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><ti…...

【鸿蒙开发】Hi3861学习笔记-Visual Studio Code安装(New)

00. 目录 文章目录 00. 目录01. Visual Studio Code概述02. Visual Studio Code下载03. Visual Studio Code安装04. Visual Studio Code插件05. 附录 01. Visual Studio Code概述 vscode是一种简化且高效的代码编辑器&#xff0c;同时支持诸如调试&#xff0c;任务执行和版本管…...

记录致远OA服务器硬盘升级过程

前言 日常使用中OA系统突然卡死&#xff0c;刷新访问进不去系统&#xff0c;ping服务器地址正常&#xff0c;立马登录服务器检查&#xff0c;一看磁盘爆了。 我大脑直接萎缩了&#xff0c;谁家OA系统配400G的空间啊&#xff0c;过我手的服务器没有50也是30台&#xff0c;还是…...

计算机网络-网络规划与设计

基本流程 需求分析—》通信规范分析—》逻辑网络设计—》物理网络设计—》实施阶段 需求分析&#xff1a; 确定需求&#xff0c;包括&#xff1a;业务需求、用户需求、应用需求、计算机平台需求、网络通信需求等。 产物&#xff1a;需求规范 通信规范分析&#xff1a; 现有…...

C#opencv 遍历图像中所有点 不在圆范围内的点变为黑色,在圆范围内的保持原色

C#opencv 遍历图像中所有点 不在圆范围内的点变为黑色,在圆范围内的保持原色 安装 Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.Windows 普通实现 using System; using System.Collections.Generic; using System.Linq; using OpenCvSharp; // 添加OpenCV引用…...

精通游戏测试笔记(持续更新)

第一章、游戏测试的两条规则 不要恐慌 不要将这次发布当作最后一次发布 不要相信任何人 把每次发布当作最后一次发布 第二章&#xff1a;成为一名游戏测试工程师...

Linux内核,mmap_pgoff在mmap.c的实现

1. mmap_pgoff的系统调用实现如下 SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, pgoff) {return ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff); }2. ksys_mma…...

深度揭秘:蓝耘 Maas 平台如何重塑深度学习格局

目录 前言 深度学习&#xff1a;技术基石与发展脉络 蓝耘 Maas 平台&#xff1a;深度学习的强大助推器 1. 高性能算力支撑 2. 丰富的模型支持 3. 便捷的开发体验 4. 完善的安全保障 代码示例&#xff1a;蓝耘 Maas 平台上的深度学习实践 1. 注册与登录 2. 代码实现 …...

深入解析操作系统进程控制:从地址空间到实战应用

引言 想象这样一个场景&#xff1a; 你的游戏本同时运行着《赛博朋克2077》、Chrome浏览器和Discord语音 突然游戏崩溃&#xff0c;但其他应用依然正常运行 此时你打开任务管理器&#xff0c;发现游戏进程已经消失&#xff0c;但内存占用却未完全释放 这背后涉及的关键机制…...

网络空间安全(33)MSF漏洞利用

前言 Metasploit Framework&#xff08;简称MSF&#xff09;是一款功能强大的开源安全漏洞利用和测试工具&#xff0c;广泛应用于渗透测试中。MSF提供了丰富的漏洞利用模块&#xff0c;允许安全研究人员和渗透测试人员利用目标系统中的已知漏洞进行攻击。 一、漏洞利用模块&…...

《Electron 学习之旅:从入门到实践》

前言 Electron 简介 Electron 是由 GitHub 开发的一个开源框架&#xff0c;基于 Chromium 和 Node.js。 它允许开发者使用 Web 技术&#xff08;HTML、CSS、JavaScript&#xff09;构建跨平台的桌面应用程序。 Electron 的优势 跨平台&#xff1a;支持 Windows、macOS 和 Linux…...

通达信软件+条件选股+code

在通达信软件中,你的选股公式需要放在 "公式管理器" 的 "条件选股公式" 分类中。以下是详细操作步骤: 一、打开公式管理器 打开通达信软件,按快捷键 Ctrl + F (或点击顶部菜单栏:"公式" → "公式管理器") 二、创建新公式 选择分…...

【2025】基于springboot+vue的汽车销售试驾平台(源码、万字文档、图文修改、调试答疑)

基于 Spring Boot Vue 的汽车销售试驾平台通过整合前后端技术&#xff0c;实现了汽车销售和试驾预约的信息化和智能化。系统为管理员和用户提供了丰富的功能&#xff0c;提升了客户体验和销售效率&#xff0c;增强了数据分析能力&#xff0c;为汽车销售行业的发展提供了新的途…...

Spring Web MVC入门

一、什么是SpringMVC 首先&#xff0c;MVC是一种架构设计模式&#xff0c;也是一种思想&#xff0c;而SpringMVC是对MVC思想的具体实现&#xff0c;除此之外&#xff0c;SpringMVC还是一个Web框架。 总的来说&#xff0c;SpringMVC就是一个实现MVC模式的Web框架。 而MVC可以…...

5G核心网实训室搭建方案:轻量化部署与虚拟化实践

5G核心网实训室 随着5G技术的广泛应用&#xff0c;行业对于5G核心网人才的需求日益增长。高校、科研机构和企业纷纷建立5G实训室&#xff0c;以促进人才培养、技术创新和行业应用研究。IPLOOK凭借其在5G核心网领域的深厚积累&#xff0c;提供了一套高效、灵活的5G实训室搭建方…...

IMX6ULL学习整理篇——Linux驱动开发的基础2 老框架的一次实战:LED驱动

IMX6ULL学习整理篇——Linux驱动开发的基础2 老框架的一次实战&#xff1a;LED驱动 ​ 在上一篇博客中&#xff0c;我们实现了从0开始搭建的字符设备驱动框架&#xff0c;但是这个框架还是空中楼阁&#xff0c;没有应用&#xff0c;很难说明我们框架的正确性。这里&#xff0c…...

网络空间安全(32)Kali MSF基本介绍

前言 Metasploit Framework&#xff08;简称MSF&#xff09;是一款功能强大的开源安全漏洞检测工具&#xff0c;被广泛应用于渗透测试中。它内置了数千个已知的软件漏洞&#xff0c;并持续更新以应对新兴的安全威胁。MSF不仅限于漏洞利用&#xff0c;还包括信息收集、漏洞探测和…...

零基础上手Python数据分析 (3):Python核心语法快速入门 (下) - 程序流程控制、函数与模块

写在前面 还记得上周我们学习的 Python 基本数据类型、运算符和变量吗? 掌握了这些基础知识,我们已经能够进行一些简单的数据操作了。 但是,在实际的数据分析工作中,仅仅掌握基本语法是远远不够的。 我们需要让程序能够 根据条件做出判断,重复执行某些操作,组织和复用代…...

C++【类和对象】(超详细!!!)

C【类和对象】 1.运算符重载2.赋值运算符重载3.日期类的实现 1.运算符重载 (1).C规定类类型运算符使用时&#xff0c;必须转换成调用运算符重载。 (2).运算符重载是具有特殊名字的函数&#xff0c;名字等于operator加需要使用的运算符&#xff0c;具有返回类型和参数列表及函数…...

Windows-PyQt5安装+PyCharm配置QtDesigner + QtUIC

个人环境 Windows 11 pycharm 2024.2 Anaconda2024.6python 3.9 1)先使用pip命令在线安装 1)pip install PyQt5 2)pip install PyQt5-tools2)配置环境变量 1&#xff1a;安装成功后可以在python的安装目录Lib\site-packahes目录下看到安装包。比如我的路径是E:\anaconda3…...

qq音乐 webpack 补环境

网址&#xff1a; aHR0cHM6Ly95LnFxLmNvbS9uL3J5cXEvcGxheWVy​ 1.接口分析 接口&#xff1a;cgi-bin/musics.fcg​ 参数&#xff1a;sign是加密的 2.代码分析 进入调用栈 先在send位置打上断点&#xff0c;页面刷新 往上一个栈找 ‍ 可以看到上面就有一个关键词sign​是从…...

【蓝桥杯】省赛:神奇闹钟

思路 python做这题很简单&#xff0c;灵活用datetime库即可 code import os import sys# 请在此输入您的代码 import datetimestart datetime.datetime(1970,1,1,0,0,0) for _ in range(int(input())):ls input().split()end datetime.datetime.strptime(ls[0]ls[1],&quo…...

计算机的结构形式

微机的机构形式 台式个人微机 最开始的微机&#xff08;计算机&#xff09;都是台式的&#xff0c;到目前为止仍是个人微机的主要形式。台式机按照电脑机箱的放置形式&#xff0c;分为卧式和立式两种。台式机需要放在桌面上或者留有专门放置机箱位置&#xff0c;他的主机、键…...

C语言【内存函数】详解

目录&#xff1a; 1. memcpy使用和模拟实现 2. memmove使用和模拟实现 3. memset函数的使用 4. memcmp函数的使用 以上函数均包含在一个头文件<string.h>里面 一、memcpy的使用和模拟实现。 memcpy函数介绍&#xff1a; 函数原型&#xff1a; void * memcpy ( void…...

软考网络安全专业

随着信息技术的迅猛发展&#xff0c;网络安全问题日益凸显&#xff0c;成为社会各界普遍关注的焦点。在这样的背景下&#xff0c;软考网络安全专业应运而生&#xff0c;为培养高素质的网络安全人才提供了有力支撑。本文将对软考网络安全专业进行深入剖析&#xff0c;探讨其在信…...

Altium Designer——CHIP类元器件PCB封装绘制

文章目录 PCB封装组成元素&#xff1a;焊盘的属性 SS34肖特基二极管SMA(DO-214AC)封装绘制资料&#xff1a;步骤&#xff1a;1.绘制焊盘&#xff1a;用到的快捷键&#xff1a;资料&#xff1a; 2.绘制丝印&#xff1a;用到的快捷键&#xff1a;资料&#xff1a; PCB封装组成元素…...

C++ unordered_map unordered_set 模拟实现

1. 关于unordered_map 和 unordered_set 区别于C的另外两个容器map和set&#xff0c;map和set的底层是红黑树&#xff1b;而unordered_map和unordered_set的底层是哈希 因为unordered_map和unordered_set的底层是哈希&#xff0c;因此他们存储的数据是没有顺序​​unordered​…...

Java使用自定义类加载器实现插件动态加载

虚拟机类加载子系统 Java虚拟机的⼀个重要子系统&#xff0c;主要负责将类的字节码加载到JVM内存的⽅法区&#xff0c;并将其转换为JVM内部的数据结构。 一个类从被加载到虚拟机开始&#xff0c;一直到卸载出内存为止&#xff0c;会经历七个阶段&#xff1a;加载&#xff0c;…...

【初级篇】如何使用DeepSeek和Dify构建高效的企业级智能客服系统

在当今数字化时代,企业面临着日益增长的客户服务需求。使用Dify创建智能客服不仅能够提升客户体验,还能显著提高企业的运营效率。关于DIfy的安装部署,大家可以参考之前的文章: 【入门级篇】Dify安装+DeepSeek模型配置保姆级教程_mindie dify deepseek-CSDN博客 AI智能客服…...

Java开发之数据库应用:记一次医疗系统数据库迁移引发的异常:从MySQL到PostgreSQL的“dual“表陷阱与突围之路

记一次医疗系统数据库迁移引发的异常&#xff1a;从MySQL到PostgreSQL的"dual"表陷阱与突围之路 一、惊魂时刻&#xff1a;数据库切换引发的系统雪崩 某医疗影像系统在进行国产化改造过程中&#xff0c;将原MySQL数据库迁移至PostgreSQL。迁移完成后&#xff0c;系…...

Langchian构建代理

文章目录 概要ReAct 代理 ReAct 使用ReAct基本用法提示词模板内存使用迭代使用返回执行每一步情况限制输出行数设置运行超时时间 不使用代理下LLM如何结合工具案例案例2 概要 单靠语言模型无法采取行动 - 它们只输出文本。 LangChain 的一个重要用例是创建 代理。 代理是使用大…...

Vim软件使用技巧

目录 Demo Vim怎么看一个文件的行号&#xff0c;不用打开文件的前提下&#xff1f;进入文件后怎么跳转到某一行? 不打开文件查看行号&#xff08;查看文件的方法&#xff09; 方法1、使用命令行工具统计行数 方法2、通过vim的 - 参数查看文件信息 进入文件后跳转到指定行…...

SQL与NoSQL的区别

以下是SQL与NoSQL数据库的详细对比&#xff0c;涵盖核心特性、适用场景及技术选型建议&#xff1a; 一、核心区别对比 特性SQL&#xff08;关系型数据库&#xff09;NoSQL&#xff08;非关系型数据库&#xff09;数据模型基于表格&#xff0c;严格预定义模式&#xff08;Schem…...