Skip to content
StackPractices
intermediate Por Mathias Paulenko

Leer Archivos Grandes

Cómo leer archivos grandes de forma eficiente sin agotar la memoria.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

Leer archivos de varios gigabytes en memoria de una sola vez puede bloquear aplicaciones o degradar severamente el rendimiento. Lo siguiente demuestra técnicas eficientes en memoria para procesar archivos grandes línea por línea o en chunks en Python, JavaScript y Java.

Cuándo Usar

Usa este recurso cuando:

  • Procesas archivos de log, volcados CSV o datasets mayores que la RAM disponible
  • Construyes pipelines ETL que ingieren archivos masivos
  • Transmites contenido de archivos para evitar bloquear el event loop o el heap

Solución

Python

# Streaming línea por línea (eficiente en memoria)
with open('large-file.log', 'r', encoding='utf-8') as f:
    for line in f:
        process(line)

# Lectura binaria por chunks
chunk_size = 1024 * 1024  # 1 MB
with open('large-file.bin', 'rb') as f:
    while chunk := f.read(chunk_size):
        process(chunk)

JavaScript

const fs = require('fs');
const readline = require('readline');

// Línea por línea con readline (Node.js)
const stream = fs.createReadStream('large-file.log');
const rl = readline.createInterface({ input: stream });

for await (const line of rl) {
    console.log(line);
}

// Lectura por chunks
const readable = fs.createReadStream('large-file.bin', { highWaterMark: 1024 * 1024 });
readable.on('data', chunk => process(chunk));

Java

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class LargeFileReader {
    // Línea por línea
    public void readLines(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                process(line);
            }
        }
    }

    // Lectura por chunks con memory mapping
    public void readChunks(String path) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
            while (channel.read(buffer) > 0) {
                buffer.flip();
                process(buffer);
                buffer.clear();
            }
        }
    }

    private void process(Object data) {}
}

Explicación

El streaming línea por línea mantiene solo una línea en memoria a la vez, ideal para logs de texto y archivos CSV. La lectura por chunks procesa bloques de bytes de tamaño fijo, adecuada para datos binarios o cuando necesitas controlar el tamaño del buffer. Los archivos mapeados en memoria (Java) dejan que el SO maneje el paging directamente, a menudo más rápido para acceso aleatorio pero usa espacio de direcciones virtuales.

Variantes

TecnologíaEnfoqueNotas
PythonMódulo mmapMapea archivo a memoria; el SO maneja paging
JavaScriptWeb Streams APIReadableStream.getReader() en navegadores
JavaFiles.lines()Stream lazy; auto-cierre

Lo que funciona

  1. Siempre usa with (Python), try-with-resources (Java) o manejo de errores en pipes (JS) para evitar fugas de descriptores de archivo
  2. Elige tamaños de buffer basados en la longitud promedio de línea; 1 MB es un valor razonable por defecto
  3. Para CSV/JSONL, parsea incrementalmente en lugar de cargar toda la estructura
  4. Monitorea el uso de memoria con herramientas del SO para verificar el comportamiento de streaming
  5. Usa mmap cuando necesites acceso aleatorio sin cargar todo el archivo

Errores Comunes

  1. Llamar read() o readFileSync() en archivos grandes carga todo en RAM
  2. Olvidar manejar errores de encoding, que rompen streams a mitad de archivo
  3. Usar tamaños de chunk demasiado pequeños, causando overhead excesivo de syscalls
  4. No cerrar handles de archivo, provocando errores de “demasiados archivos abiertos”
  5. Ignorar backpressure al pipear a consumidores lentos

Preguntas Frecuentes

¿Qué tan grande es “grande”?

Cualquier archivo que se acerque o exceda el heap/RAM del proceso (ej. >500 MB en un contenedor de 2 GB). El streaming es económico; prefírelo siempre para archivos de más de unos pocos megabytes.

¿Funciona línea por línea para archivos binarios?

