1 文本查询
1.1 简单版文本查询
我们可以在一个.text文本文件中查找任意一个单词出现的次数、出现的行号和该行的内容。
输入输出示例如下:
1.2 解决方案
我们可以先从输入输出入手,通过梳理该问题的需求把程序分为两个功能模块
- 输入功能模块:记录每个单词的行号,生成单词行号的映射关系。文件按照行号存储(因为需要打印每行的数据)
- 输出功能模块:读取输入单词关联的行号,排序行号(升序,去重),输出特定行号的内容
下面我们将用多种标准库设施来实现上述两个功能模块。
输入功能模块:
我们将使用一个vector来保存每个输入文件的一份拷贝。输入文件的每一行保存为vector中的一个元素。需要打印一行时,可以直接用下标来提取文本。
我们使用istreamstream来分解每一行文本中的单词,之后使用set来保存每个单词出现的行号,因为我们逐行处理,因此可以保证有序,其次有序set也保证了行号的唯一性。
最后我们用一个map来把每个单词和行号set关联起来
虽然我们可以直接使用vector、set和map来编写程序,但我们最好定义一个更加抽象的解决方案。我们定义一个保存输入文件的类(TextQuery)来完成上述所有操作。该类包含一个vector和一个map,vector用来保存输入文件,map用来关联每个单词和行号set。该类有一个读取文件的构造函数和一个执行查询操作的成员函数。
输出功能模块:
输出功能模块就是TextQuery的查询成员函数。该函数的任务很简单:查询map成员,检查给定单词是否出现。查询结果包括单词出现的次数,出现的行号和该行的内容。我们可以把查询结果抽象为一个QueryResult类。该类有一个print函数完成打印输出工作。
由于QueryResult所需要的数据都保存在TextQuery对象中,为了高效的访问这些数据,我们可以在QueryResult中使用指向TextQuery中数据的指针。不过如果使用普通指针,可能会导致无效指针的问题(TextQuery中对象在QueryResult前被销毁)。由于QueryResult和TextQuery是同步的,实际上他们“共享”了文本数据,因此我们可以使用shared_ptr来保存数据。
1.3 代码实现
先从整体入手,假设我们已经有了TextQuery类和QueryResult类,我们现在给出程序的主框架代码
//main.cpp
#include"QueryResult.h"
#include"TextQuery.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <iostream>
#include <string>
using std::cout;
using std::endl;
void runQueries(std::ifstream&);
int main() {
std::ifstream infile;
infile.open("./test.txt");
runQueries(infile);
}
void runQueries(std::ifstream& infile)
{
TextQuery tq(infile);
while (true) {
std::cout << "enter a word to query, or 'q' to quit: ";
std::string s;
if (!(std::cin >> s) || s == "q") break;
print(std::cout, tq.query(s)) << std::endl;
}
}
接下来定义TextQuery类,由于TextQuery类需要用到QueryResult,因此需要包含QueryResult类的头文件。我们写类的定义的时候可以先从数据成员写起。然后再编写再这些成员上的操作定义。注意:由于总总原因,很多类型别名,友元之类的语句最好写在最前面,因此我们只是编码顺序发生了变化,但最后的结果还是public再前面,private再后面
//TextQuery.h
#ifndef TEXT_QUERY_H
#define TEXT_QUERY_H
#include "QueryResult.h"
#include <map>
class TextQuery {
public:
using line_no = std::vector<std::string> ::size_type;
TextQuery(std::ifstream&);//构造函数
QueryResult query(const std::string&) const;//查寻不修改原本单词所以形参为const,采用引用的方式避免string过大导致的拷贝时间开销,方法声明为const可以使得几乎任何TextQuery对象都可以调用该方法(兼容const对象)
private:
std::shared_ptr<std::vector<std::string>> file; //输入文本
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm; //单词行号映射
};
#endif
下面给出TextQuery类的构造函数和成员函数的定义。
构造函数接收一个文件输入流,从该流中读取文件并且在vector中。我们在构造函数的初始值列表中new一个空的vector,在构造函数体中在该空vector中拷贝文件。
Query成员函数接收一个string,即查询单词,query用来在map中定位set,如果找到了,query构造一个QueryResult,保存string,TextQuery的file和wm中的set。此外如果没找到,我们需要返回一个空的set。为了解决这个问题,我们在局部定义一个static对象。
//TextQuery.cpp
//常规头文件省略
#include "TextQuery.h"
#include <fstream>
#include <sstream>
TextQuery::TextQuery(std::ifstream& is) :file(new std::vector<std::string>)
{
std::string text;
//也可以在循环外面声明一个流,之后用每行的text重置流,重置流后习惯性的最好clean一下
//istringstream line;
while (getline(is, text)) {
file->push_back(text);//保存每一行
int n = file->size() - 1;
std::istringstream line(text);
//line.str(test)
//line.clean();
std::string word;
while (line >> word) {
auto& lines = wm[word];//如果单词不在wm中,lines为空
if (!lines)//如果lines为空,要new一个set
lines.reset(new std::set<line_no>);
lines->insert(n);//将行号插入
}
}
}
QueryResult TextQuery::query(const std::string& sought) const
{
static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);//未找到
else
return QueryResult(sought, loc->second, file);
}
接下来我们实现QueryResult类的声明和定义,该类只有一个简单的构造函数,我们在声明中给出该函数的定义。同时我们在QueryResult类的声明中也声明了一个print函数用于打印输出结果。
//QueryResult.h
#ifndef QUERY_RESULT_H
#define QUERY_RESULT_H
class QueryResult{
friend std::ostream & print(std::ostream&,const QueryResult&);//把print函数声明为友元以便可以访问QueryResult对象的成员
public:
using line_no=std::vector<string>::size_type;//set中保存的是vector<stirng>的下标
QueryResult(std::string s,std::shared_ptr<std::set<line_no>>p,
std::shared_ptr<std::vector<std::string>> f):
sought(s),lines(p),file(f){}//由于使用了shared_ptr因此不会拷贝数据,只增加对应内存的引用计数
private:
std::string sought;
std::shared_ptr<std::set<line_no>> lines;
std::shared_ptr<std::vector<std::string>> file;
};
#endif
//QueryResult.cpp
#include"QueryResult.h"
ostream &pring(ostream &os,const QueryResult &qr)
{
os<<qr.sought<<" occurs "<<qr.lines->size()<<" "<<[&qr.lines->size()](){return (qr.lines->size)>1?"times":"time"}<<endl;
//打印单词出现的每一行
for (auto num:*qr.lines)
os<<"\t(line"<<num+1<<")"
<<*(qr.file->begin()+num)<<endl;
return os;
}
1.4 改进
在逻辑上,QueryResult类只能被TextQuery类访问,我们不希望用户可以跳过TextQuery类而直接使用QueryResutl类。因此我们可以将QueryResult类声明为嵌套类,嵌套在TextQuery类的内部。
代码层面的修改我们需要把QueryResult类的声明放到TextQuery类中,此外print函数需要包含TextQuery的头文件。我们把Print函数定义在TextQuery头文件中。
1.5 自己踩得坑
所有单词都是出现零次。打断点验证后发现是读取文件失败,原因是我直接从桌面把test.txt文件复制,然后在项目文件中粘贴,实际上并没有粘贴成功,推测可能采用的链接的形式。使用相对路径并不会定位到该文件。因此我们手动把文件复制到工作目录中,或采用绝对路径的形式。最后修改一下main函数,增加文件是否成功打开的判断。
int main() {
std::ifstream infile;
infile.open("./test.txt");
if (!infile.is_open()) {
std::cerr << "读取文件失败" << std::endl;
return 1;
}
else {
runQueries(infile);
}
}