【项目实践】boost 搜索引擎

1. 项目展示

boost搜索引擎具体讲解视频

2. 项目背景

对于boost库,官方是没有提供搜索功能的,我们这个项目就是来为它添加一个站内搜索的功能。

3. 项目环境与技术栈

项目环境: ubuntu22.04、vscode
技术栈: C/C++、C++11、STL、Boost、Jsoncpp、cppjieba、cpp-httplib、html5、css、js、jQuery、Ajax

4. 搜索引擎的宏观原理

后端: 首先通过爬虫程序将全网中的数据保存到磁盘中,接下来通过去标签和数据清洗得到我们想要的数据格式,接下来为这些数据建立好索引方便搜索引擎检索。
前端: 用户通过浏览器通过GET方式上传关键字,通过http请求搜索引擎提供服务,搜索引擎检索相关的数据并动态构建网页返回用户。
在这里插入图片描述

5. 数据获取

我们这里就不采用爬虫的方式获取boost库中的内容了,因为官方已经提供了下载的途径:
在这里插入图片描述
我们实际需要的只是boost_1_88_0/doc/html 这里面的内容,我们将其拷贝到我们的data/raw_input目录中方便后续使用。

6. 去标签与数据清洗

我们浏览data/raw_input其中的html文件发现其中都包含着很多的标签:
在这里插入图片描述
而这些标签中的内容对于我们来说是没有价值的,因此我们需要去除这些标签,并把处理好的数据放在data/input中。

6.1 处理策略

在我们搜索网页时可以看到其显示的内容主要分为三部分:
在这里插入图片描述
因此我们在数据清洗时也要体现出这三部分数据:将每个html文件中的内容读取成一行以\n结尾,每一行中分为三个部分(title、content、url)以\3分隔。

6.2 基本框架

#include <memory>
#include "Parse.hpp"

using namespace ParseModule;

int main()
{
    std::unique_ptr<Parse> parser = std::make_unique<Parse>();
    // 1. 枚举所有的.html文件
    if(! parser->EnumFileName())
    {
        LOG(LogLevel::FATAL) << "EnumFileName Failed";
        exit(1);
    }
    // 2. 将所有的文件名对应的文件转换成指定格式的数组
    if(!parser->ParseHtml())
    {
        LOG(LogLevel::FATAL) << "ParseHtml Failed";
        exit(2);
    }
    // 3. 将数组中的内容拼接成json字符串并保存到input_path中
    if(!parser->SaveHtml())
    {
        LOG(LogLevel::FATAL) << "SaveHtml Failed";
        exit(3);
    }
    LOG(LogLevel::DEBUG) << "Parse Succeed!";
    return 0;
}

6.3 分步实现

• 我们需要将 data/raw_input/ 下的所有 .html 文件的名字获取得到,boost库中为我们提供了相应的方法。因此我们需要引入boost库,使用boost库中的filesystem帮助我们遍历给定路径的文件,来帮我们筛选出.html的文件。
• 获取得到所有的.html文件之后,需要提取出我们想要的内容(title、content、url),所以要依次遍历所有的文件,提取出这三部分。
• 在获取到我们想要的内容之后,我们需要将这些数据以一定的格式进行保存,这里我们采用的是每个文件的内容放在一行,行内使用'\3'进行分隔,将其存储到data/input/input.bin中。
boost 库的下载

sudo apt update
sudo apt install -y libboost-all-dev

具体实现

#pragma once
#include <fstream>
#include <string>
#include <vector>
#include <cstdlib>
#include <boost/filesystem.hpp>
#include "Log.hpp"

namespace ParseModule
{
    using namespace LogModule;

    const static std::string raw_input_path = "../data/raw_input";
    const static std::string input_path = "../data/input/input.bin";

    struct DataInfo
    {
        std::string title;   // 标题
        std::string content; // 正文
        std::string url;     // url
    };
    using DataInfo_t = struct DataInfo;

