C++实践笔记(二)----实现一个简单的文本查询程序
学到关联容器,10.6,容器的综合应用:文本查询程序,决定不看课本实现它.
题目如下:
------------------------------------------------------------------------------
我们将实现一个简单的文本查询程序来结束本章。
我们的程序将读取用户指定的任意文本文件,然后允许用户从该文件中查找
单词。查询的结果是该单词出现的次数,并列出每次出现所在的行。如果某单词
在同一行中多次出现,程序将只显示该行一次。行号按升序显示,即第 7 行应
该在第 9 行之前输出,依此类推。
例如,以本章的内容作为文件输入,然后查找单词“element”。输出的前
几行应为:
element occurs 125 times
(line 62) element with a given key.
(line 64) second element with the same key.
(line 153) element |==| operator.
(line 250) the element type.
(line 398) corresponding element.
后面省略了大约 120 行。
------------------------------------------------------------------------------
看完题目,写了一个程序如下:
{
in.close();
in.clear();
in.open(file.c_str());
return in;
}
int main(int argc, char* argv[])
{
ifstream ifst;
if(!open_file(ifst,argv[1]))//文件名通过命令行指定
throw runtime_error("没有找到指定的文本文件!");
cout<<"请输入要查找的单词:"<<flush;
string target;
cin>>target;
typedef multimap<int,string> Direct;
Direct result;
int line_no=1;
string line;
while(getline(ifst,line))//读文件的每一行
{
istringstream istring(line);
string word;
while(istring>>word)//读这一行的每个单词
{
if(word==target)
result.insert(make_pair(line_no,line));//如果出现了要找的单词,把这一行放进multimap
}
line_no++;//下一行
}
cout<<target<<" occurs "<<result.size()<<" times"<<endl;//共出现的次数
Direct::iterator iter=result.begin();
while(iter!=result.end())
{
cout<<"(line "<<iter->first<<") "<<iter->second<<".";//输出行
int wcount=result.count(iter->first);
if(wcount>1)
cout<<"("<<wcount<<" times)";//如果一行出现了大于1次,在后面注上这一行出现在这个单词的次数
cout<<endl;
iter=result.upper_bound(iter->first);//下一行
}
system("PAUSE");
return 0;
}
写完了感觉不对啊,好像课本上这一节挺长的啊,就这么点?然后看了看10.6.1小节设计的部分.顿时觉得我那刚才写的能叫程序吗…首先,只能满足查询一次,查完了就退出了;其次,每次执行这些代码都要开辟大量的存储空间用来存储行的内容,如果文本相当大那么代价太高;再次,如果把这项功能想成一个模块,是写给另一个程序员使用的,或许更贴近实际;最后,bug,贴着单词的标点会被当成是这个单词的一部分.好,重写.
首先,定义一个类,类名就跟课本一致,TextQuery,至于成员我定义了三个公共的成员函数和两个私有成员:
{
public:
void input_file(ifstream &);//从文件读入文本,建立起result和text.
set<int> get_list(const string);
//查询单词在哪些行出现了,行号在返回的set容器中.
string &get_line(int);//得到行的内容,返回引用,代价较小.
private:
vector<string> text;//储存了文本文件的每一行.
map<string,set<int> > result;
//result将每个单词与记录它出现的行数的set容器关联起来.
};
下面是主程序,这里将所有的单词全部转换为小写,也就是说查找的时候不进行大小写匹配:
{
string file_name;
cout<<"请输入文件名:"<<flush;
cin>>file_name;
ifstream ifst(file_name.c_str());
if(!ifst)
{
cout<<"文件不存在!"<<endl;
system("PAUSE");
return -1;
}
TextQuery artical;
artical.input_file(ifst);
string target;
cout<<"请输入要查找的单词(以Ctrl+Z结束):"<<flush;
while(cin>>target)
{
target=to_lower(target);//不匹配大小写
set<int> line_list=artical.get_list(target);//获得行号
cout<<target<<" occurs "<<line_list.size()<<" times"<<endl;//一行出现了多次也只算一次
for(set<int>::iterator iter=line_list.begin();iter!=line_list.end();iter++)
cout<<"(line "<<*iter<<") "
<<artical.get_line(*iter)<<endl;//输出每一行
cout<<"请输入要查找的单词(以Ctrl+Z结束):"<<flush;
}
system("PAUSE");
return 0;
}
其中的to_lower()函数定义如下(使用了cctype头文件):
{
for(string::iterator iter=word.begin();iter!=word.end();iter++)
if(isupper(*iter))*iter=tolower(*iter);
return word;
}
下面实现类里面的三个公共成员函数:
1.最重要的就是input_file()函数,它读取文件中的文本,建立起map容器和vector容器供以后查询使用,注意在建立map容器的时候要准确找到每个单词,不能将标点符号也算做单词的一部分:
{
string line;//行文本
string::iterator char_iter;//指向字符元素的迭代器
int flag;//标志,0表示没有遇到字母,1表示在收集连续的字母组成单词的过程中.
string word;//单词
text.push_back("");//从text[1]开始储存文本,跟行号对应
int line_no=1;//
while(getline(file,line))
{
text.push_back(line);//建立起vector型容器text
char_iter=line.begin();
flag=0;
while(char_iter!=line.end())
{
char c=*char_iter;
if(isletter(c))//收集连续的字母组成单词
{
if(isupper(c))c=tolower(c);//不匹配大小写
if(flag==0)flag=1;
word.push_back(c);
}
if(flag==1&&((char_iter==line.end()-1)||!isletter(c)))//不考虑一个单词被断开在两行出现
{
result[word].insert(line_no);//如果没有键为word的元素则会自动建立一个再执行insert().
word.clear();//清空word
flag=0;//重置标志位
}
char_iter++;//下一个字符
}
line_no++;//下一行
}
}
其中判断是否是字母的函数isletter()如下:
{
return ((c>=97&&c<=122)||(c>=65&&c<=90)||(c>=48&&c<=57));//可以识别1st这样的单词,但it's要算两个单词.
}
2.查询函数get_list(),返回一个set容器,里面有出现了所查单词的行的行号:
{
set<int> se;
map<string,set<int> >::iterator word_iter=result.find(word);
if(word_iter!=result.end())
se=word_iter->second;
return se;
}
3.打印行文本的函数get_line():
{
return text[i];
}
写完之后自我感觉良好.然后看课本,感觉有些地方课本上的写法确实有她的好处,比如储存行号的set容器,我的定义是set<int>,而课本上是:
typedef std::vector<std::string>::size_type line_no;然后set<line_no>,这样就能保证line_no的范围合适,并且程序更具逻辑性,更易读.
再比如课本上将我写的input_file()函数分成了两个函数store_file()和build_map();这样也使得程序更易读.但我觉得这样做的代价更大,因为分成两个函数与一个函数比起来多了一次对vector容器的遍历.
不过,我也有比课本写得更完善一点的地方, 就是对于单词的判断上,课本的程序只通过空格来判断,误差比较大.