一般的字符串处理函数可以使用C标准库的string.h,字符串匹配、字符串反转、在字符串中查找子串位置等。但string.h仍然满足不了我们的需求。
来几个例题:(不允许使用c++)
1、要求把一个字符串两端的空白符(可能有多个空格和TAB)给去掉,
例如:“ asd123 qwer ”,要求处理完之后结果为:"asd123 qwer"。
有人可能会想,这么简单的功能,手写一个函数就能轻松搞定,但是如果每来一个新需求都要手写一个新函数才能完成它,那就太累了,我们将使用C标准库来完成这个事情。
2、要求把字符串的数字给删掉
例如:“ abc abc123.4--abc”要求处理完之后为:“ abc abc--abc”
3、判断一个字符串是不是网易邮箱,
例如:"[email protected]"判定成功,“[email protected]”判断成功,"egweg124"失败,"[email protected]"失败
C++的正则可以轻松搞定的事情,如果只用C就有点困难了
我们将使用sscanf函数来解决这个问题,在此之前先来学习一下sscanf的用法:
0、sscanf的返回值
它的返回值是匹配成功的次数,根据返回值我们可以检验一下是否提取成功(功能1),当然,也可以用来检测是否匹配(功能2)。
一、基本用法
1、从字符串中提取数字(int、double、float等都可以,类似printf)
char src[] = "temperature: 23.6 degree";
float resF;
int cnt = sscanf(src, "temperature: %f", &resF);//cnt结果为1
printf("%f", resF);//结果为:23.6
2、从字符串中提取子串
char src[] = "one two three four";//多个子串之间必须是空白符(如空格/TAB等)隔开的
char res[4][10];
int cnt = sscanf(src, "%s%s %s %s", res[0], res[1], res[2], res[3]); //格式串中的两个%s之间可以使任意空白符或没有间隔
printf("%s+%s+%s+%s\r\n", res[0], res[1], res[2], res[3]);//结果为:one+two+three+four
这个例子对子串的样式提了要求,限制了使用场景,后文会有更通用的解决办法。
二、高级用法
类似正则的格式串来提取或比较。
格式串的组成如下:%[*] [width] [{h|l|I64|L}] type
{h|l|I64|L}含义可以参考微软的手册:https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-fields-scanf-and-wscanf-functions?view=vs-2019 或者 man sscanf
如果链接失效,请自行搜索:sscanf MSDN,然后进入MSDN官网搜索sscanf
格式串%[*] [width] [{h|l|I64|L}] type中,中括号代表该段可由可无,我们发现,只有%和type字段是必须的,width字段、{h|l|I64|L}字段可有可无。
每一个%都要对应输出一个结果,
[*]代表该%段不向结果缓冲区输出(用正则术语来说就是:只匹配不捕获)
width字段代表从从上一个%段匹配完之后的子串开始,最多检查多少个字符。(一般可以填结果缓冲区的容量以避免越界)
type段是最常见的:
%c 一个单一的字符
%d 一个十进制整数
%i 一个整数
%e, %f, %g 一个浮点数
%o 一个八进制数
%s 一个字符串
%x 一个十六进制数
%p 一个指针
%n 一个等于读取字符数量的整数
%u 一个无符号整数
%[ ] 一个字符集 (正则就使用这个,而且仅支持贪婪模式,能匹配个多少就匹配多少个)
%% 一个精度符
尤其:在sscanf的type正则段中,^代表不允许包含
(1)提取开头和结尾的英文
char res1[5];
char res2[10];
sscanf("Abcd123--++?789efGH", "%5[a-zA-Z]%*[^a-zA-Z]%10[a-zA-Z]", res1, res2);
结果为:返回值=1,res1 = "Abcd", res2 = "efGH"
解释:格式串分了3段:
①%5[a-zA-Z],其中5代表:如果匹配成功的字符数<=5,那就输出实际的字符数;如果匹配成功的字符数>5,那么最多向结果缓冲区输出匹配出的5个字符,第二个%匹配段将从第6个字符开始检查。
②%*[^a-zA-Z],其中*号代表匹配出的结果不输出,[^a-zA-Z]代表匹配不含a-zA-Z的最长字符串。
③不再赘述,同①。
(2)只有以Tom打头的字符串才提取年龄值
uint8_t age;
int cnt = sscanf("Tom: 12", "Tom: %d", &age);//结果:cnt=1,age=12
//int cnt = sscanf("Lucy: 10", "Tom: %d", &age);//结果:cnt = 0(匹配成功了0个-->失败), age=随机值
解释:"Tom: %d"包含了两个%字段:
①Tom 代表匹配“以Tom: 打头,且后面跟着一个冒号一个空格”的字符串,但不捕获。
②%d,匹配一个整数并输出到结果缓冲区
(3)从一堆乱文中提取QQ邮箱
char src[] = "china dfashfkh [email protected]";
char res[2][12];
int cnt = sscanf(src, "%*[^0-9]%12[0-9]@qq.com", res[0]);
printf("%[email protected]", res[0]);
结果:cnt = 1, "[email protected]"
解释:用到了2个%匹配段
①%*[^0-9]代表尽可能长的匹配一个不含数字的字符串,但不捕获
②%12[0-9]代表最长捕获12位数字
实际上,上述正则段有bug,主要原因在于第①个匹配段,一旦QQ邮箱之前出现了数字,那么这段程序就不能正常工作了,例如:src[]="[email protected]",就会匹配到"888"。
这个问题怎么解决呢?
问题出在这里:我要首先匹配到一个QQ号,但不是所有的数字串都是QQ号,只有后面紧跟“@qq.com”字串的数值才是QQ号。原因分析清楚就好办了,写个循环或者递归即可,相比完全从0手写,还是要简单一点。
(4)判断一个字符串是不是163邮箱
sscanf("[email protected]", "%[0-9a-zA-Z_]@163.co%[m]", res[0], res[1]);
结果:返回值=2(判断成功),res[0] = "dfsadfa", res[1] = "m"
解释:
①%[0-9a-zA-Z_]代表捕获以数字、英文、下划线组成的最长字串
②%[m]代表捕获字符m。
为什么要单独写一个%[m]?主要是考虑到sscanf的功能太弱,匹配或判断时只会保证本%字段的前缀完全匹配,而不会考虑后缀是否匹配。为了更好的理解这一问题,我们继续看这个例子:
如果我们把正则段换成fmt[] = “%[0-9a-zA-Z_]@163.com",那么,
sscanf("[email protected]", fmt, &res[0])
sscanf("[email protected]", fmt, &res[0])
sscanf("wangyi+-?qwr", fmt, &res[0])
这三个都会返回1,无法用来判断或提取163邮箱。通过一个小技巧,把"@163.co"作为提取"m"的前缀,就能有效解决这一问题!
这个正则段还可以再优化一下,减小内存占用,写成用户名字段只匹配不捕获:"%*[0-9a-zA-Z_]@163.co%[m]",这样,只要sscanf返回1就认为是163邮箱,返回0就认为不是。
同理这个技巧,也可用在上面提取QQ邮箱的例子中。
最后,sscanf的正则实际上功能非常弱,只要表现在,对于有前缀的字段比较容易提取或判断,例如上面Tom的例子,对于利用后缀的字段,较为麻烦,需要考虑的陷阱较多。虽然功能弱了点,但总比没有强。
总结:
如果想利用后缀来判断或提取子串,那么就把后缀的最后一个字符c,写入“捕获段(不带*)”。
后记:经实测,%[0-9]、[A-E]这种区间通配符在电脑上没问题,但在STM32上不起作用,在32上[0-9]代表捕获0、-、9这三个字符,要想捕获区间只能这么写了:[0123456789]、[ABCDE],遗憾