Parser .csv em C e C++

por Fabio A. Mazzarino

Arquivos .csv são muito utilizados para troca de dados, por conta da sua simplicidade de geração e interpretação. A rigor são linhas de texto com campos separadas por vírgulas (Comma-Separated Values – valores separados por vírgulas), mas é muito comum utilizar separação com ponto-e-vírgula.

Para os exemplos vamos utilizar o seguinte arquivo .csv:

Doe;John;Max;M;32;78.7
Doe;Jane;Reese;F;28;60.3
Braço;Joao;Sem;M;61;71.4
Ninguem;Joao;Sem;M;21;60.9
Couves;Jose;das;M;58;79.9

Um registro em cada linha, 6 campos: sobrenome, primeiro nome, nome do meio, gênero, idade e peso.

C e strtok

Em C ANSI a maioria das pessoas usaria strtok. Esta é uma função muito utilizada para fazer parser de campos. Sua utilização é muito simples, apesar de necessitar de alguns cuidados. Vamos avaliar a solução:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    FILE *file = fopen("test.csv", "r");
    if (!file) {
        fprintf(stderr, "Cannot open file test.csv\n");
        return 1;
    }
    while (!feof(file)) {
        char line[256] = "";
        char *field = NULL;
        char lastname[64] = "";
        char firstname[64] = "";
        char middlename[64] = "";
        int age = 0;
        char gender = 'M';
        double weight = 0.0;

        fgets(line, 255, file);
        line[strlen(line) - 1] = '\x0';
        if (strlen(line) < 5)
            break;
        field = strtok(line, ";");
        strcpy(lastname, (field? field: ""));
        field = strtok(NULL, ";");
        strcpy(firstname, (field? field: ""));
        field = strtok(NULL, ";");
        strcpy(middlename, (field? field: ""));
        field = strtok(NULL, ";");
        gender = (field? *field: 'M');
        field = strtok(NULL, ";");
        age = (field? atoi(field): 0);
        field = strtok(NULL, ";");
        weight = (field? atof(field): 0.0);

        printf("%s, %s %s. %s, %d yo, %.2lf kg\n",
            lastname, firstname, middlename,
            (gender == 'M'? "male": "female"), age, weight
        );
    }
    fclose(file);
}

Depois de abrir o arquivo e ler linha a linha com fgets, utilizamos a função strtok para recuperar cada campo. Note a necessidade de verificar se o ponteiro field é NULL para evitar referenciamento de ponteiro nulo.

C e fscanf

Muita gente não sabe utilizar scanf e suas funções derivadas. Uma pena pois perdem uma boa chance para criar um código mais limpo e mais sucinto.

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[]) {
    FILE *file = fopen("test.csv", "r");
    if (!file) {
        fprintf(stderr, "Cannot ope file test.csv\n");
        return 1;
    }

    while (!feof(file)) {
        char lastname[64] = "";
        char firstname[64] = "";
        char middlename[64] = "";
        int age = 0;
        char gender = 'M';
        double weight = 0.0;
        int nfields = fscanf(file, "%63[^;];%63[^;];%63[^;];%c;%d;%lf\n",
            lastname, firstname, middlename,
            &gender, &age, &weight
        );
        if (nfields != 6)
            break;
        printf("%s, %s %s. %s, %d yo, %.1lf kg\n",
            lastname, firstname, middlename,
            (gender == 'M'? "Male": "Female"), age, weight
        );
    }
    fclose(file);
    return 0;
}

O código fica muito mais sucinto. Depois de abrir o arquivo o loop fica bem diferente, basta executar o fscanf para recuperar a linha e já fazer o parser dos valores, já atribuindo devidamente a cada variável.

O resultado final é exatamente o mesmo.

C++ e getline

Em C++ voltamos a uma situação parecida com C e strtok.

#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <sstream>

int main() {
    std::ifstream file;

    file.open("test.csv", std::ifstream::in);
    if (!file.is_open()) {
        std::cerr << "Cannot open file test.csv" << std::endl;
        return 1;
    }
    while (!file.eof()) {
        char lastname[64] = "";
        file.getline(lastname, 63, ';');
        if (lastname[0] == '\x0')
            break;
        char middlename[64] = "";
        file.getline(middlename, 63, ';');
        char firstname[64] = "";
        file.getline(firstname, 63, ';');
        char field[16] = "";
        file.getline(field, 15, ';');
        char gender = field[0];
        file.getline(field, 15, ';');
        int age = std::stoi(field);
        file.getline(field, 13, '\n');
        double weight = std::stof(field);
        std::cout << lastname << ", " << firstname << " " << middlename <<
            ". " << (gender == 'M'? "Male": "Female") << " " << age << " yo " <<
            std::fixed << std::setprecision(1) << weight << " kg" << std::endl
        ;
    }
    file.close();
}
 

Após abrir o arquivo os campos são lidos diretamente do stream de dados utilizando getline, campos a campo, utilizando o separador ;, porém no último campo utilizamos o separador \n (fim de linha).

Em Resumo

A simplicidade da geração e da interpretação do formato .csv faz com, em especial em linguagens scripts, faz com que seja amplamente utilizado para troca de informações. Saber interpretá-lo de forma prática e rápida facilita o trabalho de qualquer desenvolvedor.