C语言“正则表达式”字符串处理sscanf(),使用前缀技巧、匹配邮箱等

    xiaoxiao2022-07-04  179

    一般的字符串处理函数可以使用C标准库的string.h,字符串匹配、字符串反转、在字符串中查找子串位置等。但string.h仍然满足不了我们的需求。

    来几个例题:(不允许使用c++)

    1、要求把一个字符串两端的空白符(可能有多个空格和TAB)给去掉, 例如:“   asd123  qwer    ”,要求处理完之后结果为:"asd123  qwer"。 有人可能会想,这么简单的功能,手写一个函数就能轻松搞定,但是如果每来一个新需求都要手写一个新函数才能完成它,那就太累了,我们将使用C标准库来完成这个事情。

    2、要求把字符串的数字给删掉 例如:“ abc    abc123.4--abc”要求处理完之后为:“ abc    abc--abc”

    3、判断一个字符串是不是网易邮箱, 例如:"asd@126.com"判定成功,“qwert@163.com”判断成功,"egweg124"失败,"awgwge@126.cn"失败

     

    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][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 ++--??123456@qq.comtgrh525";     char res[2][12];     int cnt = sscanf(src, "%*[^0-9][0-9]@qq.com", res[0]);     printf("%s@qq.com", res[0]); 结果:cnt = 1, "23456@qq.com"

    解释:用到了2个%匹配段 ①%*[^0-9]代表尽可能长的匹配一个不含数字的字符串,但不捕获 ②[0-9]代表最长捕获12位数字

    实际上,上述正则段有bug,主要原因在于第①个匹配段,一旦QQ邮箱之前出现了数字,那么这段程序就不能正常工作了,例如:src[]="abc888ABC123@qq.com",就会匹配到"888"。

    这个问题怎么解决呢? 问题出在这里:我要首先匹配到一个QQ号,但不是所有的数字串都是QQ号,只有后面紧跟“@qq.com”字串的数值才是QQ号。原因分析清楚就好办了,写个循环或者递归即可,相比完全从0手写,还是要简单一点。

    (4)判断一个字符串是不是163邮箱

    sscanf("wangyi@163.com",  "%[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("wangyi@qq.com", fmt, &res[0]) sscanf("wangyi@163.com", 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],遗憾

    最新回复(0)