alltools.one
JSON
2025-06-02
8 min
alltools.one Team
JSONStreamingPerformanceLarge FilesMemory

JSON Streaming para Ficheiros Grandes: Processar Sem Carregar Tudo

O parsing JSON padrão carrega o documento inteiro na memória, constrói uma estrutura de dados completa e depois dá-lhe acesso. Para um ficheiro de 10 MB, isso funciona bem. Para um ficheiro de 10 GB, o seu processo fica sem memória e crasha. Os parsers de streaming resolvem isto processando o JSON de forma incremental — lendo e tratando os dados à medida que chegam, sem nunca manter o documento inteiro na memória.

O Problema com o Parsing Padrão

import json

# Isto carrega o ficheiro INTEIRO na memória
with open('huge.json') as f:
    data = json.load(f)  # ficheiro de 10 GB = 10+ GB de RAM

# O processamento acontece após o carregamento completo
for item in data['records']:
    process(item)

Para um ficheiro com 1 milhão de registos a 10 KB cada, o parsing padrão necessita de:

  • Tamanho do ficheiro: ~10 GB
  • Memória para parsing: ~10 GB (a string em bruto)
  • Memória para estrutura de dados: ~15-20 GB (os objetos Python são maiores que JSON em bruto)
  • Total: ~25-30 GB de RAM

O streaming reduz isto para megabytes.

Abordagens de Streaming

Estilo SAX (Baseado em Eventos)

O parser emite eventos à medida que encontra tokens JSON:

import ijson

# Processar itens um de cada vez - utilização de memória constante
with open('huge.json', 'rb') as f:
    for record in ijson.items(f, 'records.item'):
        process(record)  # Cada registo é analisado individualmente
        # Os registos anteriores são recolhidos pelo garbage collector

Os eventos incluem: start_map, map_key, end_map, start_array, end_array, string, number, boolean, null.

JSON Lines (JSONL / NDJSON)

Uma abordagem mais simples: um objeto JSON por linha. Cada linha é um documento JSON completo e válido:

{"id": 1, "name": "Alice", "email": "alice@example.com"}
{"id": 2, "name": "Bob", "email": "bob@example.com"}
{"id": 3, "name": "Charlie", "email": "charlie@example.com"}

O processamento é trivial — ler linha a linha:

with open('data.jsonl') as f:
    for line in f:
        record = json.loads(line)
        process(record)

Vantagens do JSON Lines:

  • Cada linha é analisável de forma independente (processamento paralelo)
  • Amigável para adição (basta adicionar uma nova linha)
  • Funciona com ferramentas Unix padrão (grep, wc, head, tail)
  • Natural para ficheiros de log e dados em streaming

Processamento por Blocos

Para arrays JSON padrão, dividir o processamento em blocos:

import ijson

def process_in_chunks(filename, chunk_size=1000):
    chunk = []
    with open(filename, 'rb') as f:
        for record in ijson.items(f, 'item'):
            chunk.append(record)
            if len(chunk) >= chunk_size:
                process_batch(chunk)
                chunk = []
    if chunk:
        process_batch(chunk)

Implementações por Linguagem

Python (ijson)

import ijson

# Stream a partir de ficheiro
with open('large.json', 'rb') as f:
    parser = ijson.parse(f)
    for prefix, event, value in parser:
        if prefix == 'records.item.name':
            print(value)

# Stream a partir de resposta HTTP
import urllib.request
response = urllib.request.urlopen('https://api.example.com/data')
for record in ijson.items(response, 'records.item'):
    process(record)

JavaScript (Node.js)

const { createReadStream } = require('fs');
const { parser } = require('stream-json');
const { streamArray } = require('stream-json/streamers/StreamArray');

const pipeline = createReadStream('large.json')
  .pipe(parser())
  .pipe(streamArray());

pipeline.on('data', ({ value }) => {
  process(value);
});

pipeline.on('end', () => {
  console.log('Done processing');
});

Linha de Comandos (jq)

# Modo stream - processar objetos individualmente
jq --stream 'select(length == 2) | .[1]' large.json

# Processar JSON Lines
cat data.jsonl | jq -c 'select(.age > 30)'

# Converter array em JSON Lines
jq -c '.[]' large_array.json > data.jsonl

Quando Utilizar Streaming

CenárioPadrãoStreaming
Ficheiro inferior a 100 MBPreferidoExcessivo
Ficheiro 100 MB a 1 GBDepende da RAMRecomendado
Ficheiro superior a 1 GBNão viávelObrigatório
Resposta HTTP (grande)Risco de timeoutStream à medida que chega
Feed de dados em tempo realNão aplicávelObrigatório
Leitura simples únicaPreferidoDesnecessário

Comparação de Desempenho

Processamento de 1 milhão de registos (ficheiro de 1 GB):

AbordagemUtilização de MemóriaTempo de ProcessamentoComplexidade
json.load()3-5 GB15 segSimples
ijson streaming50 MB45 segModerada
JSON Lines10 MB12 segSimples
Por blocos (1000)100 MB20 segModerada

O streaming utiliza muito menos memória, mas é mais lento para parsing estilo SAX devido à sobrecarga orientada por eventos. O JSON Lines é rápido e eficiente em memória porque cada linha é uma operação de parsing independente.

Boas Práticas

  1. Utilizar JSON Lines quando possível: É o formato de streaming mais simples e funciona com ferramentas padrão.
  2. Definir tamanhos de buffer: Configurar buffers de leitura para throughput ótimo (tipicamente 64 KB a 1 MB).
  3. Processar em lotes: Agrupar inserções na base de dados e chamadas de API em vez de processar um registo de cada vez.
  4. Tratar erros graciosamente: Em streaming, um registo mal formado não deve fazer crashar todo o pipeline.
  5. Monitorizar memória: Utilizar profiling para verificar que o streaming está efetivamente a manter a memória limitada.

Para formatar e validar ficheiros JSON mais pequenos, o nosso Formatador JSON lida com documentos até 100 MB com formatação em tempo real.

FAQ

Posso utilizar JSONPath com parsers de streaming?

Algumas bibliotecas de streaming suportam filtragem baseada em caminho. O Python ijson.items suporta filtragem por caminho durante o streaming. No entanto, consultas JSONPath complexas (filtros, wildcards entre níveis) tipicamente requerem o documento completo na memória. Para consultas baseadas em caminho, consulte o nosso guia JSONPath.

Como converter um array JSON grande em JSON Lines?

Utilize jq -c '.[]' input.json > output.jsonl para ficheiros que cabem na memória. Para ficheiros verdadeiramente grandes, utilize um conversor de streaming: leia o array com um parser de streaming e escreva cada elemento como uma linha.

Recursos Relacionados

Published on 2025-06-02
JSON Streaming for Large Files: Process Without Loading All | alltools.one