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:
- Detección de columnas por regex (encuentra “nif”, “cif”, “identificaci” con fuzzy matching)
- Limpiadores especializados para cada tipo de dato
- 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 None2. 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 limpio3. 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, nota4. 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 None5. 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, erroresUso 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.