Aller au contenu

Importation intelligente d'Excel en Python : Détection flexible des colonnes et nettoyage des données hétérogènes

Rogelio Guerra Riverón
Auteur
Rogelio Guerra Riverón
Construction de ma propre infrastructure web depuis zéro. Je documente chaque étape : serveurs, réseaux, conteneurs et tout ce qui se présente.

J’ai travaillé avec des feuilles de calcul Excel provenant de différents départements. Chacune utilise des noms de colonne distincts, les données sont sales (téléphones avec notes, NIFs mélangés avec du texte), et les codes ont des formats incohérents. Je documente ici la solution que j’ai construite.

Le problème réel
#

Je recevais des fichiers Excel où :

  • Les colonnes s’appelaient “NIF” dans l’un, “CIF” dans un autre, “Identification” dans le troisième
  • Les téléphones ressemblaient à “123-456-7890 (ext 5)”, “9876543210 - non disponible”
  • Les NIFs avec des tirets, des espaces et des lettres variées
  • Les codes de produit avec des préfixes incohérents

Je ne pouvais pas attendre que chaque personne formate de la même façon. J’avais besoin d’un système flexible.

Architecture de la solution
#

Mon approche utilise trois couches :

  1. Détection de colonnes par regex (trouve “nif”, “cif”, “identificaci” avec fuzzy matching)
  2. Nettoyeurs spécialisés pour chaque type de données
  3. Validation et logging pour audit

Implémentation étape par étape
#

1. Charger le fichier et détecter les colonnes
#

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. Nettoyer et valider 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. Extraire les téléphones “propres”
#

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. Normaliser les codes avec préfixes 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. Traiter le fichier complet
#

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

Utilisation en production
#

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")

Leçons apprises
#

  • Les regex sont le modèle d’or pour les données sales
  • Toujours retourner la donnée originale + nettoyée pour audit
  • Le log des erreurs spécifiques par ligne facilite le débogage
  • La détection flexible des colonnes a économisé des heures de support

Ce système est en production depuis 6 mois. J’ai dû ajouter seulement 2 nettoyeurs supplémentaires. La clé est de garder chaque nettoyeur indépendant.


Équipement recommandé
#

Liens d’affiliation. Aucun coût supplémentaire pour vous.