Importación inteligente de Excel en Python: Detección flexible de columnas y limpieza de datos heterogéneos
Etiquetas: python,excel,regex,openpyxl,procesamiento-datosHe estado trabajando con hojas de cálculo Excel que llegan de diferentes departamentos. Cada una usa nombres de columna distintos, los datos están sucios (teléfonos con notas, NIFs mezclados con texto), y los códigos tienen formatos inconsistentes. Aquí documento la solución que construí.
El problema real
Recibía archivos Excel donde:
– Columnas llamadas «NIF» en uno, «CIF» en otro, «Identificación» en el tercero
– Teléfonos como «123-456-7890 (ext 5)», «9876543210 – no disponible»
– NIFs con guiones, espacios y letras variadas
– Códigos de producto con prefijos inconsistentes
No podía esperar que cada persona formateara igual. Necesitaba un sistema que fuera flexible.
Arquitectura de la solución
Mi enfoque usa tres capas:
- Detección de columnas por regex (encuentra «nif», «cif», «identificaci» con fuzzy matching)
- Limpiadores especializados para cada tipo de dato
- Validación y logging para auditoría
Implementación paso a paso
1. Cargar el archivo y detectar columnas
from openpyxl import load_workbook
import re
def detectar_columnas(archivo_excel):
wb = load_workbook(archivo_excel)
ws = wb.active
encabezados = [cell.value for cell in ws[1]]
mapa_columnas = {
'nif': detectar_columna(encabezados, r'nif|cif|identificaci'),
'telefono': detectar_columna(encabezados, r'telef|phone|contacto'),
'nombre': detectar_columna(encabezados, r'nombre|name|razón'),
'codigo': detectar_columna(encabezados, r'código|code|articulo')
}
return wb, ws, mapa_columnas
def detectar_columna(encabezados, patron):
for idx, encabezado in enumerate(encabezados):
if encabezado and re.search(patron, str(encabezado).lower()):
return idx
return None
2. Limpiar y validar NIF/CIF
class LimpiadorNIF:
PATRON_NIF = r'^([0-9]{8}[A-Z]|[XYZ][0-9]{7}[A-Z])$'
@staticmethod
def limpiar(valor):
if not valor:
return None
# Extraer solo números y letras
limpio = re.sub(r'[^A-Z0-9]', '', str(valor).upper())
# Si tiene más de 9 caracteres, puede ser NIF + texto
match = re.search(r'([0-9]{8}[A-Z]|[XYZ][0-9]{7}[A-Z])', limpio)
if match:
return match.group(1)
return None if not re.match(LimpiadorNIF.PATRON_NIF, limpio) else limpio
3. Extraer teléfonos «limpios»
class LimpiadorTelefono:
@staticmethod
def limpiar(valor):
if not valor:
return None, None
texto = str(valor)
# Extraer solo números (mínimo 9 dígitos)
numeros = re.sub(r'\D', '', texto)
telefono = numeros[-9:] if len(numeros) >= 9 else None
# Detectar notas (texto entre paréntesis o después de guiones)
notas = re.search(r'(\([^)]+\)|-.+)', texto)
nota = notas.group(1).strip() if notas else None
return telefono, nota
4. Normalizar códigos con prefijos variables
class LimpiadorCodigo:
@staticmethod
def limpiar(valor, prefijo_esperado='PRD'):
if not valor:
return None
# Convertir a mayúsculas y eliminar espacios
limpio = str(valor).upper().strip()
# Extraer la parte numérica
match = re.search(r'([A-Z]*)?(\d+)', limpio)
if match:
numero = match.group(2)
# Garantizar formato consistente
return f"{prefijo_esperado}{numero.zfill(6)}"
return None
5. Procesar el archivo completo
def importar_excel(ruta_archivo):
wb, ws, columnas = detectar_columnas(ruta_archivo)
registros = []
errores = []
for fila_idx, fila in enumerate(ws.iter_rows(min_row=2, values_only=False), start=2):
registro = {}
try:
if columnas['nif'] is not None:
nif_raw = fila[columnas['nif']].value
registro['nif'] = LimpiadorNIF.limpiar(nif_raw)
if columnas['telefono'] is not None:
tel_raw = fila[columnas['telefono']].value
tel, nota = LimpiadorTelefono.limpiar(tel_raw)
registro['telefono'] = tel
registro['nota_telefono'] = nota
if columnas['codigo'] is not None:
cod_raw = fila[columnas['codigo']].value
registro['codigo'] = LimpiadorCodigo.limpiar(cod_raw)
registros.append(registro)
except Exception as e:
errores.append(f"Fila {fila_idx}: {str(e)}")
return registros, errores
Uso en producción
registros, errores = importar_excel('ventas_marzo.xlsx')
if errores:
print(f"⚠️ {len(errores)} errores encontrados:")
for error in errores[:5]:
print(f" - {error}")
print(f"✓ {len(registros)} registros procesados correctamente")
Lecciones aprendidas
- Los regex son el patrón de oro para datos sucios
- Siempre devuelve el dato original + limpio para auditoría
- Log de errores específicos por fila facilita debugging
- La detección flexible de columnas ahorró horas de soporte
Este sistema lleva 6 meses en producción. He tenido que añadir solo 2 limpiadores más. La clave es mantener cada limpiador independiente.
Equipamiento recomendado
- Raspberry Pi 3 B+ — Servidor ligero de bajo consumo para empezar tu homelab
- Raspberry Pi 4 (4GB) — La base perfecta para homelab, Docker y monitorización
Enlaces de afiliado. Sin coste extra para ti.