Introdução
Quando você precisa processar arquivos JSON muito grandes (centenas de megabytes ou gigabytes), carregar tudo na memória não é viável. O streaming JSON permite processar os dados token por token, consumindo memória constante independente do tamanho do arquivo.
Em Zig, std.json.Scanner oferece um parser de baixo nível que emite tokens conforme lê o JSON, permitindo processamento eficiente de dados massivos.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Familiaridade com parsing JSON em Zig
Scanner JSON Básico
Use o scanner para processar JSON token por token:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json_str =
\\{"nome": "Maria", "idade": 28, "ativo": true}
;
// Criar scanner
var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
defer scanner.deinit();
// Processar tokens
while (true) {
const token = try scanner.next();
switch (token) {
.object_begin => std.debug.print("Início de objeto\n", .{}),
.object_end => std.debug.print("Fim de objeto\n", .{}),
.array_begin => std.debug.print("Início de array\n", .{}),
.array_end => std.debug.print("Fim de array\n", .{}),
.string => |s| std.debug.print("String: \"{s}\"\n", .{s}),
.number => |n| std.debug.print("Número: {s}\n", .{n}),
.true => std.debug.print("Boolean: true\n", .{}),
.false => std.debug.print("Boolean: false\n", .{}),
.null => std.debug.print("Null\n", .{}),
.end_of_document => {
std.debug.print("Fim do documento\n", .{});
break;
},
else => {},
}
}
}
Saída esperada
Início de objeto
String: "nome"
String: "Maria"
String: "idade"
Número: 28
String: "ativo"
Boolean: true
Fim de objeto
Fim do documento
Streaming de Arquivo Grande
Processe um arquivo JSON grande sem carregar tudo na memória:
const std = @import("std");
const fs = std.fs;
const Estatisticas = struct {
total_objetos: u64 = 0,
total_strings: u64 = 0,
total_numeros: u64 = 0,
total_booleans: u64 = 0,
total_nulls: u64 = 0,
profundidade_max: u32 = 0,
profundidade_atual: u32 = 0,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Simular dados grandes criando o arquivo
const file = try fs.cwd().createFile("dados_grandes.json", .{});
{
defer file.close();
const writer = file.writer();
try writer.writeAll("[");
for (0..1000) |i| {
if (i > 0) try writer.writeAll(",");
try writer.print(
\\{{"id":{d},"nome":"Usuario {d}","ativo":true,"pontos":{d}}}
, .{ i, i, i * 10 });
}
try writer.writeAll("]");
}
// Agora ler em streaming
const data = try fs.cwd().readFileAlloc(allocator, "dados_grandes.json", 100 * 1024 * 1024);
defer allocator.free(data);
var scanner = std.json.Scanner.initCompleteInput(allocator, data);
defer scanner.deinit();
var stats = Estatisticas{};
while (true) {
const token = try scanner.next();
switch (token) {
.object_begin => {
stats.total_objetos += 1;
stats.profundidade_atual += 1;
if (stats.profundidade_atual > stats.profundidade_max) {
stats.profundidade_max = stats.profundidade_atual;
}
},
.object_end => stats.profundidade_atual -= 1,
.array_begin => {
stats.profundidade_atual += 1;
if (stats.profundidade_atual > stats.profundidade_max) {
stats.profundidade_max = stats.profundidade_atual;
}
},
.array_end => stats.profundidade_atual -= 1,
.string => stats.total_strings += 1,
.number => stats.total_numeros += 1,
.true, .false => stats.total_booleans += 1,
.null => stats.total_nulls += 1,
.end_of_document => break,
else => {},
}
}
std.debug.print("Estatísticas do JSON:\n", .{});
std.debug.print(" Objetos: {d}\n", .{stats.total_objetos});
std.debug.print(" Strings: {d}\n", .{stats.total_strings});
std.debug.print(" Números: {d}\n", .{stats.total_numeros});
std.debug.print(" Booleans: {d}\n", .{stats.total_booleans});
std.debug.print(" Nulls: {d}\n", .{stats.total_nulls});
std.debug.print(" Profundidade máxima: {d}\n", .{stats.profundidade_max});
}
Extrair Campos Específicos em Streaming
Procure campos específicos sem parsear o objeto inteiro:
const std = @import("std");
fn findFieldValue(
allocator: std.mem.Allocator,
json_str: []const u8,
target_field: []const u8,
) !?[]const u8 {
var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
defer scanner.deinit();
var depth: u32 = 0;
var in_target_object = false;
var found_key = false;
while (true) {
const token = try scanner.next();
switch (token) {
.object_begin => {
depth += 1;
if (depth == 1) in_target_object = true;
},
.object_end => {
depth -= 1;
if (depth == 0) in_target_object = false;
},
.string => |s| {
if (found_key) {
// Este é o valor que procuramos
return try allocator.dupe(u8, s);
}
if (in_target_object and depth == 1 and std.mem.eql(u8, s, target_field)) {
found_key = true;
}
},
.number => |n| {
if (found_key) {
return try allocator.dupe(u8, n);
}
},
.end_of_document => break,
else => {
if (found_key) found_key = false;
},
}
}
return null;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json =
\\{
\\ "id": "abc123",
\\ "nome": "Produto Especial",
\\ "preco": "299.90",
\\ "categoria": "eletrônicos",
\\ "descricao": "Um produto muito especial"
\\}
;
// Procurar apenas o campo "nome"
if (try findFieldValue(allocator, json, "nome")) |valor| {
defer allocator.free(valor);
std.debug.print("Nome encontrado: {s}\n", .{valor});
}
if (try findFieldValue(allocator, json, "preco")) |valor| {
defer allocator.free(valor);
std.debug.print("Preço encontrado: {s}\n", .{valor});
}
if (try findFieldValue(allocator, json, "inexistente")) |_| {
std.debug.print("Não deveria chegar aqui\n", .{});
} else {
std.debug.print("Campo 'inexistente' não encontrado (esperado)\n", .{});
}
}
Contar Elementos em Array Sem Carregar Tudo
Conte elementos sem alocar memória para o array inteiro:
const std = @import("std");
fn countArrayElements(allocator: std.mem.Allocator, json_str: []const u8) !u64 {
var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
defer scanner.deinit();
var count: u64 = 0;
var depth: u32 = 0;
var in_root_array = false;
while (true) {
const token = try scanner.next();
switch (token) {
.array_begin => {
depth += 1;
if (depth == 1) in_root_array = true;
},
.array_end => {
depth -= 1;
if (depth == 0) in_root_array = false;
},
.object_begin => {
if (in_root_array and depth == 1) count += 1;
depth += 1;
},
.object_end => depth -= 1,
.string, .number, .true, .false, .null => {
if (in_root_array and depth == 1) count += 1;
},
.end_of_document => break,
else => {},
}
}
return count;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json = "[1, 2, 3, {\"a\": 1}, [5, 6], \"texto\", null, true]";
const count = try countArrayElements(allocator, json);
std.debug.print("Elementos no array: {d}\n", .{count});
}
Saída esperada
Elementos no array: 8
Dicas e Boas Práticas
Use streaming para arquivos grandes: Se o JSON tem mais de alguns MB, streaming é mais eficiente.
Memória constante: O scanner usa memória proporcional à profundidade de aninhamento, não ao tamanho do arquivo.
Combine com parsing tipado: Use streaming para encontrar seções e parsing tipado para processar sub-objetos.
Trate erros de sintaxe: O scanner retorna erros quando encontra JSON inválido.
Performance: O scanner é significativamente mais rápido para arquivos grandes pois evita alocações.
Receitas Relacionadas
- Como parsear JSON em Zig - Parsing completo
- Como gerar JSON em Zig - Serialização
- Como ler/escrever JSON em arquivos - I/O de arquivos
- Como validar JSON em Zig - Validação