c++练手项目--实现简单的文本查询

1 文本查询

1.1 简单版文本查询

我们可以在一个.text文本文件中查找任意一个单词出现的次数、出现的行号和该行的内容。

输入输出示例如下:
在这里插入图片描述

1.2 解决方案

我们可以先从输入输出入手,通过梳理该问题的需求把程序分为两个功能模块

  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);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值