    class Parse
    {
    public:
        Parse()
        {
        }
        // 枚举所有的html文件
        bool EnumFileName()
        {
            boost::filesystem::path root_path(raw_input_path);
            // 如果节点不存在就返回 false
            if (!boost::filesystem::exists(root_path))
                return false;
            // 遍历所有的文件
            boost::filesystem::recursive_directory_iterator end;
            boost::filesystem::recursive_directory_iterator iter(root_path);
            for (; iter != end; iter++)
            {
                // 判断是不是常规文件
                if (!boost::filesystem::is_regular_file(*iter))
                    continue;
                // 判断是不是.html文件
                if (iter->path().extension() != std::string(".html"))
                    continue;
                // 走到这里一定是一个.html文件
                _files_name.push_back(move(iter->path().string()));
            }
            return true;
        }
        // 对文件中的内容进行划分
        bool ParseHtml()
        {
            for (auto &file_name : _files_name)
            {
                // 读取文件内容
                std::string message;
                if (!ReadFile(file_name, &message))
                {
                    LOG(LogLevel::FATAL) << "ReadFile Failed";
                    return false;
                }
                // 构建DataInfo
                DataInfo_t datainfo;
                if (!BuiltDataInfo(file_name, message, &datainfo))
                {
                    LOG(LogLevel::FATAL) << "BuiltDataInfo Failed";
                    return false;
                }
                // 将构建成功的datainfo插入datas
                _datas.push_back(datainfo);
            }
            return true;
        }
        // 将指定格式的数据写入指定文件
        bool SaveHtml()
        {
            // 按照二进制方式进行写入
            std::ofstream out(input_path, std::ios::out | std::ios::binary);
            if (!out.is_open())
            {
                std::cerr << "open " << input_path << " failed!" << std::endl;
                return false;
            }
            const static std::string sep = "\3";
            for (auto &data : _datas)
            {
                std::string outstr;
                outstr += data.title + sep;
                outstr += data.content + sep;
                outstr += data.url + '\n';

                out.write(outstr.c_str(), outstr.size());
            }
            out.close();
            return true;
        }
        ~Parse()
        {
        }
    private:
        bool ReadFile(const std::string &file_name, std::string *result)
        {
            std::ifstream in(file_name, std::ios::in);
            if (!in.is_open())
            {
                LOG(LogLevel::ERROR) << "open file " << file_name << " error";
                return false;
            }

            std::string line;
            while (std::getline(in, line))
                *result += line;

            in.close();
            return true;
        }
        bool BuiltDataInfoTitle(std::string &message, std::string *title)
        {
            size_t begin = message.find("<title>");
            if (begin == std::string::npos)
                return false;
            size_t end = message.find("</title>");
            if (end == std::string::npos)
                return false;
            begin += std::string("<title>").size();
            *title = message.substr(begin, end - begin);
            return true;
        }

        bool BuiltDataInfoContent(std::string &message, std::string *content)
        {
            size_t begin = message.find("<body");
            if (begin == std::string::npos)
                return false;
            size_t end = message.find("</body>");
            if (end == std::string::npos)
                return false;
            begin += std::string("<body>").size();

            // 基于一个简易的状态机去标签
            enum status
            {
                LABLE,
                CONTENT
            };
            enum status s = LABLE;
            while (begin != end)
            {
                switch (s)
                {
                case LABLE:
                    if (message[begin] == '>')
                        s = CONTENT;
                    break;
                case CONTENT:
                    if (message[begin] == '<')
                        s = LABLE;
                    else
                    {
                        // 我们不想保留原始文件中的\n,因为我们想用\n作为html解析之后文本的分隔符
                        if (message[begin] == '\n')
                            message[begin] = ' ';
                        content->push_back(message[begin]);
                    }
                    break;
                default:
                    break;
                }
                begin++;
            }
            return true;
        }
        bool BuiltDataInfoUrl(std::string &file_name, std::string *url)
        {
            std::string url_head = "https://www.boost.org/doc/libs/1_88_0/doc/html";
            std::string url_tail = file_name.substr(raw_input_path.size());

            *url = url_head + url_tail;
            return true;
        }
        bool BuiltDataInfo(std::string &filename, std::string &message, DataInfo_t *datainfo)
        {
            // 构建title
            if (!BuiltDataInfoTitle(message, &datainfo->title))
                return false;
            // 构建content
            if (!BuiltDataInfoContent(message, &datainfo->content))
                return false;
            // 构建url
            if(!BuiltDataInfoUrl(filename,&datainfo->url))
                return false;
            return true;
        }
    private:
        std::vector<std::string> _files_name; // 1. 将raw中的html文件名全部保存到files_name中
        std::vector<DataInfo_t> _datas;       // 2. 将所有的文件名对应的文件转换成指定格式的数组
    };
}

