如何读文本文件

DinS          Written on 2017/12/12

阅读本文前请确保阅读过《如何写文本文件》。

读文本文件是个技术活,先从最简单的开始看起。

一、一次性读完文本文件

这种方式有的时候很实用,特别是你知道文本文件里是一个完整的数据串,比如json,使用方式也很便捷,就一个函数getline。看代码:

使用了异常作为失败情况处理,具体可见《C++异常机制》。这里外面没有用try-catch,仅仅是意思意思,表明可以用异常来处理。在这里也可以换成return -1

getline的语义功能是从文件中读取,一直读到指定的字符为止。具体到这个例子就是从fin里一直读到EOF,读出来的内容放到strWholeTxt。EOF是文件末尾标识符

运行看结果

能够解释为什么触发了异常吗?因为没有打开文件。
为什么没有打开文件?因为路径问题。
我这里使用了另一个项目,所以默认路径与上一个写文件项目的路径不一样,所以找不到文件了,现在手动把WriteTxt.txt放到项目目录下,再运行。

成功把文件内容读入了,\n表示换行符。

一次性读入所有文件内容的写法固然简单,但是所有信息都缩到一起了我们处理起来很困难。读入文件主要依据文件固定格式,分别提取有用的信息,一次性读入不能满足需求。
接下来看看另一种方式。

二、一行一行读文本文件

这个方式具有通用性,任何文本文件都可以一行一行读取。
主要还是使用getline。之所以叫getline从字面意思看就是读取一行。这是因为getline的第三个参数默认是\n换行符。
不过另一个问题是给定一个文本文件,我们不知道该文件有多少行,解决方法是结合while和vector来处理。看代码

关于getline还需要多说几句。
一旦getline读到了标识字符,这里是\n,会停下来,这个字符会被丢掉。也就是说读到\n后,\n就丢了,下次再执行getline时从\n的下一个字符开始读取。
getline返回一个istream&。如果对流进行bool判定的话,如果流里还有内容就是true,如果没有了就是false。所以这里的while实际完成的是不断一行一行读取,当fin没内容后while就退出了,也就是文件读完了。
运行看效果

确实vecTxt里是文件一行一行的内容。
注意里面并没有\n,这是因为getline读到\n就丢弃了。
这个其实正是我们想要的,一般而言不需要在string刻意存储\n

另外引出一个问题:如果是空行会如何?

运行看效果

虽然结果符合预期,但是如何解释呢?
先看中间的那个空行。对于空行而言只有一个字符\n。根据getline的特性读到\n就停止读,并且把\n丢弃。于是对空行使用getline,得不到内容,仅仅是把\n丢掉,于是strLine为空,压入vector自然是空的。
最后一个空行为什么没有压入vector?
最后一行也只有一个\n,getline读到后丢弃,于是fin到达了末尾,没有内容了,while判定为false,所以就没有push_back这个动作了。这也是我们想要的结果。
如果空行是你想要的,那么很好。如果不是,可以在push_back前判定一下string内容,如果是空就不压

现在我们实现了一行一行读取文件,但是我们仍然需要从每一行中分别提取有用的信息,这有些麻烦,比如我们要进行字符串搜索,找到数字,然后转成整型等等。
有没有一种方法直接在读取文本内容时就转成我们想要的类型?可以的

三、格式化读取

大多数文本文件都是有固定格式的,这个格式是程序之间约定好的,这样不同程序都根据这个格式进行读写文件。比如说这里有这样一个人员数据信息文件

第一项是姓名,第二个是年龄,第三个是地址,第四个是身高,第五个是体重。以空格分隔。注意这点很重要,为什么重要后面解答。
每一行是一个人员,人员可以有很多,这里只展示3个。
现在我们要对这个格式的文本文件进行读取,把有用的信息提取出来。

固然可以使用一行一行读取的方法,然后对得到的字符串进行搜索、分解、转换,但是太曲折,更加直接的方式是直接从文本文件进行格式化读取。

首先定义一个结构体用于组织数据

很直观,不需要更多解释,接下来看看如何格式化读取文本信息

非常直观、简洁。我们使用了>>来提取格式化数据,比如第一个遇到的是Tom,把Tom变成string格式放入pp.strName;下一个遇到的是18,把18变成int类型然后放入pp.nAge,以此类推。
运行看效果

成功读入了格式化的数据,>>让一切都变得那么简单。

四、使用>>要注意的坑

everything comes with a price
使用>>的话格式必须严格符合才能够正常通过,如果输入有一点异常就会出问题,比如我们这样改一下输入文件

ss无法转换成int。如果运行会发生什么

程序卡住了。虽然在运行但是代码没法往下走,这是最要命的,比崩溃还麻烦,因为不断消耗CPU资源,而且除非外界干预否则停不下来。
为什么会卡住?
因为fin>>预期得到一个数字,但是给他的是字符,于是fin>>一直等待数字,但是对于给他的字符却不做处理,也没有其他人领走这个字符,于是就僵持在那里了。

当然这种恶意更改数据格式的情况不常见,但是接下来的情况倒是有可能,比如这样

多了一个空行,可能是意外混进去了,或者输出文件时发生意外,结尾也多了一个空行,这是endl的结果。这时会怎么表现呢?

