C++实现网站内搜索功能

news2024/9/28 3:27:16

文章目录

  • 搜索结果的结构
  • 下载我们需要的数据
  • 分析html结构
  • 数据处理
    • 去标签之标题
    • 去标签之正文内容
    • 构造url
    • 把上述的数据清理操作对每一个文件都做一遍
    • 把处理好的数据都保存到一个.bin文件
  • 构建正排索引
  • 构建倒排索引
    • 使用cpp-jieba分词
    • 计算每个文档中的每个词的权重
    • 对所有文档都进行上述的建立正排与倒排索引操作
  • Search模块
  • http server模块
    • cpp-httplib
    • 拿参,并且调用我们的search返回我们的json串
  • 最终结果

总体逻辑如下
在这里插入图片描述
几个关键问题统一写在这:

  1. 这个是站内搜索,所以不涉及爬虫。都是处理本地下载好的html数据
  2. 搜索逻辑设计的极其简单。先从浏览器拿到关键字,然后服务器上得到需要返回的文档ID,再把对应ID的文档内容以JSON的形式返回给浏览器
  3. 在判断哪些文档是最匹配用户输入的关键字的需求上,也是用的简单的逻辑。通过统计文档中出现的关键字次数,并根据关键字出现在不同地方,乘一个不同的系数来当成一个权值。权值越大,认为这个文档和关键字越匹配
  4. 正排索引和倒排索引的概念就是根据第二点提出的。
    在这里插入图片描述
    正排索引
    在这里插入图片描述
    倒排索引
    在这里插入图片描述






搜索结果的结构

在这里插入图片描述

  1. 网站的url
  2. 标题
  3. 一段随机的简介

这样我们就可以通过搜索关键字找到需要的页面的网址了

下载我们需要的数据

下面是boost官网提供的boost文档,里面都是大量的html
在这里插入图片描述



下载解压后是这样子的
在这里插入图片描述
因此我们要返回的结果都是通过解析这部分html来返回的。

分析html结构

在这里插入图片描述

由于我只需要title和其余的content,因此我们首先要做的是去标签工作,提取其中的标题和内容


数据处理

去标签之标题

如何拿到标题是什么?

思路:找到< title >和< /title >标签,里面的那段字符串就是标题



实现的基本思路就是使用find函数和substr函数,比较简单。
关于DocContent是什么后面再补充

void ParseTitle(const std::string &s, DocContent &d)
{
    int begin = 0, end = 0;
    begin = s.find("<title>");
    begin += std::string("<title>").size();
    end = s.find("</title");
    if (end < begin)
        return;
    d._title = s.substr(begin, end - begin);
}


去标签之正文内容

使用状态变化来编写代码的写法会比较简单

  1. 一开始时处于LABEL状态,如果是标签状态,继续读下一个字符,直到读到字符’>",要转换成CONTENT状态
  2. 如果处于CONTENT状态,那么把每一个字符都保存下来,如果读到一个换行符’\n’,那么把它换成空格来存下来。原因是为了方便后面使用一个getline就可以把整一个文档给读进来
void ParseContent(const std::string &s, DocContent &d)
{
    enum status
    {
        LABLE,
        CONTENT
    };
    status st = LABLE;
    for (char c : s)
    {
        switch (st)
        {
        case LABLE:
            if (c == '>')
            {
                st = CONTENT;
            }
            break;
        case CONTENT:
            if (c == '<')
            {
                st = LABLE;
            }
            else
            {
                if (c == '\n')
                    c = ' ';
                d._content += c;
            }
            break;
        default:
            break;
        }
    }
}

构造url

由于做的是站内搜索,所以url的构建是基于主网站的。


在这里插入图片描述

可以看到boost官网的文档都是在框框中的网址里的,所以根据这个网址,再加上每个文件的文件名,就可以构建出一个url了



本质就是基网址+文件名
在这里插入图片描述

把上述的数据清理操作对每一个文件都做一遍

逻辑就是遍历存放数据的文件夹,把所有后缀是.html的文件都做一遍数据清理即可。



遍历文件这个操作需要用到boost库里的一个函数(或者用c++17的语法),具体用法参考
std::filesystem::recursive_directory_iterator

void GetFileName(const std::string &src_path, std::vector<std::string> &fileName)
{
    namespace fs = boost::filesystem;
    fs::path root_path(src_path);
    fs::recursive_directory_iterator end; // 递归遍历
    for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
    {
        if (iter->path().extension() != ".html")
        {
            continue;
        }
        fileName.push_back(iter->path().string());
    }
}