7. 建立索引

7.1 正排索引与倒排索引概述

正排索引: 从文档ID找到文档内容(文档内的关键字)

文档ID文档内容
1caryon爱在CSDN写博客
2CADN上有好多优质博客

倒排索引: 根据文档内容对应联系到文档ID

关键字文档ID
caryon1
CSDN1、2
写博客1
博客1、2
优质博客2

7.2 基本框架

#pragma once

namespace IndexModule
{
    // 正排索引元素
    typedef struct ForwardElem
    {
        std::string title;   // title
        std::string content; // content
        std::string url;     // url
        int data_id;         // id
    } ForwardElem;
    // 倒排索引元素
    typedef struct InvertedElem
    {
        int data_id;          // data_id
        std::string key_word; // key_word
        long long weight;     // weight
    } InvertedElem;
    // 倒排链表
    using InvertedList = std::vector<InvertedElem>;

    class Index
    {
    public:
    	Index() {}
        // 获取正排索引对应的元素
        ForwardElem *GetForwardElem(int data_id)
        {
        }
        // 获取倒排索引对应的元素
        InvertedList *GetInvertedList(const std::string &word)
        {
        }
        // 构建索引
        bool BuiltIndex(const std::string &input_path)
        {
            // 构建正排索引
            // 构建倒排索引
        }
        ~Index() {}
    private:
    	Index* instance;
    };
}

7.3 分步实现

正排索引实际上就是对data/input/input.bin中的内容进行读取并按照一定的格式进行创建,它的标号天然就存在了(数组下标)。倒排索引的话就需要将获取的正排索引的元素拆分成若干词(这个工作我们交由jieba来做),而后将这些词与编号一一对应起来,这里有一点很重要,查阅到的文档内容我们按照什么样的顺序进行展示呢?这里我们采用了一定的相关性进行绑定的。
至于返回正排索引和倒排索引对应的元素只需要查找一下即可。
还有一点就是,我们实际上的索引只需要建立一次就可以了,因此可以设置为单例模式。
jieba库的下载
本次使用的jieba我们从git code获取,我是将它保存到了libs目录下的,需要注意的是要将dsps/limonp拷贝到include下才能正确使用,或者建立软连接也可以。

git clone https://gitee.com/mohatarem/cppjieba.git

具体实现

#pragma once
#include <mutex>
#include <fstream>
#include <vector>
#include <string>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include "Log.hpp"
#include "Jieba.hpp"
namespace IndexModule
{
    using namespace LogModule;
    // 正排索引元素
    typedef struct ForwardElem
    {
        std::string title;   // title
        std::string content; // content
        std::string url;     // url
        int data_id;         // id
    } ForwardElem;
    // 倒排索引元素
    typedef struct InvertedElem
    {
        int data_id;          // data_id
        std::string key_word; // key_word
        long long weight;     // weight

        // 这个函数是给search.hpp去重使用的
        bool operator==(const InvertedElem& e)
        {
            return data_id == e.data_id && key_word == e.key_word && weight == e.weight;
        }
    } InvertedElem;
    // 倒排链表
    using InvertedList = std::vector<InvertedElem>;

    class Index
    {
        Index() {}
        Index(const Index&) = delete;
        bool operator=(const Index&) = delete;
        static Index* instance;
        static std::mutex lock;
    public:
        static Index* GetInstance()
        {
            if(instance == nullptr)
            {
                std::lock_guard<std::mutex> lck (lock);
                if(instance == nullptr)
                    instance = new(Index);
            }
            return instance; 
        }
        // 获取正排索引对应的元素
        ForwardElem *GetForwardElem(int data_id)
        {
            if (data_id > ForwardIndex.size())
                return nullptr;
            return &ForwardIndex[data_id];
        }
        // 获取倒排索引对应的元素
        InvertedList *GetInvertedList(const std::string &word)
        {
            auto it = InvertedIndex.find(word);
            if (it == InvertedIndex.end())
                return nullptr;
            return &InvertedIndex[word];
        }
        // 构建索引
        bool BuiltIndex(const std::string &input_path)
        {
            std::ifstream in(input_path, std::ios::in | std::ios::binary);
            if (!in.is_open())
            {
                LOG(LogLevel::FATAL) << "sorry, " << input_path << " open error";
                return false;
            }
            std::string line;
            int cnt = 0;
            while (getline(in, line))
            {
                // 构建正排索引
                ForwardElem *forward_elem = BuiltForwardIndex(line);
                if (forward_elem == nullptr)
                    continue;
                // 构建倒排索引
                if (!BuiltInvertedIndex(*forward_elem))
                    continue;
                cnt++;
                if(cnt % 50 == 0)
                    LOG(LogLevel::DEBUG) << "已经建立连接:" << cnt ;
            }
            return true;
        }
        ~Index() {}

    private:
        ForwardElem *BuiltForwardIndex(const std::string &line)
        {
            // 1. 解析字符串进行切割
            std::vector<std::string> part_elem;
            const static std::string sep = "\3";
            boost::split(part_elem, line, boost::is_any_of(sep), boost::token_compress_on);
            if (part_elem.size() != 3)
                return nullptr;
            // 2. 将其填充到ForwardElem结构
            ForwardElem forward_elem;
            forward_elem.title = part_elem[0];
            forward_elem.content = part_elem[1];
            forward_elem.url = part_elem[2];
            forward_elem.data_id = ForwardIndex.size();
            // 3. 将构造好的ForwardElem结构插入ForwardIndex
            ForwardIndex.push_back(std::move(forward_elem));
            return &ForwardIndex.back();
        }
        bool BuiltInvertedIndex(ForwardElem &forward_elem)
        {
            // 统计词频,用于weight的构造
            struct word_cnt
            {
                int title_cnt;
                int content_cnt;
                word_cnt() : title_cnt(0), content_cnt(0) {}
            };
            // 用来暂存词频的映射表
            std::unordered_map<std::string, word_cnt> word_map;

            // 对title进行切分并统计
            std::vector<std::string> title_key_words;
            JiebaUtil::CutString(forward_elem.title, &title_key_words);
            for (auto &key_word : title_key_words)
            {
                // 忽略大小写
                boost::to_lower(key_word);
                word_map[key_word].title_cnt++;
            }
            // 对content进行切分并统计
            std::vector<std::string> content_key_words;
            JiebaUtil::CutString(forward_elem.content, &content_key_words);
            for (auto &key_word : content_key_words)
            {
                boost::to_lower(key_word);
                word_map[key_word].content_cnt++;
            }
            // 将关键字依次插入InvertedIndex
            for(auto& key_word:word_map)
            {
                InvertedElem elem;
                elem.data_id = forward_elem.data_id;
                elem.key_word = key_word.first;
                elem.weight = 10 * key_word.second.title_cnt + key_word.second.content_cnt; // 这里的weight构造采用了硬编码
                InvertedIndex[key_word.first].push_back(std::move(elem));
            }
            return true;
        }
    private:
        std::vector<ForwardElem> ForwardIndex;                       // 正排索引
        std::unordered_map<std::string, InvertedList> InvertedIndex; // 倒排索引
    };
    Index* Index::instance = nullptr;
    std::mutex Index::lock;
}

8. 搜索引擎

8.1 基本框架

#pragma once

namespace SearchModule
{
	using namespace IndexModule;
    class Search
    {
    public:
        Search()
        {
        }
        // 初始化搜索引擎
        void InitSearch(const std::string &bin_path)
        {
        }
        // 对查询做出反馈
        std::string Searcher(std::string query) // 这里是故意写成拷贝的
        {
            // 1. 对 query 进行切分
            // 2. 将所有的关键字构成的 InvertedElem 进行保存
            // 3. 按weight降序排序并去重
            // 4. 将所有的结果按json串的格式返回
        }
        ~Search()
        {
        }
    private:
        Index *index;
    };
}

8.2 分步实现

搜索引擎是本博客的核心内容了,但是经过前面的处理,这里我们需要做的就只有初始化引擎和对用户的查询做出反馈,这里我们采用json串进行返回是为了方便后续的网络服务。
jsoncpp的安装

