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)