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

Tags:

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.

Leave a Reply

Your email address will not be published. Required fields are marked *