sudo apt install  -y libjsoncpp-dev

具体实现

#pragma once
#include <string>
#include <algorithm>
#include <boost/algorithm/string.hpp>
#include <jsoncpp/json/json.h>
#include "Index.hpp"
#include "Jieba.hpp"
#include "Log.hpp"

namespace SearchModule
{
    using namespace IndexModule;
    using namespace LogModule;
    
    class Search
    {
    public:
        Search()
        {
        }
        // 初始化搜索引擎
        void InitSearch(const std::string &bin_path)
        {
            index = Index::GetInstance();
            LOG(LogLevel::INFO) << "获取单例成功……";
            index->BuiltIndex(bin_path);
            LOG(LogLevel::INFO) << "建立索引成功";
        }
        // 对查询做出反馈
        std::string Searcher(std::string query) // 这里是故意写成拷贝的
        {
            // 忽略大小写
            boost::to_lower(query);
            // 1. 对 query 进行切分
            std::vector<std::string> key_words;
            JiebaUtil::CutString(query, &key_words);

            // 2. 将所有的关键字构成的 InvertedElem 进行保存
            InvertedList invertedlist_all;
            for (const auto &key_word : key_words)
            {
                InvertedList *invertedlist = index->GetInvertedList(key_word);
                if (invertedlist == nullptr)
                    continue;
                invertedlist_all.insert(invertedlist_all.end(), invertedlist->begin(), invertedlist->end());
            }
            // 3. 按weight降序排序并去重
            std::sort(invertedlist_all.begin(), invertedlist_all.end(), [](const InvertedElem &e1, const InvertedElem &e2)
                      { return e1.weight > e2.weight; });
            auto last = std::unique(invertedlist_all.begin(), invertedlist_all.end());
            invertedlist_all.erase(last, invertedlist_all.end());
            // 4. 将所有的结果按json串的格式返回
            Json::Value root;
            for (auto &invertedlist : invertedlist_all)
            {
                ForwardElem *forwardelem = index->GetForwardElem(invertedlist.data_id);
                if (forwardelem == nullptr)
                {
                    continue;
                }
                Json::Value elem;
                elem["title"] = forwardelem->title;
                // content是文档的去标签的结果,但是不是我们想要的,我们要的是一部分
                elem["desc"] = GetDesc(forwardelem->content, invertedlist.key_word);
                elem["url"] = forwardelem->url;

                root.append(elem);
            }
            return Json::StyledWriter().write(root);
        }
        ~Search()
        {
        }

    private:
        std::string GetDesc(const std::string &content, const std::string &key_word)
        {
            // 找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
            const int prev_step = 50;
            const int next_step = 100;
            // 1. 找到首次出现
            auto iter = std::search(content.begin(), content.end(), key_word.begin(), key_word.end(), [](int x, int y)
                                    { return (std::tolower(x) == std::tolower(y)); });
            if (iter == content.end())
            {
                return "None1";
            }
            int pos = std::distance(content.begin(), iter);

            // 2. 获取start,end 
            int start = 0;
            int end = content.size() - 1;
            // 如果之前有50+字符,就更新开始位置
            if (pos > start + prev_step)
                start = pos - prev_step;
            if (pos < end - next_step)
                end = pos + next_step;

            // 3. 截取子串,return
            if (start >= end)
                return "None2";
            std::string desc = content.substr(start, end - start);
            desc += "...";
            return desc;
        }

    private:
        Index *index;
    };
}

9. 网络服务

网络服务这里我们采用cpp-httplib库来实现
cpp-httplib 安装

git clone https://gitee.com/welldonexing/cpp-httplib.git

具体实现

#pragma once
#include <memory>
#include "Search.hpp"
#include "../libs/cpp-httplib/httplib.h"

namespace HttpSeverModule
{
    using namespace SearchModule;
    const std::string rootpath = "../html";
    class HttpSever
    {
    public:
        HttpSever() : _searcher(std::make_unique<Search>())
        {
        }
        void Start(const std::string &bin_path)
        {
            _searcher->InitSearch(bin_path);
            _svr.set_base_dir(rootpath.c_str());
            _svr.Get("/s", [&](const httplib::Request &req, httplib::Response &rsp){
                if (!req.has_param("word"))
                {
                    rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
                    return;
                }
                std::string word = req.get_param_value("word");
                LOG(LogLevel::INFO) << "用户搜索的: " << word;
                std::string json_string = _searcher->Searcher(word);
                rsp.set_content(json_string, "application/json");
            });

            LOG(LogLevel::INFO) << "服务器启动成功...";
            _svr.listen("0.0.0.0", 8888);
        }
        ~HttpSever()
        {
        }

