天天看點

strtok拆分字元串任務:解析經緯度拆分字元串strtok_r優化一下

大家好,我是驚覺,今天聊聊字元串。字元串的使用場景非常之多,人機互動和雙機通信都會用到。比如:

  • 通過序列槽向單片機發送指令,以執行操作或配置參數。
  • 單片機讀取傳感器資料,資料格式是字元串。一般GPS資料就是字元格式。
  • 有些場景需要使用多個處理器協同工作,比如單片機+openmv,它們之間需要通信,可以采用字元格式的編碼方式。

操作字元串,無非是兩件事兒:生成字元串與解析字元串,後者往往更複雜一些。Java,Python之類的進階程式設計語言自帶了強大的字元串處理庫,提供非常豐富的操作。下圖是Java的String類函數,密密麻麻有木有,這還隻是一部分。

strtok拆分字元串任務:解析經緯度拆分字元串strtok_r優化一下

相對而言,标準C庫提供的功能有限。大家熟知的功能可能有:

  • 字元串複制追加(strcpy,strcat)
  • 字元串查找比較(strstr,strcmp)
  • 字元串轉數字(atoi,strtol)

有兩個非常有用但是可能被大家忽略的函數,介紹給大家。

任務:解析經緯度

讓我們以解析GPS中的RMC消息為例,資料如下:

$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00
           

GPS的各字段以逗号分隔。我們需要提取經緯度資訊,集中在:

A,3204.862246,N,11845.911047,E

。A表示經緯度有效,3204.862246是緯度,11845.911047是緯度,具體的解釋參見下圖:

strtok拆分字元串任務:解析經緯度拆分字元串strtok_r優化一下

拆分字元串strtok_r

由于GPS中各字段以逗号分隔,大家最先想到的可能是用strstr或strchr去查找逗号的位置,再一一處理。如果有一個函數可以幫我們完成拆分,效果如下圖,那将會很友善後續處理。

strtok拆分字元串任務:解析經緯度拆分字元串strtok_r優化一下

這個函數是有的,而且就在C标準庫中,那就是strtok。

其根據提供的分隔符集delimiters,對source進行拆分。

  • source 待拆分的字元串。
  • delimiters 分隔符集,可以包含多個字元。比如"\r\n\t "表示以換行,tab等字元進行拆分。
  • return 傳回指向子字元串的指針。

在拆分一個字元串時,需要多次調用該方法:

  • 初次調用時,source為待拆分字元串,delimiters為分隔符。函數傳回第一個子字元串位址。
  • 之後的調用,source為NULL,delimiters為分隔符,分隔符的内容并不需要與之前的一緻。函數傳回下一個子字元串位址。
  • 當某次調用後傳回NULL時,整個拆分就結束了。

其實過程并不複雜,請看拆分GPS的代碼:

#define GPS_RMC "$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00"

void split_string_example(void)
{
    char buf[128];
    int buf_len;
    char *token = NULL;
    char *saveptr = NULL;
    const char *delim = ",*";

    LOG_I("test split string");

    buf_len = snprintf(buf, sizeof(buf), "%s", GPS_RMC);

    token = strtok_r(buf, delim, &saveptr);
    while (token)
    {
        LOG_D("%s", token);
        token = strtok_r(NULL, delim, &saveptr);
    }

    LOG_HEX_V(buf, buf_len, "finally, buf:");
}
           

結果剛才已經放過了,這次放一個更完整的。

strtok拆分字元串任務:解析經緯度拆分字元串strtok_r優化一下

示例中用宏GPS_RMC來定義GPS的内容,再用snprintf把它列印到buf之中?

buf_len = snprintf(buf, sizeof(buf), "%s", GPS_RMC);
           

這可不是筆者多此一舉,而是因為strtok在拆分字元串時會修改其内容。以下兩點需要牢記:

  • strtok并不是重新配置設定記憶體以存放子字元串,其傳回的子字元串直接指向待拆分字元串中的相應位置。沒有任何的記憶體配置設定。
  • 所謂的拆分,是将字元串中的分隔符替換為’\0’,也隻有這樣,你才能進行後續操作。上圖的結尾展示了拆分後的buf的内容,紅框都是’\0’。是以,待拆分字元串必須是可被修改的,必須是變量,而不能是常量。

筆者用的不是strtok,而是strtok_r。C語言中很多函數有兩種版本,一種不帶_r,一種帶_r,_r表示可重入。可重入的概念可以單獨寫一篇文章,這裡就不多說了。strtok_r比strtok多了一個參數,其為char *指針,用于儲存拆分的狀态。其實用法很簡單,定義一個指針變量并傳入就行,不需要關注它的值。

優化一下

我們再看下GPS的資料,如果想提取其中的

A

3204.862246

11845.911047

,直接使用strtok并不友善。

$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00
           

如果使用Java的話,如下幾行代碼即可完成提取。

String gps = "$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00";
String[] sub = gps.split(",");
if (sub.length < 6) {
	System.out.println("parse fail");
} else {
	System.out.println(String.format(
			"parse succeed, valid:%s, longitude:%s, latitude:%s", 
			sub[2], sub[3], sub[5]));
}
           

輸出結果:

parse succeed, valid:A, longitude:3204.862246, latitude:11845.911047
           

Java之是以友善,關鍵在于split函數傳回了拆分後的字元串數組,可直接通過下标提取相關字段。

C語言沒有這樣的函數,那我們就自己寫一個。

static int split_string(char *str, const char *delim, char *sub_ptr[], int size)
{
    char *token = NULL;
    char *saveptr = NULL;
    int idx = 0;


    token = strtok_r(str, delim, &saveptr);
    while (token && idx < size)
    {
        sub_ptr[idx++] = token;
        token = strtok_r(NULL, delim, &saveptr);
    }

    return idx;
}
           

split_string将拆分的結果寫入sub_ptr之中,并傳回子字元串個數。有了這個函數,提取就如Java一樣友善了。

void split_string_example2(void)
{
    char buf[128];
    char *sub_buf[20];
    int num;

    LOG_I("test split string 2");

    snprintf(buf, sizeof(buf), "%s", GPS_RMC);
    num = split_string(buf, ",", sub_buf, ARRAY_SIZE(sub_buf));

    if (num < 7)
    {
        LOG_E("fail");
        return;
    }

    LOG_D("succeed, valid:%s, latitude:%s, longitude:%s", sub_buf[2], sub_buf[3], sub_buf[5]);

}
           

使用strtok或者是split_string僅僅是提取出目标字元串,想得到經緯度數值的話,還需要轉換成浮點數,可使用atof函數。其實還有一種更為簡單的方法,咱明天繼續。

文中完整的示例代碼,參見筆者基于stm32f407建立的demo工程:

位址:[email protected]:wenbodong/mcu_demo.git
示例:examples/05_string/example.c
使用時需要打開examples/examples.h中的EXAMPLE_SHOW_STRING。