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

Etiquetas:

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.


Equipamiento recomendado

Enlaces de afiliado. Sin coste extra para ti.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *