diff --git a/docs/examples/code_examples/crossbow_agent.py b/docs/examples/code_examples/crossbow_agent.py
new file mode 100644
index 0000000000..b20c77787a
--- /dev/null
+++ b/docs/examples/code_examples/crossbow_agent.py
@@ -0,0 +1,236 @@
+import asyncio
+
+from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext
+
+
+async def main() -> None:
+ """Agente que extrae información sobre ballestas desde sitios web.
+
+ Este ejemplo demuestra cómo crear un agente que puede:
+ 1. Extraer información de productos de ballestas desde tiendas en línea
+ 2. Extraer especificaciones técnicas, precios y disponibilidad
+ 3. Almacenar los datos en un formato estructurado para análisis
+
+ Esto es útil para crear comparadores de precios, bases de datos de productos
+ o sistemas de monitoreo de inventario.
+ """
+ # Crear una instancia del crawler optimizada para extraer información de productos
+ crawler = BeautifulSoupCrawler(
+ # Limitar solicitudes durante las pruebas, eliminar para crawling completo
+ max_requests_per_crawl=50,
+ # Reintentar solicitudes fallidas
+ max_request_retries=3,
+ # Configurar tiempo de espera entre solicitudes (en segundos)
+ request_handler_timeout=60.0,
+ )
+
+ # Definir el manejador de solicitudes para extraer información de ballestas
+ @crawler.router.default_handler
+ async def request_handler(context: BeautifulSoupCrawlingContext) -> None:
+ """Extraer información de productos de ballestas de la página."""
+ context.log.info(f'Procesando {context.request.url} ...')
+
+ # Extraer todos los productos de la página
+ # Este es un ejemplo genérico - ajustar los selectores según la estructura
+ # real del sitio web
+ products = context.soup.find_all('div', class_='product-item')
+
+ # Si no se encuentran con esa clase, intentar estructuras alternativas
+ if not products:
+ products = context.soup.find_all('article', class_='product')
+ if not products:
+ products = context.soup.find_all('div', {'data-product-id': True})
+
+ for product in products:
+ # Extraer nombre del producto
+ name_elem = product.find('h2', class_='product-name')
+ if not name_elem:
+ name_elem = product.find('h3', class_='product-title')
+ if not name_elem:
+ name_elem = product.find('a', class_='product-link')
+
+ product_name = name_elem.get_text(strip=True) if name_elem else 'Desconocido'
+
+ # Extraer marca
+ brand_elem = product.find('span', class_='brand')
+ if not brand_elem:
+ brand_elem = product.find('div', class_='manufacturer')
+ brand = brand_elem.get_text(strip=True) if brand_elem else ''
+
+ # Extraer precio
+ price_elem = product.find('span', class_='price')
+ if not price_elem:
+ price_elem = product.find('div', class_='product-price')
+ if not price_elem:
+ price_elem = product.find('span', {'data-price': True})
+
+ price = price_elem.get_text(strip=True) if price_elem else 'No disponible'
+
+ # Extraer descuento si existe
+ discount_elem = product.find('span', class_='discount')
+ if not discount_elem:
+ discount_elem = product.find('div', class_='sale-price')
+ discount = discount_elem.get_text(strip=True) if discount_elem else ''
+
+ # Extraer especificaciones técnicas
+ specs = {}
+
+ # Potencia (draw weight en libras)
+ power_elem = product.find('span', class_='draw-weight')
+ if not power_elem:
+ power_elem = product.find('span', {'data-spec': 'power'})
+ if power_elem:
+ specs['draw_weight_lbs'] = power_elem.get_text(strip=True)
+
+ # Velocidad (FPS - feet per second)
+ speed_elem = product.find('span', class_='fps')
+ if not speed_elem:
+ speed_elem = product.find('span', {'data-spec': 'speed'})
+ if speed_elem:
+ specs['speed_fps'] = speed_elem.get_text(strip=True)
+
+ # Peso del producto
+ weight_elem = product.find('span', class_='weight')
+ if weight_elem:
+ specs['weight'] = weight_elem.get_text(strip=True)
+
+ # Longitud
+ length_elem = product.find('span', class_='length')
+ if length_elem:
+ specs['length'] = length_elem.get_text(strip=True)
+
+ # Extraer disponibilidad
+ stock_elem = product.find('span', class_='stock-status')
+ if not stock_elem:
+ stock_elem = product.find('div', class_='availability')
+ availability = stock_elem.get_text(strip=True) if stock_elem else 'Desconocido'
+
+ # Extraer calificación si está disponible
+ rating_elem = product.find('span', class_='rating')
+ if not rating_elem:
+ rating_elem = product.find('div', class_='star-rating')
+ rating = rating_elem.get_text(strip=True) if rating_elem else ''
+
+ # Extraer número de reseñas
+ reviews_elem = product.find('span', class_='review-count')
+ reviews_count = reviews_elem.get_text(strip=True) if reviews_elem else '0'
+
+ # Extraer características adicionales
+ features = []
+ features_list = product.find('ul', class_='product-features')
+ if features_list:
+ feature_items = features_list.find_all('li')
+ features = [item.get_text(strip=True) for item in feature_items]
+
+ # Extraer URL del producto
+ link_elem = product.find('a', class_='product-link')
+ if not link_elem:
+ link_elem = product.find('a', href=True)
+ product_url = (
+ context.request.urljoin(link_elem['href'])
+ if link_elem and link_elem.get('href')
+ else context.request.url
+ )
+
+ # Extraer imagen del producto
+ img_elem = product.find('img', class_='product-image')
+ if not img_elem:
+ img_elem = product.find('img')
+ image_url = img_elem.get('src', '') if img_elem else ''
+ if image_url and not image_url.startswith('http'):
+ image_url = context.request.urljoin(image_url)
+
+ # Estructurar los datos
+ product_data = {
+ 'url': product_url,
+ 'source_url': context.request.url,
+ 'name': product_name,
+ 'brand': brand,
+ 'price': price,
+ 'discount': discount,
+ 'specifications': specs,
+ 'availability': availability,
+ 'rating': rating,
+ 'reviews_count': reviews_count,
+ 'features': features,
+ 'image_url': image_url,
+ }
+
+ # Almacenar los datos extraídos
+ await context.push_data(product_data)
+ context.log.info(f'Producto extraído: {product_name}')
+
+ # Encontrar y encolar enlaces a más productos
+ # Buscar paginación o categorías relacionadas
+ await context.enqueue_links(
+ selector='a.next-page, a.pagination-link, nav.pagination a, a.category-link',
+ label='pagination',
+ )
+
+ # También encolar enlaces a páginas de detalles de productos
+ await context.enqueue_links(
+ selector='a.product-link, a.product-details',
+ label='product-details',
+ )
+
+ # Manejador específico para páginas de detalles de productos
+ @crawler.router.handler('product-details')
+ async def product_details_handler(context: BeautifulSoupCrawlingContext) -> None:
+ """Extraer información detallada de la página de detalles del producto."""
+ context.log.info(f'Procesando detalles de producto: {context.request.url}')
+
+ # Extraer información más detallada de la página de producto individual
+ title_elem = context.soup.find('h1', class_='product-title')
+ title = title_elem.get_text(strip=True) if title_elem else ''
+
+ # Extraer descripción completa
+ desc_elem = context.soup.find('div', class_='product-description')
+ description = desc_elem.get_text(strip=True) if desc_elem else ''
+
+ # Extraer tabla de especificaciones completas
+ specs_table = context.soup.find('table', class_='specifications')
+ detailed_specs = {}
+ if specs_table:
+ rows = specs_table.find_all('tr')
+ for row in rows:
+ cells = row.find_all(['td', 'th'])
+ if len(cells) >= 2:
+ key = cells[0].get_text(strip=True)
+ value = cells[1].get_text(strip=True)
+ detailed_specs[key] = value
+
+ # Extraer todas las imágenes del producto
+ images = []
+ img_gallery = context.soup.find('div', class_='product-gallery')
+ if img_gallery:
+ img_elements = img_gallery.find_all('img')
+ for img in img_elements:
+ img_url = img.get('src', '')
+ if img_url and not img_url.startswith('http'):
+ img_url = context.request.urljoin(img_url)
+ if img_url:
+ images.append(img_url)
+
+ detailed_data = {
+ 'url': context.request.url,
+ 'title': title,
+ 'description': description,
+ 'detailed_specifications': detailed_specs,
+ 'images': images,
+ }
+
+ await context.push_data(detailed_data)
+
+ # Ejecutar el crawler con las URLs iniciales
+ # Reemplazar estas con tiendas reales de ballestas y equipamiento
+ await crawler.run(
+ [
+ 'https://example.com/crossbows',
+ 'https://example.com/archery-equipment',
+ # Agregar más URLs según sea necesario
+ ]
+ )
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/docs/examples/code_examples/crossbow_agent_pw.py b/docs/examples/code_examples/crossbow_agent_pw.py
new file mode 100644
index 0000000000..aa4f42a25b
--- /dev/null
+++ b/docs/examples/code_examples/crossbow_agent_pw.py
@@ -0,0 +1,345 @@
+import asyncio
+
+from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext
+
+# Constantes de configuración
+MAX_PRODUCT_IMAGES = 10 # Número máximo de imágenes a extraer por producto
+MAX_REVIEWS = 5 # Número máximo de reseñas a extraer por producto
+
+
+async def main() -> None:
+ """Agente que extrae información sobre ballestas usando Playwright.
+
+ Este ejemplo demuestra cómo crear un agente que puede:
+ 1. Extraer información de productos de ballestas desde tiendas con JavaScript
+ 2. Interactuar con filtros y catálogos dinámicos
+ 3. Capturar imágenes de productos en alta resolución
+ 4. Manejar carga dinámica y paginación infinita
+
+ Usar PlaywrightCrawler cuando el sitio web requiere JavaScript para mostrar
+ el contenido o tiene elementos interactivos.
+ """
+ # Crear una instancia del crawler con Playwright
+ crawler = PlaywrightCrawler(
+ # Limitar solicitudes durante las pruebas
+ max_requests_per_crawl=30,
+ # Reintentar solicitudes fallidas
+ max_request_retries=3,
+ # Configurar tiempo de espera
+ request_handler_timeout=120.0,
+ # Configurar el navegador
+ headless=True, # Cambiar a False para ver el navegador en acción
+ )
+
+ # Definir el manejador de solicitudes principal
+ @crawler.router.default_handler
+ async def request_handler(context: PlaywrightCrawlingContext) -> None:
+ """Extraer información de productos de ballestas usando Playwright."""
+ context.log.info(f'Procesando {context.request.url} ...')
+
+ # Esperar a que el contenido dinámico se cargue
+ try:
+ await context.page.wait_for_selector(
+ '.product-item, .product-card, [data-product-id]',
+ timeout=10000,
+ )
+ except Exception as e:
+ context.log.warning(f'Tiempo de espera agotado esperando productos: {e}')
+ return
+
+ # Scroll para activar carga dinámica (lazy loading)
+ await context.page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
+ await context.page.wait_for_timeout(2000) # Esperar carga de imágenes
+
+ # Extraer productos usando JavaScript
+ products_data = await context.page.evaluate("""
+ () => {
+ const products = [];
+ const productElements = document.querySelectorAll(
+ '.product-item, .product-card, article.product, [data-product-id]'
+ );
+
+ productElements.forEach(product => {
+ // Extraer nombre
+ const nameElem = product.querySelector(
+ 'h2.product-name, h3.product-title, .product-name, a.product-link'
+ );
+ const name = nameElem ? nameElem.textContent.trim() : 'Desconocido';
+
+ // Extraer marca
+ const brandElem = product.querySelector('.brand, .manufacturer');
+ const brand = brandElem ? brandElem.textContent.trim() : '';
+
+ // Extraer precio
+ const priceElem = product.querySelector(
+ '.price, .product-price, [data-price]'
+ );
+ const price = priceElem ? priceElem.textContent.trim() : 'No disponible';
+
+ // Extraer descuento
+ const discountElem = product.querySelector('.discount, .sale-price');
+ const discount = discountElem ? discountElem.textContent.trim() : '';
+
+ // Extraer especificaciones
+ const specs = {};
+ const drawWeightElem = product.querySelector(
+ '.draw-weight, [data-spec="power"]'
+ );
+ if (drawWeightElem) {
+ specs.draw_weight_lbs = drawWeightElem.textContent.trim();
+ }
+
+ const speedElem = product.querySelector('.fps, [data-spec="speed"]');
+ if (speedElem) {
+ specs.speed_fps = speedElem.textContent.trim();
+ }
+
+ const weightElem = product.querySelector('.weight, [data-spec="weight"]');
+ if (weightElem) {
+ specs.weight = weightElem.textContent.trim();
+ }
+
+ const lengthElem = product.querySelector('.length, [data-spec="length"]');
+ if (lengthElem) {
+ specs.length = lengthElem.textContent.trim();
+ }
+
+ // Extraer disponibilidad
+ const stockElem = product.querySelector('.stock-status, .availability');
+ const availability = stockElem ? stockElem.textContent.trim() : 'Desconocido';
+
+ // Extraer calificación
+ const ratingElem = product.querySelector('.rating, .star-rating');
+ const rating = ratingElem ? ratingElem.textContent.trim() : '';
+
+ // Extraer número de reseñas
+ const reviewsElem = product.querySelector('.review-count');
+ const reviews_count = reviewsElem ? reviewsElem.textContent.trim() : '0';
+
+ // Extraer características
+ const features = [];
+ const featuresList = product.querySelector('ul.product-features');
+ if (featuresList) {
+ const items = featuresList.querySelectorAll('li');
+ items.forEach(item => {
+ features.push(item.textContent.trim());
+ });
+ }
+
+ // Extraer URL del producto
+ const linkElem = product.querySelector('a.product-link, a[href]');
+ const productUrl = linkElem ? linkElem.href : '';
+
+ // Extraer imagen
+ const imgElem = product.querySelector('img.product-image, img');
+ const imageUrl = imgElem ? imgElem.src : '';
+
+ products.push({
+ name,
+ brand,
+ price,
+ discount,
+ specifications: specs,
+ availability,
+ rating,
+ reviews_count,
+ features,
+ product_url: productUrl,
+ image_url: imageUrl,
+ });
+ });
+
+ return products;
+ }
+ """)
+
+ # Procesar y almacenar cada producto
+ for product in products_data:
+ product_data = {
+ 'url': product.get('product_url', ''),
+ 'source_url': context.request.url,
+ 'name': product.get('name', ''),
+ 'brand': product.get('brand', ''),
+ 'price': product.get('price', ''),
+ 'discount': product.get('discount', ''),
+ 'specifications': product.get('specifications', {}),
+ 'availability': product.get('availability', ''),
+ 'rating': product.get('rating', ''),
+ 'reviews_count': product.get('reviews_count', '0'),
+ 'features': product.get('features', []),
+ 'image_url': product.get('image_url', ''),
+ }
+
+ await context.push_data(product_data)
+ context.log.info(f'Producto extraído: {product_data["name"]}')
+
+ # Manejar paginación
+ # Buscar botón "siguiente" o enlaces de paginación
+ try:
+ next_button = await context.page.query_selector(
+ 'a.next-page, button.next, a.pagination-next, [aria-label="Next"]'
+ )
+ if next_button:
+ next_url = await next_button.get_attribute('href')
+ if next_url:
+ await context.enqueue_links(
+ selector='a.next-page, a.pagination-next',
+ label='pagination',
+ )
+ except Exception as e:
+ context.log.warning(f'Error al buscar paginación: {e}')
+
+ # Encolar enlaces a detalles de productos
+ await context.enqueue_links(
+ selector='a.product-link, a.product-details',
+ label='product-details',
+ )
+
+ # Encolar enlaces a categorías relacionadas
+ await context.enqueue_links(
+ selector='a.category-link',
+ label='category',
+ )
+
+ # Manejador para páginas de detalles de productos
+ @crawler.router.handler('product-details')
+ async def product_details_handler(context: PlaywrightCrawlingContext) -> None:
+ """Extraer información detallada de la página de producto."""
+ context.log.info(f'Procesando detalles: {context.request.url}')
+
+ # Esperar a que se cargue el contenido principal
+ try:
+ await context.page.wait_for_selector(
+ 'h1.product-title, .product-description',
+ timeout=10000,
+ )
+ except Exception:
+ context.log.warning('No se pudo cargar la página de detalles')
+ return
+
+ # Extraer título
+ title = await context.page.text_content('h1.product-title, h1')
+ title = title.strip() if title else ''
+
+ # Extraer descripción completa
+ description = ''
+ desc_elem = await context.page.query_selector(
+ '.product-description, .description, #description'
+ )
+ if desc_elem:
+ description = await desc_elem.text_content()
+ description = description.strip() if description else ''
+
+ # Extraer especificaciones detalladas
+ detailed_specs = {}
+ spec_rows = await context.page.query_selector_all(
+ 'table.specifications tr, .specs-table tr'
+ )
+ for row in spec_rows:
+ cells = await row.query_selector_all('td, th')
+ if len(cells) >= 2:
+ key = await cells[0].text_content()
+ value = await cells[1].text_content()
+ if key and value:
+ detailed_specs[key.strip()] = value.strip()
+
+ # Capturar todas las imágenes del producto
+ images = []
+ img_elements = await context.page.query_selector_all(
+ '.product-gallery img, .product-images img, [data-image]'
+ )
+ for img in img_elements[:MAX_PRODUCT_IMAGES]:
+ img_url = await img.get_attribute('src')
+ if img_url and not img_url.startswith('data:'):
+ images.append(img_url)
+
+ # Capturar pantalla del producto (opcional)
+ # screenshot_bytes = await context.page.screenshot(full_page=False)
+ # Guardar screenshot si es necesario
+
+ # Extraer reseñas de usuarios
+ reviews = []
+ review_elements = await context.page.query_selector_all(
+ '.review-item, .user-review, [data-review]'
+ )
+ for review_elem in review_elements[:MAX_REVIEWS]:
+ try:
+ author_elem = await review_elem.query_selector('.review-author, .author')
+ author = (
+ await author_elem.text_content() if author_elem else 'Anónimo'
+ )
+
+ rating_elem = await review_elem.query_selector('.review-rating, .rating')
+ rating = (
+ await rating_elem.text_content() if rating_elem else ''
+ )
+
+ comment_elem = await review_elem.query_selector(
+ '.review-text, .comment'
+ )
+ comment = (
+ await comment_elem.text_content() if comment_elem else ''
+ )
+
+ reviews.append({
+ 'author': author.strip() if author else '',
+ 'rating': rating.strip() if rating else '',
+ 'comment': comment.strip() if comment else '',
+ })
+ except Exception as e:
+ context.log.warning(f'Error al extraer reseña: {e}')
+ continue
+
+ detailed_data = {
+ 'url': context.request.url,
+ 'title': title,
+ 'description': description,
+ 'detailed_specifications': detailed_specs,
+ 'images': images,
+ 'reviews': reviews,
+ }
+
+ await context.push_data(detailed_data)
+
+ # Manejador para páginas de categoría
+ @crawler.router.handler('category')
+ async def category_handler(context: PlaywrightCrawlingContext) -> None:
+ """Manejar páginas de categoría y aplicar filtros."""
+ context.log.info(f'Procesando categoría: {context.request.url}')
+
+ # Esperar a que se carguen los filtros
+ await context.page.wait_for_timeout(2000)
+
+ # Ejemplo: Aplicar filtros (ajustar según el sitio)
+ try:
+ # Expandir filtros si están colapsados
+ filter_buttons = await context.page.query_selector_all(
+ 'button.filter-toggle, .expand-filters'
+ )
+ for button in filter_buttons:
+ await button.click()
+ await context.page.wait_for_timeout(500)
+
+ # Seleccionar rango de precio (ejemplo)
+ # price_filter = await context.page.query_selector('#price-range')
+ # if price_filter:
+ # await price_filter.fill('100-500')
+
+ except Exception as e:
+ context.log.warning(f'Error al aplicar filtros: {e}')
+
+ # Procesar productos en la categoría (llamar al handler por defecto)
+ await request_handler(context)
+
+ # Ejecutar el crawler con las URLs iniciales
+ await crawler.run(
+ [
+ 'https://example.com/crossbows',
+ 'https://example.com/hunting-equipment',
+ # Agregar más URLs según sea necesario
+ ]
+ )
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/docs/examples/crossbow_agent.mdx b/docs/examples/crossbow_agent.mdx
new file mode 100644
index 0000000000..5467a27b6e
--- /dev/null
+++ b/docs/examples/crossbow_agent.mdx
@@ -0,0 +1,142 @@
+---
+id: crossbow-agent
+title: Agente para información de ballestas
+---
+
+import ApiLink from '@site/src/components/ApiLink';
+import CodeBlock from '@theme/CodeBlock';
+
+import BeautifulSoupSource from '!!raw-loader!./code_examples/crossbow_agent.py';
+import PlaywrightSource from '!!raw-loader!./code_examples/crossbow_agent_pw.py';
+
+Este ejemplo demuestra cómo crear un agente que extrae información sobre ballestas desde sitios web. Esto es útil para construir bases de datos de productos, comparadores de precios, o plataformas de reseñas.
+
+## Agente de información de ballestas
+
+El ejemplo muestra cómo:
+
+1. **Extraer información de productos** desde tiendas en línea y sitios especializados
+2. **Extraer datos estructurados** incluyendo especificaciones técnicas, precios y reseñas
+3. **Almacenar los datos** para análisis, comparación o integración con otros sistemas
+4. **Navegar** a través de catálogos de productos y categorías
+
+## Usando BeautifulSoupCrawler
+
+Usar `BeautifulSoupCrawler` cuando el sitio web de productos sirve contenido HTML estático. Este enfoque es más rápido y eficiente para tiendas en línea tradicionales.
+
+
+ {BeautifulSoupSource}
+
+
+El crawler extrae:
+- **Nombre del producto**: Marca y modelo de la ballesta
+- **Especificaciones técnicas**: Potencia (libras), velocidad (FPS), peso, longitud
+- **Precio**: Precio actual y descuentos si están disponibles
+- **Disponibilidad**: Estado de stock del producto
+- **Reseñas**: Calificaciones y comentarios de usuarios
+- **Características adicionales**: Accesorios incluidos, garantía, materiales
+
+Los datos se almacenan en el dataset predeterminado en el directorio `./storage/datasets/default/`.
+
+## Usando PlaywrightCrawler para sitios dinámicos
+
+Usar `PlaywrightCrawler` cuando se trabaja con tiendas en línea con JavaScript intensivo, catálogos dinámicos o filtros interactivos.
+
+
+ {PlaywrightSource}
+
+
+El agente basado en Playwright puede:
+- **Manejar contenido dinámico**: Esperar a que JavaScript cargue los productos
+- **Interactuar con filtros**: Seleccionar categorías, rangos de precio, marcas
+- **Extraer imágenes**: Capturar fotos de productos en alta resolución
+- **Navegar catálogos complejos**: Manejar paginación infinita y carga dinámica
+- **Acceder a detalles ocultos**: Hacer clic en pestañas para ver especificaciones completas
+
+## Casos de uso
+
+Este tipo de agente es útil para:
+
+- **Comparadores de precios**: Recopilar precios de múltiples vendedores
+- **Monitoreo de inventario**: Rastrear disponibilidad de productos populares
+- **Análisis de mercado**: Estudiar tendencias de precios y especificaciones
+- **Bases de datos de productos**: Crear catálogos completos para sitios de reseñas
+- **Sistemas de recomendación**: Alimentar algoritmos con datos de productos estructurados
+- **Investigación de competencia**: Analizar ofertas de competidores
+
+## Tipos de agentes aplicables
+
+Este patrón de agente puede ser aplicado a diversos dominios:
+
+### 1. **Agentes de comercio electrónico**
+ - Productos deportivos (ballestas, arcos, equipamiento)
+ - Equipamiento de caza y tiro deportivo
+ - Accesorios y repuestos
+
+### 2. **Agentes de información especializada**
+ - Sitios de reseñas y comparativas
+ - Foros y comunidades de entusiastas
+ - Blogs y artículos técnicos
+
+### 3. **Agentes de monitoreo de precios**
+ - Seguimiento de ofertas y descuentos
+ - Alertas de disponibilidad
+ - Comparación multi-sitio
+
+### 4. **Agentes de agregación de contenido**
+ - Tutoriales y guías de uso
+ - Videos de demostración
+ - Manuales y documentación técnica
+
+## Funcionalidades principales
+
+El agente de ballestas implementa las siguientes funcionalidades:
+
+1. **Extracción estructurada**: Convierte datos no estructurados en formatos consistentes
+2. **Validación de datos**: Verifica que la información extraída sea completa y válida
+3. **Manejo de errores**: Reintentos automáticos y logging detallado
+4. **Normalización**: Convierte unidades y formatos a estándares consistentes
+5. **Enriquecimiento**: Agrega metadatos y categorización automática
+6. **Deduplicación**: Evita almacenar productos duplicados
+
+## Personalización
+
+Para adaptar estos ejemplos a sitios web específicos:
+
+1. **Actualizar los selectores**: Inspeccionar el sitio web objetivo y modificar los selectores CSS para que coincidan con la estructura HTML
+2. **Ajustar las URLs iniciales**: Reemplazar las URLs de ejemplo con tiendas reales de ballestas
+3. **Agregar más campos de datos**: Extraer información adicional como dimensiones, materiales, accesorios incluidos
+4. **Implementar validación**: Agregar lógica para verificar que los precios y especificaciones sean válidos
+5. **Configurar proxies**: Para scraping a gran escala, considerar usar rotación de proxies
+6. **Personalizar almacenamiento**: Exportar datos a bases de datos o APIs específicas
+
+## Consideraciones éticas y legales
+
+Al extraer información de productos:
+
+- **Respetar robots.txt**: Siempre verificar y respetar el archivo robots.txt del sitio web
+- **Verificar términos de servicio**: Asegurar que el scraping esté permitido por el sitio web
+- **Limitación de tasa**: Usar retrasos apropiados para evitar sobrecargar los servidores
+- **Respeto a la propiedad intelectual**: No copiar descripciones o imágenes protegidas sin autorización
+- **Uso responsable**: Los datos extraídos deben usarse de manera ética y legal
+- **Privacidad**: No recopilar datos personales de usuarios sin consentimiento
+
+## Optimización del rendimiento
+
+Para mejorar el rendimiento del agente:
+
+- **Usar BeautifulSoupCrawler** cuando sea posible (más rápido que Playwright)
+- **Implementar caché** para evitar re-scraping de páginas ya procesadas
+- **Configurar concurrencia** ajustando `max_concurrency` según los recursos disponibles
+- **Filtrar URLs temprano** para evitar procesar páginas irrelevantes
+- **Usar selectores eficientes** que sean específicos pero no frágiles
+
+## Recursos relacionados
+
+- `BeautifulSoupCrawler`
+- `PlaywrightCrawler`
+- `Dataset`
+- `Request`
+- [Respetar archivo robots.txt](./respect_robots_txt_file.mdx)
+- [Crawl múltiples URLs](./crawl_multiple_urls.mdx)
+- [Exportar dataset completo a archivo](./export_entire_dataset_to_file.mdx)