Descomprimir archivos ZIP con Python es una tarea común en automatización, importación de datos, copias de seguridad y procesamiento por lotes. El módulo estándar zipfile permite listar, verificar y extraer archivos, pero una extracción segura requiere validar rutas, tamaños y contenido antes de escribir en el disco.
Para complementar esta guía, consulta los artículos sobre crear archivos ZIP, archivos en Python, manejo de excepciones, logging y automatización de tareas. Como referencias externas, revisa la documentación oficial de zipfile y la documentación oficial de shutil.unpack_archive.
Abrir y listar un ZIP
from zipfile import ZipFile
with ZipFile("documentos.zip", "r") as archivo_zip:
for nombre in archivo_zip.namelist():
print(nombre)
El modo r abre el archivo para lectura. namelist() devuelve los nombres internos, pero para inspeccionar tamaños y atributos es mejor utilizar infolist().
Inspeccionar entradas
with ZipFile("documentos.zip") as archivo_zip:
for info in archivo_zip.infolist():
print(info.filename)
print(info.file_size)
print(info.compress_size)
print(info.is_dir())
file_size representa el tamaño descomprimido y compress_size el tamaño almacenado. Una diferencia extrema puede indicar una bomba de compresión.
Comprobar que el archivo es ZIP
from zipfile import is_zipfile
if not is_zipfile("documentos.zip"):
raise ValueError("El archivo no es un ZIP válido")
La extensión .zip no garantiza que el contenido sea correcto.
Verificar integridad
with ZipFile("documentos.zip") as archivo_zip:
entrada_danada = archivo_zip.testzip()
if entrada_danada is not None:
raise ValueError(f"Entrada dañada: {entrada_danada}")
testzip() comprueba el CRC de las entradas. Esta verificación detecta corrupción, aunque no sustituye otras validaciones de seguridad.
Extraer todo
from pathlib import Path
from zipfile import ZipFile
destino = Path("extraidos")
destino.mkdir(parents=True, exist_ok=True)
with ZipFile("documentos.zip") as archivo_zip:
archivo_zip.extractall(destino)
Este patrón es adecuado para ZIP confiables. Para archivos recibidos de usuarios o de fuentes externas, valida cada entrada antes de extraer.
El riesgo Zip Slip
Una entrada maliciosa puede contener rutas como ../../archivo.txt e intentar escribir fuera de la carpeta de destino. Aunque las implementaciones modernas aplican protecciones, conviene validar explícitamente y no depender de nombres internos confiables.
from pathlib import Path
def destino_seguro(base, nombre):
base = Path(base).resolve()
candidato = (base / nombre).resolve()
if base != candidato and base not in candidato.parents:
raise ValueError(f"Ruta insegura en ZIP: {nombre}")
return candidato
La función confirma que la ruta final permanece dentro de la carpeta autorizada.
Extraer entrada por entrada
from shutil import copyfileobj
from zipfile import ZipFile
def extraer_seguro(ruta_zip, carpeta_destino):
carpeta_destino = Path(carpeta_destino)
carpeta_destino.mkdir(parents=True, exist_ok=True)
with ZipFile(ruta_zip) as archivo_zip:
for info in archivo_zip.infolist():
salida = destino_seguro(carpeta_destino, info.filename)
if info.is_dir():
salida.mkdir(parents=True, exist_ok=True)
continue
salida.parent.mkdir(parents=True, exist_ok=True)
with archivo_zip.open(info) as origen, salida.open("wb") as destino:
copyfileobj(origen, destino)
Este método ofrece control sobre cada archivo y permite aplicar límites antes de escribir.
Limitar el tamaño total
MAXIMO_TOTAL = 500 * 1024 * 1024
with ZipFile("documentos.zip") as archivo_zip:
total = sum(info.file_size for info in archivo_zip.infolist())
if total > MAXIMO_TOTAL:
raise ValueError("El contenido descomprimido supera el límite")
Elige un límite compatible con el espacio en disco y el propósito de la aplicación.
Limitar cantidad de archivos
MAXIMO_ARCHIVOS = 10_000
with ZipFile("documentos.zip") as archivo_zip:
entradas = archivo_zip.infolist()
archivos = [info for info in entradas if not info.is_dir()]
if len(archivos) > MAXIMO_ARCHIVOS:
raise ValueError("El ZIP contiene demasiados archivos")
Miles de archivos pequeños pueden agotar tiempo, inodos o recursos incluso si el tamaño total es moderado.
Limitar cada entrada
MAXIMO_POR_ARCHIVO = 100 * 1024 * 1024
for info in entradas:
if info.file_size > MAXIMO_POR_ARCHIVO:
raise ValueError(f"Archivo demasiado grande: {info.filename}")
Combina el límite individual con el total.
Detectar una relación de compresión sospechosa
def relacion_compresion(info):
if info.compress_size == 0:
return float("inf") if info.file_size else 1
return info.file_size / info.compress_size
for info in entradas:
if relacion_compresion(info) > 200:
raise ValueError(f"Compresión sospechosa: {info.filename}")
El umbral depende del contexto. Archivos de texto repetitivo pueden comprimirse mucho de forma legítima, por lo que esta regla debe formar parte de varias comprobaciones.
Filtrar extensiones
PERMITIDAS = {".csv", ".json", ".txt"}
for info in entradas:
if info.is_dir():
continue
extension = Path(info.filename).suffix.lower()
if extension not in PERMITIDAS:
raise ValueError(f"Extensión no permitida: {info.filename}")
No confíes únicamente en la extensión si vas a ejecutar, interpretar o publicar el contenido. Valida también el formato real.
Evitar sobrescrituras
if salida.exists():
raise FileExistsError(f"El destino ya existe: {salida}")
Otra estrategia es extraer en una carpeta nueva con un identificador único. Sobrescribir silenciosamente puede destruir archivos existentes.
Extraer un archivo específico
with ZipFile("documentos.zip") as archivo_zip:
archivo_zip.extract("informes/resumen.csv", path="extraidos")
Confirma primero que el nombre exista y sea exactamente el esperado.
Leer sin extraer
with ZipFile("documentos.zip") as archivo_zip:
with archivo_zip.open("datos/config.json") as archivo:
contenido = archivo.read().decode("utf-8")
print(contenido)
Esta técnica evita archivos temporales, pero establece un límite de lectura para no cargar entradas enormes en memoria.
Procesar CSV directamente
import csv
import io
with ZipFile("datos.zip") as archivo_zip:
with archivo_zip.open("ventas.csv") as binario:
texto = io.TextIOWrapper(binario, encoding="utf-8", newline="")
lector = csv.DictReader(texto)
for fila in lector:
print(fila)
El flujo se procesa sin extraer el CSV al disco.
Archivos protegidos con contraseña
with ZipFile("protegido.zip") as archivo_zip:
contenido = archivo_zip.read("archivo.txt", pwd=b"clave")
La compatibilidad depende del método de cifrado. No coloques contraseñas en el código y no confíes en cifrados ZIP antiguos para información sensible.
Usar shutil.unpack_archive
import shutil
shutil.unpack_archive("documentos.zip", "extraidos", format="zip")
unpack_archive() ofrece una interfaz común para varios formatos. Utilízalo con archivos confiables o después de aplicar validaciones apropiadas. zipfile proporciona más control específico.
Extraer primero en una carpeta temporal
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as temporal:
carpeta = Path(temporal)
extraer_seguro("documentos.zip", carpeta)
# Validar contenido antes de moverlo al destino final
Una carpeta temporal facilita limpiar resultados parciales si algo falla.
Mover al destino final
Después de validar nombres, tamaños y contenido, mueve los archivos aprobados. No consideres la extracción terminada hasta completar todas las verificaciones. Para procesos importantes, registra un manifiesto de lo extraído.
Registrar resultados
import logging
logging.basicConfig(
filename="extraccion.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logging.info("ZIP extraído: %s archivos", cantidad)
No registres contraseñas ni datos personales completos.
Manejo de errores
from zipfile import BadZipFile, LargeZipFile
try:
extraer_seguro("documentos.zip", "extraidos")
except BadZipFile:
print("El ZIP está dañado o no es válido")
except LargeZipFile:
print("El archivo requiere soporte ZIP64")
except OSError as error:
print(f"Error del sistema: {error}")
Captura excepciones específicas y no ocultes fallos con un except vacío.
Probar la función
Crea ZIP de prueba con carpetas, archivos vacíos, Unicode, rutas anidadas y entradas que intenten salir del destino. Prueba límites de tamaño, sobrescrituras y archivos corruptos. Utiliza una carpeta temporal para que las pruebas no modifiquen documentos reales.
Errores frecuentes
Los fallos habituales son llamar a extractall() sobre archivos no confiables, no comprobar espacio disponible, permitir sobrescrituras, cargar entradas gigantes en memoria, confiar en extensiones y asumir que una contraseña ZIP equivale a cifrado moderno.
Conclusión
zipfile permite listar, verificar, leer y extraer ZIP con control detallado. Para fuentes confiables, extractall() puede ser suficiente. Para archivos externos, valida que las rutas permanezcan dentro del destino, limita tamaños y cantidades, evita sobrescrituras y utiliza una carpeta temporal. La descompresión segura no consiste solo en abrir el archivo, sino en controlar todo lo que puede escribir.






