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ário | Padrão | Streaming |
|---|---|---|
| Ficheiro inferior a 100 MB | Preferido | Excessivo |
| Ficheiro 100 MB a 1 GB | Depende da RAM | Recomendado |
| Ficheiro superior a 1 GB | Não viável | Obrigatório |
| Resposta HTTP (grande) | Risco de timeout | Stream à medida que chega |
| Feed de dados em tempo real | Não aplicável | Obrigatório |
| Leitura simples única | Preferido | Desnecessário |
Comparação de Desempenho
Processamento de 1 milhão de registos (ficheiro de 1 GB):
| Abordagem | Utilização de Memória | Tempo de Processamento | Complexidade |
|---|---|---|---|
| json.load() | 3-5 GB | 15 seg | Simples |
| ijson streaming | 50 MB | 45 seg | Moderada |
| JSON Lines | 10 MB | 12 seg | Simples |
| Por blocos (1000) | 100 MB | 20 seg | Moderada |
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
- Utilizar JSON Lines quando possível: É o formato de streaming mais simples e funciona com ferramentas padrão.
- Definir tamanhos de buffer: Configurar buffers de leitura para throughput ótimo (tipicamente 64 KB a 1 MB).
- Processar em lotes: Agrupar inserções na base de dados e chamadas de API em vez de processar um registo de cada vez.
- Tratar erros graciosamente: Em streaming, um registo mal formado não deve fazer crashar todo o pipeline.
- 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
- Formatador JSON — Formatar e validar ficheiros JSON
- Guia de Consultas JSONPath — Extrair dados de JSON de forma eficiente
- Dicas do Editor JSON — Trabalhar com documentos JSON grandes