void ReadContent(const std::vector<std::string> &fileName, std::vector<DocContent> &fileContent)
{
    for (const std::string s : fileName)
    {
        std::string result;
        FileUtils::ReadFile(s, result);
        DocContent d;
        ParseTitle(result, d);
        ParseContent(result, d);
        ParseUrl(s, d);
        fileContent.push_back(std::move(d));
    }
};




上述两个函数就是用于遍历和数据清理的。这里讲一下一个c++11的语法:移动,下面的函数的std::move就是用了移动语法的一个函数

void ReadContent(const std::vector<std::string> &fileName, std::vector<DocContent> &fileContent)
{
    for (const std::string s : fileName)
    {
		...
        DocContent d;
        ...
        fileContent.push_back(std::move(d));
    }
};

本来d是一个临时变量,如果要push_back到一个vector,就要发生一次拷贝。这样开销就会比较大,因为d里面的字符串长度还是挺长的。



如果使用move函数,就可以让这个这个临时变量的内存空间的所有权转让给vector使用,这样就不需要拷贝了。

把处理好的数据都保存到一个.bin文件

读写文件用的是< fstream >中的ifstream和ofstream,具体参考链接
ifstream
ofstream


void SaveContent(const std::vector<DocContent> &fileContent)
{
    const std::string dst = "/home/mhq/boost_searcher/data/processed_data/process.txt";
    std::ofstream out(dst, std::ios::out | std::ios::binary);
    std::string s;
    for (auto &e : fileContent)
    {
        s += e._title;
        s += '\3';
        s += e._content;
        s += '\3';
        s += e._url;
        s += '\n';
    }
    out << s;
};

这里我们采用了一个策略,每一个文档之间用\n来分割,文档内部的元素之间用\3来分割。

使用\n分割的原因:getline可以一次性把换行符之前的东西读入,这样我们每一次getline都可以读完一整个完整的文档

使用\3来分隔文档内部的原因:\3在ASCII码里是一个控制字符,普通html里不可能出现这个字符,所以加入\3不会影响文档内容原来的正确性

构建正排索引

正排索引就是给每个文档编号,这样我们就可以通过ID来找到对应的文档内容。单独的正排索引没什么用,它的作用是用来构建倒排索引。



索引其实就是一个vector,每一个元素是一个ForwardElem

class ForwardElem
{
public:
    DocContent _doc;//文档内容
    int _doc_id;
    ForwardElem() = default;
};
std::vector<ForwardElem> _forward_index; // 正排索引




我们之前已经把所有文档的去标签后的结果保存到一个文件里了,现在我们每一次getline都可以读出一整个文档的内容。并且在文档内加入了\3的分隔符。因此我们可以通过\3来把更具体的信息再挖掘出来。即标题,内容,url。


因此思路就是:

  1. 用\3分割字符串,构造一个文档内容
  2. 把这个文档内容放入正排索引的vector中
  3. 给这个文档内容编号

编号的逻辑很简单,vector里面有多少个元素,该文档的id就是多少

ForwardElem *BuildForwardIndex(const std::string &file)
    {
        std::vector<std::string> subline;
        CutString(file, subline, '\3');
        ForwardElem t;
        t._doc = DocContent(subline[0], subline[1], subline[2]);
        if (subline.size() != 3)
        {
            std::cout << "建立ForwardIndex失败,具体原因是分词失败" << std::endl;
            return nullptr;
        }
        t._doc_id = _forward_index.size();
        _forward_index.push_back(std::move(t));
        return &_forward_index.back();
    }

关于CutString这个函数怎么实现,可以使用boost库里面的split函数
下面这个写法是网上copy的,照着写即可

void CutString(std::string line, std::vector<std::string> &subline, char a)
{
    boost::split(subline, line, boost::is_any_of("\3"), boost::token_compress_on);
}

构建倒排索引

倒排索引是通过关键词,我们可以返回具体的文档ID。因此我们要建立关键词和文档ID的关系。在一开始的关键问题已经说了,关系紧密的定义我们用关键字在文档中出现的次数和位置来衡量。 因此我们现在要开始分词了。只有先分词,我们才能知道关键字在文档中是否出现,出现几次等问题。

使用cpp-jieba分词

cppjieba安装和使用
这个库的使用有点小坑,得看这篇文章,不然无法正常编译过去

安装好之后作者会提供给你一个demo,把demo的代码复制过来就可以用了。
如下:

class Jieba
{
public:
    static cppjieba::Jieba jieba;
    static void CutString(const std::string &src, std::vector<std::string> &words);
};

cppjieba::Jieba Jieba::jieba(DICT_PATH,
                             HMM_PATH,
                             USER_DICT_PATH,
                             IDF_PATH,
                             STOP_WORD_PATH);

void Jieba::CutString(const std::string &s, std::vector<std::string> &words)
{
    jieba.CutForSearch(s, words);
}

计算每个文档中的每个词的权重

逻辑如下:

  1. 对文档中的标题,正文进行分词
  2. 统计标题的词频和正文的词频(哈希)
  3. weight = title_cnt * 10 + content_cnt



有一个点:对于每个词来讲,我们不需要区分大小写,boost库里面有一个函数可以把字符串都变成小写

void BuildInvertedIndex(const ForwardElem &ForwardElem)
    {
        std::vector<std::string> titleWords, contentWords;
        Jieba::CutString(ForwardElem._doc._title, titleWords);
        Jieba::CutString(ForwardElem._doc._content, contentWords);

        std::unordered_map<std::string, Cnt> wordCnt;

        for (auto &e : titleWords)
        {
            boost::to_lower(e);
            wordCnt[e]._title_cnt++;
        }

        for (auto &e : contentWords)
        {
            boost::to_lower(e);
            wordCnt[e]._content_cnt++;
        }

        for (auto &e : wordCnt)
        {
            InvertedElem invertedElem;
            invertedElem._doc_id = ForwardElem._doc_id;
            invertedElem._word = e.first;
            invertedElem._weight = e.second._title_cnt * 10 + e.second._content_cnt;
            _inverted_index[e.first].push_back(std::move(invertedElem));
        }
    }



对所有文档都进行上述的建立正排与倒排索引操作

逻辑:遍历每一个文档即可,之前已经用换行符对每一个文档进行了分割。因此现在每次getline都是一个文档。

void BuildIndex(const std::string &file)
{
    std::ifstream in(file, std::ios::in | std::ios::binary);
    std::string line;
    while (getline(in, line))
    {
        // 每一个换行符前都是整个文件
        ForwardElem *forwardElem = BuildForwardIndex(line);
        BuildInvertedIndex(*forwardElem);
    }
    std::cout << "索引已经建立完毕" << std::endl;
}

Search模块

到了这一步时数据准备部分已经完成了,现在要做的是返回数据的逻辑

逻辑如下:

  1. 先拿到用户提供的关键字,然后用jieba进行分词,拿到具体的关键字
  2. 用关键字去查倒排索引,得到要返回的文档的ID,并保存下它们的InvertedElem(里面存着weight,后面要根据weight来排序)
  3. 根据weight来排序InvertedElem
  4. 把前x个InvertedElem保存在Json串里
  5. 返回Json串
void Search(const std::string &key, std::string *json_reply)
{
    std::vector<std::string> words;
    Jieba::CutString(key, words);

    std::vector<InvertedElem> all;
    for (auto &e : words)
    {
        std::vector<InvertedElem> *t = index->GetInvertedElem(e);
        if (!t)
        {
            continue;
        }
        for (auto &e : *t)
        {
            all.push_back(e);
        }
    }
    std::sort(all.begin(), all.end(), [](const InvertedElem &e1, const InvertedElem &e2)
              { return e1._weight > e2._weight; });
    all = std::vector<InvertedElem>(all.begin(), all.begin() + 10);
    Json::Value root;
    for (auto &e : all)
    {
        ForwardElem *forwardElem = index->GetForwardElem(e._doc_id);
        if (forwardElem == nullptr)
            continue;
        Json::Value elem;
        DocContent docContent = forwardElem->_doc;
        elem["title"] = docContent._title;
        elem["content"] = GetDesc(docContent._content, e._word);
        elem["url"] = docContent._url;
        elem["weight"] = e._weight;
        elem["id"] = e._doc_id;

        root.append(elem);
        Json::StyledWriter writer;
        *json_reply = writer.write(root);
    }
}

关于jsoncpp的使用,参考这个链接
jsoncpp下载和简单使用
我这里直接用了apt install的方式下载jsoncpp,比较简答

http server模块

cpp-httplib

cpp-httplib的github
我使用的版本比较老,是0.7.15版本。文档内有说明如何调用接口
在这里插入图片描述

