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

JSON Streaming para Archivos Grandes: Procesar Sin Cargar Todo

El parseo estándar de JSON carga el documento completo en memoria, construye una estructura de datos completa y luego te da acceso. Para un archivo de 10 MB, eso funciona bien. Para un archivo de 10 GB, tu proceso se queda sin memoria y se bloquea. Los parsers de streaming resuelven esto procesando JSON incrementalmente — leyendo y manejando datos conforme llegan, sin mantener nunca el documento completo en memoria.

El Problema con el Parseo Estándar

import json

# Esto carga el archivo COMPLETO en memoria
with open('huge.json') as f:
    data = json.load(f)  # Archivo de 10 GB = 10+ GB de RAM

# El procesamiento ocurre después de la carga completa
for item in data['records']:
    process(item)

Para un archivo con 1 millón de registros de 10 KB cada uno, el parseo estándar necesita:

  • Tamaño del archivo: ~10 GB
  • Memoria para parseo: ~10 GB (la cadena cruda)
  • Memoria para estructura de datos: ~15-20 GB (los objetos Python son más grandes que el JSON crudo)
  • Total: ~25-30 GB de RAM

El streaming reduce esto a megabytes.

Enfoques de Streaming

Estilo SAX (Basado en Eventos)

El parser emite eventos conforme encuentra tokens JSON:

import ijson

# Procesa elementos uno a la vez - uso de memoria constante
with open('huge.json', 'rb') as f:
    for record in ijson.items(f, 'records.item'):
        process(record)  # Cada registro se parsea individualmente
        # Los registros anteriores son recolectados por el garbage collector

Los eventos incluyen: start_map, map_key, end_map, start_array, end_array, string, number, boolean, null.

JSON Lines (JSONL / NDJSON)

Un enfoque más simple: un objeto JSON por línea. Cada línea es un documento JSON completo y 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"}

El procesamiento es trivial — lee línea por línea:

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

Ventajas de JSON Lines:

  • Cada línea es parseable independientemente (procesamiento paralelo)
  • Amigable para agregar datos (solo añade una nueva línea)
  • Funciona con herramientas estándar de Unix (grep, wc, head, tail)
  • Natural para archivos de log y datos en streaming

Procesamiento por Chunks

Para arrays JSON estándar, divide el procesamiento en chunks:

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)

Implementaciones por Lenguaje

Python (ijson)

import ijson

# Stream desde archivo
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 desde respuesta 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');
});

Línea de Comandos (jq)

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

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

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

Cuándo Usar Streaming

EscenarioEstándarStreaming
Archivo menor a 100 MBPreferidoExcesivo
Archivo de 100 MB a 1 GBDepende de la RAMRecomendado
Archivo mayor a 1 GBNo viableRequerido
Respuesta HTTP (grande)Riesgo de timeoutStream conforme se recibe
Feed de datos en tiempo realNo aplicableRequerido
Lectura simple únicaPreferidoInnecesario

Comparación de Rendimiento

Procesando 1 millón de registros (archivo de 1 GB):

EnfoqueUso de MemoriaTiempo de ProcesamientoComplejidad
json.load()3-5 GB15 segSimple
ijson streaming50 MB45 segModerada
JSON Lines10 MB12 segSimple
Chunks (1000)100 MB20 segModerada

El streaming usa mucha menos memoria pero es más lento para el parseo estilo SAX debido a la sobrecarga del modelo basado en eventos. JSON Lines es tanto rápido como eficiente en memoria porque cada línea es una operación de parseo independiente.

Mejores Prácticas

  1. Usa JSON Lines cuando sea posible: Es el formato de streaming más simple y funciona con herramientas estándar.
  2. Configura tamaños de buffer: Configura los buffers de lectura para un rendimiento óptimo (típicamente de 64 KB a 1 MB).
  3. Procesa en lotes: Agrupa las inserciones en base de datos y las llamadas a API en lugar de procesar un registro a la vez.
  4. Maneja errores con gracia: En streaming, un registro malformado no debería bloquear todo el pipeline.
  5. Monitorea la memoria: Usa herramientas de profiling para verificar que el streaming realmente mantiene la memoria acotada.

Para formatear y validar archivos JSON más pequeños, nuestro Formateador JSON maneja documentos de hasta 100 MB con formateo en tiempo real.

FAQ

¿Puedo usar JSONPath con parsers de streaming?

Algunas bibliotecas de streaming soportan filtrado basado en rutas. Python ijson.items soporta filtrado por ruta durante el streaming. Sin embargo, las consultas JSONPath complejas (filtros, comodines entre niveles) típicamente requieren el documento completo en memoria. Para consultas basadas en rutas, consulta nuestra guía de JSONPath.

¿Cómo convierto un array JSON grande a JSON Lines?

Usa jq -c '.[]' input.json > output.jsonl para archivos que caben en memoria. Para archivos realmente grandes, usa un convertidor de streaming: lee el array con un parser de streaming y escribe cada elemento como una línea.

Recursos Relacionados

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