Parser de Campos Utilizando strtok

por Fabio A. Mazzarino

Uma ótima ferramenta para fazer parser de campos separados por caracter é o strtok. Nesse caso vamos fazer um contador de palavras e um parser básico de CSV. Apesar de ser uma função muito bem documentada tem gente que ainda se enrola um pouco ao usar, então vamos aprender como fazer certo.

Contando Palavras

Antes de usar uma função nova, convém dar uma olhada na documentação. Vamos usar o site cplusplus.com, uma ótima referência. Na documentação explica que temos que a primeira chamada deve ser com o primeiro argumento o buffer, e as demais devem ser usadas NULL.

Um detalhe que muita gente não sabe, o buffer pode ser alterado pela função strtok. Portanto não deve ser utilizado após a chamada da função. Caso seja necessário utilizar o conteúdo do buffer, devemos fazer uma cópia utilizando strcpy(), ou até mesmo memcpy().

Para começar o contador de palavras. Basicamente vamos pedir pro strtok() dividir todo o conteúdo do arquivo utilizando os separadores espaço em branco, tabulação e quebra de linha.

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

int main(int argc, char *argv[]) {
	FILE *file = NULL;
	int filesize = 0;
	char *buffer = NULL;
	char *word = NULL;
	int ct = 0;
	
	if (argc > 2) {
		printf("Utilizacao:\n\twordcount <ARQUIVO>\n\n");
		exit(1);
	}
	if ((file = fopen(argv[1], "r")) == NULL) {
		printf("Nao foi possivel abrir o arquivo\n\n");
		exit(1);
	}
	
	fseek(file, 0, SEEK_END);
	filesize = ftell(file);
	fseek(file, 0, SEEK_SET);
	buffer = (char*)malloc(filesize + 1);
	fread(buffer, 1, filesize, file);
	word = strtok(buffer, " \n\t");
	while (word) {
		if (*word)
			ct++;
		word = strtok(NULL, " \n\t");
	}
	printf("Total de palavras: %d\n", ct);
	free(buffer);
	fclose(file);
}

Note que tomei o cuidado de alocar exclusivamente o buffer do tamanho necessário, tomando cuidado para liberá-lo antes do final. Além disso tem também o cuidado com sequências de caracteres-chave, somente efetuando a contagem de palavras quando ela efetivamente existir.

Uma possível falha que não está sendo tratada no programa é a possibilidade do malloc falhar, uma probabilidade pequena em um computador pessoal, ou em um servidor; mas bem plausível de acontecer em um sistema embarcado.

Arquivo .csv

Trocar dados no formato csv é muito comum, é uma das formas mais simples para interpretar em C/C++. Vamos fazer uma implementação bem simples, um parser de um arquivo .csv com sete campos separados por ponto e vírgula, aonde o último campo é formado por 3 campos divididos por dois pontos. O parser vai imprimir os campos ímpares das linhas pares, e vai imprimir todos os campos separados por dois pontos, como uma lista separada por vírgula.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
	FILE *file = NULL;
	char line[512] = "";
	int ctrow = 0;
	
	if (argc > 2) {
		printf("Utilizacao:\n\twordcount <ARQUIVO>\n\n");
		exit(1);
	}
	if ((file = fopen(argv[1], "r")) == NULL) {
		printf("Nao foi possivel abrir o arquivo\n\n");
		exit(1);
	}
	
	while (fgets(line, sizeof(line) - 1, file)) {
		char *field = strtok(line, ";");
		int ctcol = 0;
		
		ctrow++;
		if (ctrow % 2)
			continue;
		while (field) {
			ctcol++;
			if (ctcol % 2 && ctcol != 7)
				printf("%s\t", field);
				
			if (ctcol == 7) {
				int ctextra = 0;
				char *extra = strtok(field, ":");
				while (extra) {
					if (ctextra > 0)
						printf(", ");
					printf("%s", extra);
					extra = strtok(NULL, ":");
					ctextra++;
				}
			} else {
				field = strtok(NULL, ";");
			}	
		}
		printf("\n");
	}	
	fclose(file);
}

Note como é feito o parser do último campo. Ele só funciona porque o parser está contido na string original. Ao chamar novamente o strtok() com o separador ponto e vírgula, ele continua funcionado. Caso estivéssemos utilizando espaços em memória completamente diferentes, não funcionaria. Tá aí um bom exercício: o que aconteceria se antes de fazer o parser do último campo fízemos uma cópia em outra variável?

Outro problema conhecido do código acima é a segurança. Como usamos alocação estática do buffer de linha, o que acontece caso a linha seja maior que 511 caracteres? Como resolver esse problema? Olha uma outra boa oportunidade de exercício pela frente.

Código para Todos

Como sempre o código está disponível no github: wordcount.c e csvparse.c