拿参,并且调用我们的search返回我们的json串

关于如何使用cpp-httplib看是否有参数并且拿到参数,可以参考文档中的这个demo
在这里插入图片描述

因此我们只需要编写这个逻辑即可

  1. 准备好数据部分,启动searcher
  2. 拿参
  3. 把参数放入search函数中,得到返回的json
  4. 把json返回给用户
Searcher search;
search.InitSearcher(datas);
httplib::Server svr;

svr.Get("/s", [&](const httplib::Request &request, httplib::Response &response)
        {
		    if (!request.has_param("word"))
		    {
		        response.set_content("请输入搜索内容", "text/plain: charset=utf-8");
		        return;
		    }
		    string word = request.get_param_value("word");
		    string json_str;
		    search.Search(word, &json_str);
		    response.set_content(json_str, "application/json"); 
    	});

svr.listen("0.0.0.0", 8081);

最终结果

可以成功返回结果,后序可以编写前端,完善界面
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1363952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

格密码基础:光滑参数

目录 一. 铺垫高斯函数 二. 光滑参数图形理解 三. 光滑参数与格基本区 3.1 高斯与均匀分布的统计距离 3.2 光滑参数理解 四. 光滑参数与最短向量 五. 光滑参数与连续最小值 六. 光滑参数与对偶格的上界 七. 光滑参数与格的上界 八. 小结 一. 铺垫高斯函数 定义高斯密…

Django 9 常用通用视图分析

View 提供基于不同http方法执行不同逻辑的功能。 1. 创建 terminal输入 django-admin startapp the_13回车 2.tutorial子文件夹 settings.py注册一下 INSTALLED_APPS [django.contrib.admin,django.contrib.auth,django.contrib.contenttypes,django.contrib.sessions,dja…

九州金榜|孩子步入叛逆期,常常离家出走怎么办?

孩子在拥有了独立意识后&#xff0c;就开始试图挑战父母的权威。他们会主动去质疑父母&#xff0c;主动去证明自己的成熟和独立&#xff0c;还会主动试图逃离父母的控制范围。 近日就收到了家长求助孩子离家出走问题的私信&#xff0c;在得到家长同意&#xff0c;接下来我们就…

Docker-Compose部署Redis(v7.2)分片集群(含主从)

文章目录 一、前提准备1. 文件夹结构 二、配置文件1. redis.conf2. docker-compose文件 三、构建集群1. 自动分配主从关系2.1 构建3 master集群2.2 手动配置从节点 四、测试1. 集群结构2. 分片测试 环境 docker desktop for windows 4.23.0redis 7.2 目标 搭建如下图分片主从…

利用ArcGIS探究环境与生态因子对水体、土壤、大气污染物等影响的实践技术

如何利用ArcGIS实现电子地图可视化表达&#xff1f;如何利用ArcGIS分析空间数据&#xff1f;如何利用ArcGIS提升SCI论文的层次&#xff1f;制图是地理数据展现的直观形式&#xff0c;也是地理数据应用的必要基础。本次课程从ArcGIS的基本操作、ArcGIS 的空间数据分析及ArcGIS 的…

算法基础之货仓选址

货仓选址 核心思想&#xff1a; 贪心 绝对值不等式 : ∣ x – a ∣ ∣ x – b ∣ ≥ ∣ a – b ∣ |x – a| |x – b| ≥ |a – b| ∣x–a∣∣x–b∣≥∣a–b∣ 将n个数两两分组 1~~ n-1 (奇数会剩一个) 分别用绝对值不等式 即可推出来 货仓位置应该在中位数上(奇数) 或在中…

鸿蒙应用开发 闹钟实现

后台代理提醒简介 随着生活节奏的加快&#xff0c;我们有时会忘记一些重要的事情或日子&#xff0c;所以提醒功能必不可少。应用可能需要在指定的时刻&#xff0c;向用户发送一些业务提醒通知。例如购物类应用&#xff0c;希望在指定时间点提醒用户有优惠活动。为满足此类业务…

C# Unity将地形(Terrain)导出成obj文件

C# Unity将地形(Terrain)导出成obj文件 从其他地方搬运过来的&#xff0c;只能到出obj模型&#xff0c;不能导出贴图 using System.IO; using System.Text; using UnityEditor; using UnityEngine; using System;enum SaveFormat { Triangles, Quads } enum SaveResolution {…

Landsat8的辐射定标与大气校正

目录 打开影像辐射定标大气校正计算区域高程计算研究区高程大气校正查看处理结果 打开影像 在文件夹中找到xxx_MTL.txt文件&#xff0c;拖到ENVI中 此处可能会出现无法打开的问题&#xff0c;参考该文章&#xff08;ENVI无法打开Landsat8的头文件问题和解决&#xff09; 辐…

Iceberg从入门到精通系列之十九:分区

Iceberg从入门到精通系列之十九&#xff1a;分区 一、认识分区二、Iceberg的分区三、Hive 中的分区四、Hive 分区问题五、Iceberg的隐藏分区六、分区变换七、分区变换 一、认识分区 分区是一种通过在写入时将相似的行分组在一起来加快查询速度的方法。 例如&#xff0c;从日志…

C#,冒泡排序算法(Bubble Sort)的源代码与数据可视化

排序算法是编程的基础。 常见的四种排序算法是&#xff1a;简单选择排序、冒泡排序、插入排序和快速排序。其中的快速排序的优势明显&#xff0c;一般使用递归方式实现&#xff0c;但遇到数据量大的情况则无法适用。实际工程中一般使用“非递归”方式实现。本文搜集发布四种算法…

【WPF】使用 WriteableBitmap 提升 Image 性能

【WPF】使用 WriteableBitmap 提升 Image 性能 前言WriteableBitmap 背景WriteableBitmap 渲染原理WriteableBitmap 使用技巧案例核心源码测试结果 前言 由于中所周不知的原因&#xff0c;WPF 中想要快速的更新图像的显示速率一直以来都是一大难题。在本文中&#xff0c;我将分…

leetcode“位运算”——只出现一次的数字

只出现一次的数字i&#xff1a; https://leetcode.cn/problems/single-number/ 给你一个非空整数数组 nums&#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现一次的元素。 class Solution { public:int singleNumber(vector<i…

九州金榜|孩子厌学,作为父母有想做自己的原因吗?

孩子不会天生就厌学&#xff0c;如果孩子天生厌学&#xff0c;那么孩子就不可能学会说话&#xff0c;走路&#xff0c;日常生活&#xff0c;更不可能去上学&#xff0c;孩子厌学因素非常多&#xff0c;而作为父母&#xff0c;你有没有想过是你的原因造成的呢&#xff1f;九州金…

【深度学习:Domain Adversarial Neural Networks (DANN) 】领域对抗神经网络简介

【深度学习&#xff1a;Domain Adversarial Neural Networks】领域对抗神经网络简介 前言领域对抗神经网络DANN 模型架构DANN 训练流程DANN示例 GPT示例 前言 领域适应&#xff08;DA&#xff09;指的是当不同数据集的输入分布发生变化&#xff08;这种变化通常被称为共变量变…

Redis概览

Redis存储是Key-Value结构的数据&#xff0c;其中Key是字符串类型&#xff0c;Value有5种常见的数据类型 字符串 String 哈希 hash 列表 list 集合 set 有序集合 sorted set / zset 各种数据类型的特性 字符串操作命令 : ● SET ke…

解决Vue3 中Echarts数据更新渲染不上问题

解决办法就是让Dom节点重新渲染 定义一个变量 const postLoading ref(true); 请求数据前dom节点不渲染&#xff0c;获取完数据重新渲染

2024年1月阿里云服务器租用价格表_优惠活动大全

2024年1月最新阿里云服务器租用价格表&#xff0c;云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、轻量应用服务器2核2G3M带宽轻量服务器一年61元&#xff0c;2核4G4M带宽轻量服务器一年165元12个月、2核4G服务器30元3个月&#xff0c;云服务器ECS可以选择经济型e实例、通用…

静态网页设计——中医中药网(HTML+CSS+JavaScript)(dw、sublime Text、webstorm、HBuilder X)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a;https://www.bilibili.com/video/BV11e411i7g8/?vd_source5f425e0074a7f92921f53ab87712357b 源码&#xff1a;https://space.bilibili.com…

三种主流流协议的浏览器播放解决方案

三种主流流协议的浏览器播放解决方案 流协议介绍 主流的流协议&#xff08;streaming protocol&#xff09;包括HLS、RTMP、RTSP&#xff0c;下面依次介绍下三种视频流。 HLS HLS&#xff08;Http Live Streaming) 是一个由苹果公司提出的基于HTTP的流媒体网络传输协议&…