No. Los archivos binarios deben leerse con chunks de bytes. Los enfoques basados en líneas asumen delimitadores de nueva línea y encoding de texto.

¿Es memory mapping más rápido que streaming?

Para acceso secuencial, usualmente no de forma dramática. El memory mapping brilla en patrones de acceso aleatorio o cuando múltiples procesos comparten el mismo archivo.

Soluciones Avanzadas

Python: Generadores para procesamiento lazy con seguimiento de progreso

import os
from pathlib import Path
from typing import Iterator

def read_lines_lazy(path: str, encoding: str = 'utf-8') -> Iterator[str]:
    """Yield líneas una a la vez. El uso de memoria es O(1) sin importar el tamaño del archivo."""
    with open(path, 'r', encoding=encoding) as f:
        for line in f:
            yield line.rstrip('\n\r')

def read_chunks(path: str, chunk_size: int = 1024 * 1024) -> Iterator[bytes]:
    """Yield chunks binarios. El uso de memoria es O(chunk_size)."""
    with open(path, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

def process_with_progress(path: str, process_fn, encoding: str = 'utf-8') -> int:
    """Procesa archivo línea por línea con seguimiento de progreso. Retorna conteo de líneas."""
    file_size = os.path.getsize(path)
    bytes_read = 0
    line_count = 0

    with open(path, 'r', encoding=encoding) as f:
        for line in f:
            process_fn(line.rstrip('\n\r'))
            bytes_read += len(line.encode(encoding))
            line_count += 1
            if line_count % 10000 == 0:
                pct = (bytes_read / file_size) * 100 if file_size > 0 else 0
                print(f"Progreso: {pct:.1f}% ({line_count} líneas)")
    return line_count

def read_csv_streaming(path: str, encoding: str = 'utf-8'):
    """Stream filas CSV sin cargar el archivo completo. Retorna iterador de dicts."""
    import csv
    with open(path, 'r', encoding=encoding, newline='') as f:
        reader = csv.DictReader(f)
        for row in reader:
            yield row

def read_jsonl_streaming(path: str, encoding: str = 'utf-8'):
    """Stream JSONL (un objeto JSON por línea) sin cargar el archivo completo."""
    import json
    for line in read_lines_lazy(path, encoding):
        if line.strip():
            yield json.loads(line)

# Uso
# for line in read_lines_lazy('10gb.log'):
#     if 'ERROR' in line:
#         print(line)
# count = process_with_progress('large.log', lambda l: None)
# for row in read_csv_streaming('data.csv'):
#     process_row(row)

Node.js: Streaming con backpressure y recuperación de errores

const fs = require('fs');
const readline = require('readline');
const { pipeline } = require('stream');
const { promisify } = require('util');
const pipe = promisify(pipeline);

async function readLinesProcess(path, processFn) {
    const stream = fs.createReadStream(path, { encoding: 'utf-8' });
    const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });

    let count = 0;
    for await (const line of rl) {
        await processFn(line);
        count++;
    }
    return count;
}

async function readChunksProcess(path, processFn, chunkSize = 1024 * 1024) {
    const stream = fs.createReadStream(path, { highWaterMark: chunkSize });
    let totalBytes = 0;

    for await (const chunk of stream) {
        await processFn(chunk);
        totalBytes += chunk.length;
    }
    return totalBytes;
}

