C语言:使用strtok从字符串中解析空标记

7

我的应用程序生成像下面这样的字符串。我需要解析分隔符之间的值并将其转换为单独的值。

2342|2sd45|dswer|2342||5523|||3654|Pswt

我正在使用strtok在循环中进行此操作。对于第五个标记,我得到了5523. 但是,我还需要考虑两个分隔符||之间的空值。根据我的要求,5523应该是第六个标记。

token = (char *)strtok(strAccInfo, "|");

for (iLoop=1;iLoop<=106;iLoop++) { 
            token = (char *)strtok(NULL, "|");
}

有什么建议吗?

2
strtok() 可能是 C 标准中最糟糕的东西。你可以编写自己的解析器。 - David Thornley
8个回答

8
在这种情况下,我经常更喜欢使用一个p2 = strchr(p1, '|')循环,并在内部使用memcpy(s, p1, p2-p1)。它快速,不会破坏输入缓冲区(因此可以与const char *一起使用),并且真正可移植(即使在嵌入式系统上也是如此)。
它还是可重入的;strtok则不是。(顺便说一下:可重入与多线程无关。strtok已经无法处理嵌套循环。可以使用strtok_r,但它不太可移植。)

我使用了你的输入并更新了我的代码。谢谢!如果您感兴趣,下面是我正在使用的代码作为答案。 - Bash
谢谢,受你的回答启发,我做了这个:链接 - Accountant م
抱歉Patrick,你能详细解释一下你的解决方案是如何工作的吗?我猜s是原始字符串,但p1p2是什么? - rdxdkr

3
char *mystrtok(char **m,char *s,char c)
{
  char *p=s?s:*m;
  if( !*p )
    return 0;
  *m=strchr(p,c);
  if( *m )
    *(*m)++=0;
  else
    *m=p+strlen(p);
  return p;
}
  • 可重入的
  • 线程安全的
  • 严格符合ANSI标准的
  • 需要来自调用上下文的未使用的辅助指针

例如:

char *p,*t,s[]="2342|2sd45|dswer|2342||5523|||3654|Pswt";
for(t=mystrtok(&p,s,'|');t;t=mystrtok(&p,0,'|'))
  puts(t);

e.g.

char *p,*t,s[]="2,3,4,2|2s,d4,5|dswer|23,42||5523|||3654|Pswt";
for(t=mystrtok(&p,s,'|');t;t=mystrtok(&p,0,'|'))
{
  char *p1,*t1;
  for(t1=mystrtok(&p1,t,',');t1;t1=mystrtok(&p1,0,','))
    puts(t1);
}

你的工作 :) 将char *c实现为参数3


2
在第一次调用时,该函数期望将一个C字符串作为str的参数,其第一个字符用作扫描令牌的起始位置。在后续调用中,该函数期望一个空指针,并使用最后一个令牌结束后的位置作为新的扫描起始位置。
为了确定令牌的开始和结束位置,该函数首先从起始位置开始扫描,找到第一个不包含在分隔符中的字符(成为令牌的开头)。然后从这个令牌的开头开始扫描,找到第一个包含在分隔符中的字符,成为令牌的结尾。
这意味着它将跳过任何位于令牌开头的'|'字符。使5523成为第5个令牌,这一点您已经知道。我只是想解释一下为什么会这样(我自己也必须查一下)。这还表示您不会得到任何空令牌。
由于您的数据是按此方式设置的,您有几种可能的解决方案:
1)查找所有出现的||并替换为| |(放置一个空格)
2)使用strstr 5次并找到第五个元素的开头。

