Web Scraping con Selenium de productos de Tottus.com

Desarrollo de un script en Python con Selenium que realiza web scraping en Tottus.com, extrayendo nombre, marca, modalidades de precios, SKU y url de un producto según una búsqueda definida por el usuario. La herramienta resalta productos de "buen precio" y organiza los datos en un DataFrame de Pandas.

Cliente: Personal
Ver demo
Servicios:

Web Scraping

URL del proyecto:

jsalvadorz.tech

Proyecto realizado con:
  • Python
  • Selenium
  • Pandas
  • Excel

Descripción del proyecto

En este proyecto desarrollé un script en Python usando la librería Selenium para hacer web scraping de los productos de la página web de Tottus.com. El script procesa y genera una lista de productos a partir de un parámetro de búsqueda definido por el usuario, por ejemplo "arroz", y extrae la información de cada producto de esa categoría como el nombre, la marca, las modalidades de precios ofertados (precio CMR, precio en línea y precio en tienda), el SKU y url de acceso, todo desde la web de Tottus.com.

Asimismo, permite destacar los productos que tienen un "buen precio" definido también por el usuario, por ejemplo, resaltar aquellos productos con un precio menor o igual a S/ 18.00. Finalmente, la data extraída se formatea en un DataFrame de Pandas (otra librería de Python) y a partir de allí es posible exportar la información a un archivo CSV o excel para su posterior análisis o incluso, si fuera necesario, a una base de datos o un datalake en la nube.

Tener en cuenta que el script tiene solo fines demostrativos. Se deben respetar los términos y condiciones de uso del sitio web de Tottus.com y las leyes locales relacionadas con el web scraping antes de utilizar este script en un entorno real. Los datos extraídos son únicamente para fines educativos y no comerciales.

Sobre el entorno

Se usó Python 3.11.9 para desarrollar el script, siendo las librerías principales utilizadas: Selenium para el web scraping y Pandas para el manejo de datos en un DataFrame. Como editor de código se usó Visual Studio Code con las extensiones necesarias para trabajar con Jupyter Notebooks (archivos .ipynb). Como navegador web se utilizó Microsoft Edge (Edge WebDriver) en modo headless (sin interfaz gráfica) para ejecutar el script de Selenium, aunque se podría implementar con ChromeDriver o GeckoDriver (Firefox) sin mayor problema. Por otro lado, también se podría usar en entornos como Google Colab, Deepnote o Jupyter Notebooks locales.

Video explicativo

Explicación del código

1. Importación de librerías

									import logging
									import pandas as pd
									from selenium import webdriver
									from selenium.common.exceptions import TimeoutException
									from selenium.webdriver.common.by import By
									from selenium.webdriver.edge.options import Options
									from selenium.webdriver.remote.webelement import WebElement
									from selenium.webdriver.support import expected_conditions as EC
									from selenium.webdriver.support.ui import WebDriverWait
								
2. Configuraciones previas del driver y parámetros de búsqueda

									# Configurar el logging
									logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

									# Configurar el navegador
									edge_options = Options()
									edge_options.add_argument("--headless")
									edge_options.add_argument("--no-sandbox")
									edge_options.add_argument("--disable-dev-shm-usage")

									# Crear una instancia del navegador
									driver = webdriver.Edge(options=edge_options)

									# Tiempo de espera explícito (hasta 10 seg)
									wait = WebDriverWait(driver, 10)

									# Establecer el término de búsqueda, el buen precio y la URL de la página a scrapear
									termino_busqueda = "arroz"
									buen_precio = 18.0
									url_inicial = f"https://www.tottus.com.pe/tottus-pe/buscar?Ntt={termino_busqueda}"
								
3. Función que captura los precios

									# Función para obtener el precio de un obj_producto
									def obtener_precio(elemento: WebElement, selector: str) -> float:
										elementos = elemento.find_elements(By.CSS_SELECTOR, selector)

										if elementos:
											try:
												valor = elementos[0].get_attribute(selector[3:-1])
												return float(valor.replace(",", "").strip())
											except (ValueError, AttributeError):
												return 0.0
										return 0.0
								

Un poco de contexto. La web Tottus.com tiene tres tipos de precios para cada producto: data-cmr-price, data-internet-price y data-normal-price, y aún así no todos los productos tienen los tres valores, algunos tienen dos, otros solo uno. Cada uno de estos valores de precios se encuentran en un atributo específico según se aprecia en la estructura HTML de la web y el objetivo es capturar el contenido de esos atributos, si existen, y de no ser así asignar un valor cero, conservando de esa manera la uniformidad de las longitudes de las listas extraídas. Esta función realiza todo ese proceso.

4. Bloque principal

									resultados_totales = []
									pagina_actual = 1

									try:
										# 1. Obtener la URL inicial a escrapear
										driver.get(url_inicial)
										logging.info(f"Iniciando scraping en: {url_inicial}")

										# 2. Bucle principal
										while True:
											logging.info(f"--- Procesando página {pagina_actual} ---")
											pagina_invalida = False

											# A. Esperar que carguen los productos (buscamos el contenedor de precios como referencia)
											try:
												wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, "prices")))
											except TimeoutException:
												pagina_invalida = True

											# B. Evaluar si la página es inválida
											if pagina_invalida:
												logging.warning(f"Página {pagina_actual} marcada como inválida. Se omite extracción.")
											else:
												# C. Capturar listas de propiedades de productos
												listado_marcas = driver.find_elements(By.CLASS_NAME, "title-rebrand")
												listado_productos = driver.find_elements(By.CLASS_NAME, "subTitle-rebrand")
												listado_precios = driver.find_elements(By.CLASS_NAME, "prices")
												listado_links = driver.find_elements(By.CLASS_NAME, "pod-link")

												# D. Validar que las listas tengan el mismo tamaño
												if not (
													len(listado_marcas)
													== len(listado_productos)
													== len(listado_precios)
													== len(listado_links)
												):
													logging.error(f"Error de consistencia en página {pagina_actual}. Se omite extracción.")
												else:
													# E. Iterar sobre los productos de la página actual
													for obj_marca, obj_producto, obj_precio, obj_link in zip(
														listado_marcas,
														listado_productos,
														listado_precios,
														listado_links
													):
														try:
															marca_texto = obj_marca.text.strip()
															producto_texto = obj_producto.text.strip()
															
															precio_cmr = obtener_precio(obj_precio, "li[data-cmr-price]")
															precio_internet = obtener_precio(obj_precio, "li[data-internet-price]")
															precio_normal = obtener_precio(obj_precio, "li[data-normal-price]")

															url_producto = obj_link.get_attribute("href").strip()
															sku_producto = str(url_producto.split("/")[-1]) if url_producto else "N/A"

															# Lógica de buen precio
															precio_referencia = (
																precio_cmr
																if precio_cmr > 0
																else precio_internet
																if precio_internet > 0
																else precio_normal
															)

															es_buen_precio = "SI" if 0 < precio_referencia <= buen_precio else "NO"

															# F. Agregar los resultados a una lista
															resultados_totales.append({
																"MARCA": marca_texto,
																"PRODUCTO": producto_texto,
																"SKU": sku_producto,
																"PRECIO CMR": precio_cmr,
																"PRECIO INTERNET": precio_internet,
																"PRECIO TIENDA": precio_normal,
																"BUEN PRECIO": es_buen_precio,
																"URL": url_producto
															})

														except Exception as e:
															logging.error(f"Error al procesar producto {producto_texto}: {e}")

													logging.info(f"Cantidad de productos encontrados en la página {pagina_actual}: {len(listado_productos)}")

											# G. Lógica de paginación
											try:
												# --- Preparar para detectar cambios en la página ---

												# Guardar la URL actual
												url_anterior = driver.current_url

												elemento_viejo = None
												try:
													# Guardar el primer producto del DOM actual
													elemento_viejo = driver.find_element(By.CLASS_NAME, "prices")
												except Exception:
													logging.warning(f"No se pudo capturar el primer elemento de la página {pagina_actual}.")

												# Esperar a que el botón "Siguiente" sea cliqueable y capturarlo
												selector_boton_siguiente = "button[id='testId-pagination-bottom-arrow-right']"
												boton_siguiente = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector_boton_siguiente)))
												
												# --- Cambiar de página ---

												# Hacer scroll hasta que el botón esté centrado en la página para asegurar que el clic funcione
												driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", boton_siguiente)
												boton_siguiente.click()

												# --- Validar que la página ha cambiado ---

												# Esperar que la nueva URL sea diferente a la anterior
												wait.until(EC.url_changes(url_anterior))

												# Esperar que el primer elemento del DOM anterior ya no esté presente
												if elemento_viejo is not None:
													wait.until(EC.staleness_of(elemento_viejo))

												# Incrementar el contador de página
												pagina_actual += 1
												
											except TimeoutException:
												logging.info("No se detectó cambio de página o botón siguiente. Fin del scraping.")
												break
											except Exception as e:
												logging.error(f"Error al intentar cambiar de página: {e}")
												break

										# 3. Crear un DataFrame con los resultados
										df_datos = pd.DataFrame(resultados_totales)

										if not df_datos.empty:
											pd.options.display.float_format = "S/{:,.2f}".format
											df_datos.to_excel(f"productos_tottus_{termino_busqueda}.xlsx", index=False, engine="openpyxl")
											logging.info(f"Scraping finalizado. Total productos encontrados: {len(df_datos)}")
										else:
											logging.warning("No se extrajeron datos.")

									except Exception as e:
										logging.critical(f"Ocurrió un error durante la ejecución: {e}")
									finally:
										driver.quit()
										logging.info("Navegador cerrado. Ejecución finalizada.")
								
5. Algunas anotaciones

Se ha incluido código para manejar excepciones, de darse el caso. Además, se han agregado notas para el usuario final conforme se va ejecutando el script. Por otro lado, la data extraída se almacena en un DataFrame de Pandas que luego es exportado en un archivo excel llamado productos_tottus_{termino_busqueda}.xlsx.

Contáctame