Comprimir y Descomprimir Archivos con Python
La biblioteca estándar de Python cubre todos los formatos de compresión más comunes sin dependencias externas. Esta guía muestra cómo crear, leer y extraer archivos comprimidos de forma eficiente y segura.
zipfile — crear y leer ZIP
import zipfile
import pathlib
# Crear un ZIP con múltiples archivos
with zipfile.ZipFile('mi_archivo.zip', 'w', compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
# Añadir archivo con ruta en el ZIP
zf.write('informe.pdf', arcname='documentos/informe.pdf')
zf.write('foto.jpg', arcname='imagenes/foto.jpg')
# Añadir contenido desde string (sin archivo en disco)
zf.writestr('README.txt', 'Archivo ZIP generado automáticamente\n')
# Añadir toda una carpeta recursivamente
carpeta = pathlib.Path('datos/')
for archivo in carpeta.rglob('*'):
if archivo.is_file():
zf.write(archivo, arcname=str(archivo))
print("ZIP creado")
# Listar contenido
with zipfile.ZipFile('mi_archivo.zip', 'r') as zf:
for info in zf.infolist():
print(f" {info.filename} — {info.file_size/1024:.1f} KB "
f"→ {info.compress_size/1024:.1f} KB comprimido")
# Extraer todo
with zipfile.ZipFile('mi_archivo.zip', 'r') as zf:
zf.extractall('destino/')
# Extraer un solo archivo
with zipfile.ZipFile('mi_archivo.zip', 'r') as zf:
zf.extract('documentos/informe.pdf', path='destino/')
# Leer contenido sin extraer al disco
with zipfile.ZipFile('mi_archivo.zip', 'r') as zf:
with zf.open('README.txt') as f:
contenido = f.read().decode('utf-8')
print(contenido)
ZIP con contraseña
import zipfile
# Crear ZIP con contraseña (usa ZipCrypto — no AES, pero universal)
with zipfile.ZipFile('protegido.zip', 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.setpassword(b'mi-contrasena-secreta')
zf.write('secreto.txt')
# Extraer ZIP con contraseña
with zipfile.ZipFile('protegido.zip', 'r') as zf:
zf.extractall('destino/', pwd=b'mi-contrasena-secreta')
# Verificar integridad sin extraer
with zipfile.ZipFile('mi_archivo.zip', 'r') as zf:
resultado = zf.testzip() # None si todo OK, nombre del primer archivo dañado si hay error
if resultado:
print(f"Archivo dañado: {resultado}")
else:
print("ZIP íntegro")
Prevención de zip bombs y path traversal
import zipfile
import pathlib
MAX_SIZE_BYTES = 100 * 1024 * 1024 # 100 MB descomprimido
MAX_RATIO = 50 # ratio máx comprimido/descomprimido
MAX_ARCHIVOS = 1000
def extraer_seguro(ruta_zip: str, destino: str) -> list[str]:
"""Extrae un ZIP con validaciones de seguridad."""
destino_path = pathlib.Path(destino).resolve()
extraidos = []
with zipfile.ZipFile(ruta_zip, 'r') as zf:
infos = zf.infolist()
if len(infos) > MAX_ARCHIVOS:
raise ValueError(f"ZIP contiene demasiados archivos: {len(infos)}")
tamano_total = sum(i.file_size for i in infos)
if tamano_total > MAX_SIZE_BYTES:
raise ValueError(f"ZIP demasiado grande descomprimido: {tamano_total/1024**2:.1f} MB")
for info in infos:
# Prevenir path traversal
ruta_destino = (destino_path / info.filename).resolve()
if not str(ruta_destino).startswith(str(destino_path)):
raise ValueError(f"Path traversal detectado: {info.filename}")
# Prevenir zip bomb
if info.file_size > 0 and info.compress_size > 0:
ratio = info.file_size / info.compress_size
if ratio > MAX_RATIO:
raise ValueError(f"Posible zip bomb: ratio={ratio:.1f} en {info.filename}")
zf.extract(info, destino_path)
extraidos.append(str(ruta_destino))
return extraidos
archivos = extraer_seguro('descargado.zip', 'extraccion_segura/')
tarfile — TAR, TAR.GZ, TAR.BZ2, TAR.XZ
import tarfile
import pathlib
# Crear TAR.GZ (muy común en Linux)
with tarfile.open('backup.tar.gz', 'w:gz') as tar:
tar.add('documentos/', arcname='documentos')
tar.add('config.json', arcname='config.json')
# Crear TAR.XZ (máxima compresión con lzma)
with tarfile.open('backup.tar.xz', 'w:xz') as tar:
carpeta = pathlib.Path('proyecto/')
tar.add(carpeta, arcname=carpeta.name)
# Listar contenido
with tarfile.open('backup.tar.gz', 'r:gz') as tar:
for miembro in tar.getmembers():
print(f" {miembro.name} — {miembro.size/1024:.1f} KB")
# Extraer todo
with tarfile.open('backup.tar.gz', 'r:gz') as tar:
tar.extractall('restaurado/')
# Extraer un archivo específico
with tarfile.open('backup.tar.gz', 'r:gz') as tar:
miembro = tar.getmember('documentos/informe.pdf')
tar.extract(miembro, path='restaurado/')
# Extraer con filtro (Python 3.12+ — más seguro)
with tarfile.open('backup.tar.gz', 'r:gz') as tar:
tar.extractall('restaurado/', filter='data') # 'data', 'tar', o 'fully_trusted'
TAR streaming — procesar sin extraer al disco
import tarfile
import io
def listar_tar_remoto(ruta: str) -> list[dict]:
"""Lee metadatos de un TAR sin cargarlo completo en memoria."""
resultado = []
with tarfile.open(ruta, 'r:*') as tar: # r:* detecta compresión automáticamente
for miembro in tar:
resultado.append({
'nombre': miembro.name,
'tamanyo': miembro.size,
'es_dir': miembro.isdir(),
'mtime': miembro.mtime,
})
return resultado
# Leer contenido de un archivo dentro del TAR sin extraer todos
def leer_archivo_en_tar(ruta_tar: str, nombre_archivo: str) -> bytes:
with tarfile.open(ruta_tar, 'r:*') as tar:
miembro = tar.getmember(nombre_archivo)
f = tar.extractfile(miembro)
return f.read() if f else b''
contenido = leer_archivo_en_tar('backup.tar.gz', 'config.json')
print(contenido.decode())
gzip y lzma — compresión de un solo archivo
import gzip
import lzma
import pathlib
# gzip — comprimir un archivo
ruta_original = pathlib.Path('datos.csv')
ruta_gzip = pathlib.Path('datos.csv.gz')
with ruta_original.open('rb') as f_in:
with gzip.open(ruta_gzip, 'wb', compresslevel=9) as f_out:
f_out.write(f_in.read())
# Leer directamente sin descomprimir al disco
with gzip.open('datos.csv.gz', 'rt', encoding='utf-8') as f:
for linea in f:
pass # procesar línea a línea en streaming
# lzma — máxima compresión (lento, pero mejor ratio)
ruta_xz = pathlib.Path('datos.csv.xz')
with ruta_original.open('rb') as f_in:
with lzma.open(ruta_xz, 'wb', preset=9) as f_out:
f_out.write(f_in.read())
# Comparar ratios
for ruta in [ruta_original, ruta_gzip, ruta_xz]:
print(f"{ruta.name}: {ruta.stat().st_size/1024:.1f} KB")
shutil — compresión de alto nivel
import shutil
# Crear archivos comprimidos en un solo paso
# Formatos: 'zip', 'tar', 'gztar' (tar.gz), 'bztar' (tar.bz2), 'xztar' (tar.xz)
shutil.make_archive(
'backup_proyecto', # nombre base (sin extensión)
'gztar', # formato
root_dir='.', # directorio base para los arcnames
base_dir='proyecto', # qué incluir
)
# Crea: backup_proyecto.tar.gz
# Descomprimir cualquier formato soportado
shutil.unpack_archive('backup_proyecto.tar.gz', extract_dir='restaurado/')
# Ver formatos disponibles
print(shutil.get_archive_formats())
# [('bztar', 'bz2 compressed tar archive'), ('gztar', 'gz compressed tar archive'),
# ('tar', 'uncompressed tar archive'), ('xztar', 'xz compressed tar archive'), ('zip', 'ZIP file')]
Caso práctico: backup incremental con compresión
import zipfile
import pathlib
import hashlib
import json
from datetime import datetime
MANIFEST_FILE = 'backup_manifest.json'
def calcular_hash(ruta: pathlib.Path) -> str:
h = hashlib.sha256()
with ruta.open('rb') as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
def backup_incremental(carpeta: str, destino: str) -> pathlib.Path:
"""Crea un ZIP solo con archivos nuevos o modificados desde el último backup."""
carpeta_path = pathlib.Path(carpeta)
destino_path = pathlib.Path(destino)
destino_path.mkdir(parents=True, exist_ok=True)
# Cargar manifest anterior
manifest_path = destino_path / MANIFEST_FILE
manifest_ant = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
manifest_nuevo = {}
archivos_a_incluir = []
for archivo in carpeta_path.rglob('*'):
if not archivo.is_file():
continue
clave = str(archivo.relative_to(carpeta_path))
sha = calcular_hash(archivo)
manifest_nuevo[clave] = sha
if manifest_ant.get(clave) != sha:
archivos_a_incluir.append(archivo)
if not archivos_a_incluir:
print("Sin cambios desde el último backup")
return None
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
zip_path = destino_path / f"backup_incr_{timestamp}.zip"
with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
for archivo in archivos_a_incluir:
arcname = str(archivo.relative_to(carpeta_path))
zf.write(archivo, arcname=arcname)
print(f" + {arcname}")
manifest_path.write_text(json.dumps(manifest_nuevo, indent=2))
print(f"\nBackup: {len(archivos_a_incluir)} archivos → {zip_path.name} "
f"({zip_path.stat().st_size/1024:.1f} KB)")
return zip_path
backup_incremental('proyecto/', 'backups/')
Buenas prácticas
zipfile.ZIP_DEFLATEDconcompresslevel=6-9para buena compresión;ZIP_LZMApara máxima compresión dentro de ZIP (no todos los clientes lo soportan).tarfilecon:xzpara backups de larga duración donde el tamaño importa más que la velocidad;:gzpara uso diario.- Siempre validar paths al extraer —
(destino / member.filename).resolve()debe empezar por el destino para prevenir path traversal. tar.extractall(filter='data')en Python 3.12+ — la extracción segura por defecto que ignora metadatos peligrosos (setuid, device files…).- Procesar TAR.GZ en streaming con
extractfile()en lugar de extraer al disco cuando solo necesitas leer el contenido. shutil.make_archiveyunpack_archiveson suficientes para la mayoría de casos simples; usazipfile/tarfiledirectamente cuando necesitas control granular.
Conversiones relacionadas
Conversiones de archivos comprimidos más usadas: