Ir al contenido

Importación inteligente de Excel en Python: Detección flexible de columnas y limpieza de datos heterogéneos

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

He 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:

  1. Detección de columnas por regex (encuentra “nif”, “cif”, “identificaci” con fuzzy matching)
  2. Limpiadores especializados para cada tipo de dato
  3. 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.