《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——HTML文件的清洗(上篇)》

前引:这是一个聚焦基础搜索引擎核心工作流的实操项目,基于 C/C++ 技术生态落地:从全网爬虫抓取网页资源,到服务器端完成 "去标签 - 数据清洗 - 索引构建" 的预处理,再通过 HTTP 服务接收客户端请求、检索索引并拼接结果页返回 ------ 完整覆盖了轻量级搜索引擎的端到端逻辑。项目采用 C++11、STL、Boost 等核心技术栈,搭配 CentOS 7 云服务器 + GCC 编译环境(或 VS 系列开发工具)部署,既适配后端工程的性能需求,也能通过可选的前端技术(HTML5/JS 等)优化用户交互,是理解搜索引擎底层原理与 C++ 工程实践的典型案例!

目录

【一】项目介绍

【二】技术栈与项目环境

【三】正排/倒排索引介绍

(1)正排索引

(2)倒排索引

【四】BOOST库获取与导入

【五】HTML文件的清洗

(1)主函数实现一

(2)主函数实现二

(3)主函数实现三


【一】项目介绍

本项目在于实现轻量化的"搜索引擎",什么是"搜索引擎"?比如百度,输入关键词,返回网页内容

这一整套的流程是什么?

项目流程:在你搜索关键词内容之前,服务器会使用爬虫搜全网网页的信息,去除标签等只保留"URL、摘要、包括一些图片",在内部通过建立索引来对每条信息建立编号------对混杂信息进行加工,根据用户的提示词获取一些的资源,再重建网页拼接关键词相关的信息内容,返回给用户!

【二】技术栈与项目环境

Centos相关的服务器可以实现TCP、HTTP的网络服务器搭建,替换为Ubuntu也可以!

【三】正排/倒排索引介绍

(1)正排索引

含义:对搜集到的网页内容建立ID(索引)

比如:

(2)倒排索引

含义:建立关键词集,关联对应的文档ID

例如:

【四】BOOST库获取与导入

搜索引擎需要文档内容作为知识库,即模拟爬虫获取的网页信息,但为了健康上网,我们采用BOOST提供的库作为爬取到的知识库,进行模拟,下面是BOOST库的获取与安装:

BOOST库下载地址:https://www.boost.org/releases/1.78.0/(下载1.78版本的)

选择Linux版的下载:

然后先安装 rz 工具:

复制代码
sudo apt update
sudo apt install -y lrzsz

再使用 rz - E 命令上传库:

然后对上传的库进行解压:

复制代码
tar -xzvf boost_1_78_0.tar.gz

可以看到当前.html后缀的文件有20992个:

**注意:**为什么不直接在boost_1_78_0(直接解包的目录)中筛选出.html目录?

因为大目录中包含小目录,而这个路径需要和官网的相同,比如A.html在1111111目录中,在官网的访问就是11111111/A.html,不应该省掉111111111,GET到了吗!

【五】HTML文件的清洗

获取到所有的HTML文件之后,我们需要对所有HTML文件进行清洗,获取结构化的字段:

文档标题、文本内容、该文档在官网中的URL

所以创建一个清洗干净的文件 data_clean(后面更名:data_clean.txt),来进行保存最终的数据

main函数主思路编写:

第一:我们可以先借助 vector<string> 存储每个.html文件的文件名和路径,方便读取

第二:对每个.html文件进行清洗解析

第三:把每个解析后的每个文件内容写入到 data_clean.txt 中

(其中:data_html 存储着所有的 .html 文件,最终数据结果放到 data_clean 中)

下面是主函数思路:

这里修改一下:给路径末尾加上 / ,文件不需要,更改为:

(1)主函数实现一

提取每个文件的文件名和路径,我们借助BOOST库来完成,所以我们需要安装BOOST库:

注意:那么编译的时候,需要指定库、头文件的路径、具体的库名称!

cpp 复制代码
sudo apt install libboost-all-dev

现在我们需要执行下面的操作(含顺序):(基本都是使用BOOST库文件接口)

首先我们的数据源是在 data_html 目录中

因此,在拿到这个目录中的每个文件及其路径之前,需要先判断该路径是否存在

然后采用迭代器遍历 data/data_html 这个目录中的文件,筛选出.html文件

然后将.html文件的路径及其文件名放在 vector<string>容器中,就完成了我们的第一个函数!

下面是演示:

代码:

cpp 复制代码
#include<vector>
#include<iostream>
#include<string>
#include <boost/filesystem.hpp>

//要筛选的路径
const std::string html_source_path ="data/data_html/";

//筛选函数:将该路径下的.html文件及其路径放在容器vector中
bool EnumFile(std::vector<std::string>& file_list,const std::string &html_source_path)
{
    //使用BOOST库
    namespace fs = boost::filesystem;
    //使用BOOST库:将目标路径转化为BOOST认识的形式
    fs::path target_path(html_source_path);
    //判断存在与否
    if(!fs::exists(target_path))
    {
        std::cout<<"sorry,the path is errno!"<<std::endl;
        return false;
    }
    //走到这里说明文件和路径有效,我们存储进file_list
    //使用BOOST库:迭代器拿到该路径下的所有文件和对应路径
    fs::recursive_directory_iterator end;
    for (fs::recursive_directory_iterator iter(html_source_path); iter != end; iter++) 
    {
        //跳过非普通文件(比如目录、链接文件等)
        if (!fs::is_regular_file(*iter)) 
        {
            continue;
        }
        //跳过非.html后缀的文件
        if (iter->path().extension().string() != ".html") 
        {
            continue;
        }
        //存入完整路径
        file_list.push_back(iter->path().string());

        //测试
        std::cout<<iter->path().string()<<std::endl;
    }

    return true;
}
(2)主函数实现二

这里有两点注意事项:

(1)我们获取.html文件的URL不应该是在这个.html内部,比如在本地中存在:

data_html / ***.html,那么***.html 的URL不应该在 ***.html 内部 ,我们会发现官网的 BOOST网页文档都有固定的前缀,而我们需要补充的就是后缀,那么这个后缀从哪来?这 就是为什么上面不能直接过滤出 .html 的原因,我们取的就是 ...../***.html,然后去除本地 的data_html即可,提前讲解原理:

这个url_hand就是共同的前缀,现在file_path是每个.html完整的路径(由主函数1获取)

比如:/data/data_html/boost/allowed.html

而我们需要截取**/data/data_html**后面的内容作为后缀,即去除本地的这部分链接

(2)每个.html中干净数据的获取,我建议是通过AI写一段代码,因为学习的时候我发现截取的没 有AI取的完整........有很多多余的数据

效果展示:(仅查看标题和URL)

现在我们随便取一个URL去浏览器访问,可以看到没有问题:https://www.boost.org/doc/libs/1_78_0/doc/html/thread/acknowledgements.html

代码:内嵌函数在下面

cpp 复制代码
bool ParseHtml(std::vector<std::string> &file_list, std::vector<Part_result> &result)
{
    size_t n = file_list.size();
    result.resize(n);
    for (size_t i = 0; i < n; i++)
    {
        std::string S;
        // 打开文件
        std::ifstream file_stream(file_list[i]);
        if (!file_stream.is_open())
        {
            std::cout << "sorry,open_file is errno" << std::endl;
            continue;
        }
        // 读取文件内容到S
        S = std::string(
            std::istreambuf_iterator<char>(file_stream),
            std::istreambuf_iterator<char>());

        // 从S中提取标题
        if (!Pare_title(S, result[i]))continue;
        //从S中提取内容
        if (!Parse_content(S, result[i]))continue;
        //提取URL
        if (!Pare_chain(file_list[i], result[i])) continue; 


        file_stream.close();
    }

    return true;
}

提取标题:

cpp 复制代码
//提取标题
bool Pare_title(std::string &S, Part_result &result)
{
    const std::string title_tag = "<title>"; 
    //确认左边界
    std::size_t begin = S.find(title_tag);
    if (begin == std::string::npos)
    {
        result.title = "no title";
        return false;
    }
    //确认右边界
    std::size_t end = S.find("</title>",begin+title_tag.size());
    if (end == std::string::npos)
        return false;
    //截取
    result.title = S.substr(begin + title_tag.size(), end - (begin + title_tag.size()));

    return true;
}

提取纯文本内容:

cpp 复制代码
//提取纯文本内容
bool Parse_content(std::string &S, Part_result &result) 
{
    // 步骤1:先做简单判空,避免无效处理
    if (S.empty()) {
        result.source = "无有效HTML文本";
        return false;
    }

    std::string clean_content = S;

    // 步骤2:提取<body>标签内的内容(核心正文区域,剔除<head>等无关内容)
    const std::string body_start = "<body";  // 匹配<body (兼容<body class="xxx">的情况)
    const std::string body_end = "</body>";
    size_t body_start_pos = clean_content.find(body_start);
    size_t body_end_pos = clean_content.rfind(body_end);  // 从后往前找,避免匹配到嵌套的</body>

    if (body_start_pos != std::string::npos && body_end_pos != std::string::npos && body_start_pos < body_end_pos) {
        // 找到<body>的结束位置(跳过<body...>标签本身,找到>的位置)
        size_t body_tag_end = clean_content.find('>', body_start_pos);
        if (body_tag_end != std::string::npos && body_tag_end < body_end_pos) {
            // 截取<body>标签内的所有内容
            clean_content = clean_content.substr(body_tag_end + 1, body_end_pos - (body_tag_end + 1));
        }
    } else 
    {
        // 未找到<body>标签,使用原文本继续清洗(兼容无<body>的简单HTML)
        ;
    }
    const std::string section_start = "<section";
    const std::string section_end = "</section>";
    size_t section_start_pos = clean_content.find(section_start);
    size_t section_end_pos = clean_content.rfind(section_end);
    if (section_start_pos != std::string::npos && section_end_pos != std::string::npos && section_start_pos < section_end_pos) {
        size_t section_tag_end = clean_content.find('>', section_start_pos);
        if (section_tag_end != std::string::npos && section_tag_end < section_end_pos) {
            clean_content = clean_content.substr(section_tag_end + 1, section_end_pos - (section_tag_end + 1));
        }
    }

    // 步骤3:剔除所有HTML标签(<xxx>格式的内容)
    std::string no_tag_content;
    bool in_tag = false;  //标记是否处于HTML标签内
    for (char c : clean_content) {
        if (c == '<') {
            in_tag = true;  //进入标签,开始跳过字符
            continue;
        }
        if (c == '>') {
            in_tag = false;  //退出标签,恢复写入
            continue;
        }
        if (!in_tag) {
            no_tag_content += c;  // 只保留非标签内的字符
        }
    }
    clean_content = std::move(no_tag_content);

    // 步骤4:替换HTML常见实体字符(避免乱码和冗余)
    std::replace(clean_content.begin(), clean_content.end(), '&', ' ');  // 替换&(实体开头)
    std::replace(clean_content.begin(), clean_content.end(), ';', ' ');  // 替换;(实体结尾)
    // 可扩展更多实体替换,比如&nbsp;(空格)、&copy;(版权)等
    const std::pair<std::string, std::string> html_entities[] = {
        {"nbsp", " "},
        {"copy", "(c)"},
        {"amp", "&"}
    };
    for (const auto &entity : html_entities) {
        size_t entity_pos = clean_content.find(entity.first);
        while (entity_pos != std::string::npos) {
            clean_content.replace(entity_pos, entity.first.size(), entity.second);
            entity_pos = clean_content.find(entity.first, entity_pos + entity.second.size());
        }
    }

    // 步骤5:清洗多余空白字符(连续空格、换行、制表符压缩为单个空格)
    std::string final_content;
    bool prev_is_whitespace = false;
    for (char c : clean_content) {
        if (isspace(static_cast<unsigned char>(c))) {  // isspace判断空白字符(空格、\n、\t等)
            if (!prev_is_whitespace) {
                final_content += ' ';  // 只保留一个空格
                prev_is_whitespace = true;
            }
        } else {
            final_content += c;
            prev_is_whitespace = false;
        }
    }

    // 步骤6:最终判空并赋值
    if (final_content.empty() || final_content.size() < 10) {  // 过滤过短的无效内容
        result.source = "未提取到有效正文内容";
        return false;
    }

    result.source = final_content;
    return true;
}

提取URL:

cpp 复制代码
//提取URL
bool Pare_chain(const std::string &file_path, Part_result &result)
{
    std::string url_hand ="https://www.boost.org/doc/libs/1_78_0/doc/html/";
    //替换路径中的\为/
    std::string url_tail = file_path.substr(html_source_path.size());
    std::replace(url_tail.begin(), url_tail.end(), '\\', '/');

    result.chain.clear();
    result.chain = url_hand + url_tail;
    return true;
}
(3)主函数实现三

现在我们已经将每个.html文件的标题、内容、URL全部放在了vector<Part_result>result中,因此遍历这个vector,然后对每个文件的内容进行拼接即可,如何拼接,这是其中一种:

标题+'\3'+内容+'\3'+URL+'\n'

注意:尽量选择不可显字符,防止和文档里面的字符冲突,导致解析错误

我们写入的时候利用ofstream按行(每个.html为一行)写入,方便后面解析!

cpp 复制代码
bool SaveHTML(const std::vector<Part_result> result,const std::string& target_path)
{
    //打开文件
    std::ofstream file_writer(target_path, std::ofstream::out);
    if(!file_writer.is_open())
    {
        std::cout<<"SaveHTML writer is errno"<<std::endl;
        return false;
    }
    //写入内容
    for(size_t i=0;i<result.size();i++)
    {
        std::string S;
        S+=result[i].title;
        S+='\3';
        S+=result[i].source;
        S+='\3';
        S+=result[i].chain;
        file_writer<<S<<std::endl;
    }
    //关闭文件描述符
    file_writer.close();

    return true;
}

效果展示多行的内容:

相关推荐
布朗克1681 小时前
Go 入门到精通-13-指针与内存
开发语言·c++·golang·指针与内存
蚁库3 小时前
Oracle 12c + ASM + UDEV 实战教程:从磁盘规划到日常管理,10节视频完整通关
数据库·oracle
码兄科技9 小时前
Java AI智能体开发实战:从零构建智能对话系统指南
java·开发语言·人工智能
问君能有几多愁~9 小时前
C++ 数据结构复习笔记
数据结构·c++·笔记
tntxia10 小时前
C++ 基础教程:从入门到精通
c++
Drone_xjw10 小时前
从 GDB 到 CDB:C/C++ 程序调试的两把“手术刀”
c语言·开发语言·c++
字节跳动的猫10 小时前
支持二次开发的开源商城系统推荐:LikeShop、ShopXO 等主流商城源码解析
开源
SL-staff10 小时前
智慧园区2000+设备统一管理:JVS-IoT如何降低运维成本40%
java·开发语言·物联网·智慧园区·设备管理·运维优化·jvs-iot