async function streamToTransform(srcPath, destPath, transformFn) {
    const src = fs.createReadStream(srcPath, { encoding: 'utf-8' });
    const dest = fs.createWriteStream(destPath, { encoding: 'utf-8' });

    const { Transform } = require('stream');
    let lineBuffer = '';

    const transform = new Transform({
        transform(chunk, encoding, callback) {
            lineBuffer += chunk;
            const lines = lineBuffer.split('\n');
            lineBuffer = lines.pop();
            for (const line of lines) {
                const result = transformFn(line);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
        flush(callback) {
            if (lineBuffer) {
                const result = transformFn(lineBuffer);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
    });

    await pipe(src, transform, dest);
}

// Uso
// const count = await readLinesProcess('large.log', async (line) => {
//     if (line.includes('ERROR')) console.error(line);
// });
// console.log(`Procesadas ${count} líneas`);
// await streamToTransform('input.log', 'output.log', line => line.toUpperCase());

Java: Archivos NIO mapeados en memoria y procesamiento paralelo de líneas

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;

public class AdvancedFileReader {

    // Lectura de archivo mapeado en memoria para acceso aleatorio
    public static void readMapped(String path, long chunkSize) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
            long fileSize = channel.size();
            long position = 0;
            while (position < fileSize) {
                long remaining = fileSize - position;
                long mapSize = Math.min(chunkSize, remaining);
                MappedByteBuffer buffer = channel.map(
                    FileChannel.MapMode.READ_ONLY, position, mapSize
                );
                processBuffer(buffer);
                position += mapSize;
            }
        }
    }

    private static void processBuffer(MappedByteBuffer buffer) {
        buffer.load();
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            // Procesar byte
        }
    }

    // Procesamiento paralelo de líneas con Files.lines()
    public static long processLinesParallel(String path,
                                            java.util.function.Consumer<String> processor)
            throws IOException {
        try (Stream<String> lines = Files.lines(Paths.get(path), StandardCharsets.UTF_8)) {
            return lines.parallel()
                .peek(processor)
                .count();
        }
    }

    // Lectura bufferizada con tamaño de buffer configurable
    public static int readBuffered(String path, int bufferSize,
                                   java.util.function.Consumer<String> lineHandler)
            throws IOException {
        int count = 0;
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream(path), StandardCharsets.UTF_8), bufferSize)) {
            String line;
            while ((line = reader.readLine()) != null) {
                lineHandler.accept(line);
                count++;
            }
        }
        return count;
    }

    // Leer archivo en chunks usando NIO
    public static long readChunksNIO(String path, int chunkSize) throws IOException {
        long totalBytes = 0;
        try (SeekableByteChannel channel = Files.newByteChannel(
                Paths.get(path), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(chunkSize);
            while (channel.read(buffer) > 0) {
                buffer.flip();
                totalBytes += buffer.remaining();
                buffer.clear();
            }
        }
        return totalBytes;
    }
}

// Uso
// AdvancedFileReader.readMapped("large.bin", 1024 * 1024 * 100); // chunks de 100MB
// long count = AdvancedFileReader.processLinesParallel("large.log",
//     line -> { if (line.contains("ERROR")) System.err.println(line); });
// int lines = AdvancedFileReader.readBuffered("data.csv", 65536, System.out::println);

Bash: Lectura eficiente de archivos con while-read y awk

#!/usr/bin/env bash
set -euo pipefail

# Leer línea por línea (eficiente en memoria, maneja caracteres especiales)
read_lines() {
    local file="$1"
    while IFS= read -r line || [[ -n "$line" ]]; do
        echo "$line"
    done < "$file"
}

# Procesar con awk (lo más rápido para procesamiento de texto)
process_with_awk() {
    local file="$1"
    local pattern="${2:-ERROR}"
    awk -v pat="$pattern" '$0 ~ pat { count++ } END { print count " coincidencias" }' "$file"
}

# Leer en chunks usando dd (para archivos binarios)
read_chunks_dd() {
    local file="$1"
    local chunk_size="${2:-1048576}"  # 1MB default
    local offset=0
    local file_size
    file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
    while (( offset < file_size )); do
        dd if="$file" bs="$chunk_size" skip=$((offset / chunk_size)) count=1 2>/dev/null
        offset=$((offset + chunk_size))
    done
}

# Contar líneas eficientemente (wc -l es más rápido que while-read)
count_lines() {
    local file="$1"
    wc -l < "$file" | tr -d ' '
}

