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 :
- Détection de colonnes par regex (trouve “nif”, “cif”, “identificaci” avec fuzzy matching)
- Nettoyeurs spécialisés pour chaque type de données
- 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 None2. 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 limpio3. 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, nota4. 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 None5. 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, erroresUtilisation 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é#
- Raspberry Pi 3 B+ — Serveur léger et économe pour débuter votre homelab
- Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring
Liens d’affiliation. Aucun coût supplémentaire pour vous.