接上一篇文章,笔者给大家介绍一个更加简单的解析工具,那就是sscanf。
热身
大家在上C语言课,做C语言课程设计或实验时,应该经常接触printf和scanf,前者打印字符中到标准输出,而后者从标准输入读取并解析字符串。
sscanf和scanf类似,只不过它并不从标准输入读取,而是直接解析用户传入的字符串。
int sscanf(const char *str, const char *format, ...);
- str 待解析的字符串
- format 格式化参数
- … 变长参数,为一系列用于存放解析结果的变量的地址
- 返回成功解析的字段数量
格式化参数是啥意思?我们先来热身下。
static void test(void)
{
const char *str = "Today is 2021.7.31";
int year = 0;
int month = 0;
int day = 0;
int ret;
ret = sscanf(str, "Today is %d.%d.%d", &year, &month, &day);
printf("ret=%d, year:%d, month:%d, day:%d\n", ret, year, month, day);
}
"Today is %d.%d.%d" 为格式化参数,里面有普通字符,如Today is ,和格式说明符,%d 表int类型。sscanf依据格式化参数来解析str。对于普通字符,sscanf检查str是否与其一致。对于格式说明符,则按其含义来提取str中的内容,并将结果存入地址参数中。上述代码演示了提取年月日信息的方法,结果如下:
ret=3, year:2021, month:7, day:31
如果还是对格式化参数没有印象的话,同学,你肯定没认真上C语言课,要不翻书复习下呗。笔者今天不是想从零开始讲sscanf,而是介绍一个鲜为人知的用法。
问题
再看一个例子,解析域名和端口号。
static void test2(void)
{
const char *str = "www.baidu.com:80";
char addr[64] = "";
int port = 0;
int ret;
ret = sscanf(str, "%s:%d", addr, &port);
printf("ret=%d, addr:%s, port:%d\n", ret, addr, port);
}
%s 用于解析字符串(域名),%d 用于解析int(端口号)。结果如下:
ret=1, addr:www.baidu.com:80, port:0
这并不是我们期望的结果,sscanf将域名和端口号都当字符串来解析了。这是因为%s对应的字符串,遇到空白字符(空格、换行)或者是’\0’才算结束。结束之后,sscanf才会去理会:%d 。
上述情况属于:想让字符串结束却没结束,从而解析了过多的内容。有时还会遇到相反的情况,请看下一个例子:
static void test3(void)
{
const char *str = "how are you";
char buf[64] = "";
sscanf(str, "%s", buf);
printf("%s\n", buf);
}
本想解析出完整的how are you ,而输出的结果只有how 。
模式
使用%s来匹配字符串,会受到很大的限制。但这并不意为sscanf不好用,其还有一种匹配字符串的方法,那就是模式匹配。
模式的格式为:%[pattern] ,其中的pattern用于定义一个字符集,待匹配的字符串由这个字符集组成。pattern可以是多个字符,也可以使用- 定义一个范围,还可以使用^ 反向定义字符集。说着有点抽象,让我们看些具体的示例吧。
- %[abcd] 匹配由a,b,c和d组成的字符串。比如对于
abcdefg 这个字符串,其会匹配abcd 。 - %[^abcd] 当pattern由
^ 开头时,其匹配pattern字符集以外的字符。因此,对于gfedcba 这个字符串,其会匹配gfe 。 - %[0-9a-fA-F] 其组成的字符集为0123456789abcdefABCDEF,其实就是16进制数字字符。
当使用- 定义范围时,需要注意,起始字符必须小于结束字符。%[z-a] 匹配的就不是范围,而是z,-和a这3个字符。
如果你学过正则表达式的话,对上述模式应该很熟悉。只不过,sscanf提供的模式匹配的功能比正则表达式简单的多。笔者在知道sscanf的这种隐藏用法后,屡试不爽,用的最多的就是^ 。
现在大家知道如何解析域名和端口号了吗?
要不再思考一下?
好了,答案如下:
static void test2_fix(void)
{
const char *str = "www.baidu.com:80";
char addr[64] = "";
int port = 0;
int ret;
ret = sscanf(str, "%[^:]:%d", addr, &port);
printf("ret=%d, addr:%s, port:%d", ret, addr, port);
}
是不是非常简单,既然域名是: 之前的内容,那就定义为%[^:] 。
解析GPS
现在可以用sscanf来解析GPS了,GPS样例如下:
$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00
直接上代码:
static void parse_gps(const char *gps)
{
char valid = ' ';
double longitude = 0;
double latitude = 0;
int ret;
ret = sscanf(gps,
"$GNRMC,%*[^,],%c,%lf,%*c,%lf,%*c,",
&valid, &latitude, &longitude);
LOG_D("parse gps(%s)", gps);
if (ret != 3)
{
LOG_E("fail");
}
else
{
LOG_D("succeed, valid:%c, latitude:%lf, longitude:%lf", valid, latitude, longitude);
}
}
下图标出了格式化参数中各格式说明符对应的字段。 第二个说明符,%*[^,] 用于匹配时间122921.000 。与之前不同的是,这里多了一个* ,这表示不用解析对应字段的内容,后面的地址参数中也没有相关变量。你看,&valid, &latitude, &longitude 分别存放有效标志字符,纬度和纬度,并没有时间变量的地址。%*c 同理。
测试时,用3个用例进行测试,以测试成功和失败的场景。
void parse_string_example(void)
{
const char *strs[] =
{
"$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00",
"hello world",
"$GNRMC,,,,,,,,,,,,*00"
};
LOG_I("test parse string");
for (int i = 0; i < ARRAY_SIZE(strs); i++)
{
parse_gps(strs[i]);
}
}
结果如下:
文中完整的示例代码,参见笔者基于stm32f407创建的demo工程:
地址:git@gitee.com:wenbodong/mcu_demo.git
示例:examples/05_string/example.c
使用时需要打开examples/examples.h中的EXAMPLE_SHOW_STRING。
|