# Filtrar y contar en una pasada (evitar múltiples lecturas)
filter_and_count() {
    local file="$1"
    local pattern="$2"
    grep -c "$pattern" "$file"
}

# Leer rango específico de líneas (sed para rangos pequeños, tail+head para grandes)
read_line_range() {
    local file="$1"
    local start="$2"
    local end="$3"
    sed -n "${start},${end}p" "$file"
}

# Uso
# read_lines large.log | head -100
# process_with_awk large.log ERROR
# count_lines large.log
# read_line_range large.log 1000 1010

Mejores Prácticas Adicionales

  1. Usa csv.DictReader para archivos CSV en lugar de parsing manual. Maneja campos entre comillas, comas embebidas y registros multi-línea correctamente:
import csv

def process_csv(path: str) -> int:
    """Stream filas CSV como dicts. Maneja campos entre comillas y nuevas líneas embebidas."""
    count = 0
    with open(path, 'r', encoding='utf-8', newline='') as f:
        reader = csv.DictReader(f)
        for row in reader:
            process_row(row)
            count += 1
    return count

# process_csv('10gb_export.csv')
  1. Establece highWaterMark explícitamente en streams de Node.js. El default es 16KB lo que causa syscalls excesivos. Para archivos grandes, usa 256KB-1MB:
const fs = require('fs');

// Bueno: highWaterMark explícito reduce overhead de syscalls
const stream = fs.createReadStream('large.log', {
    highWaterMark: 1024 * 1024,  // 1MB
    encoding: 'utf-8',
});

// Default: 16KB causa muchas lecturas pequeñas
// const stream = fs.createReadStream('large.log');
  1. Usa Files.lines() con try-with-resources en Java. Retorna un Stream<String> lazy que lee líneas bajo demanda. El stream debe cerrarse para liberar el handle de archivo:
import java.nio.file.*;
import java.util.stream.Stream;

// Bueno: try-with-resources asegura que el handle de archivo se cierre
try (Stream<String> lines = Files.lines(Path.of("large.log"))) {
    lines.filter(l -> l.contains("ERROR"))
         .forEach(System.out::println);
}

// Malo: stream no cerrado, fuga de descriptor de archivo
// Stream<String> lines = Files.lines(Path.of("large.log"));
// lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
// // handle de archivo nunca cerrado

Errores Comunes Adicionales

  1. Usar readlines() para archivos grandes en Python. readlines() carga todas las líneas en una lista. Para archivos mayores a unos pocos MB, itera directamente:
# Mal: carga todo el archivo en memoria como una lista
with open('large.log') as f:
    lines = f.readlines()
    for line in lines:
        process(line)

# Bien: itera de forma lazy, una línea a la vez
with open('large.log') as f:
    for line in f:
        process(line)
  1. No manejar errores de stream en Node.js. Eventos 'error' no manejados crashean el proceso. Siempre adjunta handlers de error o usa pipeline():
const fs = require('fs');
const { pipeline } = require('stream');
const { promisify } = require('util');

// Mal: sin manejo de errores, crashea si el archivo no existe
// const stream = fs.createReadStream('missing.log');
// stream.on('data', chunk => process(chunk));

// Bien: pipeline maneja errores y limpieza
async function safeRead(path) {
    const stream = fs.createReadStream(path);
    const { Writable } = require('stream');
    const sink = new Writable({
        write(chunk, encoding, callback) {
            process(chunk);
            callback();
        },
    });
    await promisify(pipeline)(stream, sink);
}
  1. Usar Files.readAllLines() para archivos grandes en Java. readAllLines() carga todas las líneas en un List<String>. Usa Files.lines() para streaming lazy:
import java.nio.file.*;
import java.util.List;

// Mal: carga todo el archivo en memoria
// List<String> lines = Files.readAllLines(Path.of("10gb.log"));

// Bien: stream lazy, una línea a la vez
try (Stream<String> lines = Files.lines(Path.of("10gb.log"))) {
    lines.forEach(line -> process(line));
}