感谢提供的信息,希望下次需要时我还能记得。:-D 您提供的第一种解决方案会对结果造成一些影响,因为字符串中存在有效组件,在管道符之间会返回一个空格。 第二个解决方案可能会变得很繁琐,而且可能无法实现,因为不同数据集的字符串可能是不同的。 - Bash
@Bash - 抱歉我无法提供更多帮助 :( - Romain Hippeau
哦,你真的帮了我很多……在我们这个领域里,信息就是力量,不是吗? - Bash

2
这是 `strtok` 的一个限制。设计者考虑了基于空格分隔的标记。`strtok` 实际上并不做太多事情;您可以自己编写解析器。C FAQ 有一个示例

1
我从你贴出的链接中获取了一些有用的信息。谢谢! - Bash

1

使用除了strtok之外的东西。它根本不是为你所要求的功能而设计的。当我需要这个功能时,通常我会使用strcspnstrpbrk,然后自己处理其余部分的标记化。如果你不介意像strtok一样修改输入字符串,那么应该很简单。至少从现在开始,像这样的东西似乎应该可以工作:

// Warning: untested code. Should really use something with a less-ugly interface.
char *tokenize(char *input, char const *delim) { 
    static char *current;    // just as ugly as strtok!
    char *pos, *ret;
    if (input != NULL)
        current = input;

    if (current == NULL)
        return current;

    ret = current;
    pos = strpbrk(current, delim);
    if (pos == NULL) 
        current = NULL;
    else {
        *pos = '\0';
        current = pos+1;
    }
    return ret;
}

由于 OP 只在搜索一个分隔符字符,因此可以使用 strchr() 而不是 strpbrk() - caf
我做了一点不同。无论如何还是谢谢你。 - Bash

1
Patrick Schlüter答案启发,我编写了这个函数,它应该是线程安全的,支持空令牌,并且不更改原始字符串。
char* strTok(char** newString, char* delimiter)
{
    char* string = *newString;
    char* delimiterFound = (char*) 0;
    int tokLenght = 0;
    char* tok = (char*) 0;

    if(!string) return (char*) 0;

    delimiterFound = strstr(string, delimiter);

    if(delimiterFound){
        tokLenght = delimiterFound-string;
    }else{
        tokLenght = strlen(string);
    }

    tok = malloc(tokLenght + 1);
    memcpy(tok, string, tokLenght);
    tok[tokLenght] = '\0';

    *newString = delimiterFound ? delimiterFound + strlen(delimiter) : (char*)0;

    return tok;
}

你可以像这样使用它。
char* input = "1,2,3,,5,";
char** inputP = &input;
char* tok;
while( (tok=strTok(inputP, ",")) ){
    printf("%s\n", tok);
}

这应该输出什么。
1
2
3

5

我测试了它的简单字符串,但尚未在生产中使用,并在代码审查上发布了它,这样你就可以看到其他人对它的看法了。

1
如果您使用的是Posix机器,您可以通过tok = strndup(string, tokLength);简单地替换tok = malloc(tokLenght + 1); memcpy(tok, string, tokLenght); tok[tokLenght] = '\0'; - Patrick Schlüter

1

哦,好吧 :-) 我大部分的编程都在UNIX上进行,现在肯定会派上用场 :-)) 以前从未听说过。 - clearlight

0
以下是目前对我有效的解决方案。感谢所有回复我的人。
我正在使用LoadRunner。因此,有些不熟悉的命令,但我相信流程可以很容易地理解。
char strAccInfo[1024], *p2;
int iLoop;

Action() {  //This value would come from the wrsp call in the actual script.
    lr_save_string("323|90||95|95|null|80|50|105|100|45","test_Param");

    //Store the parameter into a string - saves memory. 
    strcpy(strAccInfo,lr_eval_string("{test_Param}"));
    //Get the first instance of the separator "|" in the string
    p2 = (char *) strchr(strAccInfo,'|');

    //Start a loop - Set the max loop value to more than max expected.
    for (iLoop = 1;iLoop<200;iLoop++) { 

        //Save parameter names in sequence.
        lr_param_sprintf("Param_Name","Parameter_%d",iLoop);

        //Get the first instance of the separator "|" in the string (within the loop).
        p2 = (char *) strchr(strAccInfo,'|');           

        //Save the value for the parameters in sequence. 
        lr_save_var(strAccInfo,p2 - strAccInfo,0,lr_eval_string("{Param_Name}"));   

        //Save string after the first instance of p2, as strAccInfo - for looping.
        strcpy(strAccInfo,p2+1);

        //Start conditional loop for checking for last value in the string.
        if (strchr(strAccInfo,'|')==NULL) {
            lr_param_sprintf("Param_Name","Parameter_%d",iLoop+1);
            lr_save_string(strAccInfo,lr_eval_string("{Param_Name}"));
            iLoop = 200;    
        }
    }
}

在某些时候,你需要解释为什么要使用全局变量而不是局部变量,以及为什么函数没有返回类型(这是非常老式的C语言)。或者更好的方法是修复代码,使其在严格的编译器警告下能够干净地编译。使用“iLoop = 200;”来实现“break;”是脆弱的。很难理解为什么在循环控制中使用200。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接