    private:
        std::unique_ptr<Search> _searcher;
        httplib::Server _svr;
    };
}

10. 前端界面

这一部分内容只要自己能够实现一个搜索功能即可,谨放上我的代码供大家查看

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Boost 搜索引擎</title>
  <style>
    /* 可复用 Google 风格样式 */
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      height: 100vh;
      font-family: Arial, sans-serif;
    }
    .logo {
      font-size: 64px;
      font-weight: bold;
      color: #4285f4;
      margin-bottom: 30px;
    }
    .search {
      display: flex;
      max-width: 600px;
      width: 100%;
      border: 1px solid #ccc;
      border-radius: 24px;
      padding: 5px 10px;
    }
    .search input {
      flex: 1;
      border: none;
      outline: none;
      font-size: 16px;
    }
    .search button {
      border: none;
      background: none;
      font-size: 16px;
      color: #4285f4;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="logo">Boost</div>
  <div class="search">
    <input type="text" id="searchInput" placeholder="请输入搜索关键字">
    <button onclick="jump()">🔍</button>
  </div>

  <script>
    function jump() {
      const input = document.getElementById("searchInput").value.trim();
      if (input !== "") {
        location.href = `search.html?word=${encodeURIComponent(input)}`;
      }
    }
  </script>
</body>
</html>
<!-- search.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>搜索结果 - Boost</title>
    <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f8f9fa;
            margin: 0;
            padding: 0;
        }

        .container {
            max-width: 720px;
            margin: 0 auto;
            padding: 20px;
        }

        .search-bar {
            display: flex;
            margin: 20px 0;
            background: white;
            border: 1px solid #ddd;
            border-radius: 24px;
            padding: 6px 12px;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .search-bar input {
            flex: 1;
            border: none;
            outline: none;
            font-size: 16px;
            padding: 8px;
        }

        .search-bar button {
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 20px;
            padding: 8px 16px;
            font-size: 14px;
            cursor: pointer;
        }

        .result {
            margin-top: 20px;
            padding: 0 30px;
        }

        .result .item {
            margin-bottom: 25px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        }

        .result .item a {
            display: block;
            font-size: 18px;
            font-weight: bold;
            color: #1a0dab;
            text-decoration: none;
            margin-bottom: 5px;
        }

        .result .item a:hover {
            text-decoration: underline;
        }

        .result .item p {
            font-size: 14px;
            line-height: 1.6;
            color: #4d5156;
            margin: 0;
            white-space: normal;
            /* 允许换行 */
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="search-bar">
            <input type="text" id="searchInput">
            <button onclick="jump()">搜索</button>
        </div>

        <div class="result"></div>
    </div>

    <script>
        const urlParams = new URLSearchParams(window.location.search);
        const query = urlParams.get('word') || '';

        document.getElementById("searchInput").value = query;

        if (query !== '') {
            Search(query);
        }

        function Search(q) {
            $.ajax({
                type: "GET",
                url: "/s?word=" + encodeURIComponent(q),
                success: function (data) {
                    BuildHtml(data);
                }
            });
        }

        function BuildHtml(data) {
            const result_label = $(".result");
            result_label.empty();

            for (let elem of data) {
                let a_label = $("<a>", {
                    text: elem.title,
                    href: elem.url,
                    target: "_blank"
                });
                let p_label = $("<p>", {
                    text: elem.desc
                });
                let div_label = $("<div>", {
                    class: "item"
                });

                a_label.appendTo(div_label);
                p_label.appendTo(div_label);  // 不再添加网址
                div_label.appendTo(result_label);
            }
        }

        function jump() {
            const input = document.getElementById("searchInput").value.trim();
            if (input !== "") {
                location.href = `search.html?word=${encodeURIComponent(input)}`;
            }
        }
    </script>
</body>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值