こんにちは、ナナです。
C言語のプログラムで文字列として構成された複数の情報を、「変数」や「構造体」へと取り込みたいというシーンがあります。
代表的な例が、CSVファイルの読み込みといったシーンです。CSVファイルとは次のような、カンマ区切りの文字情報のことです。
このようにsscanf関数を利用するとかなり複雑な文字列情報であっても、「文字」「整数」「浮動小数点数」などを切り分けながら変数へと格納することが可能です。
本記事では次の悩みを解消する内容となっています。
では、「文字列」から複数の情報を抜き取る方法を学んでいきましょう。
sscanf系の標準ライブラリ関数を紹介
sscanf関数には、セキュア版も含めて2種類の関数が存在します。
#include <stdio.h>
int sscanf(const char * buffer, const char * format, ...); // non secure
int sscanf_s(const char * buffer, const char * format, ...); // secure version
sscanfとは「s(string:文字列)」「scan(読み込む)」「f(format:書式)」の3つの要素から構成されている、「書式指定可能な文字列読み取り関数」です。
いずれの関数も可変個引数で、複数の引数を指定できる構成となっています。
sscanf_s関数は安全性を高めた関数設計になっています。この2つの違いの詳細は記事終盤で解説しましょう!
sscanf関数の仕様
sscanf関数は、第1引数で入力された文字を、第2引数のフォーマットに従い解析(パース)して、指定された変数へ変換して格納します。
includeファイル | stdio.h |
関数仕様 | int sscanf(const char * buffer, const char * format, …); |
引数1 | 解析対象の入力文字列へのポインタ。 |
引数2 | 解析するフォーマットを指定する。 フォーマットには後述の変換指定子が使用できる。 |
引数3以降 | 解析フォーマットに応じた変数を必要なだけ順に並べる。 |
戻り値 | 実際に変換ができた個数。 |
特記事項 | 第3引数以降は解析フォーマットと変数の型と数を合わせる必要がある。 |
解析フォーマットで指定できる基本的な変換指定子は次のものです。
変換指定子 | 変数データ型 | 詳細 |
---|---|---|
%hhd | char unsigned char | 10進数で解釈し、1バイト変数へ格納する |
%hd | short unsigned short | 10進数で解釈し、2バイト変数へ格納する |
%d | int unsigned int | 10進数で解釈し、int型変数へ格納する |
%ld | long unsigned long | 10進数で解釈し、4バイト変数へ格納する |
%hhx | char unsigned char | 16進数で解釈し、1バイト変数へ格納する |
%hx | short unsigned short | 16進数で解釈し、2バイト変数へ格納する |
%x | int unsigned int | 16進数で解釈し、int型変数へ格納する |
%lx | long unsigned long | 16進数で解釈し、4バイト変数へ格納する |
%f | float | 単精度浮動小数点数で解釈し、float型変数へ格納する |
%lf | double | 倍精度浮動小数点数で解釈し、double型変数へ格納する |
%c | char | 1文字として解釈し、char型変数へ文字として格納する |
%s | char * | 文字列として解釈し、文字列を格納する。 |
%p | void * | 16進数の番地として解釈し、番地を格納する。 |
非常に多くのパターンがありますが、規則性がありますので覚えておくと便利です。
番号 | 役割 | 指定可能な記号 | 説明 |
---|---|---|---|
① | サイズ指定 | hh、h、l、ll | 出力対象となる変数のデータ型のサイズを示す。 hh:1バイト、h:2バイト、l:4バイト、ll:8バイト 省略時はint型サイズ。 |
② | 進数指定 | d、o、x | 入力対象となる文字列の進数の解釈方法を示す。 d:10進数、o:8進数、x:16進数 |
①②の記述順番は決められています。”%dhh”といった指定では警告が発生し、正常に動作しないため注意しましょう。
sscanf関数のプログラム使用例
まずは、扱うのが簡単な方の「sscanf関数」から利用してみましょう。ただし、最初に伝えておかなければならないことがあります。
Visual Studio 2017 環境では、ノンセキュア版であるsscanf関数の使用は推奨されておらず、使用すると、次のようなエラーが発生します。
「error C4996 ‘sscanf’: This function or variable may be unsafe. Consider using sscanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. 」
「sscanf関数」は安全ではないため「sscanf_s関数」を使うように促しています。ただし、「
_CRT_SECURE_NO_WARNINGS」をマクロ定義することで使用することができます。
ただし、次のようにstdio.hのインクルードよりも手前でマクロ定義をする必要があることに注意しましょう。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char * pMoji = "123";
int num;
// "123"を123へ変換
sscanf(pMoji, "%d", &num);
printf("%d", num); // 123が表示される
return 0;
}
このプログラム例は最も基本的なsscanf関数の使い方になります。
まずは、sscanf関数の引数の指定方法を把握しましょう。
sscanf関数による複数の情報取得例
sscanf関数は1つだけでなく、複数の情報を一括で変数へ取り込むことも可能です。次の例はスペース区切りの2つの数字を、異なる変数へ数値として取得するプログラムです。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char * pMoji = "29 179";
char age = 0;
short height = 0;
// "29"と"179"をage変数とheight変数へ数値へ変換して格納
sscanf(pMoji, "%hhd %hd", &age, &height);
printf("年齢:%hhd\n", age);
printf("身長:%hd\n", height);
return 0;
}
年齢:29
身長:179
変換指定子には2つの “%hhd” と “%hd” を指定することにより、文字列の中から2つの数字を抜き出して、変数に格納することができたのがわかりますね。
変換指定子を指定した数だけ、入力文字の中から文字や数字を抜き取ることができます。これがsscanf関数の便利なところです。
様々な型に対する変換指定子が存在する理由
変換指定子に非常に多くのバリエーションがあることがわかりますが、いったいなぜこんなに多くの種類があるのでしょうか?
それは、sscanf関数が可変個引数であることが理由です。このように3つ目以降の引数が省略されているのがわかりますね。これが可変個引数です。
int sscanf(const char * buffer, const char * format, ...);
可変個引数のメリットは、引数の構成を自由にカスタマイズできることにあります。しかし、それが故に可変個部分のデータ型を固定化できない側面があります。
このように変数の番地を引数で渡されたものの、sscanf関数からは番地先にアクセスするためのデータ型がわからないのです。
この問題を解決するのが、変換指定子による「データ型」の特定です。
第3引数以降に入力された番地の参照先のデータ型を、変換指定子によって表現し「データ型」を特定しているのです。
メモリにアクセスするためには「場所」と「データ型」が必要なんでしたね。「データ型」の情報を変換指定子から得ているということです。
このあたりが理解できていない人は、次のポインタの記事をしっかりと読み込むとよいでしょう!
》参考:ポインタのメリットと必要性【なぜなぜから真相に迫る】
sscanf関数を使った様々なデータの取得例:サンプルプログラム集
ここからは様々な情報を取得するためのサンプルプログラムを紹介します。
日付形式文字列
日付のような文字列から、「年」「月」「日」の数値を抜き出すプログラムです。
入力文字列:”2020 4 1″、”2020/4/1″、”2020年4月1日”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
int year = 0;
short month = 0;
char day = 0;
// スペース区切り形式
sscanf("2020 4 1", "%d %hd %hhd", &year, &month, &day);
// スラッシュ区切り形式
sscanf("2020/4/1", "%d/%hd/%hhd", &year, &month, &day);
// 漢字区切り形式
sscanf("2020年4月1日", "%d年%hd月%hhd日", &year, &month, &day);
return 0;
}
区切りの文字をフォーマット指定側にも入れてあげることで、目的の数字のみを数値として抜き出しています。
文字列、整数、浮動小数点数の複合情報の場合
文字列の中に様々な情報が混在しているようなケースもあるでしょう。
入力文字列:”錦織圭 1989年12月29日 178cm 75.2kg”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char name[32]; // 名前
long year; // 年
short month; // 月
char day; // 日
int height; // 身長
double weight; // 体重
sscanf("錦織圭 1989年12月29日 178cm 75.2kg", "%s %ld年%hd月%hhd日 %dcm %lfkg",
name, &year, &month, &day, &height, &weight);
return 0;
}
文字情報、整数、浮動小数点数など様々な形式を、分離して各変数へ格納することができます。
このような文字列情報をファイルから1行ずつ読みだして、プログラム内に取り込むといったこともsscanf関数を使えば簡単にできます。
16進数形式のバイナリデータ
16進数で表現された次のようなメモリ情報を、16進数の情報として取り込む例になります。
入力文字列:”0x0119F9E7 01 19 04 bc 75 00 c0 26″
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
unsigned char * padr = NULL;
unsigned char data[8] = { 0 };
sscanf("0x0119F9E7 01 19 04 bc 75 00 c0 26", "0x%p %hhx %hhx %hhx %hhx %hhx %hhx %hhx %hhx",
&padr, &data[0], &data[1], &data[2], &data[3], &data[4], &data[5], &data[6], &data[7]);
return 0;
}
このように、16進数の数字を、16進数の数値として取り込むことが可能です。先頭の4バイトに関しては番地として解釈し、ポインタ変数に格納しているのも特徴的です。
変換指定子に「%x」を指定することで、入力文字を16進数の文字として扱うことができます。
「数字」と「文字」が連続しているデータ
空白といった記号で区切られていない「数字」と「文字」が連続しているときに切り分けて取り込む例です。
入力文字列:”21世紀”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
int num = 0;
char moji[16] = {0};
sscanf("21世紀", "%d%s", &num, moji);
printf("num:%d\n", num);
printf("moji:%s\n", moji);
return 0;
}
num:21
moji:世紀
「数字」+「文字」の場合は”%d%s”とすることで「数値」と「文字列」を自動で切り分けてくれました。
「文字」と「数字」が連続しているデータ
それでは「文字」+「数字」の場合はどうでしょう。この場合は注意が必要です。
入力文字列:”世紀21″
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
int num = 0;
char moji[16] = {0};
sscanf("世紀21", "%s%d", moji, &num);
printf("num:%d\n", num);
printf("moji:%s\n", moji);
return 0;
}
num:0
moji:世紀21
「数字」も含めて全てが文字列の変数側に格納されてしまいました。これは”21″も含めて「文字」と判断されたためです。
このような場合は「文字」として扱う範囲を明確にする書式があります。次のような細工を施すと切り分けることができます。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
int num = 0;
char moji[16] = {0};
sscanf("世紀21", "%[^0-9]%d", moji, &num);
printf("num:%d\n", num);
printf("moji:%s\n", moji);
return 0;
}
num:21
moji:世紀
変換指定子の”%[]”の使い方は後述しましょう。
このような文字列を抜き取るケースでは、場合によっては別々に切り分けたい部分も連続した文字と判断されてしまうことがあるので注意です。
CSV形式のデータを取り込む特殊な変換指定子の使い方
CSV形式のデータとはカンマ「,」で区切られた連続データのことを示します。
大量のデータを扱うときに、データをCSV形式でファイルに保存しておくといったことは、よく行われる手法です。
C言語でCSV形式のデータを読み込みたい場合も、sscanf関数は利用できます。しかし、この形式のデータも扱う際にも注意が必要です。
CSV形式データの取り込みが正常にできないプログラム
まずは、正常に取り込みができないプログラム例を示しましょう。
入力文字:”錦織圭,日本,1989年12月29日,178cm,75.2kg”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char name[128] = { 0 }; // 氏名
char country[128] = { 0 }; // 国籍
int year = 0; // 誕生年
int month = 0; // 誕生月
int day = 0; // 誕生日
int height = 0; // 身長
double weight = 0; // 体重
int ret = 0; // 戻り値
ret = sscanf("錦織圭,日本,1989年12月29日,178cm,75.2kg", "%s,%s,%d年%d月%d日,%dcm,%lfkg",
name, country, &year, &month, &day, &height, &weight);
printf("戻値:%d\n", ret);
printf("氏名:%s\n", name);
printf("国籍:%s\n", country);
printf("年:%d\n", year);
printf("月:%d\n", month);
printf("日:%d\n", day);
printf("身長:%d\n", height);
printf("体重:%lf\n", weight);
return 0;
}
戻値:1
氏名:錦織圭,日本,1989年12月29日,178cm,75.2kg
国籍:
年:0
月:0
日:0
身長:0
体重:0.000000
よく見ると戻り値が「1」になっていますね。これは変換が1つだけ成功したことを表しています。
「氏名」を格納すべきname配列に全ての文字が取り込まれてしまいました。カンマも含めて文字として判断されたのが原因です。
このように、カンマ「,」という記号も文字として判断されるケースがあります。
CSV形式データを正しく読み込むプログラム
それではプログラムを次の形式に変更しました。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char name[128] = { 0 }; // 氏名
char country[128] = { 0 }; // 国籍
int year = 0; // 誕生年
int month = 0; // 誕生月
int day = 0; // 誕生日
int height = 0; // 身長
double weight = 0; // 体重
int ret = 0; // 戻り値
ret = sscanf("錦織圭,日本,1989年12月29日,178cm,75.2kg", "%[^,],%[^,],%d年%d月%d日,%dcm,%lfkg",
name, country, &year, &month, &day, &height, &weight);
printf("戻値:%d\n", ret);
printf("氏名:%s\n", name);
printf("国籍:%s\n", country);
printf("年:%d\n", year);
printf("月:%d\n", month);
printf("日:%d\n", day);
printf("身長:%d\n", height);
printf("体重:%lf\n", weight);
return 0;
}
戻値:7
氏名:錦織圭
国籍:日本
年:1989
月:12
日:29
身長:178
体重:75.200000
正しく読み込めるようになりました。戻り値も7個の変換が正常に終わったとされています。
変更点としては、フォーマット指定が一部変更されています。
"%s,%s,%d年%d月%d日,%dcm,%lfkg" // 変更前
"%[^,],%[^,],%d年%d月%d日,%dcm,%lfkg" // 変更後
変換指定子として”%[]”という形式が再び登場しましたね。それでは、この変換指定子を解説しましょう。
特殊な変換指定子の使い方:%[]指定とは
“%s”は文字列を取得するための変換指定子ですが、次の2パターンの使い方があります。
変換指定子 | 使用例 | 意味 |
---|---|---|
“%[(文字)]” | “%[a-zA-Z]” | 文字としてa~zとA~Zを含める。 それ以外の文字が登場したら終了。 |
“%[^(文字)]” | “%[^0-9]” | 文字として0~9以外を含める。 0~9の文字が登場したら終了。 |
変換指定子:%[]を使ったプログラム
変換指定子の%[]は、括弧の中で指定した文字であれば連続した文字列とみなします。
例えば次のようなメールアドレスで、「ユーザー名」@「ドメイン名」をそれぞれ別の文字列で抜き出したいとします。
ユーザー名の制約として英字しか認めないものとした場合は、次のようにプログラムできます。
入力文字:”abcXYZ@gmail.com”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char user[32];
char domain[32];
sscanf("abcXYZ@gmail.com", "%[a-zA-Z]@%s", user, domain);
printf("ユーザー名:%s\n", user);
printf("ドメイン名:%s\n", domain);
return 0;
}
ユーザー名:abcXYZ
ドメイン名:gmail.com
変換指定子:%[^]を使ったプログラム
変換指定子の”%[^]”は、括弧の中に指定した文字が出てくるまで連続した文字とします。
ユーザー名の制約として@以外の記号は認める場合は、次のようにプログラムできます。
入力文字:”!abc_XYZ#@gmail.com”
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char user[32];
char domain[32];
sscanf("!abc_XYZ#@gmail.com", "%[^@]@%s", user, domain);
printf("ユーザー名:%s\n", user);
printf("ドメイン名:%s\n", domain);
return 0;
}
ユーザー名:!abc_XYZ#
ドメイン名:gmail.com
このように、文字列を継続する条件を指定することで、文字の切れ目をコントロールすることができるのです。
CSV形式データの場合は”%[^,]”とすることで、カンマが登場するまでは文字列として扱ったということになりますね。
sscanf_s関数とsscanf関数の使い方の違い
ここまでsscanf関数を扱ってきましたが、冒頭で述べた通りこの関数の使用は推奨されていません。それはバッファオーバーランを発生させやすく、危険性があるためです。
sscanf関数の危険性とは?
例えば、次のようなプログラムはuser配列の領域をオーバーランして破壊してしまいます。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char user[10] = { 0 };
char domain[10] = { 0 };
int ret = 0;
ret = sscanf("abcdefghijklmn@gmail.com", "%[^@]@%s", user, domain);
printf("戻り値:%d\n", ret);
printf("ユーザー名:%s\n", user);
printf("ドメイン名:%s\n", domain);
return 0;
}
このように用意していた文字配列以上に、文字列がコピーされてしまうケースがあり危険性があるのです。
sscanf_s関数の使い方
使い方の違いは文字列である”%s”、文字である”%c”を使う時に出てきます。
これらの変換指定子を使う場合は、書き込み先変数の次の引数に配列サイズを指定することになります。
#include <stdio.h>
int main(void)
{
char user[10] = { 0 };
char domain[10] = { 0 };
int ret = 0;
ret = sscanf_s("abcdefghijklmn@gmail.com", "%[^@]@%s",
user, sizeof(user), domain, sizeof(domain));
printf("戻り値:%d\n", ret);
printf("ユーザー名:%s\n", user);
printf("ドメイン名:%s\n", domain);
return 0;
}
戻り値:0
ユーザー名:
ドメイン名:
ユーザー名のバッファオーバーランを検知してコピー処理を実施していません。このようにsscanf_s関数は不正なプログラムに対して安全性が高くなっています。
簡単な文字列を数値に変換したいならatoi関数が使用できます。用途によって使い分けましょう!
》参考:atoi関数【文字列の数字を数値へ変換する簡単な方法】