テキストデータの行単位処理
テキストファイルからの読み込みは、行単位で行うと便利なことが多い。しかし、C++での標準ストリームの演算子 >> では、空白文字などの区切り文字で区切られた文字列単位で読み込まれる。
例えば、プログラム5.1の場合、cin によってコンソールからデータが読み込まれるが、これは空白文字などで区切られた文字列単位で行われる。
プログラム5.1
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
char
str[1000];
do
{
cin
>> str;
if
(str[0] == 'z') break;
cout
<< str << endl;
}
while (true);
return
0;
}
プログラム5.1の実行例が図1に示されている。
図1
「1 23 456 7890」と数字キーを押してからEnterキーが押されているが、
cin >> str;
による読み込みは、「1」、「23」、「456」、「7890」と空白で区切られた文字列単位で do 文内のループによって行われている。
プログラム5.2では、cin の代わりにCの関数 fgets によって読み込みが行われている。C++では、Cの機能も使える。
プログラム5.2
#include <stdio.h>
#include <iostream>
using namespace std;
int main(){
char
str[1000];
do
{
fgets(str,
1000, stdin);
if
(str[0] == 'z') break;
cout
<< str << endl;
}
while (true);
return
0;
}
このプログラムの場合、図2に示されているように、図1と同じ入力に対して、「1 23 456 7890」が行単位で1度に str に読み込まれていることがわかる。
図2
関数 fgets は
fgets( 文字型配列, 読み込み文字数, ストリーム )
の形式で呼び出す。ストリームとして stdin を指定するとコンソール(キーボード)からの入力となる。
読み込み文字数は、文字列の終わりを示すchar(0)も含む。また、Enterキーが押されたときの’\n’文字も含まれる。読み込み文字数が、読み込みの対象となっている文字列行の文字数より小さいときは、読み込み文字数に対応するところまで読み込まれて、そこで一旦読み込みが中断されて次に進む。
プログラム5.2をプログラム5.3のように書き換えて実行してみると、図3のような結果になる。
プログラム5.3
#include
<stdio.h>
#include <iostream>
using namespace std;
int main(){
char
str[1000];
do
{
fgets(str,
7, stdin);
if
(str[0] == 'z') break;
cout
<< str << endl;
}
while (true);
return
0;
}
図3
プログラム5.3では、
fgets(str, 7, stdin);
と、読み込み文字数は7文字までとなっている。7文字目は文字列の終了を表す文字char(0) に使われるので、実際の読み込みは6文字分である。
関数fgets によって行単位で読み込むことが出来るが、読み込んだ行からデータを取り出すためのクラス型ExtrctStrを用意した。これを用いて、プログラム5.2をプログラム5.4のように書き直した。
プログラム5.4
#include <stdio.h>
#include <iostream>
#include "extrctstr.h"
using namespace std;
using namespace extrctsubstr;
int main() {
char
str[1000];
char
array_c[100];
ExtrctStr
extrct;
do
{
fgets(str,
1000, stdin);
if
(str[0] == 'z') break;
extrct.CheckStr(string(str));
for
(int i = 0; i < extrct.nR; i++)
cout
<< "<" << s_to_c(extrct.getStrR(i), array_c) << ">"
<< endl;
}
while (true);
return
0;
}
プログラム5.4を実行すると、図4のようになる。
図4
空白文字で区切られた文字列が1つずつ取り出されていることが分かる。
読み込んだ行からデータを取り出すためのクラス型ExtrctStrは、extrctstr.h および extrctstr.cpp に以下のように定義されている。
ヘッダーファイル extrctstr.h
#ifndef EXTRCTSTR_CK
#define EXTRCTSTR_CK
#include <iostream>
#include <vector>
namespace extrctsubstr {
using
namespace std;
//
// string型 s を、char
* 型に変換
//
char
* s_to_c(string s, char * c);
//
// 文字列を区切り文字で区切って取り出す。
//
class
ExtrctStr {
public:
string
seps;
vector<string>
wd_list; // 取り出した文字列を格納するベクトル
vector<string>
wd_listR; // 取り出した文字列の内、空文字列でないものだけを格納するベクトル
int
n_match, // 区切り文字の数
nR; // 空文字でない文字列の数
ExtrctStr()
{ seps = "\t \n,\f\r"; } // デフォルト区切り文字
ExtrctStr(string
str) { set_seps(str); } // 区切り文字の設定
void
set_seps(string str) {
seps
= str + "\n\f\r";
}
bool
checkChar(char c);
//
// str
に設定された文字列から区切り文字で区切られた文字列を取り出す
//
void
CheckStr(string str);
//
// 空の文字列も含めて p 番目の文字列を返す
// 文字列は 0 番目から数える
// p
が大き過ぎて該当する文字列がないときは空文字列を返す
//
string
getStr(int p);
//
// 空の文字列は無視して、p 番目の文字列を返す
// p
が大き過ぎて該当する文字列がないときは空文字列を返す
//
string
getStrR(int p);
};
}
#endif
ソースコードファイル extrctstr.cpp
#include "extrctstr.h"
namespace extrctsubstr {
using
namespace std;
char
* s_to_c(string s, char * c) {
if
(s.length() > 0)
for
(int i = 0; i < s.length(); i++)
c[i]
= s[i];
c[s.length()]
= 0;
return
c;
}
bool
ExtrctStr::checkChar(char c) {
bool
v = false;
for
(int i = 0; i < seps.length(); i++)
if
(c == seps[i]) v = true;
return
v;
}
void
ExtrctStr::CheckStr(string str) {
if
(str.length() <= 0) {
n_match
= -1;
wd_list.clear();
nR
= -1;
wd_listR.clear();
return;
}
n_match
= 0;
for
(int i = 0; i < str.length(); i++)
if
(checkChar(str[i])) n_match++;
wd_list.clear();
if
(n_match < 1) {
wd_list.insert(wd_list.end(),
str);
}
else
{
int
s_pos = -1, e_pos = 0;
for
(int k = 0; k < n_match; k++) {
while
(!checkChar(str[e_pos])) e_pos++;
if
(s_pos + 1 < e_pos) {
wd_list.insert(wd_list.end(),
str.substr(s_pos + 1, e_pos - s_pos - 1));
}
else
{
wd_list.insert(wd_list.end(),
"");
}
s_pos
= e_pos++;
}
if
(s_pos >= (str.length() - 1)) {
wd_list.insert(wd_list.end(),
"");
}
else
{
wd_list.insert(wd_list.end(),
str.substr(s_pos + 1));
}
}
nR
= 0;
wd_listR.clear();
for
(int i = 0; i < n_match + 1; i++) {
if
(wd_list[i] != "") {
wd_listR.insert(wd_listR.end(),
wd_list[i]);
nR++;
}
}
}
string
ExtrctStr::getStr(int p) {
if
(n_match < 0)
return
"";
string
str;
if
(wd_list.empty()) {
str
= "";
}
else
{
if
(p > n_match) {
str
= "";
}
else
{
str
= wd_list[p];
}
}
return
str;
}
string
ExtrctStr::getStrR(int p) {
if
(n_match < 0)
return
"";
string
str;
if
(nR < 1) {
str
= "";
}
else
if (p < nR) {
str
= wd_listR[p];
}
else
{
str
= "";
}
return
str;
}
}
ベクトルwd_list とwd_listR の違いは、空の文字列を保存しているかどうかである。区切り文字が連続すると隣り合う区切り文字の間には文字列がないが、このとき、空の文字列が挟まれているとして扱うものがwd_list であり、区切り文字が連続したときは一まとまりの区切りとして扱って空の文字列を無視したものがwd_listR である。
区切り文字は、文字列
string
seps;
に設定された文字列に現れる文字である。これは、以下のコード
ExtrctStr()
{ seps = "\t \n,\f\r"; } // デフォルト区切り文字
ExtrctStr(string
str) { set_seps(str); } // 区切り文字の設定
void
set_seps(string str) {
seps
= str + "\n\f\r";
}
によって設定される。
データは、ファイルから読み込まれることが多い。ファイルから入力する場合のプログラムを以下のように作成してみた。
プログラム5.5
#include <iostream>
#include <fstream>
#include "extrctstr.h"
using namespace std;
using namespace extrctsubstr;
int main() {
cout
<< "Input file name = ";
char
fin_name[100];
cin
>> fin_name;
FILE
*fin;
if
(fopen_s(&fin, fin_name, "r") != 0) {
cout
<< "Error in opening the input file..." << endl;
cout
<< "Input file name = " << fin_name << endl;
cout
<< "Press any character key, then Enter key." << endl;
char
ck;
cin
>> ck;
return
0;
}
cout
<< "Output file name = ";
char
fout_name[100];
cin
>> fout_name;
ofstream
fout(fout_name);
char
cstr[1000];
while
(true) {
if
(fgets(cstr, 1000, fin) == NULL) break;
fout
<< "data..." << endl;
int
ckpos = 0;
while
(cstr[ckpos] != 0) ckpos++;
if
(ckpos > 0)
if
(cstr[ckpos - 1] == '\n') cstr[ckpos - 1] = 0;
fout
<< "<" << cstr << ">" <<
endl;
ExtrctStr
extrct;
extrct.CheckStr(string(cstr));
fout
<< "n_match = " << extrct.n_match << endl;
for
(int i = 0; i < extrct.n_match + 1; i++) {
s_to_c(extrct.getStr(i),
cstr);
fout
<< i << ": <" << cstr << ">"
<< endl;
}
fout
<< "nR = " << extrct.nR << endl;
for
(int i = 0; i < extrct.nR; i++) {
s_to_c(extrct.getStrR(i),
cstr);
fout
<< i << ": <" << cstr << ">"
<< endl;
}
}
fclose(fin);
fout.close();
cout
<< endl << "Press any key, then Enter key." <<
endl;
char
ck;
cin
>> ck;
return
0;
}
プログラム5.5を図5を入力ファイルとして実行すると、図6のようになる。
図5
図6
出力ファイルResult.txtの内容は、以下のようになっている。
data...
<>
n_match = -1
nR = -1
data...
<1>
n_match = 0
0: <1>
nR = 1
0: <1>
data...
< 1 23 456 7890 12345>
n_match = 16
0: <>
1: <1>
2: <>
3: <23>
4: <>
5: <>
6: <456>
7: <>
8: <>
9: <>
10: <7890>
11: <>
12: <>
13: <>
14: <>
15: <>
16: <12345>
nR = 5
0: <1>
1: <23>
2: <456>
3: <7890>
4: <12345>
data...
<ab c de,fgh ,ijk>
n_match = 6
0: <ab>
1: <c>
2: <>
3: <de>
4: <fgh>
5: <>
6: <ijk>
nR = 5
0: <ab>
1: <c>
2: <de>
3: <fgh>
4: <ijk>
data...
<end>
n_match = 0
0: <end>
nR = 1
0: <end>
プログラム5.5のソースコードファイルなどは圧縮ファイルCheckExtrctStr_f2.zipとして用意した。クリックしてダウンロード・展開すれば、Visual Studio 2015で開くことができる。