Como Parsear CSV em Zig

Introdução

CSV (Comma-Separated Values) é um dos formatos mais comuns para dados tabulares. Apesar de parecer simples, parsear CSV corretamente requer lidar com campos entre aspas, caracteres de escape e diferentes separadores. Nesta receita, você aprenderá a implementar um parser CSV funcional em Zig.

Pré-requisitos

Parser CSV Básico

Parsear CSV simples (sem campos entre aspas):

const std = @import("std");

fn parsearLinhaCSV(allocator: std.mem.Allocator, linha: []const u8, delimitador: u8) ![][]const u8 {
    var campos = std.ArrayList([]const u8).init(allocator);
    errdefer campos.deinit();

    var iter = std.mem.splitScalar(u8, linha, delimitador);
    while (iter.next()) |campo| {
        const trimmed = std.mem.trim(u8, campo, " ");
        try campos.append(trimmed);
    }

    return campos.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const csv =
        \\nome,idade,cidade
        \\Maria,28,São Paulo
        \\João,35,Rio de Janeiro
        \\Ana,22,Belo Horizonte
        \\Pedro,31,Curitiba
    ;

    var linhas = std.mem.splitScalar(u8, csv, '\n');

    // Cabeçalho
    if (linhas.next()) |header| {
        const campos = try parsearLinhaCSV(allocator, header, ',');
        defer allocator.free(campos);

        std.debug.print("Colunas: ", .{});
        for (campos, 0..) |c, i| {
            if (i > 0) std.debug.print(" | ", .{});
            std.debug.print("{s}", .{c});
        }
        std.debug.print("\n{s}\n", .{"-" ** 40});
    }

    // Dados
    while (linhas.next()) |linha| {
        if (linha.len == 0) continue;
        const campos = try parsearLinhaCSV(allocator, linha, ',');
        defer allocator.free(campos);

        for (campos, 0..) |c, i| {
            if (i > 0) std.debug.print(" | ", .{});
            std.debug.print("{s}", .{c});
        }
        std.debug.print("\n", .{});
    }
}

Saída esperada

Colunas: nome | idade | cidade
----------------------------------------
Maria | 28 | São Paulo
João | 35 | Rio de Janeiro
Ana | 22 | Belo Horizonte
Pedro | 31 | Curitiba

CSV para Structs Tipados

const std = @import("std");

const Funcionario = struct {
    nome: []const u8,
    cargo: []const u8,
    salario: f64,
};

fn parsearFuncionario(linha: []const u8) !Funcionario {
    var iter = std.mem.splitScalar(u8, linha, ',');

    const nome = std.mem.trim(u8, iter.next() orelse return error.CampoFaltando, " ");
    const cargo = std.mem.trim(u8, iter.next() orelse return error.CampoFaltando, " ");
    const salario_str = std.mem.trim(u8, iter.next() orelse return error.CampoFaltando, " ");

    const salario = try std.fmt.parseFloat(f64, salario_str);

    return .{
        .nome = nome,
        .cargo = cargo,
        .salario = salario,
    };
}

pub fn main() !void {
    const csv =
        \\Alice,Desenvolvedora,8500.00
        \\Bob,Designer,7200.00
        \\Carlos,Gerente,12000.00
        \\Diana,Analista,6800.00
    ;

    var linhas = std.mem.splitScalar(u8, csv, '\n');
    var total_salario: f64 = 0;
    var count: u32 = 0;

    std.debug.print("{s:<15} {s:<20} {s:>10}\n", .{ "Nome", "Cargo", "Salário" });
    std.debug.print("{s}\n", .{"-" ** 47});

    while (linhas.next()) |linha| {
        if (linha.len == 0) continue;

        if (parsearFuncionario(linha)) |func| {
            std.debug.print("{s:<15} {s:<20} R${d:>8.2}\n", .{
                func.nome, func.cargo, func.salario,
            });
            total_salario += func.salario;
            count += 1;
        } else |err| {
            std.debug.print("Erro ao parsear linha: {}\n", .{err});
        }
    }

    std.debug.print("{s}\n", .{"-" ** 47});
    std.debug.print("{s:<35} R${d:>8.2}\n", .{ "Total:", total_salario });
    std.debug.print("{s:<35} R${d:>8.2}\n", .{ "Média:", total_salario / @as(f64, @floatFromInt(count)) });
}

Gerar CSV a partir de Dados

const std = @import("std");

const Produto = struct {
    nome: []const u8,
    preco: f64,
    estoque: u32,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const produtos = [_]Produto{
        .{ .nome = "Teclado", .preco = 199.90, .estoque = 45 },
        .{ .nome = "Mouse", .preco = 89.90, .estoque = 120 },
        .{ .nome = "Monitor", .preco = 1299.00, .estoque = 15 },
        .{ .nome = "Headset", .preco = 349.90, .estoque = 30 },
    };

    // Gerar CSV
    var csv = std.ArrayList(u8).init(allocator);
    defer csv.deinit();
    const writer = csv.writer();

    // Cabeçalho
    try writer.writeAll("nome,preco,estoque\n");

    // Dados
    for (&produtos) |produto| {
        try writer.print("{s},{d:.2},{d}\n", .{ produto.nome, produto.preco, produto.estoque });
    }

    std.debug.print("CSV gerado:\n{s}", .{csv.items});
}

CSV com Campos entre Aspas

const std = @import("std");

fn parsearCampoCSV(allocator: std.mem.Allocator, input: []const u8) !struct { campo: []u8, rest: []const u8 } {
    var campo = std.ArrayList(u8).init(allocator);
    errdefer campo.deinit();

    var pos: usize = 0;

    if (pos < input.len and input[pos] == '"') {
        // Campo entre aspas
        pos += 1;
        while (pos < input.len) {
            if (input[pos] == '"') {
                if (pos + 1 < input.len and input[pos + 1] == '"') {
                    try campo.append('"');
                    pos += 2;
                } else {
                    pos += 1;
                    break;
                }
            } else {
                try campo.append(input[pos]);
                pos += 1;
            }
        }
    } else {
        // Campo simples
        while (pos < input.len and input[pos] != ',') {
            try campo.append(input[pos]);
            pos += 1;
        }
    }

    // Pular vírgula
    if (pos < input.len and input[pos] == ',') pos += 1;

    return .{
        .campo = try campo.toOwnedSlice(),
        .rest = input[pos..],
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const linhas = [_][]const u8{
        \\simples,campo,normal
        ,
        \\"com espaço","outro campo",valor
        ,
        \\"aspas ""dentro""","vírgula, no campo",fim
        ,
    };

    for (linhas) |linha| {
        if (linha.len == 0) continue;

        std.debug.print("Entrada: {s}\n", .{linha});
        std.debug.print("Campos:  ", .{});

        var rest = linha;
        var primeiro = true;
        while (rest.len > 0) {
            const resultado = try parsearCampoCSV(allocator, rest);
            defer allocator.free(resultado.campo);
            rest = resultado.rest;

            if (!primeiro) std.debug.print(" | ", .{});
            std.debug.print("[{s}]", .{resultado.campo});
            primeiro = false;
        }
        std.debug.print("\n\n", .{});
    }
}

Dicas e Boas Práticas

  1. Cuidado com campos entre aspas: CSV real frequentemente tem campos com vírgulas e quebras de linha dentro de aspas.

  2. Valide os dados: Sempre trate erros ao converter strings para números.

  3. Considere o encoding: Dados em português podem ter caracteres UTF-8 que ocupam mais bytes.

  4. Use ArenaAllocator: Para processar CSV grande, arena simplifica a limpeza de memória.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.