GPS項目實戰系列之:GPS數據解析2

topsemic 發佈 2020-03-02T07:20:10+00:00

首先給大家介紹一下strtok函數,它是標準函數庫中的一員,標準函數庫是一個工具箱,它能極大地擴展C程式設計師的能力,我們需要熟悉並且靈活的應用。

接著上一篇GPS項目實戰系列1:GPS數據解析1,咱們繼續說GPS數據解析的問題,GPS數據解析的核心問題可以歸結為如何解析以逗號作為分隔符的字符串問題。看似很簡單的一個功能,真正實現起來也那不是那麼容易,在調試的過程中,我就遇到了很多的小問題,在此做個完整的記錄與總結,希望對大家有幫助。

首先給大家介紹一下strtok函數,它是標準函數庫中的一員,標準函數庫是一個工具箱,它能極大地擴展C程式設計師的能力,我們需要熟悉並且靈活的應用。

char *strtok(char *str, const char *delim),功能是分解字符串str 為一組字符串,delim為分隔符。

該函數返回被分解的第一個子字符串,如果沒有可檢索的字符串,則返回一個空指針。

我們看一下這個函數的使用例子,

程序清單1: strtok函數使用示例1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char str[] ="Apple,Pear,Potato,11";
    char* tokens = strtok (str,",");

    //iterate over tokens.. .
    while (tokens!= NULL)
    {
                printf ("%s\r\n",tokens);
                tokens = strtok (NULL,",");
    }

    return 0;
}

它的輸出結果為:

Apple

Pear

Potato

11

上述代碼,有一個地方,不知道大家注意到沒有,第一次調用strtok的時候,第一個參數為str,後面每次調用時參數都是NULL。The first call to strtok must pass the C string to tokenize, and subsequent calls must specify NULL as the first argument, which tells the function to continue tokenizing the string you passed in first.

如果逗號之間為空,情況會是什麼樣子呢?看一下下面的例子:

程序清單2: strtok函數使用示例2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char str[] ="Apple,Pear,,Potato,11";
    char *tokens = strtok (str,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

輸出結果如下:

Apple

Pear

Potato

11

和第一個程序輸出的結果完全一致,起初我對這個結果很不理解,我本能的以為第一次調用strtok的返回值是"Apple",第二次調用strtok的返回值為"Pear",第三次調用後,由於2個逗號之間是空的,我以為返回值會是NULL,然後在第四次調用後,得到"Potato"。

事實證明我的想法是錯的,錯在第三次調用strok函數後的返回值,並不是我想的那樣返回NULL,實際上第三次調用後,返回值是"Potato"。也就說當檢索到兩個連續的逗號之間沒有字符串,它會自動往後檢索,把後面的下一個逗號前的字符串返回。

strtok熟悉後,我們需要思考一個重要的問題,就是如何判斷出逗號間為空的狀況。不然直接使用strtok循環的去解析,當出現逗號間為空時,就會出現欄位無法再一一對應的情況。什麼意思呢,看上面的代碼,就是程序並沒法知道第三個欄位是空,解析出來的"Potato"也不知道對應是第幾個欄位的。

可以考慮採用以下方式來解決,程序里先去判斷是否有連續逗號(",,"),如果有則將",,"替換為",@,"形式,其中@是一個正常情況下該欄位不會出現的字符。這樣操作之後逗號分隔的各個欄位就都有了內容,再進行解析就不會出現上述的問題了。那如何用程序實現字符串的替換功能呢?

即對於上述字符串:"Apple,Pear,,Potato,11"

我們希望經過替換後字符串變為:

"Apple,Pear,@,Potato,11"

大家可以看一下下面的代碼(替換函數strrpl是直接谷哥出來的)

程序清單3:實現字符串替換功能

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        return NULL;
    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace
    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 
    strcat(firstStr, replace);
    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)
        str[i] = firstStr[i];
    return str;
}

int main(void)
{
    char str[] ="Apple,Pear,,Potato,11";
    
    strrpl(str,",,",",@,");
    printf ("%s\n",str);
    
    char *tokens = strtok (str,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

輸出的結果是:

這樣就實現了兩個逗號替換的功能,如果字符串是下面這個呢? 該字符串中間出現了連續3個逗號,並且後面還有一次連續2個逗號,

char str[] ="Apple,Pear,,,Potato,,11";

運行一下,我們看看結果

結果是只替換了第一個連續逗號的地方,如何實現讓字符串里所有的連續逗號都被替換呢?重複的做一件事,只需要加一個循環即可,修改後的代碼如下:

程序清單4:循環替換字符串功能

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        return NULL;
    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace
    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 
    strcat(firstStr, replace);
    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)
        str[i] = firstStr[i];
    return str;
}

