連携処理2:テキスト処理

これまでにも利用してきた文字列処理のための 標準ライブラリ関数の仕組を理解しよう. そして,プログラミング言語処理系などで必要となるテキスト処理 (テキストからトークンへの分解,トークンの解釈・変換,等) の基礎部分を作成してみよう.

教科書の該当範囲:第2.7,4.2,7.7,7.8節
参考書の該当範囲:第12.7,13.2,13.4節

文字列の入出力と変換

文字列の入出力

文字入出力の標準ライブラリ関数:(stdio.h

文字列入出力の標準ライブラリ関数:(stdio.h

では,fgets()fputs() のクローンを作成し, それらの仕組みを理解しよう. (最も基本的な入出力関数 fgetc()fputc() については, そのまま利用する.)

lio.c

#include <stdio.h>
#include <stdlib.h>

/*
行文字列を入力する標準ライブラリ関数 fgets() のクローン
buf:行文字列のバッファ配列
n:バッファサイズ
fp:入力ファイルのポインタ
*/
char *myfgets(char *buf, int n, FILE *fp)
{
	int	i;	// 文字数のカウンタ
	int	c;	// 文字が入力されるよ
	char	*s;	// buf 内の文字へのポインタ

	s = buf;
	n--;			// 終端記号の余地を空けておく
	for (i = 0; i < n; i++) {	// 最大 n 文字まで...
		c = fgetc(fp);		// 文字を入力
		if (c == EOF) break;

		*s = (char)c;		// 文字を buf に書き込む
		s++;

		if (c == '\n') break;	// 改行文字(書込済)で入力を終了
	}
	if (s == buf) return (NULL);	// 何も入力していない場合,関数を終了
	*s = '\0';		// 終端記号を書き込む
	return (buf);
}

/*
行文字列を出力する標準ライブラリ関数 fputs() のクローン
s:行文字列のポインタ
fp:出力ファイルのポインタ
return:正常終了では 0,異常終了では EOF
*/
int myfputs(char *s, FILE *fp)
{
	while (*s != '\0') {
		if (fputc((int)*s, fp) == EOF) return (EOF);
		s++;
	}
	return (0);
}

#define	BUFLEN	256

int main(void)
{
	char	buf[BUFLEN];

	while (1) {
//		if (fgets(buf, BUFLEN, stdin) == NULL) break;
//		fputs(buf, stdout);

		if (myfgets(buf, BUFLEN, stdin) == NULL) break;
		myfputs(buf, stdout);
	}
	return (EXIT_SUCCESS);
}
$ ./fio < fio.c | less

ライブラリ関数とクローン関数とが同じ結果となるか確認しよう.

数字列の数値化

数字列(数字の文字列)を数値化したり, その逆も必要な場合がある.

文字種を検査する標準ライブラリ関数:(ctype.h

数字列(数字の文字列)を数値化する標準ライブラリ関数:(stdlib.h

標準ライブラリ関数 atoi() のクローンを作成し, その仕組みを理解しよう.

atoi.c

#include <stdio.h>
#include <stdlib.h>	// atoi(), etc.
#include <ctype.h>	// isspace(), isdigit(), etc.

/*
数字列を整数値へ変換する関数 atoi() のクローン
str:数字列
return:整数値
*/
int myatoi(char *str)
{
	int	val;	// 数値の絶対値
	int	sgn;	// 数値の符号 +1/-1
	char	*s;	// 数字列内の数字へのポインタ

	// 先頭の空白を除去
	s = str;
	while (isspace(*s)) s++;

	// 符号を取得
	sgn = +1;
	if (*s == '-') {
		sgn = -1; s++;
	} else if (*s == '+') {
		s++;
	}

	// 絶対値を算出
	val = 0;
	while (isdigit(*s)) {
		val = val*10 + (int)(*s - '0');
		s++;
	}

	return (val*sgn);
}

#define	BUFLEN	256

int main(void)
{
	char	buf[BUFLEN];

	while (1) {
		printf("行文字列 > ");
		if (fgets(buf, BUFLEN, stdin) == NULL) break;
		printf("atoi()   : %d\n", atoi(buf));
		printf("myatoi() : %d\n", myatoi(buf));
		printf("\n");
	}
	printf("終了\n");
	return (EXIT_SUCCESS);
}
$ ./atoi
行文字列 > +123
atoi()   : 123
myatoi() : 123
...

文字列の分解

空白区切によるトークンの抽出

行文字列からトークン(単語,部分文字列)を取り出してみよう. この処理は,sscanf(..., "%s", ...) や argv などで利用される.

例:"var = var + 123 ;""var""=""var""+""123"";"

トークン抽出用の標準ライブラリ関数 strtok() もあるが, 元の文字列の内容を書き換えてしまう等, 少々クセのあるものなので,今回は利用しないでおく.

tok-1.c

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>	// strcmp()

/*
文字列から単語トークンを1個だけ抽出する関数
(要するに sscanf(str, "%s", tok) の拡張版)
str : 処理対象の行文字列
tok : トークンの文字列バッファ
return : 次に処理すべき文字のアドレス
*/
char *sgetword(char *str, char *tok, int n)
{
	char	*s;	// 行文字列内の文字へのポインタ
	char	*t;	// トークン内の文字へのポインタ
	int	i;	// 文字番号のカウンタ

	s = str;	// or ... s = &str[0];
	t = tok;	// or ... t = &tok[0];		or ... なし

	while (isspace(*s)) s++;	// 先頭の空白文字を無視

	n--;		// 終端記号の分だけコピー可能な文字数を減
	for (i = 0; i < n; i++) {
		if (*s == '\0') break;
		if (isspace(*s)) break;
		*t = *s; t++;	// or ... tok[i] = *s;
		s++;
	}
	if (t == tok) return (NULL);	// トークンなしの場合,エラー
	*t = '\0';	// トークンを終端		or ... tok[i] = '\0';

	return (s);
}

#define	BUFLEN	256

int main(void)
{
	char	line[BUFLEN];	// 行文字列のバッファ配列
	char	tok[BUFLEN];	// トークン文字列のバッファ配列
	char	*expr;		// line 内の次のトークンへのポインタ

	while (1) {
		printf("数式 > ");
		if (fgets(line, BUFLEN, stdin) == NULL) break;

		expr = line;
		while (1) {
			expr = sgetword(expr, tok, BUFLEN);
				// tok を抽出,expr を更新
			if (expr == NULL) break;
			if (strcmp(tok, "quit") == 0) goto END;
			printf("[%s]\n", tok);
		}
	}
END:
	printf("終了\n");
	return (EXIT_SUCCESS);
}
$ ./tok-1
数式 > 123 + 45
[123]
[+]
[45]
数式 > 123+45
[123+45]
...

このようにテキストをトークンに分解できれば, 数式計算やスクリプトの処理に応用できそうだ. しかし,空白を省略できないのは不便だ...

文字種別のトークンの抽出

区切文字だけに頼らずにトークンを抽出する方法として, 文字種の変化部分を区切とみなしてみよう. (もちろん空白区切もそのまま使えるように.)

例:"var=var+123;""var""=""var""+""123"";"

tok-2.c:(tok-1.c を元に改造)

...

/*
文字列から数字トークンまたは数字以外トークンを1個だけ抽出する関数
数字トークン:数字だけを含む文字列
数字以外トークン:数字以外・空白以外だけを含む文字列
(sgetword() の改良版)
str : 処理対象の行文字列
tok : トークンの文字列バッファ
return : 次に処理すべき文字のアドレス
*/
char *sgettok(char *str, char *tok, int n)
{
	char	*s;
	char	*t;
	int	i;
	int	d0, d1;	// 直前と現在の数字フラグ(数字ならゼロ以外)

	s = str;
	t = tok;

	while (isspace(*s)) s++;

	d0 = isdigit(*s);		// 先頭の文字の数字フラグ
	n--;
	for (i = 0; i < n; i++) {
		if (*s == '\0') break;
		if (isspace(*s)) break;

		d1 = isdigit(*s);	// 現在の文字の数字フラグを設定
		if (d1 != d0) break;	// 文字種が変わった場合,トークンを終端

		*t = *s; t++;
		s++;

		d0 = d1;		// 直前の文字の数字フラグを更新
	}
	if (t == tok) return (NULL);
	*t = '\0';

	return (s);
}

int main(void)
{
	...
	while (1) {
		...
		while (1) {
//			expr = sgetword(expr, tok, BUFLEN);
			expr = sgettok(expr, tok, BUFLEN);
			...
		}
	}
	...
}
$ ./tok-2
数式 > 123 + 45
[123]		# 空白区切は tok-1 と同様
[+]
[45]
数式 > 123+45
[123]		# 空白なしでも分解できた
[+]
[45]
...

これで数式計算にも便利に使えそうだ.


本日の課題

問題 A 〜 C のいずれか1問に取り組め:

  1. 標準ライブラリ関数 double atof(char *s) のクローン double myatof(char *s) を定義せよ.
  2. (もちろん,テスト用メイン関数なども必要. ソースファイル名を atof.c などとせよ.)

  3. 標準ライブラリ関数 int printf("%d", int val) のクローン int printd(int val) を作成せよ.
  4. (もちろん,テスト用メイン関数なども必要. ソースファイル名を printd.c などとせよ.)

  5. 今回作成した関数 sgettok() と標準ライブラリ関数 atoi() 等を利用し, 整数の四則演算だけからなる数式(文字列)を標準入力すると, その式の値を算出するプログラム calc.c を作成せよ.
  6. $ ./calc
    数式 > 123+45
    168
    数式 > 1*2-3
    -1
    数式 > 1+2*3
    9	# 本来なら 1+(2*3)=7 のハズだが...
    	# 簡単のため演算子の優先順位を考慮せず...
    	# (1+2)*3=9でよい
    数式 > -1
    式が変	# 最初のトークンは数字のハズ
    数式 > 1++2
    式が変	# 演算子は1文字のハズ
    ...
    
    要するに,計算言語インタプリタ bc の劣化版クローンです.

    簡単のための仮定・制限:

    • 数式は整数の四則演算だけからなる文字列とする,
    • 演算子の優先順位は考慮しない.
    • (ゼロから数えて)偶数番目のトークンは数字の文字列, 奇数番目のトークンは演算子の文字列とする.
    • 演算子の文字列長は1.

なお,数字列のバッファ配列が必要となる場合, int型の整数の表現可能範囲(±20億程度) が最大 10桁であることを利用して, バッファサイズを決め打ちしてよい.

提出:

  • 方法: 電子メール
    • 宛先:yanagawa@kushiro-ct.ac.jp
    • 件名:c-1016
    • 発信者:p16xxxx@cc.kushiro-ct.ac.jp
    • 「p16xxxx」は各自のユーザ名ですよ. もし,発信者アドレスが変な場合,メールの設定を変更... [設定]→[識別情報]→識別情報:p16xxxx@...→ 電子メール:p16xxxx@cc.kushiro-ct.ac.jp
  • 期限: 10月29日(月)17:00
  • 内容(本文):
    • 学年学科,出席番号,氏名
    • 各問のソースコードと実行結果

(c) 2018, yanagawa@kushiro-ct.ac.jp