最后那个Bob重复了。
仔细研究一下。中间那个空行并没有干扰到正常的fin>>。究其原因,当>>遇到空格换行这类字符会自动丢掉,于是\n就被忽略了,fin>>直接往下继续读。
最后为什么多了一行Bob?主要不是fin>>,而是!fin.eof()作祟。当程序读完Bob那行后,文件并没有结束,还有一个\n,即最后一行空行。此时while依然是true,进入循环,但是由于此时已经没有实质内容,fin>>提取不出信息。
但是注意pp仍然是有内容的,因为pp在循环之外声明的,所以上次读取的Bob的信息还留着,所以Bob的结构体再次被push_back,就出现了两次。
这也就是为什么在写文件一节说到:在文件末尾使用flush而不是endl

除了这些情况,使用>>依然要留意别的情况。
>>最好使用空格分隔数据,这是因为>>读到空格会认为数据结束,停下来并把空格丢掉。符合我们的预期。如果你使用别的分隔符,比如逗号,那么逗号之间只能是数字,如果有文本格式的数据就会悲剧。
先来看这样一个例子

这是一个逗号分隔的数据文件,意义不管他,注意最后一行有一个087,相当于非正常格式的数,看看>>能不能出处理。
同样先定义结构体

然后如下代码

跟之前用空格分隔的代码差别不大,唯一要注意的就是那个char c。这个是专门用来吃掉分隔符的,比如第一行读入104后,接下来是一个逗号,必须把这个逗号干掉才能继续往下读,不然就会卡住,于是我们使用一个字符专门来过滤分隔符。
看看运行结果

符合预期

明白了这种处理手法,就应该能理解为什么分隔符之间只能有数字了。
咱们试这样一个例子

还是刚才的人员表,不过不使用空格而是使用逗号分隔,仿照padding处理写出如下代码。

大图点击这里查看

看起来貌似没问题,运行一下看看

卡住了。为什么卡住了?
注意第一个数据是Tom,我们使用了string,但是Tom后面是一个逗号,逗号也是一个字符,所以实际上pp.StrName读入的是Tom,
这样一来那个分隔符逗号就消失了,接下来使用的>>c就是大问题了。
换句话说如果分隔符之间是字符串,那么用>>读入时一定会把分隔符也当作字符串读进去,结论是如果使用>>一定要使用空格作为分隔符,于是这也对我们写文件<<提出了相应的要求

五、自定义类型读写文本文件

既然>>的坑有点多,我们还有理由使用>>吗?答案是肯定的。是因为便捷吗?
上面展示的读入人员表的代码确实便捷,但是还有更重要:c++是面向对象的,如果像上述方法那样读文件会导致代码冗长。
我们设想的理想代码是fin>>class,自动把各个数据项读入到类中,这个可以使用重载>>做到。同时我们还可以重载<<做到简洁地写文件。
先来看看类是如何写的

大图点击这里查看

重载>>和<<有固定格式,这么写即可。
注意>>里面有可能抛出异常,这是为了应对意外文件格式

再来看看如何使用,即main部分。代码是非常简洁的,跟上面的区别在于只有类的>>而没有展示内部成员变量,实际上完全可以把对象放到while循环内部。运行看效果

成功读入!继续运行完看输出文件

也成功了,不过注意因为代码中使用了范围for循环,所以最后多了一个endl。如果不想有这个只能额外处理一下了。

再来试试输入异常的情况,把18改成ss,运行

成功抛出了异常,这也意味着我们可以应对意外输入的情况了。

结论就是:使用类重载>>和<<来进行读写文件
代码简洁,逻辑清晰,能够处理意外输入,没有理由拒绝它。

有一点要指出:使用<<和>>的效率要慢一点,但是硬件升级的代价要远小于代码难以维护,只要不是特别在意效率的场合,使用>>和<<。

六、处理格式不一致的文本文件

至此我们碰到过的文件格式都是一致的,实际上你应该制定一致的文件格式,这样方便处理。不过有些情况下文件格式会略有变化,并不完全一致,这种情况如何处理呢?
答案就是一行一行读取,然后使用string流来处理,看这样一个例子

还是刚才的人员表,但是末尾增加了一些数字,代表可联系到该人员的电话。电话可能有多个,这是符合常理的,但是这给读文件造成了困难,因为我们不知道确切的个数。实际上如果可能可以考虑输出文件时增加一个标志位指明个数。但是现在我们只拿到了这个文件,只能硬着头皮改了
为了处理这种不定的情况,我们只能先使用通用的办法:一行一行读入,再做处理。先定义结构体。为了集中展示sstream就不做成类了

一行一行读取我们知道如何操作,关键是读取之后怎么办,这里的要点是把string当作流来处理。看代码

大图点击这里查看

注意循环里面的那个while循环,因为我们不知道到底有多少电话,所以我们就把string当作流不断读取,直到末尾
运行结果

可见是成功了。
这里只是展示使用sstream的一种用法,根据具体情况不同,考虑不同的处理手法。当然格式不确定也是有一定限度的,完全没有格式也是没法读取的。如果能保证,还是格式一致最好

至此读写文本文件结束,对于二进制文件可见《如何读写二进制文件》。