吉安微信网站,网络设计与实施一般包括哪几个阶段,上海贸易公司有哪些,儿童网页设计素材#x1f525; 个人主页#xff1a;大耳朵土土垚 #x1f525; 所属专栏#xff1a;Linux系统编程 这里将会不定期更新有关Linux的内容#xff0c;欢迎大家点赞#xff0c;收藏#xff0c;评论#x1f973;#x1f973;#x1f389;#x1f389;#x1f389; 文章目… 个人主页大耳朵土土垚 所属专栏Linux系统编程 这里将会不定期更新有关Linux的内容欢迎大家点赞收藏评论 文章目录 1. 获取数据并清洗下载Boost官方网站中数据获取html文件名清洗文件的内容保存清洗的数据 2. 构建索引代码结构构建正排索引Forward Index结构与组成使用场景正排索引代码 构建倒排索引Inverted Index结构与组成特点安装下载cppjieba库倒排索引代码 索引模块 3. 搜索模块代码结构搜索测试 4. 自主HttpServer模块TcpServer模块HttpProtocol模块HttpServer.hppHttpServer.cc 5. 运行结果6. 去除重复文档7. 不足与改进点 1. 获取数据并清洗
下载Boost官方网站中数据
Boost库官方网址https://www.boost.org/下载相关数据 选择boost_1_87_0t.ar.gz下载然后上传到云服务器中如果出现乱码可能是上传文件太大可以加上-E选项 结果如下
拷贝下载文件中的html文件成为原始数据 清洗原始数据 清洗之前 可以看出原始html文件中有很多特定的标签为了方便我们查找与阅读我们需要对其进行去标签化 因为我们期望查找后返回给我们的格式是类似于百度等搜索引擎返回的格式大致包含以下三个部分 1. 标题 2. 内容 3. 网址URL 如下图所示 所以我们可以定义一个结构体存放每个文件去标签化后的内容然后再使用一个vector来汇总存放每个文件去标签化后的结构体方便我们进行管理。 代码结构如下 #include boost/filesystem.hpp
#include iostream
#include string
#include vector
#include fstream#include Util.hpp
#include Log.hppusing namespace LogModule;
const std::string original_file_path data/originaldata;
const std::string parse_file_path data/parsedata/parse.txt;//清洗文件后的结构体
typedef struct FileParseData{std::string title; //文档标题std::string content; //文档内容std::string url; //文档url
}ParseData;//保存所有html文件名
bool SaveFileName(std::vectorstd::string* filenames, const std::string filepath);//遍历获取保存的html所有文件内容进行清洗去标签后按照上面ParseData结构体保存
bool ParseFile(const std::vectorstd::string filenames,std::vectorParseData* ParseDatas);//保存清洗后的ParseData到一个文档中
bool SaveParseData(const std::string parse_file_path, const std::vectorParseData* ParseDatas);//清洗标签
int main()
{ENABLE_FILE_LOG_STRATEGY();//1.将文件名字保存在vector中方便后续遍历打开std::vectorstd::string original_file_names;if(SaveFileName(original_file_names,original_file_path) false){LOG(LogLevel::ERROR)SaveAllFileName fail ...;return 1;}LOG(LogLevel::DEBUG)SaveAllFileName success ...;//2. 遍历每个文件进行清洗标签后分为三个部分标题、内容、url; 并存储在vector中管理std::vectorParseData ParseDatas;if(ParseFile(original_file_names,ParseDatas) false){LOG(LogLevel::ERROR)ParseFile fail ...;return 2;}LOG(LogLevel::DEBUG)ParseFile success ...;//3. 保存清洗之后的内容if(SaveParseData(parse_file_path,ParseDatas) false){LOG(LogLevel::ERROR)SaveParseData fail ...;return 3;}LOG(LogLevel::DEBUG)SaveParseData success ...;return 0;
}
获取html文件名 获取html文件名方便后续遍历读取文件内容进行清洗可以使用C第三方库Boost来读取目录内容 //1.将文件名字保存在filenames中方便后续遍历打开
bool SaveFileName(std::vectorstd::string* filenames, const std::string filepath)
{//使用boost库来打开并读取文件夹namespace fs boost::filesystem;fs::path root_path(filepath);//判断路径是否存在不存在就没有必要再往后走了if(!fs::exists(root_path)){LOG(LogLevel::ERROR) filepath not exists ;return false;}//使用迭代器进行遍历文件内容//先定义一个空的迭代器用来进行判断递归结束fs::recursive_directory_iterator end;for(fs::recursive_directory_iterator iter(root_path); iter ! end; iter){//判断文件是否是普通文件html都是普通文件 同时 判断文件路径名的后缀是否符合要求if(!fs::is_regular_file(*iter)||iter-path().extension() ! .html){ continue;}//LOG(LogLevel::DEBUG)iter-path().string();//当前的路径一定是一个合法的以.html结束的普通网页文件//最后将所有带路径的html保存在filenames中,方便后续进行文本分析filenames-push_back(iter-path().string()); }return true;
}结果如下 可以看出成功获取了html文件名 清洗文件的内容 遍历所有html文件进行清洗标签每个文件内容清洗之后的内容应该保存在下面的数据结构中 typedef struct FileParseData{std::string title; //文档标题std::string content; //文档内容std::string url; //文档url
}ParseData;同时使用vector来管理所有文档清洗之后的FileParseData static bool ParseTitle(const std::string html,std::string* title)
{//要查找titlestd::string html_title title;auto pos html.find(html_title);if(pos std::string::npos){LOG(LogLevel::WARNING)ParseTitle fail ...;return false;}//查找/titleauto endpos html.find(/title);if(endpos std::string::npos){LOG(LogLevel::WARNING)ParseTitle fail ...;return false;}if(pos endpos){return false;}//截取中间titleposhtml_title.size();*title html.substr(pos,endpos-pos);//后一个参数是截取个数nreturn true;
}static bool ParseContent(const std::string html,std::string* content)
{//使用状态机进行清洗标签char statue l;//表示lable标签for(auto c : html){switch(statue){case l://lable不能读取内容等到读到标签最后一个字符时才可以读取内容此时要转换状态为c——contentif(c ) statue c;//表示内容break;case c:if(c ) statue l;//表示标签else {if(c \n) c ;//不保留换行符*contentc;}break;default:break;}}return true;
}static bool ParseUrl(const std::string htmlpath,std::string* url)
{std::string url_head https://www.boost.org/doc/libs/1_87_0/doc/html;std::string url_tail htmlpath.substr(original_file_path.size());*url url_head url_tail;return true;
}//测试
static int count 0;
void ShowDoc(ParseData doc)
{std::coutcount :std::endl;std::couttitle: doc.titlestd::endl;std::coutcontent: doc.contentstd::endl;std::couturl: doc.urlstd::endl;std::cout --------------------------------------------std::endl;count;
}//2. 遍历每个文件进行清洗标签
bool ParseFile(const std::vectorstd::string filenames,std::vectorParseData* ParseDatas)
{for(auto file : filenames){//1. 读取文件Read();std::string html_content;if(!UtilModule::FileUtil::ReadFile(file, html_content)){continue;}ParseData doc;//2. 解析指定的文件提取titleif(!ParseTitle(html_content, doc.title)){continue;}//3. 解析指定的文件提取content,就是去标签if(!ParseContent(html_content, doc.content)){continue;}//4. 解析指定的文件路径构建urlif(!ParseUrl(file, doc.url)){continue;}//for debug//ShowDoc(doc);//done,一定是完成了解析任务当前文档的相关结果都保存在了doc里面ParseDatas-push_back(std::move(doc)); //细节不move会发生拷贝效率可能会比较低}return true;
}
测试结果如下 共获取并清洗了8760个html文件的内容 保存清洗的数据 使用’\3’来分开每个文件中的标题、内容和url文件之间使用’\n’来分隔 这样一行就是一个文件的内容 // 3. 保存清洗之后的内容,使用\3来分开每个文件中的标题、内容和url文件之间使用\n来分隔
// 这样一行就是一个文件的内容
bool SaveParseData(const std::string parse_file_path, const std::vectorParseData* ParseDatas)
{//按照二进制方式写入文件std::fstream out(parse_file_path,std::ios::out | std::ios::binary);if(!out.is_open()){LOG(LogLevel::ERROR)SaveParseData: parse_file_path open fail ...;return false;}//遍历每个文件读取清洗后内容并合并到out文件中
#define SEP \3for(auto data : *ParseDatas){std::string parsefile data.title SEP data.content SEP data.url \n;out.write(parsefile.c_str(),parsefile.size());}//记得关闭文件描述符out.close();return true;
}
结果如下 可以看到在const std::string parse_file_path data/parsedata/parse.txt;路径下保存了大量数据。 清洗之后 去掉标签内容以及不必要的换行符 2. 构建索引
代码结构
cpp
#pragma once#include iostream
#include fstream
#include string
#include vector
#include unordered_map
#include Log.hpp
#include Util.hppnamespace IndexModule
{using namespace LogModule;class Index{public:// 构建索引bool BuildIndex(const std::string parsedata){int count 0; // 记录构建的索引数LOG(LogLevel::DEBUG)BuildIndex ...;// 1.打开文件//std::fstream in(parsedata, std::ios::out | std::ios::binary);//!!!!不能是out^std::fstream in(parsedata, std::ios::in | std::ios::binary);if (!in.is_open()){LOG(LogLevel::ERROR) BuildIndex: parsedata open fail ...;return false;}LOG(LogLevel::DEBUG) BuildIndex: parsedata open success ...;// 2. 读取文件内容// 一行就是一个文件std::string line;while (std::getline(in, line)){//LOG(LogLevel::DEBUG) BuildIndex: line :get a file ...;// 3. 构建正排索引if (BuildForwardIndex(line) false)continue;// 4. 构建倒排索引BuildInvertedIndex(_forward.back()); // 通过新插入的正排索引元素来构建倒排索引// for debugcount;if (count % 100 0)LOG(LogLevel::DEBUG) 已经构建的索引数 count;}// 5. 关闭文件in.close();return true;}private:// 构建正排索引bool BuildForwardIndex(const std::string file){return true;}// 构建倒排索引// 标题与正文对应单词权重不同,需单独切分——使用jieba库bool BuildInvertedIndex(const ForwardElem file){return true;}~Index(){}private:std::vectorForwardElem _forward;std::unordered_mapstd::string, std::vectorInvertedElem _inverted;};};构建正排索引Forward Index 正排索引也称为前向索引或文档索引是信息检索领域中的一种数据结构。它主要用于存储和组织文档中的内容以便于快速检索。与倒排索引Inverted Index不同正排索引直接记录了每个文档的内容及其相关信息。 也就是说通过文档id或编号直接找到文档内容以及相关信息 结构与组成
一个典型的正排索引包括以下部分
文档ID唯一标识每个文档的编号。文档内容文档的实际内容可以是文本、图像、视频等多媒体数据。元数据关于文档的附加信息如作者、创建时间、修改时间等。
例如假设我们有一个包含三篇文档的集合
文档1This is the first document.文档2This document is the second document.文档3And this is the third one.
正排索引可能如下所示
文档ID文档内容1This is the first document.2This document is the second document.3And this is the third one.
使用场景
正排索引通常用于以下几种场景
全文检索系统在某些情况下正排索引可以作为辅助结构帮助快速定位和提取原始文档内容。数据库系统在关系型数据库中表的数据行可以视为一种正排索引的形式。文件系统文件系统的目录结构也是一种形式的正排索引它记录了文件名及其路径。 对于本次搜索引擎来说正排索引结构应包括 struct ForwardElem{
std::string title; //⽂档的标题
std::string content; //⽂档对应的去标签之后的内容
std::string url; //官⽹⽂档url
uint64_t doc_id; //⽂档的ID
};正排索引代码 // 正排索引——根据文档id找到内容ForwardElem *GetForwardIndex(uint64_t id){if (id _forward.size()){LOG(LogLevel::ERROR) GetForwardIndex: id 对应的文档未找到....;return nullptr;}return _forward[id];}// 构建正排索引bool BuildForwardIndex(const std::string file){if (file.empty()){LOG(LogLevel::DEBUG)file is empty;return false;}ForwardElem elem;// 1.将已经清洗过的文件内容按照\3进行分割std::vectorstd::string split_file;UtilModule::StringUtil::Split(split_file, file, \3);if (split_file.size() ! 3){LOG(LogLevel::WARNING) BuildForwardIndex::Split fail ...;return false;}// 2. 填充正排索引字段elem.title split_file[0];elem.content split_file[1];elem.url split_file[2];elem.doc_id _forward.size();// 3. 将数据保存到正排索引vector结构中_forward.push_back(std::move(elem));return true;}
构建倒排索引Inverted Index
结构与组成
倒排索引Inverted Index 是信息检索系统中的一种数据结构用于快速查找包含特定词项的文档。它广泛应用于搜索引擎、数据库和其他需要高效文本检索的应用中。
倒排索引主要由两部分组成
词汇表Dictionary存储所有词项及其位置信息。倒排列表Postings List每个词项对应的文档ID列表表示哪些文档包含该词项。
例如假设有以下三个文档
文档1: “我喜欢编程”文档2: “编程是一种艺术”文档3: “我喜欢学习”
倒排索引可能如下所示
{我喜欢: [1, 3],编程: [1, 2],是: [2],一种: [2],艺术: [2],学习: [3]
}特点
高效查询通过倒排索引可以快速定位包含某个词项的所有文档而不需要逐个扫描文档内容。空间优化通常会对词项和文档ID进行压缩存储以节省空间。扩展性强支持多种扩展功能如词频统计、位置信息等有助于提高检索精度。
安装下载cppjieba库 构建倒排索引需要将词与文档对应所以我们需要将文档的内容进行分词可以借助第三方库jieba库来分词。
git clone https://github.com/yanyiwu/cppjieba.git为了防止jieba头文件找不到我们需要建立以下两个连接 同时将cppjieba中limonp目录拷贝到include/cppjieba/目录下
cp -rf deps/limonp include/cppjieba/可以使用下面的代码测试是否成功安装jieba库
#include jieba/cppjieba/Jieba.hpp//这里是你自己建立连接的路径
#include iostream
#include string
#include vector
using namespace std;int main() {cppjieba::Jieba jieba(dict/jieba.dict.utf8,dict/hmm_model.utf8,dict/user.dict.utf8,dict/idf.utf8,dict/stop_words.utf8);std::vectorstd::string words;std::string sentence 小明硕士毕业于中国科学院计算所后在日本京都大学深造;// 使用 CutForSearch 方法进行分词jieba.CutForSearch(sentence, words);for (const auto word : words) {std::cout word /;}std::cout std::endl;return 0;
}结果如下 倒排索引代码 倒排索引通过单词查询相关文档除了找出相关文档我们还需要将找出的文档按照相关性进行排序所以倒排索引的结构应该如下面所示 // 倒排索引元素typedef struct InvertedElem{std::string word; // 倒排索引单词uint64_t doc_id; // 单词对应文档idint weight; // 单词所占权重用来排序} InvertedElem; 将所有与该词相关的文档存在一个vector中同时使用unordered_map将其与单词之间链接起来方便我们查询。 // 倒排索引——根据单词找到相关文档std::vectorInvertedElem *GetInvertedIndex(const std::string word) {auto pos _inverted.find(word);if (pos _inverted.end()){LOG(LogLevel::INFO) word :未找到相关内容...;return nullptr;}return (pos-second);}// 构建倒排索引// 标题与正文对应单词权重不同,需单独切分——使用jieba库bool BuildInvertedIndex(const ForwardElem file){typedef struct word_count{int title_count;int content_count;} word_count;// 1. 将file中标题进行切分std::vectorstd::string title_words;UtilModule::JiebaUtil::CutString(title_words, file.title);// 2.将标题切分的词保存在unordered_map中std::unordered_mapstd::string, word_count words;for (auto word : title_words){boost::to_lower(word);//需要统一切换成小写方便查询时大小都有words[word].title_count;}// 3. 将file中content内容进行切分std::vectorstd::string content_words;UtilModule::JiebaUtil::CutString(content_words, file.content);// 4. 将content切分的词保存在unordered_map中for (auto word : content_words){boost::to_lower(word);//需要统一切换成小写方便查询时大小都有words[word].content_count;}// 5. 根据自定义相关性来构建倒排元素InvertedElem elem;for (auto word : words){elem.doc_id file.doc_id;elem.word word.first;elem.weight word.second.title_count * 10 word.second.content_count; // 相关性 标题 : 正文 10 : 1// 6. 将elem插入倒排unordered_map中_inverted[elem.word].push_back(elem);}return true;}索引模块 每次索引都需要将所有文档进行遍历或分词构建索引这样效率太低我们可以引入单例模式即整个程序中只要有一份索引对象即可如下面代码所示构建索引类时使用静态指针如果未定义就创建定义了就直接使用之前创建的即可这样可以大大减少空间时间的浪费提高效率
#pragma once#include iostream
#include fstream
#include string
#include vector
#include unordered_map
#include Log.hpp
#include Util.hppnamespace IndexModule
{using namespace LogModule;// 正排索引元素typedef struct ForwardElem{std::string title; // ⽂档的标题std::string content; // ⽂档对应的去标签之后的内容std::string url; // 官⽹⽂档urluint64_t doc_id; // ⽂档的ID} ForwardElem;// 倒排索引元素typedef struct InvertedElem{std::string word; // 倒排索引单词uint64_t doc_id; // 单词对应文档idint weight; // 单词所占权重用来排序} InvertedElem;class Index{public://单例模式static Index* GetInstance()//static修饰只有一个{if(nullptr instance){mutex.Lock();if(nullptr instance)instance new Index();mutex.Unlock();}return instance;}// 正排索引——根据文档id找到内容ForwardElem *GetForwardIndex(uint64_t id){if (id _forward.size()){LOG(LogLevel::ERROR) GetForwardIndex: id 对应的文档未找到....;return nullptr;}return _forward[id];}// 倒排索引——根据单词找到相关文档std::vectorInvertedElem *GetInvertedIndex(const std::string word) {auto pos _inverted.find(word);if (pos _inverted.end()){LOG(LogLevel::INFO) word :未找到相关内容...;return nullptr;}return (pos-second);}// 构建索引bool BuildIndex(const std::string parsedata){int count 0; // 记录构建的索引数LOG(LogLevel::DEBUG)BuildIndex ...;// 1.打开文件//std::fstream in(parsedata, std::ios::out | std::ios::binary);//!!!!不能是out^std::fstream in(parsedata, std::ios::in | std::ios::binary);if (!in.is_open()){LOG(LogLevel::ERROR) BuildIndex: parsedata open fail ...;return false;}LOG(LogLevel::DEBUG) BuildIndex: parsedata open success ...;// 2. 读取文件内容// 一行就是一个文件std::string line;while (std::getline(in, line)){//LOG(LogLevel::DEBUG) BuildIndex: line :get a file ...;// 3. 构建正排索引if (BuildForwardIndex(line) false)continue;// 4. 构建倒排索引BuildInvertedIndex(_forward.back()); // 通过新插入的正排索引元素来构建倒排索引// for debugcount;if (count % 100 0)LOG(LogLevel::DEBUG) 已经构建的索引数 count;}// 5. 关闭文件in.close();return true;}private:// 构建正排索引bool BuildForwardIndex(const std::string file){if (file.empty()){LOG(LogLevel::DEBUG)file is empty;return false;}ForwardElem elem;// 1.将已经清洗过的文件内容按照\3进行分割std::vectorstd::string split_file;UtilModule::StringUtil::Split(split_file, file, \3);if (split_file.size() ! 3){LOG(LogLevel::WARNING) BuildForwardIndex::Split fail ...;return false;}// 2. 填充正排索引字段elem.title split_file[0];elem.content split_file[1];elem.url split_file[2];elem.doc_id _forward.size();// 3. 将数据保存到正排索引vector结构中_forward.push_back(std::move(elem));return true;}// 构建倒排索引// 标题与正文对应单词权重不同,需单独切分——使用jieba库bool BuildInvertedIndex(const ForwardElem file){typedef struct word_count{int title_count;int content_count;} word_count;// 1. 将file中标题进行切分std::vectorstd::string title_words;UtilModule::JiebaUtil::CutString(title_words, file.title);// 2.将标题切分的词保存在unordered_map中std::unordered_mapstd::string, word_count words;for (auto word : title_words){boost::to_lower(word);//需要统一切换成小写方便查询时大小都有words[word].title_count;}// 3. 将file中content内容进行切分std::vectorstd::string content_words;UtilModule::JiebaUtil::CutString(content_words, file.content);// 4. 将content切分的词保存在unordered_map中for (auto word : content_words){boost::to_lower(word);//需要统一切换成小写方便查询时大小都有words[word].content_count;}// 5. 根据自定义相关性来构建倒排元素InvertedElem elem;for (auto word : words){elem.doc_id file.doc_id;elem.word word.first;elem.weight word.second.title_count * 10 word.second.content_count; // 相关性 标题 : 正文 10 : 1// 6. 将elem插入倒排unordered_map中_inverted[elem.word].push_back(elem);}return true;}~Index(){}private:std::vectorForwardElem _forward;std::unordered_mapstd::string, std::vectorInvertedElem _inverted;Index(){}//构建单例模式//静态成员变量保存唯一的实例static Index* instance;// 互斥锁确保线程安全static MutexModule::Mutex mutex;Index(const Index ) delete;Index operator(const Index ) delete;};Index* Index::instance nullptr;MutexModule::Mutex Index::mutex;//自定义锁类
};3. 搜索模块 当我们构建好正排和倒排索引后就可以根据索引类提供的查询接口进行搜索文档
输入搜索词将搜索词进行分词分词后逐一倒排查询到相关文档将相关文档按照相关性进行排序通过正排查询获取文档内容构建json串返回上层
代码结构
#pragma once#include Index.hpp
#include algorithm
#include unordered_map
#include jsoncpp/json/json.hnamespace SearchModule{class Searcher{public:Searcher(){}~Searcher(){}//初始化建立索引void InitSearcher(const std::string input){//1. 获取或者创建Index对象 //2. 根据Index对象建立索引 } //搜索函数//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果——使用json序列化void Search(const std::string query, std::string *json_string){//1.将query分词逐一搜索//2.分好词之后逐一进行倒排索引——根据单词找文档//3. 将倒排索引元素按照词的weight——相关性进行排序//4. 然后根据倒排元素id通过正排索引找到文档并保存Forward_List中//5. 将查找到的文档合并为json串返回 private:IndexModule::Index *index; //供系统进⾏查找的索引};
}在seach函数中相关性按照标题 : 正文 10 : 1 来分配的也就是说一个词在标题中出现1次就10在正文中出现1次就1 同时返回文档的内容不能将所有内容全部返回应当返回与搜索词相关的一部分所有这里我们选择正文中第一次出现搜索词前50和后100的位置截取为正文摘要。 代码如下
#pragma once#include Index.hpp
#include algorithm
#include unordered_map
#include jsoncpp/json/json.hnamespace SearchModule{class Searcher{public:Searcher(){}~Searcher(){}//初始化建立索引void InitSearcher(const std::string input){//1. 获取或者创建Index对象index IndexModule::Index::GetInstance();LOG(LogLevel::DEBUG)InitSearcher: 获取单例Index对象成功;//2. 根据Index对象建立索引index-BuildIndex(input);LOG(LogLevel::DEBUG)建立正排和倒排索引成功;}//搜索函数//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果——使用json序列化void Search(const std::string query, std::string *json_string){//1.将query分词逐一搜索std::vectorstd::string words;UtilModule::JiebaUtil::CutString(words,query);//2.分好词之后逐一进行倒排索引——根据单词找文档std::vectorIndexModule::InvertedElem Inverted_List;for(auto word : words){boost::to_lower(word);//记得切换小写std::vectorIndexModule::InvertedElem* Inverted_Elems index-GetInvertedIndex(word);if(Inverted_Elems nullptr)continue;//找到倒排索引元素后存放在Inverted_List中Inverted_List.insert(Inverted_List.end(),Inverted_Elems-begin(),Inverted_Elems-end()); //注意可能有重复文档出现 }//3. 将倒排索引元素按照词的weight——相关性进行排序std::sort(Inverted_List.begin(),Inverted_List.end(),[](IndexModule::InvertedElem elem1,IndexModule::InvertedElem elem2){return elem1.weight elem2.weight;});//4. 然后根据倒排元素id通过正排索引找到文档并保存Forward_List中//5. 将查找到的文档合并为json串返回//std::vectorIndexModule::ForwardElem Forward_List;Json::Value root;for(auto inverted_elem : Inverted_List){IndexModule::ForwardElem forward_elem *index-GetForwardIndex(inverted_elem.doc_id);Json::Value value;value[title] forward_elem.title;//value[content] forward_elem.content;//应该是摘要value[content] GetAbstract(forward_elem.content,inverted_elem.word);value[url] forward_elem.url;root.append(value);}Json::StyledWriter writer;//Json::FastWriter writer;*json_string writer.write(root);//不能分开保存返回摘要时需要单词来查找// //4. 然后根据倒排元素id通过正排索引找到文档并保存Forward_List中// std::vectorIndexModule::ForwardElem Forward_List;// for(auto inverted_elem : Inverted_List)// Forward_List.push_back(*index-GetForwardIndex(inverted_elem.doc_id));// //5. 将查找到的文档合并为json串返回// Json::Value root;// for(auto forward_elem : Forward_List)// {// Json::Value value;// value[title] forward_elem.title;// //value[content] forward_elem.content;//应该是摘要// value[content] GetAbstract(forward_elem.content,);// value[url] forward_elem.url;// root.append(value);// }// Json::FastWriter writer;// *json_string writer.write(root);}private://在单词首次出现的位置往前50和往后100截取为摘要std::string GetAbstract(const std::string content,const std::string word)//word已经是小写{int pos content.find(word);if(pos std::string::npos)return None1;int begin pos - 50;if(begin 0)begin 0;int end pos 100;if(end content.size())end content.size()-1;if(begin end)return None2;std::string abs content.substr(begin,end - begin) ...;return abs;}private:IndexModule::Index *index; //供系统进⾏查找的索引};
}搜索测试
#include Searcher.hpp
#include cstdio
#include cstring
#include iostream
#include string
const std::string file_path data/parsedata/parse.txt;
int main()
{SearchModule::Searcher *search new SearchModule::Searcher();search-InitSearcher(file_path);std::string query;std::string json_string;char buffer[1024];while(true){std::cout Please Enter You Search Query# ;fgets(buffer, sizeof(buffer)-1, stdin);buffer[strlen(buffer)-1] 0;query buffer;search-Search(query, json_string);std::cout json_string std::endl;}return 0;
}结果如下 可以看出构建了8700条索引与之前清洗的文件个数对应上了同时我们发现输入搜索词split时返回了有关正文、标题以及url的内容但是我们不清楚是否按照相关性进行排序的所以我们可以再输出一些内容用来测试 //debugvalue[id] (int)inverted_elem.doc_id;value[weight] inverted_elem.weight;结果如下 发现确实是按照相关性进行排序的但是这个weight是否准确呢 我们可以通过url查询相关文档进行对比 因为我们使用的jieba库分词可能和我们预想的不太一样所以weight值可能有些偏差但是影响不大此外我们在查找正文中词的数量时包括了标题的词所以有可能weight会比实际的大一点可以设置weight-1更准确一点但我们也不准备输出weight在网页上所以就不做处理了。 4. 自主HttpServer模块 当搜索服务在本地测试完毕后就可以将其引入到网络中实现网络服务这里可以借助cpp第三方http库也可以自主实现http服务器进行网络搜索服务。
TcpServer模块 其实Http服务器底层是借助Tcp实现网络传输的所以我们需要一个TcpServer.hpp 此外因为套接字不管是udp、tcp都要使用我们就可以封装一个套接字类方便我们调用
#pragma once#include iostream
#include string
#include cstdlib
#include sys/socket.h
#include unistd.h
#include sys/types.h
#include netinet/in.h
#include arpa/inet.h
#include strings.h
#include Log.hpp
#include Common.hpp
#include InetAddr.hppnamespace SocketModule
{using namespace LogModule;class Socket;using SockPtr std::shared_ptrSocket;// 基类规定创建socket的方法// 提供一个/若干个/固定模式的socket的方法// 模版方法模式class Socket{public:virtual ~Socket() default;virtual void SocketOrDie() 0;virtual void SetSocketOpt() 0;virtual bool BindOrDie(int port) 0;virtual bool ListenOrDie() 0;virtual SockPtr Accepter(InetAddr *client) 0;virtual void Close() 0;virtual int Recv(std::string *out) 0;virtual int Send(const std::string in) 0;virtual int Fd() 0;// 提供一个创建listensockfd的固定讨论void BuildTcpSocketMethod(int port){SocketOrDie();SetSocketOpt();BindOrDie(port);ListenOrDie();}// 其他方法需要的时候在加// #ifdef WIN// // 提供一个创建listensockfd的固定讨论// void BuildTcpSocket()// {// SocketOrDie();// SetSocketOpt();// BindOrDie();// ListenOrDie();// }// // 提供一个创建udpsockfd的固定讨论// void BuildUdpSocket()// {// }// #else // Linux// // 提供一个创建listensockfd的固定讨论// void BuildTcpSocket()// {// SocketOrDie();// SetSocketOpt();// BindOrDie();// ListenOrDie();// }// // 提供一个创建udpsockfd的固定讨论// void BuildUdpSocket()// {// }// #endif};// class UdpSocket : public Socket// {// public:// virtual ~TcpSocket()// {}// virtual int SocketOrDie() 0;// virtual void SetSocketOpt() 0;// virtual bool BindOrDie(int listensockfd) 0;// virtual bool ListenOrDie(int listensockfd) 0;// virtual int Accepter(int listensockfd) 0;// virtual void Close(int fd) 0;// private:// };class TcpSocket : public Socket{public:TcpSocket() : _sockfd(gdefaultsockfd){}TcpSocket(int sockfd) : _sockfd(sockfd){}virtual ~TcpSocket(){}virtual void SocketOrDie() override{_sockfd ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd 0){LOG(LogLevel::ERROR) socket error;exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) socket create success: _sockfd;}virtual void SetSocketOpt() override{// 保证我们的服务器异常断开之后可以立即重启不会有bind问题int opt 1;int n ::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));(void)n;}virtual bool BindOrDie(int port) override{if (_sockfd gdefaultsockfd)return false;InetAddr addr(port);int n ::bind(_sockfd, addr.NetAddr(), addr.NetAddrLen());if (n 0){LOG(LogLevel::ERROR) bind error;exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) bind create success: _sockfd;return true;}virtual bool ListenOrDie() override{if (_sockfd gdefaultsockfd)return false;int n ::listen(_sockfd, gbacklog);if (n 0){LOG(LogLevel::ERROR) listen error;exit(LISTEN_ERR);}LOG(LogLevel::DEBUG) listen create success: _sockfd;return true;}// 1. 文件描述符 2. client infovirtual SockPtr Accepter(InetAddr *client) override{if (!client)return nullptr;struct sockaddr_in peer;socklen_t len sizeof(peer);int newsockfd ::accept(_sockfd, CONV(peer), len);if (newsockfd 0){LOG(LogLevel::WARNING) accept error;return nullptr;}client-SetAddr(peer, len);return std::make_sharedTcpSocket(newsockfd);}virtual void Close() override{if (_sockfd gdefaultsockfd)return;::close(_sockfd);}virtual int Recv(std::string *out) override{char buffer[1024*8];auto size ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if (size 0){buffer[size] 0;*out buffer;}return size;}virtual int Send(const std::string in) override{auto size ::send(_sockfd, in.c_str(), in.size(), 0);return size;}virtual int Fd() override{return _sockfd;}private:int _sockfd;};// for test// int main()// {// Socket *sk new TcpSocket();// sk-BuildTcpSocket(8080);// }
}TcpServer.hpp:
#pragma once#include iostream
#include memory
#include functional
#include sys/wait.h
#include Socket.hppnamespace TcpServerModule
{using namespace SocketModule;using namespace LogModule;using tcphandler_t std::functionbool(SockPtr, InetAddr);// 是它只负责进行IO流式的IO不对协议进行任何处理class TcpServer{public:TcpServer(int port): _listensockp(std::make_uniqueTcpSocket()),_running(false),_port(port){}void InitServer(tcphandler_t handler){_handler handler;_listensockp-BuildTcpSocketMethod(_port);}void Loop(){_running true;while (_running){// 1. AcceptInetAddr clientaddr;auto sockfd _listensockp-Accepter(clientaddr);if (sockfd nullptr)continue;// 2. IO处理LOG(LogLevel::DEBUG) get a new client, info is: clientaddr.Addr();// sockfd-Recv();// sockfd-Send();//使用多进程进行通信pid_t id fork();if (id 0){_listensockp-Close();if (fork() 0)exit(0);_handler(sockfd, clientaddr);exit(0);}sockfd-Close();waitpid(id, nullptr, 0);}_running false;}~TcpServer(){_listensockp-Close();}private:// 一定要有一个Listensockstd::unique_ptrSocket _listensockp;bool _running;tcphandler_t _handler;int _port;};
}HttpProtocol模块 Http服务请求和应答都是有自己的特殊格式的如下图所示 所以我们需要定制协议来完成网络通信
#pragma once#include iostream
#include string
#include vector
#include sstream
#include unordered_map
#include fstream
#include Common.hpp
#include Log.hppconst std::string Sep \r\n;
const std::string LineSep ;
const std::string HeaderLineSep : ;
const std::string BlankLine Sep;
const std::string defaulthomepage wwwroot;
const std::string http_version HTTP/1.0;
const std::string page404 wwwroot/404.html;
const std::string firstpage index.html;using namespace LogModule;
// B/S
class HttpRequest
{
public:HttpRequest() {}~HttpRequest() {}bool IsHasArgs(){return _isexec;}// GET /favicon.ico HTTP/1.1\r\n// Host: 8.137.19.140:8080// Connection: keep-alive// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0// Accept: image/avif,image/webp,image/apng,image/svgxml,image/*,*/*;q0.8// Referer: http://8.137.19.140:8080/?msgi_have_sent_a_message_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_he_// Accept-Encoding: gzip, deflate// Accept-Language: zh-CN,zh;q0.9,en;q0.8,en-GB;q0.7,en-US;q0.6// dnt: 1// sec-gpc: 1//bool ParseHeaderkv(){std::string key, value;for (auto header : _req_header){// Connection: keep-aliveif (SplitString(header, HeaderLineSep, key, value)){_headerkv.insert(std::make_pair(key, value));}}return true;}bool ParseHeader(std::string request_str){std::string line;while (true){bool r ParseOneLine(request_str, line, Sep);if (r !line.empty()){_req_header.push_back(line);}else if (r line.empty()){_blank_line Sep;break;}else{return false;}}ParseHeaderkv();return true;}void Deserialize(std::string request_str){if (ParseOneLine(request_str, _req_line, Sep)){// 提取请求行中的详细字段ParseReqLine(_req_line, LineSep);ParseHeader(request_str);_body request_str;// 分析请求中是否含有参数..if (_method POST){_isexec true; // 参数在正文_args _body;_path _uri;LOG(LogLevel::DEBUG) POST: _path: _path;LOG(LogLevel::DEBUG) POST: _args: _args;}}}std::string GetContent(const std::string path){std::string content;std::ifstream in(path, std::ios::binary);if (!in.is_open())return std::string();in.seekg(0, in.end);int filesize in.tellg();in.seekg(0, in.beg);content.resize(filesize);in.read((char *)content.c_str(), filesize);in.close();LOG(LogLevel::DEBUG) content length: content.size();return content;}void Print(){std::cout _method: _method std::endl;std::cout _uri: _uri std::endl;std::cout _version: _version std::endl;for (auto kv : _headerkv){std::cout kv.first # kv.second std::endl;}std::cout _blank_line: _blank_line std::endl;std::cout _body: _body std::endl;}std::string Uri(){return _uri;}void SetUri(const std::string newuri){_uri newuri;}std::string Path() { return _path; }std::string Args() { return _args; }std::string Suffix()//后缀{// _uri - wwwroot/index.html wwwroot/image/1.jpg wwwroot/login.htmlauto pos _uri.rfind(.);if (pos std::string::npos)return std::string(.html);elsereturn _uri.substr(pos);}private:// GET /favicon.ico HTTP/1.1 - GET /favicon.ico HTTP/1.1void ParseReqLine(std::string _req_line, const std::string sep){(void)sep;std::stringstream ss(_req_line);ss _method _uri _version;}private:std::string _req_line;std::vectorstd::string _req_header;std::string _blank_line;std::string _body; // body内部可能会包含参数// 在反序列化的过程中细化我们解析出来的字段std::string _method;std::string _uri; // 用户想要这个 // /login.html || /login?xxxxxstd::string _path;std::string _args;std::string _version;std::unordered_mapstd::string, std::string _headerkv;bool _isexec false;
};// 对于http任何请求都要有应答
class HttpResponse
{
public:HttpResponse() : _verion(http_version), _blank_line(Sep){}~HttpResponse(){}void Build(HttpRequest req){std::string uri defaulthomepage req.Uri(); // wwwroot/ wwwroot/a/b/if (uri.back() /){uri firstpage; // wwwroot/index.html// req.SetUri(uri);}LOG(LogLevel::DEBUG) ------客户端在请求: req.Uri();req.Print();LOG(LogLevel::DEBUG) ----------------------------;_content req.GetContent(uri);// LOG(LogLevel::DEBUG) content length: _content.size();if (_content.empty()){// 用户请求的资源并不存在_status_code 404;// req.SetUri(page404);_content req.GetContent(page404);}else{_status_code 200;}_status_desc Code2Desc(_status_code); // 和状态码是强相关的if (!_content.empty()){SetHeader(Content-Length, std::to_string(_content.size()));}std::string mime_type Suffix2Desc(req.Suffix());SetHeader(Content-Type, mime_type);_body _content;}void SetCode(int code){_status_code code;_status_desc Code2Desc(_status_code);}void SetBody(const std::string body){_body body;}void SetHeader(const std::string k, const std::string v){_header_kv[k] v;}void Serialize(std::string *resp_str){for (auto header : _header_kv){_resp_header.push_back(header.first HeaderLineSep header.second);}_resp_line _verion LineSep std::to_string(_status_code) LineSep _status_desc Sep;// 序列化*resp_str _resp_line;for (auto line : _resp_header){*resp_str (line Sep);}*resp_str _blank_line;*resp_str _body;}private:std::string Code2Desc(int code){switch (code){case 200:return OK;case 404:return Not Found;case 301:return Moved Permanently;case 302:return Found;default:return std::string();}}std::string Suffix2Desc(const std::string suffix){if (suffix .html)return text/html;else if (suffix .jpg)return application/x-jpg;elsereturn text/html;}private:// 必备的要素std::string _verion;int _status_code;std::string _status_desc;std::string _content;std::unordered_mapstd::string, std::string _header_kv;// 最终要这4部分构建应答std::string _resp_line;std::vectorstd::string _resp_header;std::string _blank_line;std::string _body;
};HttpServer.hpp http服务器除了能返回网页外还应该能够提供搜索、登录等一系列服务所以我们可以设置回调方法方便我们注册一些函数这里只设置搜索服务
#pragma once
#include iostream
#include string
#include functional
#include unordered_map
#include TcpServer.hpp
#include HttpProtocol.hppusing namespace TcpServerModule;using http_handler_t std::functionvoid(HttpRequest , HttpResponse );class HttpServer
{
public:HttpServer(int port): _tsvr(std::make_uniqueTcpServer(port)){}void Resgiter(std::string funcname, http_handler_t func){_route[funcname] func;}bool SafeCheck(const std::string service){auto iter _route.find(service);return iter ! _route.end();}void Start(){_tsvr-InitServer([this](SockPtr sockfd, InetAddr client){ return this-HandlerHttpRequest(sockfd, client); });_tsvr-Loop();}// 就是我们处理http的入口bool HandlerHttpRequest(SockPtr sockfd, InetAddr client){LOG(LogLevel::DEBUG) HttpServer: get a new client: sockfd-Fd() addr info: client.Addr();std::string http_request;sockfd-Recv(http_request); // 在HTTP这里我们不做报文完整性的处理HttpRequest req;req.Deserialize(http_request);HttpResponse resp;// 请求应该被分成两类: 1. 请求一般的静态资源 2. 提交参数携带参数需要我们进行交互设置if (req.IsHasArgs()){std::cout -----IsHasArgs() std::endl;// 2. 提交参数携带参数需要我们进行交互设置std::string service req.Path();if (SafeCheck(service))_route[req.Path()](req, resp); // /loginelseresp.Build(req); // debug}else{std::cout -----Non IsHasArgs() std::endl;resp.Build(req);}std::string resp_str;resp.Serialize(resp_str);sockfd-Send(resp_str);return true;}~HttpServer() {}private:std::unique_ptrTcpServer _tsvr;std::unordered_mapstd::string, http_handler_t _route; // 功能路由
};HttpServer.cc 这个文件就可以帮助我们组合搜索模块与Http服务模块完成网络服务了
#include Searcher.hpp
#include HttpServer.hpp
#include HttpProtocol.hpp
const std::string input data/parsedata/parse.txt;
const std::string root_path ./wwwroot;#include HttpServer.hpp
#include Session.hppusing namespace LogModule;
SearchModule::Searcher search;//搜索服务
void Searcher(HttpRequest req, HttpResponse resp)
{// req.Path(): /s// 根据req动态构建usernamelisipasswordabcdefgLOG(LogLevel::DEBUG) 进入搜索模块 req.Path() , req.Args();std::string req_args req.Args();//wordsplit// 1. 解析参数格式得到要的参数auto pos req_args.find();if(pos std::string::npos){resp.SetBody(请输入要查询的关键词...);resp.SetCode(404);resp.SetHeader(Content-Type, text/plain);return;}std::string keyword req_args.substr(pos1);LOG(LogLevel::DEBUG)用户在搜索: keyword;//2. 通过关键词搜索返回给用户std::string json_string;search.Search(keyword, json_string);resp.SetBody(json_string);resp.SetCode(200);resp.SetHeader(Content-Length, std::to_string(json_string.size()));resp.SetHeader(Content-Type, application/json);}int main()
{search.InitSearcher(input);auto httpserver std::make_uniqueHttpServer(8888);// 要让服务器具有登录功能httpserver-Resgiter(/s, Searcher); // restful风格的网络请求接口httpserver-Start();return 0;
}
5. 运行结果 输入关键词split返回相关文档标题、摘要和url 6. 去除重复文档 当我们在搜索时不同的词可能对应一个文档这样就有多篇相同的文档被索引是不必要的所以我们需要在搜索时去重 可以使用unordered_map根据文档id进行去重但是要注意文档的权值应该累计相加同时不同的词也要记录下来所以仅仅是倒排索引元素中包含的文档id、关键词以及权值weight已经不能很好的满足我们了 我们可以创建一个测试的html文档来验证是否去重 去重前 我们需要重新定义一个结构来保证需要完成的功能
typedef struct InvertedElemR{std::vectorstd::string words; // 倒排索引单词数组uint64_t doc_id; // 单词对应文档idint weight; // 单词所占权重用来排序} InvertedElemR;代码如下
//搜索函数//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果——使用json序列化void Search(const std::string query, std::string *json_string){//1.将query分词逐一搜索std::vectorstd::string words;UtilModule::JiebaUtil::CutString(words,query);//2.分好词之后逐一进行倒排索引——根据单词找文档std::vectorInvertedElemR Inverted_List;//用来排序std::unordered_mapuint64_t,InvertedElemR OnlyInvertedList;//去重for(auto word : words){boost::to_lower(word);//记得切换小写std::vectorIndexModule::InvertedElem* Inverted_Elems index-GetInvertedIndex(word);if( nullptr Inverted_Elems )continue;//找到倒排索引元素后进行去重for(const auto elem : *Inverted_Elems){auto elemr OnlyInvertedList[elem.doc_id];elemr.doc_id elem.doc_id;elemr.weightelem.weight;//权重累加elemr.words.push_back(elem.word);//同一个文档的单词入vector}//找到倒排索引元素后存放在Inverted_List中//Inverted_List.insert(Inverted_List.end(),Inverted_Elems-begin(),Inverted_Elems-end()); //注意可能有重复文档出现 }for(const auto item : OnlyInvertedList){Inverted_List.push_back(std::move(item.second));}//3. 将倒排索引元素按照词的weight——相关性进行排序std::sort(Inverted_List.begin(),Inverted_List.end(),[](const InvertedElemR elem1,const InvertedElemR elem2){return elem1.weight elem2.weight;});//4. 然后根据倒排元素id通过正排索引找到文档并保存Forward_List中//5. 将查找到的文档合并为json串返回//std::vectorIndexModule::ForwardElem Forward_List;Json::Value root;for(auto inverted_elem : Inverted_List){IndexModule::ForwardElem forward_elem *index-GetForwardIndex(inverted_elem.doc_id);Json::Value value;value[title] forward_elem.title;//value[content] forward_elem.content;//应该是摘要value[content] GetAbstract(forward_elem.content,inverted_elem.words[0]);//按照第一个单词查找value[url] forward_elem.url;// //debug// value[id] (int)inverted_elem.second.doc_id;// value[weight] inverted_elem.second.weight;root.append(value);}Json::StyledWriter writer;//Json::FastWriter writer;*json_string writer.write(root);
}去重后 7. 不足与改进点 暂停词的去除 MySQL数据库引入登录 建⽴整站搜索 设计⼀个在线更新的方案信号爬虫完成整个服务器的设计 在我们的搜索引擎中添加竞价排名 热次统计智能显示搜索关键词字典树优先级队列
易错点 清洗数据时结果有很多在使用状态机时要注意遇到时不要获取 文章转载自: http://www.morning.rshijie.com.gov.cn.rshijie.com http://www.morning.dpqwq.cn.gov.cn.dpqwq.cn http://www.morning.kjrlp.cn.gov.cn.kjrlp.cn http://www.morning.snxbf.cn.gov.cn.snxbf.cn http://www.morning.qcdtzk.cn.gov.cn.qcdtzk.cn http://www.morning.qbwyd.cn.gov.cn.qbwyd.cn http://www.morning.rhkmn.cn.gov.cn.rhkmn.cn http://www.morning.rwzmz.cn.gov.cn.rwzmz.cn http://www.morning.smcfk.cn.gov.cn.smcfk.cn http://www.morning.wwthz.cn.gov.cn.wwthz.cn http://www.morning.gwdkg.cn.gov.cn.gwdkg.cn http://www.morning.pumali.com.gov.cn.pumali.com http://www.morning.krklj.cn.gov.cn.krklj.cn http://www.morning.smpb.cn.gov.cn.smpb.cn http://www.morning.playmi.cn.gov.cn.playmi.cn http://www.morning.yrjhr.cn.gov.cn.yrjhr.cn http://www.morning.jjnry.cn.gov.cn.jjnry.cn http://www.morning.lznqb.cn.gov.cn.lznqb.cn http://www.morning.tzzkm.cn.gov.cn.tzzkm.cn http://www.morning.wtbzt.cn.gov.cn.wtbzt.cn http://www.morning.qrzqd.cn.gov.cn.qrzqd.cn http://www.morning.lsbjj.cn.gov.cn.lsbjj.cn http://www.morning.txzmy.cn.gov.cn.txzmy.cn http://www.morning.dtrz.cn.gov.cn.dtrz.cn http://www.morning.ssqwr.cn.gov.cn.ssqwr.cn http://www.morning.jqmqf.cn.gov.cn.jqmqf.cn http://www.morning.xdxpq.cn.gov.cn.xdxpq.cn http://www.morning.qsswb.cn.gov.cn.qsswb.cn http://www.morning.qsxxl.cn.gov.cn.qsxxl.cn http://www.morning.pgmyn.cn.gov.cn.pgmyn.cn http://www.morning.ptmsk.cn.gov.cn.ptmsk.cn http://www.morning.nyqm.cn.gov.cn.nyqm.cn http://www.morning.lfcfn.cn.gov.cn.lfcfn.cn http://www.morning.stbfy.cn.gov.cn.stbfy.cn http://www.morning.nyhtf.cn.gov.cn.nyhtf.cn http://www.morning.kfbth.cn.gov.cn.kfbth.cn http://www.morning.kqyyq.cn.gov.cn.kqyyq.cn http://www.morning.qgfhr.cn.gov.cn.qgfhr.cn http://www.morning.brwwr.cn.gov.cn.brwwr.cn http://www.morning.fphbz.cn.gov.cn.fphbz.cn http://www.morning.wqrdx.cn.gov.cn.wqrdx.cn http://www.morning.ydrn.cn.gov.cn.ydrn.cn http://www.morning.pbbzn.cn.gov.cn.pbbzn.cn http://www.morning.pctql.cn.gov.cn.pctql.cn http://www.morning.rykmz.cn.gov.cn.rykmz.cn http://www.morning.kyzxh.cn.gov.cn.kyzxh.cn http://www.morning.tfwg.cn.gov.cn.tfwg.cn http://www.morning.xwrhk.cn.gov.cn.xwrhk.cn http://www.morning.txgjx.cn.gov.cn.txgjx.cn http://www.morning.dhqyh.cn.gov.cn.dhqyh.cn http://www.morning.mumgou.com.gov.cn.mumgou.com http://www.morning.bjjrtcsl.com.gov.cn.bjjrtcsl.com http://www.morning.hdrrk.cn.gov.cn.hdrrk.cn http://www.morning.xdwcg.cn.gov.cn.xdwcg.cn http://www.morning.yxdrf.cn.gov.cn.yxdrf.cn http://www.morning.rwlsr.cn.gov.cn.rwlsr.cn http://www.morning.smdnl.cn.gov.cn.smdnl.cn http://www.morning.lktjj.cn.gov.cn.lktjj.cn http://www.morning.gtbjc.cn.gov.cn.gtbjc.cn http://www.morning.zbqry.cn.gov.cn.zbqry.cn http://www.morning.mhbcy.cn.gov.cn.mhbcy.cn http://www.morning.wxlzr.cn.gov.cn.wxlzr.cn http://www.morning.rkwwy.cn.gov.cn.rkwwy.cn http://www.morning.gsyns.cn.gov.cn.gsyns.cn http://www.morning.rdxp.cn.gov.cn.rdxp.cn http://www.morning.qcygd.cn.gov.cn.qcygd.cn http://www.morning.nrxsl.cn.gov.cn.nrxsl.cn http://www.morning.pyzt.cn.gov.cn.pyzt.cn http://www.morning.ttnfc.cn.gov.cn.ttnfc.cn http://www.morning.pndw.cn.gov.cn.pndw.cn http://www.morning.pbzlh.cn.gov.cn.pbzlh.cn http://www.morning.xhhqd.cn.gov.cn.xhhqd.cn http://www.morning.njstzsh.com.gov.cn.njstzsh.com http://www.morning.kqylg.cn.gov.cn.kqylg.cn http://www.morning.c7627.cn.gov.cn.c7627.cn http://www.morning.zylzk.cn.gov.cn.zylzk.cn http://www.morning.dysgr.cn.gov.cn.dysgr.cn http://www.morning.jzfrl.cn.gov.cn.jzfrl.cn http://www.morning.tpfny.cn.gov.cn.tpfny.cn http://www.morning.rdbj.cn.gov.cn.rdbj.cn