int main(void)
{
    char str[] ="Apple,Pear,,,Potato,,11";
    
    while (strstr(str, ",,"))
        strrpl(str, ",,", ",@,");

    printf("%s\n",str);
    
    char *tokens = strtok (str,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    return 0;
}

這個代碼運行後出現了如下問題:

看起來像是數組越界了,經過分析可知是str數組越界導致的,由於",,"被替換成",@," ,導致數組長度變長從而產生越界。所以上述代碼不能那麼寫,我們可以通過定義一個新的更長長度的數組來解決。另外還有一點需要注意的是:strok函數執行任務時,它會修改它所處理的字符串,如果源字符串不能被修改,就必須得複製一份,將這份拷貝傳給strok函數。

改進後的代碼如下:

程序清單5:字符串操作時要防止越界

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);
    char *firstStr;
    if(pt == NULL){
        printf("cannot find string \n");
        return NULL;
    }

    firstStr = (char* )malloc(100 * sizeof(char));

    // copy just until i find what i need to replace
    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 
    strcat(firstStr, replace);
    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)
        str[i] = firstStr[i];
    return str;
}

int main(void)
{
    char str[] ="Apple,Pear,,,Potato,,11";
    
    char *buff;

    buff = malloc(sizeof(str)+100);
    memset(buff, 0, sizeof(str)+100);
    memcpy(buff, str, sizeof(str));

    
    while (strstr(buff, ",,"))
        strrpl(buff, ",,", ",@,");

    printf("%s\n",buff);
    
    char *tokens = strtok (buff,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    free(buff);

    return 0;
}

輸出結果如下:

經過修改了的這份代碼是不是就沒有問題了呢?答案是否!如果我將str數組變長,變成下面的這一串內容,

 char str[] = "$GNRMC,051035.00,A,4000.74054,N,11628.03344,E,0.253,,020320,6.91,W,D*23\n\
$GNVTG,,T,,M,0.253,N,0.468,K,D*36\n\
$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D\n\
$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F\n\
$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F\n\
$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B\n\
$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77\n\
$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77\n\
$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D\n\
$GPGSV,5,5,17,50,42,164,34*48\n\
$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E\n\
$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60\n\
$GLGSV,3,3,10,87,26,128,32,88,00,163,*61\n\
$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A";

其他代碼不變,運行結果會是:

在出現這個問題之前,我都沒有仔細的閱讀直接拷貝過來strrpl函數的內部實現細節,這時就得好好看看了,經過很長時間調試,找到問題出在下面這句話上面,

firstStr = (char* )malloc(100 * sizeof(char));

和這句話相關,有3個非常重要的值得大家注意的地方:

1)分配100字節顯然是不合理的,firstStr是用來存放經過替換後的字符串的,所以它的長度取決於源字符串長度,以及替換和被替換的字符串長度,不能暴力的隨便設置一個數。

2)在調用malloc函數後,這個空間沒有賦初值,這是相當危險的。

3)在調用malloc後,沒有調用free函數,會產生內存泄露。

針對以上3個問題需要做對應的修改,改後的代碼如下:

程序清單6:修改strrpl函數

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* strrpl(char *str, char* find, char *replace)
{
    int i;
    char *pt = strstr(str, find);   
    char *firstStr;
   
    if(pt == NULL){
        printf("cannot find string \n");
        return NULL;
    }
    
    int len = strlen(str)+1+strlen(replace)-strlen(find);
    firstStr = (char* )malloc(len);
    memset(firstStr,0,len);

    // copy just until i find what i need to replace
    // i tried to specify the length of firstStr just with pt - str

    strncpy(firstStr, str, strlen(str) - strlen(pt)); 
    strcat(firstStr, replace);
    strcat(firstStr, pt + strlen(find));

    for(i = 0; i < strlen(firstStr); i++)
        str[i] = firstStr[i];
    
    free(firstStr);
    return str;
}

int main(void)
{
    char str[] = "$GNRMC,051035.00,A,4000.74054,N,11628.03344,E,0.253,,020320,6.91,W,D*23\n\
$GNVTG,,T,,M,0.253,N,0.468,K,D*36\n\
$GNGGA,051035.00,4000.74054,N,11628.03344,E,2,08,2.08,3.3,M,-8.3,M,,0000*5D\n\
$GNGSA,A,3,29,14,27,42,03,,,,,,,,3.33,2.08,2.60*1F\n\
$GNGSA,A,3,87,66,67,,,,,,,,,,3.33,2.08,2.60*1F\n\
$GPGSV,5,1,17,03,15,250,28,04,47,302,17,08,03,196,09,09,16,318,13*7B\n\
$GPGSV,5,2,17,14,23,157,32,16,72,264,19,21,08,092,20,22,07,230,34*77\n\
$GPGSV,5,3,17,23,41,303,,26,72,027,21,27,29,179,28,29,15,039,30*77\n\
$GPGSV,5,4,17,31,47,089,15,40,13,251,,41,32,226,31,42,35,140,31*7D\n\
$GPGSV,5,5,17,50,42,164,34*48\n\
$GLGSV,3,1,10,66,12,192,26,67,44,240,28,68,34,310,,76,25,063,*6E\n\
$GLGSV,3,2,10,77,58,357,,78,29,287,,85,01,012,,86,30,057,*60\n\
$GLGSV,3,3,10,87,26,128,32,88,00,163,*61\n\
$GNGLL,4000.74054,N,11628.03344,E,051035.00,A,D*7A";
    
    char *buff;

    buff = malloc(sizeof(str)+100);
    memset(buff, 0, sizeof(str)+100);
    memcpy(buff, str, sizeof(str));

    while (strstr(buff, ",,"))
        strrpl(buff, ",,", ",@,");

    printf("%s\n",buff);
    
    char *tokens = strtok (buff,",");
        
    //iterate over tokens.. .
    while (tokens!= NULL)
    {
        printf ("%s\r\n",tokens);
        tokens = strtok (NULL,",");
    }

    free(buff);

    return 0;
}

這樣再次運行代碼,就可以得到正確的結果了。

有了以上基礎,就可以實際來寫GPS數據解析的代碼了,在上一篇文章的基礎上,我對整個目錄結構做了調整,新的工程總共有5個文件,mian.c為主程序,gnss.c和gnss.h和GNSS數據解析相關,uart.c和uart.h對應串口配置。

運行後,會輸出如下信息:

上述代碼中重點是gnss.c文件中的gps_analyse函數,大家可以好好看看,

int gps_analyse(char *buff,int buff_len,GNSS *gps_data)
{
    char *ptr = NULL;
    if(strlen(buff)<10)
    {
        return -1;
    }
        
    /* 如果buff字符串中包含字符"$GPRMC"則將$GPRMC的地址賦值給ptr */
    if( NULL==(ptr=strstr(buff,"$GPRMC")) && NULL==(ptr=strstr(buff,"$GNRMC")) )
    {
        return -2;
    }

    if(check_nmea_message(ptr, 0, buff_len) <0 )
    {
        printf("check error!\n");
        return -3;
    }
    
    char *tmpbuf;
    tmpbuf = (char *)malloc(strlen(ptr)+100);
    memset(tmpbuf, 0, strlen(ptr)+100);
    memcpy(tmpbuf, ptr, strlen(ptr));
    
    while (strstr(tmpbuf, ",,"))
        strrpl(tmpbuf, ",,", ",@,");
    
    printf("tmpbuf:%s \n",tmpbuf);

    char* pch = strtok(tmpbuf, ",");
    
    // 1 time
    pch = strtok(NULL, ",");
    nmea_get_time(pch, &gps_data->time);
    
    // 2 status
    pch = strtok(NULL, ",");
    gps_data->pos_state = *pch;
    
    //3 latitude
    pch = strtok(NULL, ",");
    nmea_lat_long_to_double(&gps_data->latitude, pch, strlen(pch));
    
    //4 latitude direction
    pch = strtok(NULL, ",");
    gps_data->NS = *pch;
    
    //5 longitude
    pch = strtok(NULL, ",");
    nmea_lat_long_to_double(&gps_data->longitude, pch, strlen(pch));

    //6 long direct
    pch = strtok(NULL, ",");
    gps_data->EW = *pch;
    
    //7 speed
    pch = strtok(NULL, ",");
    gps_data->speed = 1.852 * strtof(pch, (char **) NULL ) / 3.6;
    
    //8 direction
    pch = strtok(NULL, ",");
    gps_data->direction = strtof(pch, (char**)NULL);

    //9 date
    pch = strtok(NULL, ",");
    nmea_get_date(pch, &gps_data->time);
        
    //10 不處理
    pch = strtok(NULL, ",");
    
    //11 不處理
    pch = strtok(NULL, ",");
    
    //12 mode
    pch = strtok(NULL, ",");
    gps_data->pos_mode = *pch;
    
    free(tmpbuf);
    
    return 0;
}

我在調試過程中遇到了很多的問題,通過自己實際動手搬運、修改、調試代碼收穫了很多知識,主要有以下幾點:

1) 在使用strtof、strtod函數時,一定要加上頭文件#include <stdlib.h>,否則雖然能編譯通過(有警告),但是轉換後的結果不對。另外一定要養成不放過編譯的任何一個警告。

2) strrpl函數中,malloc分配的空間大小一定要注意,我一開始因為少加了個1,導致程序出現異常,調試了很久才找到問題。加1的原因是你分配的大小要能能容納字符串(尾部以'\0'結尾),而strlen(str)的長度不包含尾部的'\0'。

3) 要養成初始化指針、內存空間後,立刻賦初值的習慣。

4)strok函數適合用來分割字符串,解析各個欄位。

5)操作字符串/字符數組時一定要注意越界的問題。

關鍵字: