DevOps & Development 7 min de lectura 13 de marzo de 2026

Script en Python para buscar texto en Word y Excel

Construye una herramienta de terminal en Python con búsqueda exacta, por palabras clave y difusa para escanear documentos Word y Excel sin depender de índices.

Solución en "Dos Clics" (TL;DR)

Creación de un script en Python para buscar texto, palabras clave o coincidencias difusas (fuzzy) en archivos Word y Excel desde la terminal.

Encontrar una cadena de texto específica o un fragmento mal redactado dentro de cientos de documentos de Microsoft Word y hojas de cálculo de Excel dispersos en múltiples carpetas suele ser una tarea frustrante cuando se depende del explorador de Windows. Diseñar una utilidad directa desde la terminal permite automatizar este escaneo con rapidez, precisión y tolerancia a errores tipográficos.

1. El problema: buscar en archivos comprimidos de Office

Cuando necesitamos localizar referencias como "Reunión de Junta Directiva" en estructuras complejas de carpetas, los comandos tradicionales de terminal se quedan cortos. Los archivos modernos con extensiones .docx y .xlsx no son texto plano; son archivos comprimidos en formato ZIP que albergan árboles internos de documentos XML.

Antes de implementar una solución definitiva, evaluamos tres alternativas habituales en entornos Windows:

  • CMD (findstr): Inviable. findstr solo examina texto plano continuo y no interpreta estructuras comprimidas ni esquemas XML.
  • PowerShell: Viable pero engorroso. Permite descomprimir y leer word/document.xml mediante ensamblados de System.IO.Compression.FileSystem, pero procesar libros de Excel con múltiples hojas y limpiar etiquetas XML sin romper el flujo demanda scripts extensos y difíciles de mantener.
  • Python: La opción técnica superior. Cuenta con librerías maduras como python-docx y openpyxl, además de soporte para coincidencia difusa (fuzzy matching) mediante algoritmos de distancia de Levenshtein.

2. Más allá de la coincidencia exacta: variantes y errores

En auditorías y búsquedas documentales reales, el texto buscado rara vez aparece siempre idéntico. Un documento puede contener "Reunión Junta Directiva" (omitiendo la preposición), presentar variaciones de tildes o tener palabras en diferente orden. Por ello, el motor de búsqueda debe contemplar tres modalidades:

  • Exacta (exact): Coincidencia literal de subcadenas.
  • Palabras clave (keywords): Divide la frase en términos independientes y verifica que todos existan dentro del mismo párrafo o celda, sin importar su orden.
  • Difusa (fuzzy): Calcula el porcentaje de similitud mediante la librería rapidfuzz, tolerando erratas y omisiones si se supera un umbral configurable (por ejemplo, 80%).
Comportamiento en entornos reales: Al escanear carpetas reales surgieron archivos temporales de Office (con prefijo ~$) que arrojaban errores de tipo "File is not a zip file" o "Permission denied", además de documentos antiguos en formato .xls que openpyxl no puede leer de forma nativa.

3. La solución: script CLI parametrizado

Para convertir el script en una herramienta práctica, implementamos argumentos por línea de comandos con argparse, detección automática del directorio de trabajo actual (Path.cwd()), filtrado de archivos temporales, supresión de advertencias innecesarias de estilos/imágenes en Excel y soporte dual para .xlsx (vía openpyxl) y .xls tradicional (vía xlrd).

Instalación de dependencias
pip install python-docx openpyxl xlrd rapidfuzz
buscar_docs_avanzado.py
import os
import argparse
from pathlib import Path
import warnings
from docx import Document
from openpyxl import load_workbook
import xlrd
from rapidfuzz import fuzz

# Suprimir advertencias internas de openpyxl sobre estilos o imagenes
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")

def search_word(file_path, search_text, search_mode, keywords, fuzzy_threshold):
    try:
        doc = Document(file_path)
        results = []
        for i, p in enumerate(doc.paragraphs, start=1):
            paragraph = p.text.strip()
            if not paragraph:
                continue
            if search_mode == "exact" and search_text.lower() in paragraph.lower():
                results.append((i, paragraph))
            elif search_mode == "keywords" and all(word.lower() in paragraph.lower() for word in keywords):
                results.append((i, paragraph))
            elif search_mode == "fuzzy" and fuzz.ratio(paragraph.lower(), search_text.lower()) >= fuzzy_threshold:
                results.append((i, paragraph))
        return results
    except Exception as e:
        print(f"[ERROR] {file_path}: {e}")
        return []

def search_excel(file_path, search_text, search_mode, keywords, fuzzy_threshold):
    results = []
    try:
        if file_path.suffix.lower() == ".xls":
            wb = xlrd.open_workbook(file_path)
            for sheet in wb.sheets():
                for row_idx in range(sheet.nrows):
                    for col_idx in range(sheet.ncols):
                        cell = sheet.cell(row_idx, col_idx).value
                        if cell is None:
                            continue
                        cell_text = str(cell).strip()
                        if search_mode == "exact" and search_text.lower() in cell_text.lower():
                            results.append((sheet.name, row_idx + 1, col_idx + 1, cell_text))
                        elif search_mode == "keywords" and all(word.lower() in cell_text.lower() for word in keywords):
                            results.append((sheet.name, row_idx + 1, col_idx + 1, cell_text))
                        elif search_mode == "fuzzy" and fuzz.ratio(cell_text.lower(), search_text.lower()) >= fuzzy_threshold:
                            results.append((sheet.name, row_idx + 1, col_idx + 1, cell_text))
        else:
            wb = load_workbook(file_path, data_only=True)
            for sheet in wb.worksheets:
                for row_idx, row in enumerate(sheet.iter_rows(values_only=True), start=1):
                    for col_idx, cell in enumerate(row, start=1):
                        if cell is None:
                            continue
                        cell_text = str(cell).strip()
                        if search_mode == "exact" and search_text.lower() in cell_text.lower():
                            results.append((sheet.title, row_idx, col_idx, cell_text))
                        elif search_mode == "keywords" and all(word.lower() in cell_text.lower() for word in keywords):
                            results.append((sheet.title, row_idx, col_idx, cell_text))
                        elif search_mode == "fuzzy" and fuzz.ratio(cell_text.lower(), search_text.lower()) >= fuzzy_threshold:
                            results.append((sheet.title, row_idx, col_idx, cell_text))
        return results
    except Exception as e:
        print(f"[ERROR] {file_path}: {e}")
        return []

def get_args():
    parser = argparse.ArgumentParser(description="Buscador de contenido en archivos Office (.docx, .xlsx, .xls)")
    parser.add_argument("--search_text", type=str, required=True, help="Texto o palabras clave a buscar.")
    parser.add_argument("--search_mode", choices=["exact", "keywords", "fuzzy"], default="exact", help="Modo de busqueda.")
    parser.add_argument("--file_type", choices=["word", "excel", "all"], default="all", help="Tipo de archivo a escanear.")
    parser.add_argument("--file_path", type=str, default=str(Path.cwd()), help="Ruta raiz a escanear (por defecto la carpeta actual).")
    parser.add_argument("--threshold", type=int, default=80, help="Umbral minimo de similitud para modo fuzzy (0-100).")
    return parser.parse_args()

def main():
    args = get_args()
    root_path = Path(args.file_path)
    keywords = args.search_text.split() if args.search_mode == "keywords" else []

    for item in root_path.rglob("*"):
        if item.name.startswith("~$"):
            continue

        ext = item.suffix.lower()
        if (args.file_type in ["word", "all"]) and ext == ".docx":
            matches = search_word(item, args.search_text, args.search_mode, keywords, args.threshold)
            for para_idx, text in matches:
                print(f"[WORD] {item} | Parrafo {para_idx}")
        elif (args.file_type in ["excel", "all"]) and ext in [".xls", ".xlsx"]:
            matches = search_excel(item, args.search_text, args.search_mode, keywords, args.threshold)
            for sheet, row, col, text in matches:
                print(f"[EXCEL] {item} | Hoja: {sheet} | Fila {row} Columna {col}")

if __name__ == "__main__":
    main()

4. Ejemplos de uso en terminal

Al encontrarse Python configurado en las variables de entorno del sistema, el script puede almacenarse en un directorio centralizado (por ejemplo, C:\utilidades\) y ejecutarse directamente en la carpeta donde nos encontremos o especificando la ruta destino mediante --file_path.

Símbolo del sistema / PowerShell
python C:\utilidades\buscar_docs_avanzado.py --search_text "Reunión de Junta Directiva" --search_mode "keywords" --file_type "word" --file_path "D:\Documentos\Actas"
Consejo Práctico: Si estás ubicado dentro del directorio que deseas examinar, simplemente omite el parámetro --file_path. El script utilizará automáticamente la carpeta de trabajo actual.
La historia detrás de la nota

Frecuentemente el índice de Windows omite documentos o ignora términos por variaciones mínimas de sintaxis. Centralizar este escáner en Python resolvió la localización de actas históricas dispersas en cuestión de segundos.