主頁 > 軟體設計 > 簡易的站內搜索引擎 (萬字長文!!絕對值得一看!!)

簡易的站內搜索引擎 (萬字長文!!絕對值得一看!!)

2021-08-05 08:01:45 軟體設計

搜索引擎的實作

  • 專案簡介
    • 專案背景
    • 專案開始前
      • 開始前的準備
    • 四個模塊
      • 預處理模塊
      • 索引模塊
      • 搜索模塊
      • 服務器模塊

專案簡介

這里所實作的并非如同百度、谷歌一樣的全網搜索,我們的硬體條件達不到,并且技術實力也不夠,但是我們可以按照搜索引擎的基本原理,來實作一個站內搜索,實作原理也算是大同小異,此專案分為四個模塊預處理模塊索引模塊搜索模塊服務器模塊
在這里插入圖片描述

專案背景

想要寫一個搜索引擎也是源于偶然,在知乎上看到一篇文章,說是百度搜索為什么可以那么快?回答里說了很多方面的技術,其中最核心的就是倒排索引,這讓我產生了濃厚的興趣,但是因為實力和設備都有欠缺,沒法做一個像是百度和搜狗一樣的全網搜索,但我可以做一個站內搜索,來理解相應的技術,

專案開始前

雖然我們要實作的是站內搜索,但是與全網搜索大同小異,那么我們可以看一看搜索應該要包含什么
在這里插入圖片描述
我們可以看到當我們在上面的文本框輸入關鍵字之后點擊搜索,在下面會顯示出許多具有相關性的搜索結果,而這些搜索結果中至少需要包含以下四個部分:
標題
描述(網頁正文摘要出一部分)
展示url
點擊url,點擊標題會跳轉到另外一個url中
因此我們可以仿照百度的方式,來實作我們的搜索引擎

開始前的準備

httplib
g++版本必須得是4.9以上

jieba分詞的庫
用這個庫的時候直接用include是不夠的,需要將deps目錄下的limonp也拷貝到include目錄下

boost: yum install boost-devel.x86_64

jsoncpp: yum install jsoncpp-devel.x86_64

在這里插入圖片描述
創建這樣的一個目錄結構

四個模塊

預處理模塊

這里就要提一下我為什么會選擇boost網站來實作站內搜索了,

  1. 我們暫時無法實作一個全網搜索,只能完成站內搜索
  2. boost官方檔案沒有一個搜索功能,但是boost檔案很常用,沒有搜索功能比較麻煩
  3. boost檔案提供了兩個版本,離線版本在線版本
    boost提供了離線版本,那么我們就可以基于離線版本,分析檔案頁面的內容,為搜索功能提供支持,點擊搜索結果的標題的時候,就能夠跳轉到在線版本的檔案上,而如果網站內容無法直接下載,就必須使用爬蟲來抓取頁面到本地,

boost 在線檔案.

我為什么選擇1.53版本的boost呢?其實主要是因為我用的Linux版本是contos7,它默認就是1.53

那么我們現在得到了boost檔案的離線版本之后需要干什么呢?
我們下載下來的boost檔案,其實就是一個個的html,而這些html里面是有很多內容和標簽混合在一起的,而我們需要的只是其中的標題正文url,而我們預處理階段所要做的就是讀取原始的html檔案內容,提取我們所要的,整理成一個行文本,以供下面的模塊使用
接下來我們就開始寫預處理模塊的代碼了
首先我們需要定義兩個變數來表示從哪里讀資料和要把最后生成的行文本放到哪個路徑下面展示一些 行內代碼片

這個變數表示從哪個目錄中讀取boost檔案的html

string g_input_path ="../data/input";

這個變數表示預處理模塊輸出結果放到哪里

string g_output_path ="../data/tmp/raw_input";

我們還需要一個結構體,來表示一個個的html

struct DocInfo{
     string title;//檔案的標題
     string url;//檔案的url
     string content;//檔案的正文
 };

既然我們要將所有的html都決議成行檔案,那么我們首要做的就是將所下載的所有html都列舉加載進來,那么我們就可以封裝一個函式

bool EnumFile(const string &input_path, vector<string> *file_list)