Preguntas Frecuentes Adicionales

¿Cómo leo rangos específicos de líneas sin cargar todo el archivo?

En Python, usa itertools.islice con un generador. En Bash, usa sed -n 'start,end p'. En Java, usa Files.lines().skip(n).limit(m):

from itertools import islice

def read_line_range(path: str, start: int, count: int) -> list[str]:
    """Lee `count` líneas empezando desde la línea `start` (0-indexed)."""
    with open(path, 'r', encoding='utf-8') as f:
        return list(islice(f, start, start + count))

# lines = read_line_range('large.log', 1000, 10)  # Líneas 1000-1009
# Bash: leer líneas 1000-1010
sed -n '1000,1010p' large.log

# Más rápido para offsets grandes: saltar con tail
tail -n +1000 large.log | head -n 10

¿Esta solución está lista para producción?

Sí. El protocolo de iteración de archivos de Python es usado por la standard library, los handlers de subida de archivos de Django, y el lector CSV por chunks de pandas. readline y createReadStream de Node.js con pipeline() son usados por Express.js, Next.js, y el SDK de AWS para descargas S3. BufferedReader y Files.lines() de Java son usados por Spring Batch, Apache Spark para ingesta de archivos de texto, y Elasticsearch para parsing de logs. while read de Bash es el estándar para procesamiento de logs en logrotate, fail2ban, y procesamiento de journal de systemd. El patrón de archivo mapeado en memoria es usado por el storage engine de MongoDB (WiredTiger), Lucene para segmentos de índice, y Kafka para lectura de segmentos de log.

¿Cuáles son las características de rendimiento?

for line in f de Python procesa 500K-1M líneas/s con ~0.01ms de overhead por línea. readline de Node.js procesa 300K-800K líneas/s; createReadStream con highWaterMark de 1MB procesa 50-200MB/s. BufferedReader.readLine() de Java procesa 500K-1.2M líneas/s con buffer default de 8KB; aumentar a 64KB mejora throughput 20-40%. Files.lines().parallel() de Java escala linealmente con cores para procesamiento CPU-bound pero añade 5-10ms de overhead para splitting de stream. MappedByteBuffer lee a 500-2000MB/s para acceso secuencial, limitado por I/O de disco. while read de Bash procesa 50K-200K líneas/s debido al overhead de subshell por línea; awk procesa 500K-2M líneas/s. grep procesa 200-800MB/s para patrones simples. Uso de memoria: lectura línea por línea usa O(max_line_length) por línea. Lectura por chunks usa O(chunk_size). Lectura mapeada en memoria usa O(map_size) de espacio de direcciones virtuales pero la memoria física es gestionada por el OS page cache.

¿Cómo depuro problemas con este enfoque?

Para problemas de memoria, monitorea con ps aux | grep <pid> (columna RSS) o top -p <pid> — RSS debería mantenerse plano durante streaming. Para lecturas lentas, verifica I/O de disco con iostat -x 1 (Linux) o Activity Monitor > Disk (macOS). Para errores de encoding, inspecciona bytes con xxd file.txt | head o hexdump -C file.txt | head para detectar BOM o encodings mixtos. Para fugas de descriptores en Python, verifica len(psutil.Process().open_files()) o lsof -p <pid> | wc -l. En Node.js, verifica process._getActiveHandles() para streams no cerrados. En Java, verifica ManagementFactory.getPlatformMBeanServer() para java.nio:type=BufferPool,name=direct para monitorear uso de direct buffers. Para problemas de backpressure en Node.js, busca warnings de evento 'drain' o usa stream.writableNeedDrain. Para while read lento de Bash, reemplaza con awk o grep que son 10-50x más rápidos para procesamiento de texto. Para problemas de archivos mapeados en memoria en Java, verifica ulimit -v para límites de memoria virtual y asegúrate que mapSize no exceda el espacio de direcciones disponible.