第十四篇:一个文本查询程序的实现

前言

       本文将讲解一个经典的文本查询程序,对前面所学的容器相关知识进行一个从理论到实际的升华,同时也对即将学习的面向对象知识来一次初体验。

程序描述

       要求实现这样一个程序:读取用户指定的文件,然后允许用户从中查找某个单词所在的位置。

一个面向过程的落后的设计思想

  将待检索文件以行为单位存放到Vector容器中,然后遍历容器,将容器内元素依次转存到字符串流对象中,然后在内层遍历这个字符串流对象,检索是否存在与给定单词匹配的单词。如果有则输出该行内容以及该行序号。

落后的原因及先进的设计思想

       这是我以前尝试解决这个问题的思路。其本质是一个典型的面向过程思想,只不过用容器简化了些操作罢了。

       这种思路使得每次执行检索都要重新遍历一次文件。如果当文件比较大的时候,这样的检索效率是不能为用户所接受的。

       最好的方法应该是采用面向对象的方法,设定一个类,该类封装一个数据结构专门记载关于单词与行号的信息,该类还同时封装初始化函数,查询函数等功能函数。

       另外,如果我这里不设定一个类,而是直接全局定义一个数据结构来记载关于单词与行号的信息,那么当还要对文本实现一些其他功能的时候,程序将会变得杂乱无章,代码里到处都是乱七八糟的数据结构和全局变量。这就是类封装性的好处,也是面向对象的美妙之处之一。

下面,将用面向对象的思想“美妙”地设计出这个文本查询程序... ...

第一步:设计类

       第1步:确定类所包含的方法

       1. read_file 函数:将指定的待检索文件存入容器并初始化”单词-行号“数据结构

       2. run_query 函数:获取待查询单词并返回单词在文本中的行号

       3. text_line 函数:获取某个行号,返回文件中该行的内容。

       第2步:确定类所包含的数据

       1. 一个string对象存放要查询的单词

       2. 一个vector容器存放待检索的文本

       3. 一个map容器存放单词和它对应的行号

       类定义如下():

 1 class TextQuery {
 2 public:
 3     // 为行号类型取个别名( 行号的类型实在是太长了 )
 4     typedef std::vector<std::string>::size_type line_no;
 5     // 将数据存入vector容器并初始化单词 - 行号数据结构
 6     void read_file(std::ifstream &is) {
 7         // 将待检索文件存入容器
 8         store_file(is);
 9         // 建立单词 - 行号数据结构
10         build_map();
11     }
12     // 根据用户指定的单词执行查询并返回结果行号( 结果是放在一个set容器中的 )。
13     std::set<line_no> run_query(const std::string&) const;
14     // 根据行号返回该行内容
15     std::string text_line(line_no) const;
16 private:
17     // 下面这两个函数是上面read_file 函数的实现函数,是内部函数因此设为私有。
18     void store_file(std::ifstream&);    // 将数据存入vector容器
19     void build_map();    // 初始化单词 - 行号数据结构
20     // 一个vector容器
21     std::vector<std::string> lines_of_text;
22     // 一个单词 - 行号数据结构
23     std::map< std::string, std::set<line_no> > word_map;    // 注意这个是容器的容器 因此尖括号后面要留空格
24 };

       如此一来,整个程序的框架就显得豁然开朗。可见设计类这一环节的重要性。事实上,工程中常用UML之类的技术专门处理这个环节。

第二步:实现类

       1. 实现store_file 函数:

1 void TextQuery::store_file(ifstream & is) {
2     string textline;
3     while (getline(is, textline))
4         lines_of_text.push_back(textline);
5 }

       2. 实现bulid_map 函数:

1 void TextQuery::build_map()
2 {
3     for (line_no line_num = 0; line_num != lines_of_text.size()) {
4         istringstream line(lines_of_text[line_num]);
5         string word;
6         while (line >> word)
7             word_map[word].insert(line_num);
8     }
9 }

       3. 实现run_query 函数:

1 set<TextQuery::line_no>
2 TextQuery::run_query(const string &query_word) const {
3     map< string, set<line_no> >::const_iterator loc = word_map.find(query_word);
4     if (loc == word_map.end())
5         return set<line_no>();
6     else
7         return loc->second;
8 }

       4. 实现text_line 函数:

1 string TextQuery::text_line(line_no line) const {
2     if (line < lines_of_text.size()) {
3         return lines_of_text(line);
4     }
5     throw std::out_of_range("line number out of range");
6 }

第三步:编写主函数( 其实可以和第二步同时进行 提高效率 )

 1 void print_results(const set<TextQuery::line_no> & locs, const string &sought, const TextQuery &file);
 2 ifstream & open_file(ifstream &in, const string &file);
 3 string make_plural(size_t ctr, const string &word, const string &ending);
 4 
 5 int main(int argc, char **argv)
 6 {
 7     ifstream infile;
 8     if (argc < 2 || !open_file(infile, argv[1])) {
 9         cerr << "No input file!" << endl;
10         return EXIT_FAILURE;
11     }
12 
13     TextQuery tq;
14     tq.read_file(infile);
15 
16     while (true) {
17         cout << "inter a word to query:" << endl;
18         string s;
19         cin >> s;
20         if (!cin || s == "q") break;
21         set<TextQuery::line_no> locs = tq.run_query(s);
22         print_results(locs, s, tq);
23     }
24 
25     return 0;
26 }
27 
28 void print_results(const set<TextQuery::line_no> & locs, const string &sought, const TextQuery &file) {
29     // 为了表示下面这个size_type类型,还真煞费苦心。
30     typedef set<TextQuery::line_no> line_nums;
31     line_nums::size_type size = locs.size();
32     cout << endl << sought << " occurs "  << size << " " << make_plural(size, "time", "s") << endl;
33 
34     line_nums::const_iterator it = locs.begin();
35     for (; it != locs.end(); ++it) {
36         cout << "\t(line " << (*it) + 1 << ")" << file.text_line(*it) << endl;
37     }
38 }
39 
40 ifstream & open_file(ifstream &in, const string &file)
41 {
42     in.close();
43     in.clear();
44 
45     in.open(file.c_str());
46 
47     return in;
48 }
49 
50 string make_plural(size_t ctr, const string &word, const string &ending)
51 {
52     return (ctr == 1) ? word : word+ending;
53 
54 }

说明

       第一步,第二步,第三步的代码文件分别为主函数源代码文件( .cpp ),类定义头文件( .h )和类实现源代码文件( .cpp )。

       在构建这种工程时,头文件和命名空间的设定是有讲究的( 若要生成真正的可执行程序,上面的代码还要根据一些原则进行改动 ),本文不进行详述。

小结

       1. 体验面向对象”工程“ ( 这一部分目前也有点还没弄清楚 待深入学习 )

       2. 灵活使用容器

       3. 感受面向对象思想

posted @ 2017-01-26 22:36  穆晨  阅读(344)  评论(0编辑  收藏  举报