其中input_path為輸入引數,file_list是輸出引數,將上面定義的輸入變數傳進來,然后將所有的html都列舉到輸出引數file_list中,我們的列舉程序中是會遇到目錄的,而我們只要列舉html,C++的標準庫中沒有能做到這個的,但是boost庫中可以,

//把boost::filesystem這個命名空間定義一個別名
 39     namespace fs = boost::filesystem;
 40     fs::path root_path(input_path);
 41     if(!fs::exists(root_path)){
 42       std::cout << "當前的目錄不存在" << std::endl;
 43     }
 44
 45     //遞回目錄迭代器,迭代器使用回圈實作的時候可以自動完成遞回
 46
 47     fs::recursive_directory_iterator end_iter;
 48     for(fs::recursive_directory_iterator begin_iter(root_path); begin_iter != end_iter; ++begin_iter){
 49         //需要判定當前的路徑對應的是不是一個普通檔案
 50         //如果是目錄直接跳過
 51         if(!fs::is_regular_file(*begin_iter))
 52         {
 53             continue;
 54         }
 55         //當前路徑對應的檔案是不是一個html檔案,如果不是也跳過
 56         if(begin_iter->path().extension() != ".html"){
 57           continue;
 58         }
 59 
 60         //把得到的html加入到最終結果的vector中                                                                                                          
 61         file_list->push_back(begin_iter->path().string());

接下來就是遍歷vector里面的html了,打開每一個html,然后決議其中的內容,決議出標題、正文、url,對應我們之前創建好的結構體
找到標題

70 //找到html中的title標簽
 71 bool ParseTitle(const string& html, string *title){
 72     size_t beg = html.find("<title>");
 73     if(beg == string::npos){
 74         std::cout << "標題未找到" << std::endl;
 75         return false;
 76     }
 77     size_t end = html.find("</title>");
 78     if(end == string::npos){
 79         std::cout <<"標簽未找到" <<std::endl;
 80         return false;
 81     }
 82     beg += string("<title>").size();
 83     if(beg >= end){
 84         std::cout <<"標題位置不合法" <<std::endl;
 85         return false;
 86     }
 87     *title = html.substr(beg, end-beg);
 88     return true;
 89 
 90 }

獲取url

 70 //找到html中的title標簽
 71 bool ParseTitle(const string& html, string *title){
 72     size_t beg = html.find("<title>");
 73     if(beg == string::npos){
 74         std::cout << "標題未找到" << std::endl;
 75         return false;
 76     }
 77     size_t end = html.find("</title>");
 78     if(end == string::npos){
 79         std::cout <<"標簽未找到" <<std::endl;
 80         return false;
 81     }
 82     beg += string("<title>").size();
 83     if(beg >= end){
 84         std::cout <<"標題位置不合法" <<std::endl;
 85         return false;
 86     }
 87     *title = html.substr(beg, end-beg);
 88     return true;
 89 
 90 }

找到正文

101 //獲取正文
102 bool ParseContent(const string& html, string* content){
103     //這里引入一個bool變數,當進入標簽之后,此值為false
104     //判斷這個變數就能知道是在標簽內還是在標簽外了
105     bool is_content = true;
106     for(auto c : html){
107         if(is_content){
108             //當前是正文
109             if(c == '<'){
110                 is_content = false;
111             }
112             else{
113                 //當前是普通字符,就把結果寫入到content中
114                 if(c == '\n'){
115                     c = ' ';    
116                 }
117                 content->push_back(c);
118             }
119         }
120         else{
121             //當前是在標簽內
122             if(c == '>')
123             {
124                 //標簽結束
125                 is_content = true;
126             }                                                                                                                                           
127             //標簽里的其他內容都直接忽略掉
128         }
129     }
130     return true;
131 }

最后只需要將我們決議出來的結果寫入到我們最開始設定好的輸出檔案中就可以了,不過要記得選擇一個不可見分隔符對標題、正文、url進行分割!!不然會出現粘包問題!!
以上就是預處理模塊的全部內容了,可以編譯運行一下,看看輸出檔案中是否如愿決議出來了,

索引模塊

現在我們已經有了決議好的行文本了,那么我們接下來就要進入索引模塊了,索引模塊是最核心的部分,在這個模塊我們就要引入倒排索引了!!!
不管是站內搜索還是全網搜索,面對大量的資料,最核心的點就是效率,我們需要在大量的資料中,快速而準確的找出我們所要的資料,那么解決這個問題,最核心的就是倒排索引,
那么什么是倒排索引呢?
我們來看一個經典的例子,我所學習倒排索引的時候,就是看的這個例子
在這里插入圖片描述
中文和英文等語言不同,單詞之間沒有明確分隔符號,所以首先要用分詞系統將檔案自動切分成單詞序列,這樣每個檔案就轉換為由單詞序列所組成,然后對每個不同的單詞賦予唯一的單詞編號,同時記錄下哪些檔案包含這個單詞,在如此處理結束后,我們可以得到最簡單的倒排索引,
在這里插入圖片描述
我們可以概括性的理解為:正排索引就是利用檔案id來得到文本內容,倒排索引就是根據文本內容來得到文本id
那么我們的基本邏輯就是構建出正排索引,并且對字串進行分詞切割,然后切分出來的單詞,再根據切分的單詞和正排索引構建倒排索引,通過倒排索引找到對應的檔案id,再通過檔案id查找正排索引,從正排索引找到檔案的內容,
因此我們現在要做的就是:構建正排索引構建倒排索引進行分詞查找倒排索引查找正排索引
下面我們構建一下正排索引和倒排索引的基本結構體

 23 struct DocInfo {                                                        
 24     int64_t doc_id;
 25     string title;
 26     string url;
 27     string content;
 28 };
 29 
 30 // 倒排索引是給定詞, 映射到包含該詞的檔案 id 串列. (此處不光要有檔案 id,
 31 // 還得有權重資訊, 以及該詞的內容)
 32 struct Weight {
 33     // 該詞在哪個檔案中出現
 34     int64_t doc_id;
 35     // 對應的權重是多少
 36     int weight;
 37     // 詞是什么                                                                                                                                       
 38     string word;                                                                                                                            
 39 };                                                                                                                     

這就是我們需要做的所有操作

 37 typedef vector<Weight> InvertedList;
 38 
 39 // Index 類用于表示整個索引結構, 并且提供一些供外部呼叫的 API
 40 class Index {
 41   private:
 42     // 索引結構
 43     // 正排索引, 陣列下標就對應到 doc_id
 44     vector<DocInfo> forward_index;
 45     // 倒排索引, 使用一個 hash 表來表示這個映射關系
 46     unordered_map<string, InvertedList> inverted_index;
 47 
 48   public:
 49     Index();
 50     // 提供一些對外呼叫的函式
 51     // 1. 查正排
 52     const DocInfo* GetDocInfo(int64_t doc_id);
 53     // 2. 查倒排
 54     const InvertedList* GetInvertedList(const string& key);
 55     // 3. 構建索引
 56     bool Build(const string& input_path);
 57     // 4. 分詞函式
 58     void CutWord(const string& input, vector<string>* output);
 59 
 60   private:                                                                                                                                            
 61     DocInfo* BuildForward(const string& line);
 62     void BuildInverted(const DocInfo& doc_info);
 63 
 64     cppjieba::Jieba jieba;
 65 };

接下來就具體實作一下,咱們先挑簡單的查正排和查倒排來實作一下

   28 const DocInfo* Index::GetDocInfo(int64_t doc_id) {                             
   29     if (doc_id < 0 || doc_id >= forward_index.size()) {                                                                          
   30         return nullptr;                                                                                                               
   31     }                                                                                                 
   32     return &forward_index[doc_id];                                                  
   33 }                                                                                                                
   34                                                                                 
   35 const InvertedList* Index::GetInvertedList(const string& key) {                
   36     auto it = inverted_index.find(key);                                                                                                       
   37     if (it == inverted_index.end()) {                                                                                 
   38         return nullptr;                                                                                             
   39     }                                                                                                 
   40     return &it->second;                                                             
   41 }                                                                                                     

這倆就是查找正排和倒排,查不到就回傳nullptr,查到了就回傳對應的指標即可
接下來要做的就是構建索引了

   43 bool Index::Build(const string& input_path) {
   44     // 1. 按行讀取輸入檔案內容(上個環節預處理模塊生成的 raw_input 檔案)
   45     //    每一行又分成三個部分, 使用 \3 來切分, 分別是標題, url, 正文
   46     std::cerr << "開始構建索引" << std::endl;
   47     std::ifstream file(input_path.c_str());
   48     if (!file.is_open()) { 
   49         std::cout << "raw_input 檔案打開失敗" << std::endl;
   50         return false;      
   51     }                      
   52     string line;           
   53     while (std::getline(file, line)) {
   54         // 2. 決議成 正排結構的DocInfo 物件, 并構造為正排索引
   55         DocInfo* doc_info = BuildForward(line);
   56         if (doc_info == nullptr) {
   57             std::cout << "構建正排失敗!" << std::endl;
   58             continue;      
   59         }                  
   60         // 3.構造成倒排索引.
   61         BuildInverted(*doc_info);
   62                            
   63         //此時需要知道進度是多少,但是如果每一個都進行列印
   64         //每個資料都進行I/O操作的話過于影響效率
   65         if (doc_info->doc_id % 100 == 0) {                                                                                                            
   66             std::cerr << doc_info->doc_id << std::endl;
   67         }
   68     }
   69     std::cerr << "結束構建索引" << std::endl;
   70     file.close();
   71     return true;
   72 }

這是整體的構建思路,我們接下來就分別完成倒排索引和正排索引
=正排索引=
我們要做的核心操作: 按照 設定的分隔符行文本 進行切分, 第一個部分就是標題, 第二個部分就是 url, 第三個部分就是正文,然后將這三個部分填充到正排索引對應的結構體中即可,

   77 DocInfo* Index::BuildForward(const string& line) {
   78     // 1. 先把 line 按照 \3 切分成 3 個部分
   79     vector<string> tokens;
   80     common::Util::Split(line, "\3", &tokens);
   81     if (tokens.size() != 3) {
   82         return nullptr;                                                                                                                               
   83     }                         
   84     // 2. 把切分結果填充到 DocInfo 物件中
   85     DocInfo doc_info;                    
   86     doc_info.doc_id = forward_index.size();
   87     doc_info.title = tokens[0];            
   88     doc_info.url = tokens[1];              
   89     doc_info.content = tokens[2];
   90     forward_index.push_back(std::move(doc_info));
   91     return &forward_index.back();                
   92 }                                                

這里的切分,在C++的標準庫中并沒有,但是boost庫中是有的,因此我們用boost庫中的split函式,不過這里我將split封裝了一下,比boost庫中的split函式少了一個引數,也就是boost::token_compress_on:將連續多個分隔符當一個,默認沒有打開,當用的時候一般是要打開的,
boost:: token_compress_off:不會壓縮分割結果,連續的分隔符時會回傳 ""字串
我默認選擇了compress_off因為也許有的html中的欄位是空的,那我們也不能將其壓縮,
并且最后我選擇了將doc_info轉化成了右值(將亡值)進行了右值參考(效率就是一點點省出來的哦)
接下來就是構建倒排索引了,而我們構建倒排索引要做的首先就是構建正排和分詞,現在正排已經構建完畢了,那我們就需要分詞了,分詞我們選擇了cppjieba分詞庫來幫我們完成

  147 void Index::CutWord(const string& input, vector<string>* output) {
  148     jieba.CutForSearch(input, *output);
  149 }

那么我們現在終于可以開始期待已久的倒排索引了

   97 void Index::BuildInverted(const DocInfo& doc_info) {
   98     // 創建專門用于統計詞頻的結構
   99     struct WordCnt {       
  100         int title_cnt;     
  101         int content_cnt;   
  102         WordCnt() : title_cnt(0), content_cnt(0) {
  103         }                  
  104     };                     
  105     unordered_map<string, WordCnt> word_cnt_map;
  106     // 針對標題進行分詞    
  107     vector<string> title_token;
  108     CutWord(doc_info.title, &title_token);
  109     // 遍歷分詞結果, 統計每個詞出現的次數
  110     for (string word : title_token) {
  111         //將只是大小寫不同的詞當作同一個詞來處理
  112         //因此都轉化成小寫                                                                                                                            
  113         boost::to_lower(word);                                        
  114         ++word_cnt_map[word].title_cnt;                               
  115     }                                                                 
  116     // 針對正文進行分詞                                               
  117     vector<string> content_token;                                     
  118     CutWord(doc_info.content, &content_token);                        
  119     // 遍歷分詞結果, 統計每個詞出現的次數     
  120     for (string word : content_token) {                            
  121         boost::to_lower(word);
  122         ++word_cnt_map[word].content_cnt;
  123     }
  124     // 填充Weight物件
  125     for (const auto& word_pair : word_cnt_map) {
  126         // 構造 Weight 物件
  127         Weight weight;
  128         weight.doc_id = doc_info.doc_id;
  129         // 權重 = 標題出現次數 * 10 + 正文出現次數
  130         weight.weight = 10 * word_pair.second.title_cnt + word_pair.second.content_cnt;
  131         weight.word = word_pair.first;
  132 
  133         // 把 weight 物件插入到倒排索引中                                                                                                             
  134         InvertedList& inverted_list = inverted_index[word_pair.first];
  135         inverted_list.push_back(std::move(weight));                   
  136     }                                                                 
  137 }                                                  

整體思路就是將倒排索引設定成一個hash表,key是關鍵詞,value 是倒排拉鏈 (包含若干個 Weight 物件),同時設定一個hash表,來統計詞頻key是關鍵詞,value是WordCnt結構體,因此統計出標題的詞頻和正文的詞頻,根據這兩者的詞頻算出權重,將權重和分詞結果、檔案id填入Weight物件,再將Weight物件填入到第一個hash表中,

以上就是索引部分的全部內容了,

搜索模塊

上面我們構建好了索引模塊,現在我們的搜索模塊就是要利用上面的索引,首先我們還是將這個模塊進行一個封裝

   68 class Searcher {                                                                                                                       
   69   private:                                                                                                                                 
   70     Index* index;                                                                                                  
   71                                                                                                                                           
   72   public:                                                               
   73     Searcher() : index(new Index()) {                                                                          
   74     }                                                                                                                                     
   75     bool Init(const string& input_path);                                                                                               
   76     bool Search(const string& query, string* results);                                                                                    
   77                                                                                                                                             
   78   private:                                                              
   79     string GenerateDesc(const string& content, const string& word);                                                                         
   80 }; 

這里的物件是Index的一個指標,因為我們要使用到上一個類,而Init初始化也是這個用處

  145 bool Searcher::Init(const string& input_path) {
  146     return index->Build(input_path);
  147 }

也就是將所有的正排索引和倒排索引都建立好,以便我們接下來使用,接下來就是查詢倒排了,并且按照權重進行排序了

  149 // 搜索查詢詞, 得到搜索結果.                                                                                                                          
  150 bool Searcher::Search(const string& query, string* output) {
  151     // 針對查詢詞進行分詞 
  152     vector<string> tokens;
  153     index->CutWord(query, &tokens);
  154     // 根據分詞結果, 查倒排, 把相關的檔案都獲取到
  155     vector<Weight> all_token_result;
  156     for (string word : tokens) {
  157         boost::to_lower(word);
  158         auto* inverted_list = index->GetInvertedList(word);
  159         if (inverted_list == nullptr) {
  160             continue;
  161         }
  162         //  將搜索到的結果都合并到一起進行統一的排序
  163         all_token_result.insert(all_token_result.end(), inverted_list->begin(), inverted_list->end());
  164     }
  165     std::sort(all_token_result.begin(), all_token_result.end(), [](const Weight& w1, const Weight& w2) {
  166         return w1.weight > w2.weight;
  167     });

然后用得到的檔案id,去查找正排,再將正排查到的行文本轉化成json格式以便我們使用,使用 jsoncpp 這個庫來實作 json 的操作.

  170     Json::Value results; 
  171     for (const auto& weight : all_token_result) {                                                                                                     
  172         // 根據 weight 中的 doc_id 查正排        
  173         const DocInfo* doc_info = index->GetDocInfo(weight.doc_id);
  174         // 把這個 doc_info 物件再進一步的包裝成一個 JSON 物件      
  175         Json::Value result;                                        
  176         result["title"] = doc_info->title;                   
  177         result["url"] = doc_info->url;    
  178         result["desc"] = GenerateDesc(doc_info->content, weight.word);
  179         results.append(result);                                       
  180     }                                                                 
  181     // 把得到的 results 這個 JSON 物件序列化成字串. 寫入 output 中
  182     Json::FastWriter writer;                                        
  183     *output = writer.write(results);                                
  184     return true;                    
  185 }                            

而這里的desc,就是在網頁上所顯示的,文本的部分摘要的內容,而我們要用多少文本來作為摘要呢?
我們先根據正文, 找到 word 出現的位置. 以該位置為中心, 往前找 80 個位元組, 作為描述的起始位置.
再從起始位置開始往后找 160 個位元組, 作為整個描述. 這里的取值完全根據個人自定義就行

  187 string Searcher::GenerateDesc(const string& content, const string& word) {
  188     // 1. 先找到 word 在正文中出現的位置.
  189     size_t first_pos = content.find(word);
  190     size_t desc_beg = 0;
  191     if (first_pos == string::npos) {
  192         // 如果找不到, 就直接從頭開始作為起始位置.
  193         if (content.size() < 160) {
  194             return content;
  195         }
  196         string desc = content.substr(0, 160);
  197         desc[desc.size() - 1] = '.';
  198         desc[desc.size() - 2] = '.';
  199         desc[desc.size() - 3] = '.';
  200         return desc;
  201     }
  202     // 2. 找到了 first_pos 位置, 以這個位置為基準, 往前找一些位元組.
  203     desc_beg = first_pos < 80 ? 0 : first_pos - 80;
  204     if (desc_beg + 160 >= content.size()) {
  205         // desc_beg 后面的內容不夠 160 了, 直接到末尾結束即可                                                                                         
  206         return content.substr(desc_beg);                     
  207     } else {                            
  208         string desc = content.substr(desc_beg, 160);
  209         desc[desc.size() - 1] = '.';                
  210         desc[desc.size() - 2] = '.';
  211         desc[desc.size() - 3] = '.';
  212         return desc;                
  213     }               
  214 }    

這就是搜索模塊所實作的所有功能了,像是測驗,我就不在這里寫了,不過完成每一個模塊的時候都要測驗一下,不要最后發現出錯了都不知道去哪里找

服務器模塊

接下來就是最后的一個模塊了,服務器模塊,這里我們就用cpp-httplib庫來搭建服務器了,就不自己手動用stocket套接字搭建了,根據專案檔案來使用即可,這里我就不多介紹了

   25     Server server;
   26     server.Get("/searcher", [&searcher](const Request& req, Response& resp) {
   27         if (!req.has_param("query")) {
   28             resp.set_content("您發的請求引數錯誤", "text/plain");
   29             return;
   30         }
   31         string query = req.get_param_value("query");
   32         string results;
   33         searcher.Search(query, &results);
   34         resp.set_content(results, "application/json");                                                                                                
   35     });                                                                              

先創建Server物件,然后呼叫Get函式,這個函式有兩個引數,第一個表示的是,你要用哪個http路徑來觸發get操作,第二個引數是一個lambda運算式,是一個回呼函式,也就是當服務器收到第一個引數的請求的時候,會觸發回呼函式的操作,Request就是請求,Response就是相應,我們收到請求之后就可以決議請求,根據請求內容,來完成回應,set_content就是將我們的結果寫到回應的body中,然后回傳,他的第二個引數就是回傳的body的資料格式,text/plain就是純文本,application/json就是json格式,
最后將我們的所做的前端頁面,也就是靜態資源掛載就可以了,

   36     // 告訴服務器, 靜態資源存放在 wwwroot 目錄下. (html, css, js, 圖片....)          
   37     server.set_mount_point("/", "./ShySearcher");                                    
   38     // 3. 啟動服務器                                                                 
   39     server.listen("0.0.0.0", 10001);                                                 

我前端做的不太好,也就不給大家展示了,
咱們的站內搜索引擎也基本就結束了,
在這里插入圖片描述在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/291904.html

標籤:其他

上一篇:《測驗架構師學習進階寶典》終于完成了!花費9個月,16大熱門知識板塊,400頁126508字!(附學習視頻)

下一篇:STM32 舵機控制器

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more