chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:12:13 +08:00
commit 0446c45d8e
898 changed files with 328024 additions and 0 deletions
@@ -0,0 +1,235 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🚀 Crawl4AI v0.3.72 Release Announcement\n",
"\n",
"Welcome to the new release of **Crawl4AI v0.3.72**! This notebook highlights the latest features and demonstrates how they work in real-time. Follow along to see each feature in action!\n",
"\n",
"### Whats New?\n",
"- ✨ `Fit Markdown`: Extracts only the main content from articles and blogs\n",
"- 🛡️ **Magic Mode**: Comprehensive anti-bot detection bypass\n",
"- 🌐 **Multi-browser support**: Switch between Chromium, Firefox, WebKit\n",
"- 🔍 **Knowledge Graph Extraction**: Generate structured graphs of entities & relationships from any URL\n",
"- 🤖 **Crawl4AI GPT Assistant**: Chat directly with our AI assistant for help, code generation, and faster learning (available [here](https://tinyurl.com/your-gpt-assistant-link))\n",
"\n",
"---\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📥 Setup\n",
"To start, we'll install `Crawl4AI` along with Playwright and `nest_asyncio` to ensure compatibility with Colabs asynchronous environment."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Install Crawl4AI and dependencies\n",
"!pip install crawl4ai\n",
"!playwright install\n",
"!pip install nest_asyncio"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import nest_asyncio and apply it to allow asyncio in Colab\n",
"import nest_asyncio\n",
"nest_asyncio.apply()\n",
"\n",
"print('Setup complete!')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ✨ Feature 1: `Fit Markdown`\n",
"Extracts only the main content from articles and blog pages, removing sidebars, ads, and other distractions.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"from crawl4ai import AsyncWebCrawler\n",
"\n",
"async def fit_markdown_demo():\n",
" async with AsyncWebCrawler() as crawler:\n",
" result = await crawler.arun(url=\"https://janineintheworld.com/places-to-visit-in-central-mexico\")\n",
" print(result.fit_markdown) # Shows main content in Markdown format\n",
"\n",
"# Run the demo\n",
"await fit_markdown_demo()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🛡️ Feature 2: Magic Mode\n",
"Magic Mode bypasses anti-bot detection to make crawling more reliable on protected websites.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def magic_mode_demo():\n",
" async with AsyncWebCrawler() as crawler: # Enables anti-bot detection bypass\n",
" result = await crawler.arun(\n",
" url=\"https://www.reuters.com/markets/us/global-markets-view-usa-pix-2024-08-29/\",\n",
" magic=True # Enables magic mode\n",
" )\n",
" print(result.markdown) # Shows the full content in Markdown format\n",
"\n",
"# Run the demo\n",
"await magic_mode_demo()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🌐 Feature 3: Multi-Browser Support\n",
"Crawl4AI now supports Chromium, Firefox, and WebKit. Heres how to specify Firefox for a crawl.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def multi_browser_demo():\n",
" async with AsyncWebCrawler(browser_type=\"firefox\") as crawler: # Using Firefox instead of default Chromium\n",
" result = await crawler.arun(url=\"https://crawl4i.com\")\n",
" print(result.markdown) # Shows content extracted using Firefox\n",
"\n",
"# Run the demo\n",
"await multi_browser_demo()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ✨ Put them all together\n",
"\n",
"Let's combine all the features to extract the main content from a blog post, bypass anti-bot detection, and generate a knowledge graph from the extracted content."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from crawl4ai import LLMExtractionStrategy\n",
"from pydantic import BaseModel\n",
"import json, os\n",
"from typing import List\n",
"\n",
"# Define classes for the knowledge graph structure\n",
"class Landmark(BaseModel):\n",
" name: str\n",
" description: str\n",
" activities: list[str] # E.g., visiting, sightseeing, relaxing\n",
"\n",
"class City(BaseModel):\n",
" name: str\n",
" description: str\n",
" landmarks: list[Landmark]\n",
" cultural_highlights: list[str] # E.g., food, music, traditional crafts\n",
"\n",
"class TravelKnowledgeGraph(BaseModel):\n",
" cities: list[City] # Central Mexican cities to visit\n",
"\n",
"async def combined_demo():\n",
" # Define the knowledge graph extraction strategy\n",
" strategy = LLMExtractionStrategy(\n",
" # provider=\"ollama/nemotron\",\n",
" provider='openai/gpt-4o-mini', # Or any other provider, including Ollama and open source models\n",
" pi_token=os.getenv('OPENAI_API_KEY'), # In case of Ollama just pass \"no-token\"\n",
" schema=TravelKnowledgeGraph.schema(),\n",
" instruction=(\n",
" \"Extract cities, landmarks, and cultural highlights for places to visit in Central Mexico. \"\n",
" \"For each city, list main landmarks with descriptions and activities, as well as cultural highlights.\"\n",
" )\n",
" )\n",
"\n",
" # Set up the AsyncWebCrawler with multi-browser support, Magic Mode, and Fit Markdown\n",
" async with AsyncWebCrawler(browser_type=\"firefox\") as crawler:\n",
" result = await crawler.arun(\n",
" url=\"https://janineintheworld.com/places-to-visit-in-central-mexico\",\n",
" extraction_strategy=strategy,\n",
" bypass_cache=True,\n",
" magic=True\n",
" )\n",
" \n",
" # Display main article content in Fit Markdown format\n",
" print(\"Extracted Main Content:\\n\", result.fit_markdown)\n",
" \n",
" # Display extracted knowledge graph of cities, landmarks, and cultural highlights\n",
" if result.extracted_content:\n",
" travel_graph = json.loads(result.extracted_content)\n",
" print(\"\\nExtracted Knowledge Graph:\\n\", json.dumps(travel_graph, indent=2))\n",
"\n",
"# Run the combined demo\n",
"await combined_demo()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🤖 Crawl4AI GPT Assistant\n",
"Chat with the Crawl4AI GPT Assistant for code generation, support, and learning Crawl4AI faster. Try it out [here](https://tinyurl.com/crawl4ai-gpt)!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
File diff suppressed because it is too large Load Diff
+408
View File
@@ -0,0 +1,408 @@
"""
🚀 Crawl4AI v0.7.0 Release Demo
================================
This demo showcases all major features introduced in v0.7.0 release.
Major Features:
1. ✅ Adaptive Crawling - Intelligent crawling with confidence tracking
2. ✅ Virtual Scroll Support - Handle infinite scroll pages
3. ✅ Link Preview - Advanced link analysis with 3-layer scoring
4. ✅ URL Seeder - Smart URL discovery and filtering
5. ✅ C4A Script - Domain-specific language for web automation
6. ✅ Chrome Extension Updates - Click2Crawl and instant schema extraction
7. ✅ PDF Parsing Support - Extract content from PDF documents
8. ✅ Nightly Builds - Automated nightly releases
Run this demo to see all features in action!
"""
import asyncio
import json
from typing import List, Dict
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box
from crawl4ai import (
AsyncWebCrawler,
CrawlerRunConfig,
BrowserConfig,
CacheMode,
AdaptiveCrawler,
AdaptiveConfig,
AsyncUrlSeeder,
SeedingConfig,
c4a_compile,
CompilationResult
)
from crawl4ai.async_configs import VirtualScrollConfig, LinkPreviewConfig
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
console = Console()
def print_section(title: str, description: str = ""):
"""Print a section header"""
console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
console.print(f"[bold yellow]{title}[/bold yellow]")
if description:
console.print(f"[dim]{description}[/dim]")
console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
async def demo_1_adaptive_crawling():
"""Demo 1: Adaptive Crawling - Intelligent content extraction"""
print_section(
"Demo 1: Adaptive Crawling",
"Intelligently learns and adapts to website patterns"
)
# Create adaptive crawler with custom configuration
config = AdaptiveConfig(
strategy="statistical", # or "embedding"
confidence_threshold=0.7,
max_pages=10,
top_k_links=3,
min_gain_threshold=0.1
)
# Example: Learn from a product page
console.print("[cyan]Learning from product page patterns...[/cyan]")
async with AsyncWebCrawler() as crawler:
adaptive = AdaptiveCrawler(crawler, config)
# Start adaptive crawl
console.print("[cyan]Starting adaptive crawl...[/cyan]")
result = await adaptive.digest(
start_url="https://docs.python.org/3/",
query="python decorators tutorial"
)
console.print("[green]✓ Adaptive crawl completed[/green]")
console.print(f" - Confidence Level: {adaptive.confidence:.0%}")
console.print(f" - Pages Crawled: {len(result.crawled_urls)}")
console.print(f" - Knowledge Base: {len(adaptive.state.knowledge_base)} documents")
# Get most relevant content
relevant = adaptive.get_relevant_content(top_k=3)
if relevant:
console.print("\nMost relevant pages:")
for i, page in enumerate(relevant, 1):
console.print(f" {i}. {page['url']} (relevance: {page['score']:.2%})")
async def demo_2_virtual_scroll():
"""Demo 2: Virtual Scroll - Handle infinite scroll pages"""
print_section(
"Demo 2: Virtual Scroll Support",
"Capture content from modern infinite scroll pages"
)
# Configure virtual scroll - using body as container for example.com
scroll_config = VirtualScrollConfig(
container_selector="body", # Using body since example.com has simple structure
scroll_count=3, # Just 3 scrolls for demo
scroll_by="container_height", # or "page_height" or pixel value
wait_after_scroll=0.5 # Wait 500ms after each scroll
)
config = CrawlerRunConfig(
virtual_scroll_config=scroll_config,
cache_mode=CacheMode.BYPASS,
wait_until="networkidle"
)
console.print("[cyan]Virtual Scroll Configuration:[/cyan]")
console.print(f" - Container: {scroll_config.container_selector}")
console.print(f" - Scroll count: {scroll_config.scroll_count}")
console.print(f" - Scroll by: {scroll_config.scroll_by}")
console.print(f" - Wait after scroll: {scroll_config.wait_after_scroll}s")
console.print("\n[dim]Note: Using example.com for demo - in production, use this[/dim]")
console.print("[dim]with actual infinite scroll pages like social media feeds.[/dim]\n")
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
"https://example.com",
config=config
)
if result.success:
console.print("[green]✓ Virtual scroll executed successfully![/green]")
console.print(f" - Content length: {len(result.markdown)} chars")
# Show example of how to use with real infinite scroll sites
console.print("\n[yellow]Example for real infinite scroll sites:[/yellow]")
console.print("""
# For Twitter-like feeds:
scroll_config = VirtualScrollConfig(
container_selector="[data-testid='primaryColumn']",
scroll_count=20,
scroll_by="container_height",
wait_after_scroll=1.0
)
# For Instagram-like grids:
scroll_config = VirtualScrollConfig(
container_selector="main article",
scroll_count=15,
scroll_by=1000, # Fixed pixel amount
wait_after_scroll=1.5
)""")
async def demo_3_link_preview():
"""Demo 3: Link Preview with 3-layer scoring"""
print_section(
"Demo 3: Link Preview & Scoring",
"Advanced link analysis with intrinsic, contextual, and total scoring"
)
# Configure link preview
link_config = LinkPreviewConfig(
include_internal=True,
include_external=False,
max_links=10,
concurrency=5,
query="python tutorial", # For contextual scoring
score_threshold=0.3,
verbose=True
)
config = CrawlerRunConfig(
link_preview_config=link_config,
score_links=True, # Enable intrinsic scoring
cache_mode=CacheMode.BYPASS
)
console.print("[cyan]Analyzing links with 3-layer scoring system...[/cyan]")
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://docs.python.org/3/", config=config)
if result.success and result.links:
# Get scored links
internal_links = result.links.get("internal", [])
scored_links = [l for l in internal_links if l.get("total_score")]
scored_links.sort(key=lambda x: x.get("total_score", 0), reverse=True)
# Create a scoring table
table = Table(title="Link Scoring Results", box=box.ROUNDED)
table.add_column("Link Text", style="cyan", width=40)
table.add_column("Intrinsic Score", justify="center")
table.add_column("Contextual Score", justify="center")
table.add_column("Total Score", justify="center", style="bold green")
for link in scored_links[:5]:
text = link.get('text', 'No text')[:40]
table.add_row(
text,
f"{link.get('intrinsic_score', 0):.1f}/10",
f"{link.get('contextual_score', 0):.2f}/1",
f"{link.get('total_score', 0):.3f}"
)
console.print(table)
async def demo_4_url_seeder():
"""Demo 4: URL Seeder - Smart URL discovery"""
print_section(
"Demo 4: URL Seeder",
"Intelligent URL discovery and filtering"
)
# Configure seeding
seeding_config = SeedingConfig(
source="cc+sitemap", # or "crawl"
pattern="*tutorial*", # URL pattern filter
max_urls=50,
extract_head=True, # Get metadata
query="python programming", # For relevance scoring
scoring_method="bm25",
score_threshold=0.2,
force = True
)
console.print("[cyan]URL Seeder Configuration:[/cyan]")
console.print(f" - Source: {seeding_config.source}")
console.print(f" - Pattern: {seeding_config.pattern}")
console.print(f" - Max URLs: {seeding_config.max_urls}")
console.print(f" - Query: {seeding_config.query}")
console.print(f" - Scoring: {seeding_config.scoring_method}")
# Use URL seeder to discover URLs
async with AsyncUrlSeeder() as seeder:
console.print("\n[cyan]Discovering URLs from Python docs...[/cyan]")
urls = await seeder.urls("docs.python.org", seeding_config)
console.print(f"\n[green]✓ Discovered {len(urls)} URLs[/green]")
for i, url_info in enumerate(urls[:5], 1):
console.print(f" {i}. {url_info['url']}")
if url_info.get('relevance_score'):
console.print(f" Relevance: {url_info['relevance_score']:.3f}")
async def demo_5_c4a_script():
"""Demo 5: C4A Script - Domain-specific language"""
print_section(
"Demo 5: C4A Script Language",
"Domain-specific language for web automation"
)
# Example C4A script
c4a_script = """
# Simple C4A script example
WAIT `body` 3
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
CLICK `.search-button`
TYPE "python tutorial"
PRESS Enter
WAIT `.results` 5
"""
console.print("[cyan]C4A Script Example:[/cyan]")
console.print(Panel(c4a_script, title="script.c4a", border_style="blue"))
# Compile the script
compilation_result = c4a_compile(c4a_script)
if compilation_result.success:
console.print("[green]✓ Script compiled successfully![/green]")
console.print(f" - Generated {len(compilation_result.js_code)} JavaScript statements")
console.print("\nFirst 3 JS statements:")
for stmt in compilation_result.js_code[:3]:
console.print(f"{stmt}")
else:
console.print("[red]✗ Script compilation failed[/red]")
if compilation_result.first_error:
error = compilation_result.first_error
console.print(f" Error at line {error.line}: {error.message}")
async def demo_6_css_extraction():
"""Demo 6: Enhanced CSS/JSON extraction"""
print_section(
"Demo 6: Enhanced Extraction",
"Improved CSS selector and JSON extraction"
)
# Define extraction schema
schema = {
"name": "Example Page Data",
"baseSelector": "body",
"fields": [
{
"name": "title",
"selector": "h1",
"type": "text"
},
{
"name": "paragraphs",
"selector": "p",
"type": "list",
"fields": [
{"name": "text", "type": "text"}
]
}
]
}
extraction_strategy = JsonCssExtractionStrategy(schema)
console.print("[cyan]Extraction Schema:[/cyan]")
console.print(json.dumps(schema, indent=2))
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
"https://example.com",
config=CrawlerRunConfig(
extraction_strategy=extraction_strategy,
cache_mode=CacheMode.BYPASS
)
)
if result.success and result.extracted_content:
console.print("\n[green]✓ Content extracted successfully![/green]")
console.print(f"Extracted: {json.dumps(json.loads(result.extracted_content), indent=2)[:200]}...")
async def demo_7_performance_improvements():
"""Demo 7: Performance improvements"""
print_section(
"Demo 7: Performance Improvements",
"Faster crawling with better resource management"
)
# Performance-optimized configuration
config = CrawlerRunConfig(
cache_mode=CacheMode.ENABLED, # Use caching
wait_until="domcontentloaded", # Faster than networkidle
page_timeout=10000, # 10 second timeout
exclude_external_links=True,
exclude_social_media_links=True,
exclude_external_images=True
)
console.print("[cyan]Performance Configuration:[/cyan]")
console.print(" - Cache: ENABLED")
console.print(" - Wait: domcontentloaded (faster)")
console.print(" - Timeout: 10s")
console.print(" - Excluding: external links, images, social media")
# Measure performance
import time
start_time = time.time()
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://example.com", config=config)
elapsed = time.time() - start_time
if result.success:
console.print(f"\n[green]✓ Page crawled in {elapsed:.2f} seconds[/green]")
async def main():
"""Run all demos"""
console.print(Panel(
"[bold cyan]Crawl4AI v0.7.0 Release Demo[/bold cyan]\n\n"
"This demo showcases all major features introduced in v0.7.0.\n"
"Each demo is self-contained and demonstrates a specific feature.",
title="Welcome",
border_style="blue"
))
demos = [
demo_1_adaptive_crawling,
demo_2_virtual_scroll,
demo_3_link_preview,
demo_4_url_seeder,
demo_5_c4a_script,
demo_6_css_extraction,
demo_7_performance_improvements
]
for i, demo in enumerate(demos, 1):
try:
await demo()
if i < len(demos):
console.print("\n[dim]Press Enter to continue to next demo...[/dim]")
input()
except Exception as e:
console.print(f"[red]Error in demo: {e}[/red]")
continue
console.print(Panel(
"[bold green]Demo Complete![/bold green]\n\n"
"Thank you for trying Crawl4AI v0.7.0!\n"
"For more examples and documentation, visit:\n"
"https://github.com/unclecode/crawl4ai",
title="Complete",
border_style="green"
))
if __name__ == "__main__":
asyncio.run(main())
+338
View File
@@ -0,0 +1,338 @@
"""
🚀 Crawl4AI v0.7.5 Release Demo - Working Examples
==================================================
This demo showcases key features introduced in v0.7.5 with real, executable examples.
Featured Demos:
1. ✅ Docker Hooks System - Real API calls with custom hooks (string & function-based)
2. ✅ Enhanced LLM Integration - Working LLM configurations
3. ✅ HTTPS Preservation - Live crawling with HTTPS maintenance
Requirements:
- crawl4ai v0.7.5 installed
- Docker running with crawl4ai image (optional for Docker demos)
- Valid API keys for LLM demos (optional)
"""
import asyncio
import requests
import time
import sys
from crawl4ai import (AsyncWebCrawler, CrawlerRunConfig, BrowserConfig,
CacheMode, FilterChain, URLPatternFilter, BFSDeepCrawlStrategy,
hooks_to_string)
from crawl4ai.docker_client import Crawl4aiDockerClient
def print_section(title: str, description: str = ""):
"""Print a section header"""
print(f"\n{'=' * 60}")
print(f"{title}")
if description:
print(f"{description}")
print(f"{'=' * 60}\n")
async def demo_1_docker_hooks_system():
"""Demo 1: Docker Hooks System - Real API calls with custom hooks"""
print_section(
"Demo 1: Docker Hooks System",
"Testing both string-based and function-based hooks (NEW in v0.7.5!)"
)
# Check Docker service availability
def check_docker_service():
try:
response = requests.get("http://localhost:11235/", timeout=3)
return response.status_code == 200
except:
return False
print("Checking Docker service...")
docker_running = check_docker_service()
if not docker_running:
print("⚠️ Docker service not running on localhost:11235")
print("To test Docker hooks:")
print("1. Run: docker run -p 11235:11235 unclecode/crawl4ai:latest")
print("2. Wait for service to start")
print("3. Re-run this demo\n")
return
print("✓ Docker service detected!")
# ============================================================================
# PART 1: Traditional String-Based Hooks (Works with REST API)
# ============================================================================
print("\n" + "" * 60)
print("Part 1: String-Based Hooks (REST API)")
print("" * 60)
hooks_config_string = {
"on_page_context_created": """
async def hook(page, context, **kwargs):
print("[String Hook] Setting up page context")
await context.route("**/*.{png,jpg,jpeg,gif,webp}", lambda route: route.abort())
return page
""",
"before_retrieve_html": """
async def hook(page, context, **kwargs):
print("[String Hook] Before retrieving HTML")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(1000)
return page
"""
}
payload = {
"urls": ["https://httpbin.org/html"],
"hooks": {
"code": hooks_config_string,
"timeout": 30
}
}
print("🔧 Using string-based hooks for REST API...")
try:
start_time = time.time()
response = requests.post("http://localhost:11235/crawl", json=payload, timeout=60)
execution_time = time.time() - start_time
if response.status_code == 200:
result = response.json()
print(f"✅ String-based hooks executed in {execution_time:.2f}s")
if result.get('results') and result['results'][0].get('success'):
html_length = len(result['results'][0].get('html', ''))
print(f" 📄 HTML length: {html_length} characters")
else:
print(f"❌ Request failed: {response.status_code}")
except Exception as e:
print(f"❌ Error: {str(e)}")
# ============================================================================
# PART 2: NEW Function-Based Hooks with Docker Client (v0.7.5)
# ============================================================================
print("\n" + "" * 60)
print("Part 2: Function-Based Hooks with Docker Client (✨ NEW!)")
print("" * 60)
# Define hooks as regular Python functions
async def on_page_context_created_func(page, context, **kwargs):
"""Block images to speed up crawling"""
print("[Function Hook] Setting up page context")
await context.route("**/*.{png,jpg,jpeg,gif,webp}", lambda route: route.abort())
await page.set_viewport_size({"width": 1920, "height": 1080})
return page
async def before_goto_func(page, context, url, **kwargs):
"""Add custom headers before navigation"""
print(f"[Function Hook] About to navigate to {url}")
await page.set_extra_http_headers({
'X-Crawl4AI': 'v0.7.5-function-hooks',
'X-Test-Header': 'demo'
})
return page
async def before_retrieve_html_func(page, context, **kwargs):
"""Scroll to load lazy content"""
print("[Function Hook] Scrolling page for lazy-loaded content")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(500)
await page.evaluate("window.scrollTo(0, 0)")
return page
# Use the hooks_to_string utility (can be used standalone)
print("\n📦 Converting functions to strings with hooks_to_string()...")
hooks_as_strings = hooks_to_string({
"on_page_context_created": on_page_context_created_func,
"before_goto": before_goto_func,
"before_retrieve_html": before_retrieve_html_func
})
print(f" ✓ Converted {len(hooks_as_strings)} hooks to string format")
# OR use Docker Client which does conversion automatically!
print("\n🐳 Using Docker Client with automatic conversion...")
try:
client = Crawl4aiDockerClient(base_url="http://localhost:11235")
# Pass function objects directly - conversion happens automatically!
results = await client.crawl(
urls=["https://httpbin.org/html"],
hooks={
"on_page_context_created": on_page_context_created_func,
"before_goto": before_goto_func,
"before_retrieve_html": before_retrieve_html_func
},
hooks_timeout=30
)
if results and results.success:
print(f"✅ Function-based hooks executed successfully!")
print(f" 📄 HTML length: {len(results.html)} characters")
print(f" 🎯 URL: {results.url}")
else:
print("⚠️ Crawl completed but may have warnings")
except Exception as e:
print(f"❌ Docker client error: {str(e)}")
# Show the benefits
print("\n" + "=" * 60)
print("✨ Benefits of Function-Based Hooks:")
print("=" * 60)
print("✓ Full IDE support (autocomplete, syntax highlighting)")
print("✓ Type checking and linting")
print("✓ Easier to test and debug")
print("✓ Reusable across projects")
print("✓ Automatic conversion in Docker client")
print("=" * 60)
async def demo_2_enhanced_llm_integration():
"""Demo 2: Enhanced LLM Integration - Working LLM configurations"""
print_section(
"Demo 2: Enhanced LLM Integration",
"Testing custom LLM providers and configurations"
)
print("🤖 Testing Enhanced LLM Integration Features")
provider = "gemini/gemini-2.5-flash-lite"
payload = {
"url": "https://example.com",
"f": "llm",
"q": "Summarize this page in one sentence.",
"provider": provider, # Explicitly set provider
"temperature": 0.7
}
try:
response = requests.post(
"http://localhost:11235/md",
json=payload,
timeout=60
)
if response.status_code == 200:
result = response.json()
print(f"✓ Request successful with provider: {provider}")
print(f" - Response keys: {list(result.keys())}")
print(f" - Content length: {len(result.get('markdown', ''))} characters")
print(f" - Note: Actual LLM call may fail without valid API key")
else:
print(f"❌ Request failed: {response.status_code}")
print(f" - Response: {response.text[:500]}")
except Exception as e:
print(f"[red]Error: {e}[/]")
async def demo_3_https_preservation():
"""Demo 3: HTTPS Preservation - Live crawling with HTTPS maintenance"""
print_section(
"Demo 3: HTTPS Preservation",
"Testing HTTPS preservation for internal links"
)
print("🔒 Testing HTTPS Preservation Feature")
# Test with HTTPS preservation enabled
print("\nTest 1: HTTPS Preservation ENABLED")
url_filter = URLPatternFilter(
patterns=["^(https:\/\/)?quotes\.toscrape\.com(\/.*)?$"]
)
config = CrawlerRunConfig(
exclude_external_links=True,
stream=True,
verbose=False,
preserve_https_for_internal_links=True,
deep_crawl_strategy=BFSDeepCrawlStrategy(
max_depth=2,
max_pages=5,
filter_chain=FilterChain([url_filter])
)
)
test_url = "https://quotes.toscrape.com"
print(f"🎯 Testing URL: {test_url}")
async with AsyncWebCrawler() as crawler:
async for result in await crawler.arun(url=test_url, config=config):
print("✓ HTTPS Preservation Test Completed")
internal_links = [i['href'] for i in result.links['internal']]
for link in internal_links:
print(f"{link}")
async def main():
"""Run all demos"""
print("\n" + "=" * 60)
print("🚀 Crawl4AI v0.7.5 Working Demo")
print("=" * 60)
# Check system requirements
print("🔍 System Requirements Check:")
print(f" - Python version: {sys.version.split()[0]} {'' if sys.version_info >= (3, 10) else '❌ (3.10+ required)'}")
try:
import requests
print(f" - Requests library: ✓")
except ImportError:
print(f" - Requests library: ❌")
print()
demos = [
("Docker Hooks System", demo_1_docker_hooks_system),
("Enhanced LLM Integration", demo_2_enhanced_llm_integration),
("HTTPS Preservation", demo_3_https_preservation),
]
for i, (name, demo_func) in enumerate(demos, 1):
try:
print(f"\n📍 Starting Demo {i}/{len(demos)}: {name}")
await demo_func()
if i < len(demos):
print(f"\n✨ Demo {i} complete! Press Enter for next demo...")
input()
except KeyboardInterrupt:
print(f"\n⏹️ Demo interrupted by user")
break
except Exception as e:
print(f"❌ Demo {i} error: {str(e)}")
print("Continuing to next demo...")
continue
print("\n" + "=" * 60)
print("🎉 Demo Complete!")
print("=" * 60)
print("You've experienced the power of Crawl4AI v0.7.5!")
print("")
print("Key Features Demonstrated:")
print("🔧 Docker Hooks - String-based & function-based (NEW!)")
print(" • hooks_to_string() utility for function conversion")
print(" • Docker client with automatic conversion")
print(" • Full IDE support and type checking")
print("🤖 Enhanced LLM - Better AI integration")
print("🔒 HTTPS Preservation - Secure link handling")
print("")
print("Ready to build something amazing? 🚀")
print("")
print("📖 Docs: https://docs.crawl4ai.com/")
print("🐙 GitHub: https://github.com/unclecode/crawl4ai")
print("=" * 60)
if __name__ == "__main__":
print("🚀 Crawl4AI v0.7.5 Live Demo Starting...")
print("Press Ctrl+C anytime to exit\n")
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n👋 Demo stopped by user. Thanks for trying Crawl4AI v0.7.5!")
except Exception as e:
print(f"\n❌ Demo error: {str(e)}")
print("Make sure you have the required dependencies installed.")
+359
View File
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.7.6 Release Demo
============================
This demo showcases the major feature in v0.7.6:
**Webhook Support for Docker Job Queue API**
Features Demonstrated:
1. Asynchronous job processing with webhook notifications
2. Webhook support for /crawl/job endpoint
3. Webhook support for /llm/job endpoint
4. Notification-only vs data-in-payload modes
5. Custom webhook headers for authentication
6. Structured extraction with JSON schemas
7. Exponential backoff retry for reliable delivery
Prerequisites:
- Crawl4AI Docker container running on localhost:11235
- Flask installed: pip install flask requests
- LLM API key configured (for LLM examples)
Usage:
python docs/releases_review/demo_v0.7.6.py
"""
import requests
import json
import time
from flask import Flask, request, jsonify
from threading import Thread
# Configuration
CRAWL4AI_BASE_URL = "http://localhost:11235"
WEBHOOK_BASE_URL = "http://localhost:8080"
# Flask app for webhook receiver
app = Flask(__name__)
received_webhooks = []
@app.route('/webhook', methods=['POST'])
def webhook_handler():
"""Universal webhook handler for both crawl and LLM extraction jobs."""
payload = request.json
task_id = payload['task_id']
task_type = payload['task_type']
status = payload['status']
print(f"\n{'='*70}")
print(f"📬 Webhook Received!")
print(f" Task ID: {task_id}")
print(f" Task Type: {task_type}")
print(f" Status: {status}")
print(f" Timestamp: {payload['timestamp']}")
if status == 'completed':
if 'data' in payload:
print(f" ✅ Data included in webhook")
if task_type == 'crawl':
results = payload['data'].get('results', [])
print(f" 📊 Crawled {len(results)} URL(s)")
elif task_type == 'llm_extraction':
extracted = payload['data'].get('extracted_content', {})
print(f" 🤖 Extracted: {json.dumps(extracted, indent=6)}")
else:
print(f" 📥 Notification only (fetch data separately)")
elif status == 'failed':
print(f" ❌ Error: {payload.get('error', 'Unknown')}")
print(f"{'='*70}\n")
received_webhooks.append(payload)
return jsonify({"status": "received"}), 200
def start_webhook_server():
"""Start Flask webhook server in background."""
app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)
def demo_1_crawl_webhook_notification_only():
"""Demo 1: Crawl job with webhook notification (data fetched separately)."""
print("\n" + "="*70)
print("DEMO 1: Crawl Job - Webhook Notification Only")
print("="*70)
print("Submitting crawl job with webhook notification...")
payload = {
"urls": ["https://example.com"],
"browser_config": {"headless": True},
"crawler_config": {"cache_mode": "bypass"},
"webhook_config": {
"webhook_url": f"{WEBHOOK_BASE_URL}/webhook",
"webhook_data_in_payload": False,
"webhook_headers": {
"X-Demo": "v0.7.6",
"X-Type": "crawl"
}
}
}
response = requests.post(f"{CRAWL4AI_BASE_URL}/crawl/job", json=payload)
if response.ok:
task_id = response.json()['task_id']
print(f"✅ Job submitted: {task_id}")
print("⏳ Webhook will notify when complete...")
return task_id
else:
print(f"❌ Failed: {response.text}")
return None
def demo_2_crawl_webhook_with_data():
"""Demo 2: Crawl job with full data in webhook payload."""
print("\n" + "="*70)
print("DEMO 2: Crawl Job - Webhook with Full Data")
print("="*70)
print("Submitting crawl job with data included in webhook...")
payload = {
"urls": ["https://www.python.org"],
"browser_config": {"headless": True},
"crawler_config": {"cache_mode": "bypass"},
"webhook_config": {
"webhook_url": f"{WEBHOOK_BASE_URL}/webhook",
"webhook_data_in_payload": True,
"webhook_headers": {
"X-Demo": "v0.7.6",
"X-Type": "crawl-with-data"
}
}
}
response = requests.post(f"{CRAWL4AI_BASE_URL}/crawl/job", json=payload)
if response.ok:
task_id = response.json()['task_id']
print(f"✅ Job submitted: {task_id}")
print("⏳ Webhook will include full results...")
return task_id
else:
print(f"❌ Failed: {response.text}")
return None
def demo_3_llm_webhook_notification_only():
"""Demo 3: LLM extraction with webhook notification (NEW in v0.7.6!)."""
print("\n" + "="*70)
print("DEMO 3: LLM Extraction - Webhook Notification Only (NEW!)")
print("="*70)
print("Submitting LLM extraction job with webhook notification...")
payload = {
"url": "https://www.example.com",
"q": "Extract the main heading and description from this page",
"provider": "openai/gpt-4o-mini",
"cache": False,
"webhook_config": {
"webhook_url": f"{WEBHOOK_BASE_URL}/webhook",
"webhook_data_in_payload": False,
"webhook_headers": {
"X-Demo": "v0.7.6",
"X-Type": "llm"
}
}
}
response = requests.post(f"{CRAWL4AI_BASE_URL}/llm/job", json=payload)
if response.ok:
task_id = response.json()['task_id']
print(f"✅ Job submitted: {task_id}")
print("⏳ Webhook will notify when LLM extraction completes...")
return task_id
else:
print(f"❌ Failed: {response.text}")
return None
def demo_4_llm_webhook_with_schema():
"""Demo 4: LLM extraction with JSON schema and data in webhook (NEW in v0.7.6!)."""
print("\n" + "="*70)
print("DEMO 4: LLM Extraction - Schema + Full Data in Webhook (NEW!)")
print("="*70)
print("Submitting LLM extraction with JSON schema...")
schema = {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Page title"},
"description": {"type": "string", "description": "Page description"},
"main_topics": {
"type": "array",
"items": {"type": "string"},
"description": "Main topics covered"
}
},
"required": ["title"]
}
payload = {
"url": "https://www.python.org",
"q": "Extract the title, description, and main topics from this website",
"schema": json.dumps(schema),
"provider": "openai/gpt-4o-mini",
"cache": False,
"webhook_config": {
"webhook_url": f"{WEBHOOK_BASE_URL}/webhook",
"webhook_data_in_payload": True,
"webhook_headers": {
"X-Demo": "v0.7.6",
"X-Type": "llm-with-schema"
}
}
}
response = requests.post(f"{CRAWL4AI_BASE_URL}/llm/job", json=payload)
if response.ok:
task_id = response.json()['task_id']
print(f"✅ Job submitted: {task_id}")
print("⏳ Webhook will include structured extraction results...")
return task_id
else:
print(f"❌ Failed: {response.text}")
return None
def demo_5_global_webhook_config():
"""Demo 5: Using global webhook configuration from config.yml."""
print("\n" + "="*70)
print("DEMO 5: Global Webhook Configuration")
print("="*70)
print("💡 You can configure a default webhook URL in config.yml:")
print("""
webhooks:
enabled: true
default_url: "https://myapp.com/webhooks/default"
data_in_payload: false
retry:
max_attempts: 5
initial_delay_ms: 1000
max_delay_ms: 32000
timeout_ms: 30000
""")
print("Then submit jobs WITHOUT webhook_config - they'll use the default!")
print("This is useful for consistent webhook handling across all jobs.")
def demo_6_webhook_retry_logic():
"""Demo 6: Webhook retry mechanism with exponential backoff."""
print("\n" + "="*70)
print("DEMO 6: Webhook Retry Logic")
print("="*70)
print("🔄 Webhook delivery uses exponential backoff retry:")
print(" • Max attempts: 5")
print(" • Delays: 1s → 2s → 4s → 8s → 16s")
print(" • Timeout: 30s per attempt")
print(" • Retries on: 5xx errors, network errors, timeouts")
print(" • No retry on: 4xx client errors")
print("\nThis ensures reliable webhook delivery even with temporary failures!")
def print_summary():
"""Print demo summary and results."""
print("\n" + "="*70)
print("📊 DEMO SUMMARY")
print("="*70)
print(f"Total webhooks received: {len(received_webhooks)}")
crawl_webhooks = [w for w in received_webhooks if w['task_type'] == 'crawl']
llm_webhooks = [w for w in received_webhooks if w['task_type'] == 'llm_extraction']
print(f"\nBreakdown:")
print(f" 🕷️ Crawl jobs: {len(crawl_webhooks)}")
print(f" 🤖 LLM extraction jobs: {len(llm_webhooks)}")
print(f"\nDetails:")
for i, webhook in enumerate(received_webhooks, 1):
icon = "🕷️" if webhook['task_type'] == 'crawl' else "🤖"
print(f" {i}. {icon} {webhook['task_id']}: {webhook['status']}")
print("\n" + "="*70)
print("✨ v0.7.6 KEY FEATURES DEMONSTRATED:")
print("="*70)
print("✅ Webhook support for /crawl/job")
print("✅ Webhook support for /llm/job (NEW!)")
print("✅ Notification-only mode (fetch data separately)")
print("✅ Data-in-payload mode (get full results in webhook)")
print("✅ Custom headers for authentication")
print("✅ JSON schema for structured LLM extraction")
print("✅ Exponential backoff retry for reliable delivery")
print("✅ Global webhook configuration support")
print("✅ Universal webhook handler for both job types")
print("\n💡 Benefits:")
print(" • No more polling - get instant notifications")
print(" • Better resource utilization")
print(" • Reliable delivery with automatic retries")
print(" • Consistent API across crawl and LLM jobs")
print(" • Production-ready webhook infrastructure")
def main():
"""Run all demos."""
print("\n" + "="*70)
print("🚀 Crawl4AI v0.7.6 Release Demo")
print("="*70)
print("Feature: Webhook Support for Docker Job Queue API")
print("="*70)
# Check if server is running
try:
health = requests.get(f"{CRAWL4AI_BASE_URL}/health", timeout=5)
print(f"✅ Crawl4AI server is running")
except:
print(f"❌ Cannot connect to Crawl4AI at {CRAWL4AI_BASE_URL}")
print("Please start Docker container:")
print(" docker run -d -p 11235:11235 --env-file .llm.env unclecode/crawl4ai:0.7.6")
return
# Start webhook server
print(f"\n🌐 Starting webhook server at {WEBHOOK_BASE_URL}...")
webhook_thread = Thread(target=start_webhook_server, daemon=True)
webhook_thread.start()
time.sleep(2)
# Run demos
demo_1_crawl_webhook_notification_only()
time.sleep(5)
demo_2_crawl_webhook_with_data()
time.sleep(5)
demo_3_llm_webhook_notification_only()
time.sleep(5)
demo_4_llm_webhook_with_schema()
time.sleep(5)
demo_5_global_webhook_config()
demo_6_webhook_retry_logic()
# Wait for webhooks
print("\n⏳ Waiting for all webhooks to arrive...")
time.sleep(30)
# Print summary
print_summary()
print("\n" + "="*70)
print("✅ Demo completed!")
print("="*70)
print("\n📚 Documentation:")
print(" • deploy/docker/WEBHOOK_EXAMPLES.md")
print(" • docs/examples/docker_webhook_example.py")
print("\n🔗 Upgrade:")
print(" docker pull unclecode/crawl4ai:0.7.6")
if __name__ == "__main__":
main()
+628
View File
@@ -0,0 +1,628 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.7.7 Release Demo
============================
This demo showcases the major feature in v0.7.7:
**Self-Hosting with Real-time Monitoring Dashboard**
Features Demonstrated:
1. System health monitoring with live metrics
2. Real-time request tracking (active & completed)
3. Browser pool management (permanent/hot/cold pools)
4. Monitor API endpoints for programmatic access
5. WebSocket streaming for real-time updates
6. Control actions (kill browser, cleanup, restart)
7. Production metrics (efficiency, reuse rates, memory)
Prerequisites:
- Crawl4AI Docker container running on localhost:11235
- Python packages: pip install httpx websockets
Usage:
python docs/releases_review/demo_v0.7.7.py
"""
import asyncio
import httpx
import json
import time
from datetime import datetime
from typing import Dict, Any
# Configuration
CRAWL4AI_BASE_URL = "http://localhost:11235"
MONITOR_DASHBOARD_URL = f"{CRAWL4AI_BASE_URL}/dashboard"
def print_section(title: str, description: str = ""):
"""Print a formatted section header"""
print(f"\n{'=' * 70}")
print(f"📊 {title}")
if description:
print(f"{description}")
print(f"{'=' * 70}\n")
def print_subsection(title: str):
"""Print a formatted subsection header"""
print(f"\n{'-' * 70}")
print(f"{title}")
print(f"{'-' * 70}")
async def check_server_health():
"""Check if Crawl4AI server is running"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{CRAWL4AI_BASE_URL}/health")
return response.status_code == 200
except:
return False
async def demo_1_system_health_overview():
"""Demo 1: System Health Overview - Live metrics and pool status"""
print_section(
"Demo 1: System Health Overview",
"Real-time monitoring of system resources and browser pool"
)
async with httpx.AsyncClient(timeout=30.0) as client:
print("🔍 Fetching system health metrics...")
try:
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/health")
health = response.json()
print("\n✅ System Health Report:")
print(f"\n🖥️ Container Metrics:")
print(f" • CPU Usage: {health['container']['cpu_percent']:.1f}%")
print(f" • Memory Usage: {health['container']['memory_percent']:.1f}% "
f"({health['container']['memory_mb']:.0f} MB)")
print(f" • Network RX: {health['container']['network_rx_mb']:.2f} MB")
print(f" • Network TX: {health['container']['network_tx_mb']:.2f} MB")
print(f" • Uptime: {health['container']['uptime_seconds']:.0f}s")
print(f"\n🌐 Browser Pool Status:")
print(f" Permanent Browser:")
print(f" • Active: {health['pool']['permanent']['active']}")
print(f" • Total Requests: {health['pool']['permanent']['total_requests']}")
print(f" Hot Pool (Frequently Used Configs):")
print(f" • Count: {health['pool']['hot']['count']}")
print(f" • Total Requests: {health['pool']['hot']['total_requests']}")
print(f" Cold Pool (On-Demand Configs):")
print(f" • Count: {health['pool']['cold']['count']}")
print(f" • Total Requests: {health['pool']['cold']['total_requests']}")
print(f"\n📈 Overall Statistics:")
print(f" • Total Requests: {health['stats']['total_requests']}")
print(f" • Success Rate: {health['stats']['success_rate_percent']:.1f}%")
print(f" • Avg Latency: {health['stats']['avg_latency_ms']:.0f}ms")
print(f"\n💡 Dashboard URL: {MONITOR_DASHBOARD_URL}")
except Exception as e:
print(f"❌ Error fetching health: {e}")
async def demo_2_request_tracking():
"""Demo 2: Real-time Request Tracking - Generate and monitor requests"""
print_section(
"Demo 2: Real-time Request Tracking",
"Submit crawl jobs and watch them in real-time"
)
async with httpx.AsyncClient(timeout=60.0) as client:
print("🚀 Submitting crawl requests...")
# Submit multiple requests
urls_to_crawl = [
"https://httpbin.org/html",
"https://httpbin.org/json",
"https://example.com"
]
tasks = []
for url in urls_to_crawl:
task = client.post(
f"{CRAWL4AI_BASE_URL}/crawl",
json={"urls": [url], "crawler_config": {}}
)
tasks.append(task)
print(f" • Submitting {len(urls_to_crawl)} requests in parallel...")
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = sum(1 for r in results if not isinstance(r, Exception) and r.status_code == 200)
print(f"{successful}/{len(urls_to_crawl)} requests submitted")
# Check request tracking
print("\n📊 Checking request tracking...")
await asyncio.sleep(2) # Wait for requests to process
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/requests")
requests_data = response.json()
print(f"\n📋 Request Status:")
print(f" • Active Requests: {len(requests_data['active'])}")
print(f" • Completed Requests: {len(requests_data['completed'])}")
if requests_data['completed']:
print(f"\n📝 Recent Completed Requests:")
for req in requests_data['completed'][:3]:
status_icon = "" if req['success'] else ""
print(f" {status_icon} {req['endpoint']} - {req['latency_ms']:.0f}ms")
async def demo_3_browser_pool_management():
"""Demo 3: Browser Pool Management - 3-tier architecture in action"""
print_section(
"Demo 3: Browser Pool Management",
"Understanding permanent, hot, and cold browser pools"
)
async with httpx.AsyncClient(timeout=60.0) as client:
print("🌊 Testing browser pool with different configurations...")
# Test 1: Default config (permanent browser)
print("\n🔥 Test 1: Default Config → Permanent Browser")
for i in range(3):
await client.post(
f"{CRAWL4AI_BASE_URL}/crawl",
json={"urls": [f"https://httpbin.org/html?req={i}"], "crawler_config": {}}
)
print(f" • Request {i+1}/3 sent (should use permanent browser)")
await asyncio.sleep(2)
# Test 2: Custom viewport (cold → hot promotion after 3 uses)
print("\n♨️ Test 2: Custom Viewport → Cold Pool (promoting to Hot)")
viewport_config = {"viewport": {"width": 1280, "height": 720}}
for i in range(4):
await client.post(
f"{CRAWL4AI_BASE_URL}/crawl",
json={
"urls": [f"https://httpbin.org/json?viewport={i}"],
"browser_config": viewport_config,
"crawler_config": {}
}
)
print(f" • Request {i+1}/4 sent (cold→hot promotion after 3rd use)")
await asyncio.sleep(2)
# Check browser pool status
print("\n📊 Browser Pool Report:")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/browsers")
browsers = response.json()
print(f"\n🎯 Pool Summary:")
print(f" • Total Browsers: {browsers['summary']['total_count']}")
print(f" • Total Memory: {browsers['summary']['total_memory_mb']} MB")
print(f" • Reuse Rate: {browsers['summary']['reuse_rate_percent']:.1f}%")
print(f"\n📋 Browser Pool Details:")
if browsers['permanent']:
for browser in browsers['permanent']:
print(f" 🔥 Permanent: {browser['browser_id'][:8]}... | "
f"Requests: {browser['request_count']} | "
f"Memory: {browser['memory_mb']:.0f} MB")
if browsers['hot']:
for browser in browsers['hot']:
print(f" ♨️ Hot: {browser['browser_id'][:8]}... | "
f"Requests: {browser['request_count']} | "
f"Memory: {browser['memory_mb']:.0f} MB")
if browsers['cold']:
for browser in browsers['cold']:
print(f" ❄️ Cold: {browser['browser_id'][:8]}... | "
f"Requests: {browser['request_count']} | "
f"Memory: {browser['memory_mb']:.0f} MB")
async def demo_4_monitor_api_endpoints():
"""Demo 4: Monitor API Endpoints - Complete API surface"""
print_section(
"Demo 4: Monitor API Endpoints",
"Programmatic access to all monitoring data"
)
async with httpx.AsyncClient(timeout=30.0) as client:
print("🔌 Testing Monitor API endpoints...")
# Endpoint performance statistics
print_subsection("Endpoint Performance Statistics")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/endpoints/stats")
endpoint_stats = response.json()
print("\n📊 Per-Endpoint Analytics:")
for endpoint, stats in endpoint_stats.items():
print(f" {endpoint}:")
print(f" • Requests: {stats['count']}")
print(f" • Avg Latency: {stats['avg_latency_ms']:.0f}ms")
print(f" • Success Rate: {stats['success_rate_percent']:.1f}%")
# Timeline data for charts
print_subsection("Timeline Data (for Charts)")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/timeline?minutes=5")
timeline = response.json()
print(f"\n📈 Timeline Metrics (last 5 minutes):")
print(f" • Data Points: {len(timeline['memory'])}")
if timeline['memory']:
latest = timeline['memory'][-1]
print(f" • Latest Memory: {latest['value']:.1f}%")
print(f" • Timestamp: {latest['timestamp']}")
# Janitor logs
print_subsection("Janitor Cleanup Events")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/logs/janitor?limit=3")
janitor_logs = response.json()
print(f"\n🧹 Recent Cleanup Activities:")
if janitor_logs:
for log in janitor_logs[:3]:
print(f"{log['timestamp']}: {log['message']}")
else:
print(" (No cleanup events yet - janitor runs periodically)")
# Error logs
print_subsection("Error Monitoring")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/logs/errors?limit=3")
error_logs = response.json()
print(f"\n❌ Recent Errors:")
if error_logs:
for log in error_logs[:3]:
print(f"{log['timestamp']}: {log['error_type']}")
print(f" {log['message'][:100]}...")
else:
print(" ✅ No recent errors!")
async def demo_5_websocket_streaming():
"""Demo 5: WebSocket Streaming - Real-time updates"""
print_section(
"Demo 5: WebSocket Streaming",
"Live monitoring with 2-second update intervals"
)
print("⚡ WebSocket Streaming Demo")
print("\n💡 The monitoring dashboard uses WebSocket for real-time updates")
print(f" • Connection: ws://localhost:11235/monitor/ws")
print(f" • Update Interval: 2 seconds")
print(f" • Data: Health, requests, browsers, memory, errors")
print("\n📝 Sample WebSocket Integration Code:")
print("""
import websockets
import json
async def monitor_realtime():
uri = "ws://localhost:11235/monitor/ws"
async with websockets.connect(uri) as websocket:
while True:
data = await websocket.recv()
update = json.loads(data)
print(f"Memory: {update['health']['container']['memory_percent']:.1f}%")
print(f"Active Requests: {len(update['requests']['active'])}")
print(f"Browser Pool: {update['health']['pool']['permanent']['active']}")
""")
print("\n🌐 Open the dashboard to see WebSocket in action:")
print(f" {MONITOR_DASHBOARD_URL}")
async def demo_6_control_actions():
"""Demo 6: Control Actions - Manual browser management"""
print_section(
"Demo 6: Control Actions",
"Manual control over browser pool and cleanup"
)
async with httpx.AsyncClient(timeout=30.0) as client:
print("🎮 Testing control actions...")
# Force cleanup
print_subsection("Force Immediate Cleanup")
print("🧹 Triggering manual cleanup...")
try:
response = await client.post(f"{CRAWL4AI_BASE_URL}/monitor/actions/cleanup")
if response.status_code == 200:
result = response.json()
print(f" ✅ Cleanup completed")
print(f" • Browsers cleaned: {result.get('cleaned_count', 0)}")
print(f" • Memory freed: {result.get('memory_freed_mb', 0):.1f} MB")
else:
print(f" ⚠️ Response: {response.status_code}")
except Exception as e:
print(f" ️ Cleanup action: {e}")
# Get browser list for potential kill/restart
print_subsection("Browser Management")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/browsers")
browsers = response.json()
cold_browsers = browsers.get('cold', [])
if cold_browsers:
browser_id = cold_browsers[0]['browser_id']
print(f"\n🎯 Example: Kill specific browser")
print(f" POST /monitor/actions/kill_browser")
print(f" JSON: {{'browser_id': '{browser_id[:16]}...'}}")
print(f" → Kills the browser and frees resources")
print(f"\n🔄 Example: Restart browser")
print(f" POST /monitor/actions/restart_browser")
print(f" JSON: {{'browser_id': 'browser_id_here'}}")
print(f" → Restart a specific browser instance")
# Reset statistics
print_subsection("Reset Statistics")
print("📊 Statistics can be reset for fresh monitoring:")
print(f" POST /monitor/stats/reset")
print(f" → Clears all accumulated statistics")
async def demo_7_production_metrics():
"""Demo 7: Production Metrics - Key indicators for operations"""
print_section(
"Demo 7: Production Metrics",
"Critical metrics for production monitoring"
)
async with httpx.AsyncClient(timeout=30.0) as client:
print("📊 Key Production Metrics:")
# Overall health
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/health")
health = response.json()
# Browser efficiency
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/browsers")
browsers = response.json()
print("\n🎯 Critical Metrics to Track:")
print(f"\n1️⃣ Memory Usage Trends")
print(f" • Current: {health['container']['memory_percent']:.1f}%")
print(f" • Alert if: >80%")
print(f" • Action: Trigger cleanup or scale")
print(f"\n2️⃣ Request Success Rate")
print(f" • Current: {health['stats']['success_rate_percent']:.1f}%")
print(f" • Target: >95%")
print(f" • Alert if: <90%")
print(f"\n3️⃣ Average Latency")
print(f" • Current: {health['stats']['avg_latency_ms']:.0f}ms")
print(f" • Target: <2000ms")
print(f" • Alert if: >5000ms")
print(f"\n4️⃣ Browser Pool Efficiency")
print(f" • Reuse Rate: {browsers['summary']['reuse_rate_percent']:.1f}%")
print(f" • Target: >80%")
print(f" • Indicates: Effective browser pooling")
print(f"\n5️⃣ Total Browsers")
print(f" • Current: {browsers['summary']['total_count']}")
print(f" • Alert if: >20 (possible leak)")
print(f" • Check: Janitor is running correctly")
print(f"\n6️⃣ Error Frequency")
response = await client.get(f"{CRAWL4AI_BASE_URL}/monitor/logs/errors?limit=10")
errors = response.json()
print(f" • Recent Errors: {len(errors)}")
print(f" • Alert if: >10 in last hour")
print(f" • Action: Review error patterns")
print("\n💡 Integration Examples:")
print(" • Prometheus: Scrape /monitor/health")
print(" • Alerting: Monitor memory, success rate, latency")
print(" • Dashboards: WebSocket streaming to custom UI")
print(" • Log Aggregation: Collect /monitor/logs/* endpoints")
async def demo_8_self_hosting_value():
"""Demo 8: Self-Hosting Value Proposition"""
print_section(
"Demo 8: Why Self-Host Crawl4AI?",
"The value proposition of owning your infrastructure"
)
print("🎯 Self-Hosting Benefits:\n")
print("🔒 Data Privacy & Security")
print(" • Your data never leaves your infrastructure")
print(" • No third-party access to crawled content")
print(" • Keep sensitive workflows behind your firewall")
print("\n💰 Cost Control")
print(" • No per-request pricing or rate limits")
print(" • Predictable infrastructure costs")
print(" • Scale based on your actual needs")
print("\n🎯 Full Customization")
print(" • Complete control over browser configs")
print(" • Custom hooks and strategies")
print(" • Tailored monitoring and alerting")
print("\n📊 Complete Transparency")
print(" • Real-time monitoring dashboard")
print(" • Full visibility into system performance")
print(" • Detailed request and error tracking")
print("\n⚡ Performance & Flexibility")
print(" • Direct access, no network overhead")
print(" • Integrate with existing infrastructure")
print(" • Custom resource allocation")
print("\n🛡️ Enterprise-Grade Operations")
print(" • Prometheus integration ready")
print(" • WebSocket for real-time dashboards")
print(" • Full API for automation")
print(" • Manual controls for troubleshooting")
print(f"\n🌐 Get Started:")
print(f" docker pull unclecode/crawl4ai:0.7.7")
print(f" docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.7.7")
print(f" # Visit: {MONITOR_DASHBOARD_URL}")
def print_summary():
"""Print comprehensive demo summary"""
print("\n" + "=" * 70)
print("📊 DEMO SUMMARY - Crawl4AI v0.7.7")
print("=" * 70)
print("\n✨ Features Demonstrated:")
print("=" * 70)
print("✅ System Health Overview")
print(" → Real-time CPU, memory, network, and uptime monitoring")
print("\n✅ Request Tracking")
print(" → Active and completed request monitoring with full details")
print("\n✅ Browser Pool Management")
print(" → 3-tier architecture: Permanent, Hot, and Cold pools")
print(" → Automatic promotion and cleanup")
print("\n✅ Monitor API Endpoints")
print(" → Complete REST API for programmatic access")
print(" → Health, requests, browsers, timeline, logs, errors")
print("\n✅ WebSocket Streaming")
print(" → Real-time updates every 2 seconds")
print(" → Build custom dashboards with live data")
print("\n✅ Control Actions")
print(" → Manual browser management (kill, restart)")
print(" → Force cleanup and statistics reset")
print("\n✅ Production Metrics")
print(" → 6 critical metrics for operational excellence")
print(" → Prometheus integration patterns")
print("\n✅ Self-Hosting Value")
print(" → Data privacy, cost control, full customization")
print(" → Enterprise-grade transparency and control")
print("\n" + "=" * 70)
print("🎯 What's New in v0.7.7?")
print("=" * 70)
print("• 📊 Complete Real-time Monitoring System")
print("• 🌐 Interactive Web Dashboard (/dashboard)")
print("• 🔌 Comprehensive Monitor API")
print("• ⚡ WebSocket Streaming (2-second updates)")
print("• 🎮 Manual Control Actions")
print("• 📈 Production Integration Examples")
print("• 🏭 Prometheus, Alerting, Log Aggregation")
print("• 🔥 Smart Browser Pool (Permanent/Hot/Cold)")
print("• 🧹 Automatic Janitor Cleanup")
print("• 📋 Full Request & Error Tracking")
print("\n" + "=" * 70)
print("💡 Why This Matters")
print("=" * 70)
print("Before v0.7.7: Docker was just a containerized crawler")
print("After v0.7.7: Complete self-hosting platform with enterprise monitoring")
print("\nYou now have:")
print(" • Full visibility into what's happening inside")
print(" • Real-time operational dashboards")
print(" • Complete control over browser resources")
print(" • Production-ready observability")
print(" • Zero external dependencies")
print("\n" + "=" * 70)
print("📚 Next Steps")
print("=" * 70)
print(f"1. Open the dashboard: {MONITOR_DASHBOARD_URL}")
print("2. Read the docs: https://docs.crawl4ai.com/basic/self-hosting/")
print("3. Try the Monitor API endpoints yourself")
print("4. Set up Prometheus integration for production")
print("5. Build custom dashboards with WebSocket streaming")
print("\n" + "=" * 70)
print("🔗 Resources")
print("=" * 70)
print(f"• Dashboard: {MONITOR_DASHBOARD_URL}")
print(f"• Health API: {CRAWL4AI_BASE_URL}/monitor/health")
print(f"• Documentation: https://docs.crawl4ai.com/")
print(f"• GitHub: https://github.com/unclecode/crawl4ai")
print("\n" + "=" * 70)
print("🎉 You're now in control of your web crawling destiny!")
print("=" * 70)
async def main():
"""Run all demos"""
print("\n" + "=" * 70)
print("🚀 Crawl4AI v0.7.7 Release Demo")
print("=" * 70)
print("Feature: Self-Hosting with Real-time Monitoring Dashboard")
print("=" * 70)
# Check if server is running
print("\n🔍 Checking Crawl4AI server...")
server_running = await check_server_health()
if not server_running:
print(f"❌ Cannot connect to Crawl4AI at {CRAWL4AI_BASE_URL}")
print("\nPlease start the Docker container:")
print(" docker pull unclecode/crawl4ai:0.7.7")
print(" docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.7.7")
print("\nThen re-run this demo.")
return
print(f"✅ Crawl4AI server is running!")
print(f"📊 Dashboard available at: {MONITOR_DASHBOARD_URL}")
# Run all demos
demos = [
demo_1_system_health_overview,
demo_2_request_tracking,
demo_3_browser_pool_management,
demo_4_monitor_api_endpoints,
demo_5_websocket_streaming,
demo_6_control_actions,
demo_7_production_metrics,
demo_8_self_hosting_value,
]
for i, demo_func in enumerate(demos, 1):
try:
await demo_func()
if i < len(demos):
await asyncio.sleep(2) # Brief pause between demos
except KeyboardInterrupt:
print(f"\n\n⚠️ Demo interrupted by user")
return
except Exception as e:
print(f"\n❌ Demo {i} error: {e}")
print("Continuing to next demo...\n")
continue
# Print comprehensive summary
print_summary()
print("\n" + "=" * 70)
print("✅ Demo completed!")
print("=" * 70)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n👋 Demo stopped by user. Thanks for trying Crawl4AI v0.7.7!")
except Exception as e:
print(f"\n\n❌ Demo failed: {e}")
print("Make sure the Docker container is running:")
print(" docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.7.7")
+910
View File
@@ -0,0 +1,910 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.7.8 Release Demo - Verification Tests
==================================================
This demo ACTUALLY RUNS and VERIFIES the bug fixes in v0.7.8.
Each test executes real code and validates the fix is working.
Bug Fixes Verified:
1. ProxyConfig JSON serialization (#1629)
2. Configurable backoff parameters (#1269)
3. LLM Strategy input_format support (#1178)
4. Raw HTML URL variable (#1116)
5. Relative URLs after redirects (#1268)
6. pypdf migration (#1412)
7. Pydantic v2 ConfigDict (#678)
8. Docker ContentRelevanceFilter (#1642) - requires Docker
9. Docker .cache permissions (#1638) - requires Docker
10. AdaptiveCrawler query expansion (#1621) - requires LLM API key
11. Import statement formatting (#1181)
Usage:
python docs/releases_review/demo_v0.7.8.py
For Docker tests:
docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.7.8
python docs/releases_review/demo_v0.7.8.py
"""
import asyncio
import json
import sys
import warnings
import os
import tempfile
from typing import Tuple, Optional
from dataclasses import dataclass
# Test results tracking
@dataclass
class TestResult:
name: str
issue: str
passed: bool
message: str
skipped: bool = False
results: list[TestResult] = []
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f"{title}")
print(f"{'=' * 70}")
def print_test(name: str, issue: str):
print(f"\n[TEST] {name} ({issue})")
print("-" * 50)
def record_result(name: str, issue: str, passed: bool, message: str, skipped: bool = False):
results.append(TestResult(name, issue, passed, message, skipped))
if skipped:
print(f" SKIPPED: {message}")
elif passed:
print(f" PASSED: {message}")
else:
print(f" FAILED: {message}")
# =============================================================================
# TEST 1: ProxyConfig JSON Serialization (#1629)
# =============================================================================
async def test_proxy_config_serialization():
"""
Verify BrowserConfig.to_dict() properly serializes ProxyConfig to JSON.
BEFORE: ProxyConfig was included as object, causing JSON serialization to fail
AFTER: ProxyConfig.to_dict() is called, producing valid JSON
"""
print_test("ProxyConfig JSON Serialization", "#1629")
try:
from crawl4ai import BrowserConfig
from crawl4ai.async_configs import ProxyConfig
# Create config with ProxyConfig
proxy = ProxyConfig(
server="http://proxy.example.com:8080",
username="testuser",
password="testpass"
)
browser_config = BrowserConfig(headless=True, proxy_config=proxy)
# Test 1: to_dict() should return dict for proxy_config
config_dict = browser_config.to_dict()
proxy_dict = config_dict.get('proxy_config')
if not isinstance(proxy_dict, dict):
record_result("ProxyConfig Serialization", "#1629", False,
f"proxy_config is {type(proxy_dict)}, expected dict")
return
# Test 2: Should be JSON serializable
try:
json_str = json.dumps(config_dict)
json.loads(json_str) # Verify valid JSON
except (TypeError, json.JSONDecodeError) as e:
record_result("ProxyConfig Serialization", "#1629", False,
f"JSON serialization failed: {e}")
return
# Test 3: Verify proxy data is preserved
if proxy_dict.get('server') != "http://proxy.example.com:8080":
record_result("ProxyConfig Serialization", "#1629", False,
"Proxy server not preserved in serialization")
return
record_result("ProxyConfig Serialization", "#1629", True,
"BrowserConfig with ProxyConfig serializes to valid JSON")
except Exception as e:
record_result("ProxyConfig Serialization", "#1629", False, f"Exception: {e}")
# =============================================================================
# TEST 2: Configurable Backoff Parameters (#1269)
# =============================================================================
async def test_configurable_backoff():
"""
Verify LLMConfig accepts and stores backoff configuration parameters.
BEFORE: Backoff was hardcoded (delay=2, attempts=3, factor=2)
AFTER: LLMConfig accepts backoff_base_delay, backoff_max_attempts, backoff_exponential_factor
"""
print_test("Configurable Backoff Parameters", "#1269")
try:
from crawl4ai import LLMConfig
# Test 1: Default values
default_config = LLMConfig(provider="openai/gpt-4o-mini")
if default_config.backoff_base_delay != 2:
record_result("Configurable Backoff", "#1269", False,
f"Default base_delay is {default_config.backoff_base_delay}, expected 2")
return
if default_config.backoff_max_attempts != 3:
record_result("Configurable Backoff", "#1269", False,
f"Default max_attempts is {default_config.backoff_max_attempts}, expected 3")
return
if default_config.backoff_exponential_factor != 2:
record_result("Configurable Backoff", "#1269", False,
f"Default exponential_factor is {default_config.backoff_exponential_factor}, expected 2")
return
# Test 2: Custom values
custom_config = LLMConfig(
provider="openai/gpt-4o-mini",
backoff_base_delay=5,
backoff_max_attempts=10,
backoff_exponential_factor=3
)
if custom_config.backoff_base_delay != 5:
record_result("Configurable Backoff", "#1269", False,
f"Custom base_delay is {custom_config.backoff_base_delay}, expected 5")
return
if custom_config.backoff_max_attempts != 10:
record_result("Configurable Backoff", "#1269", False,
f"Custom max_attempts is {custom_config.backoff_max_attempts}, expected 10")
return
if custom_config.backoff_exponential_factor != 3:
record_result("Configurable Backoff", "#1269", False,
f"Custom exponential_factor is {custom_config.backoff_exponential_factor}, expected 3")
return
# Test 3: to_dict() includes backoff params
config_dict = custom_config.to_dict()
if 'backoff_base_delay' not in config_dict:
record_result("Configurable Backoff", "#1269", False,
"backoff_base_delay missing from to_dict()")
return
record_result("Configurable Backoff", "#1269", True,
"LLMConfig accepts and stores custom backoff parameters")
except Exception as e:
record_result("Configurable Backoff", "#1269", False, f"Exception: {e}")
# =============================================================================
# TEST 3: LLM Strategy Input Format (#1178)
# =============================================================================
async def test_llm_input_format():
"""
Verify LLMExtractionStrategy accepts input_format parameter.
BEFORE: Always used markdown input
AFTER: Supports "markdown", "html", "fit_markdown", "cleaned_html", "fit_html"
"""
print_test("LLM Strategy Input Format", "#1178")
try:
from crawl4ai import LLMExtractionStrategy, LLMConfig
llm_config = LLMConfig(provider="openai/gpt-4o-mini")
# Test 1: Default is markdown
default_strategy = LLMExtractionStrategy(
llm_config=llm_config,
instruction="Extract data"
)
if default_strategy.input_format != "markdown":
record_result("LLM Input Format", "#1178", False,
f"Default input_format is '{default_strategy.input_format}', expected 'markdown'")
return
# Test 2: Can set to html
html_strategy = LLMExtractionStrategy(
llm_config=llm_config,
instruction="Extract data",
input_format="html"
)
if html_strategy.input_format != "html":
record_result("LLM Input Format", "#1178", False,
f"HTML input_format is '{html_strategy.input_format}', expected 'html'")
return
# Test 3: Can set to fit_markdown
fit_strategy = LLMExtractionStrategy(
llm_config=llm_config,
instruction="Extract data",
input_format="fit_markdown"
)
if fit_strategy.input_format != "fit_markdown":
record_result("LLM Input Format", "#1178", False,
f"fit_markdown input_format is '{fit_strategy.input_format}'")
return
record_result("LLM Input Format", "#1178", True,
"LLMExtractionStrategy accepts all input_format options")
except Exception as e:
record_result("LLM Input Format", "#1178", False, f"Exception: {e}")
# =============================================================================
# TEST 4: Raw HTML URL Variable (#1116)
# =============================================================================
async def test_raw_html_url_variable():
"""
Verify that raw: prefix URLs pass "Raw HTML" to extraction strategy.
BEFORE: Entire HTML blob was passed as URL parameter
AFTER: "Raw HTML" string is passed as URL parameter
"""
print_test("Raw HTML URL Variable", "#1116")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.extraction_strategy import ExtractionStrategy
# Custom strategy to capture what URL is passed
class URLCapturingStrategy(ExtractionStrategy):
captured_url = None
def extract(self, url: str, html: str, *args, **kwargs):
URLCapturingStrategy.captured_url = url
return [{"content": "test"}]
html_content = "<html><body><h1>Test</h1></body></html>"
strategy = URLCapturingStrategy()
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url=f"raw:{html_content}",
config=CrawlerRunConfig(
extraction_strategy=strategy
)
)
captured = URLCapturingStrategy.captured_url
if captured is None:
record_result("Raw HTML URL Variable", "#1116", False,
"Extraction strategy was not called")
return
if captured == html_content or captured.startswith("<html"):
record_result("Raw HTML URL Variable", "#1116", False,
f"URL contains HTML content instead of 'Raw HTML': {captured[:50]}...")
return
if captured != "Raw HTML":
record_result("Raw HTML URL Variable", "#1116", False,
f"URL is '{captured}', expected 'Raw HTML'")
return
record_result("Raw HTML URL Variable", "#1116", True,
"Extraction strategy receives 'Raw HTML' as URL for raw: prefix")
except Exception as e:
record_result("Raw HTML URL Variable", "#1116", False, f"Exception: {e}")
# =============================================================================
# TEST 5: Relative URLs After Redirects (#1268)
# =============================================================================
async def test_redirect_url_handling():
"""
Verify that redirected_url reflects the final URL after JS navigation.
BEFORE: redirected_url was the original URL, not the final URL
AFTER: redirected_url is captured after JS execution completes
"""
print_test("Relative URLs After Redirects", "#1268")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
# Test with a URL that we know the final state of
# We'll use httpbin which doesn't redirect, but verify the mechanism works
test_url = "https://httpbin.org/html"
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url=test_url,
config=CrawlerRunConfig()
)
# Verify redirected_url is populated
if not result.redirected_url:
record_result("Redirect URL Handling", "#1268", False,
"redirected_url is empty")
return
# For non-redirecting URL, should match original or be the final URL
if not result.redirected_url.startswith("https://httpbin.org"):
record_result("Redirect URL Handling", "#1268", False,
f"redirected_url is unexpected: {result.redirected_url}")
return
# Verify links are present and resolved
if result.links:
# Check that internal links have full URLs
internal_links = result.links.get('internal', [])
external_links = result.links.get('external', [])
all_links = internal_links + external_links
for link in all_links[:5]: # Check first 5 links
href = link.get('href', '')
if href and not href.startswith(('http://', 'https://', 'mailto:', 'tel:', '#', 'javascript:')):
record_result("Redirect URL Handling", "#1268", False,
f"Link not resolved to absolute URL: {href}")
return
record_result("Redirect URL Handling", "#1268", True,
f"redirected_url correctly captured: {result.redirected_url}")
except Exception as e:
record_result("Redirect URL Handling", "#1268", False, f"Exception: {e}")
# =============================================================================
# TEST 6: pypdf Migration (#1412)
# =============================================================================
async def test_pypdf_migration():
"""
Verify pypdf is used instead of deprecated PyPDF2.
BEFORE: Used PyPDF2 (deprecated since 2022)
AFTER: Uses pypdf (actively maintained)
"""
print_test("pypdf Migration", "#1412")
try:
# Test 1: pypdf should be importable (if pdf extra is installed)
try:
import pypdf
pypdf_available = True
pypdf_version = pypdf.__version__
except ImportError:
pypdf_available = False
pypdf_version = None
# Test 2: PyPDF2 should NOT be imported by crawl4ai
# Check if the processor uses pypdf
try:
from crawl4ai.processors.pdf import processor
processor_source = open(processor.__file__).read()
uses_pypdf = 'from pypdf' in processor_source or 'import pypdf' in processor_source
uses_pypdf2 = 'from PyPDF2' in processor_source or 'import PyPDF2' in processor_source
if uses_pypdf2 and not uses_pypdf:
record_result("pypdf Migration", "#1412", False,
"PDF processor still uses PyPDF2")
return
if uses_pypdf:
record_result("pypdf Migration", "#1412", True,
f"PDF processor uses pypdf{' v' + pypdf_version if pypdf_version else ''}")
return
else:
record_result("pypdf Migration", "#1412", True,
"PDF processor found, pypdf dependency updated", skipped=not pypdf_available)
return
except ImportError:
# PDF processor not available
if pypdf_available:
record_result("pypdf Migration", "#1412", True,
f"pypdf v{pypdf_version} is installed (PDF processor not loaded)")
else:
record_result("pypdf Migration", "#1412", True,
"PDF support not installed (optional feature)", skipped=True)
return
except Exception as e:
record_result("pypdf Migration", "#1412", False, f"Exception: {e}")
# =============================================================================
# TEST 7: Pydantic v2 ConfigDict (#678)
# =============================================================================
async def test_pydantic_configdict():
"""
Verify no Pydantic deprecation warnings for Config class.
BEFORE: Used deprecated 'class Config' syntax
AFTER: Uses ConfigDict for Pydantic v2 compatibility
"""
print_test("Pydantic v2 ConfigDict", "#678")
try:
import pydantic
from pydantic import __version__ as pydantic_version
# Capture warnings during import
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always", DeprecationWarning)
# Import models that might have Config classes
from crawl4ai.models import CrawlResult, MarkdownGenerationResult
from crawl4ai.async_configs import CrawlerRunConfig, BrowserConfig
# Filter for Pydantic-related deprecation warnings
pydantic_warnings = [
warning for warning in w
if 'pydantic' in str(warning.message).lower()
or 'config' in str(warning.message).lower()
]
if pydantic_warnings:
warning_msgs = [str(w.message) for w in pydantic_warnings[:3]]
record_result("Pydantic ConfigDict", "#678", False,
f"Deprecation warnings: {warning_msgs}")
return
# Verify models work correctly
try:
# Test that models can be instantiated without issues
config = CrawlerRunConfig()
browser = BrowserConfig()
record_result("Pydantic ConfigDict", "#678", True,
f"No deprecation warnings with Pydantic v{pydantic_version}")
except Exception as e:
record_result("Pydantic ConfigDict", "#678", False,
f"Model instantiation failed: {e}")
except Exception as e:
record_result("Pydantic ConfigDict", "#678", False, f"Exception: {e}")
# =============================================================================
# TEST 8: Docker ContentRelevanceFilter (#1642)
# =============================================================================
async def test_docker_content_filter():
"""
Verify ContentRelevanceFilter deserializes correctly in Docker API.
BEFORE: Docker API failed to import/instantiate ContentRelevanceFilter
AFTER: Filter is properly exported and deserializable
"""
print_test("Docker ContentRelevanceFilter", "#1642")
# First verify the fix in local code
try:
# Test 1: ContentRelevanceFilter should be importable from crawl4ai
from crawl4ai import ContentRelevanceFilter
# Test 2: Should be instantiable
filter_instance = ContentRelevanceFilter(
query="test query",
threshold=0.3
)
if not hasattr(filter_instance, 'query'):
record_result("Docker ContentRelevanceFilter", "#1642", False,
"ContentRelevanceFilter missing query attribute")
return
except ImportError as e:
record_result("Docker ContentRelevanceFilter", "#1642", False,
f"ContentRelevanceFilter not exported: {e}")
return
except Exception as e:
record_result("Docker ContentRelevanceFilter", "#1642", False,
f"ContentRelevanceFilter instantiation failed: {e}")
return
# Test Docker API if available
try:
import httpx
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get("http://localhost:11235/health")
if response.status_code != 200:
raise Exception("Docker not available")
# Docker is running, test the API
async with httpx.AsyncClient(timeout=30.0) as client:
request = {
"urls": ["https://httpbin.org/html"],
"crawler_config": {
"deep_crawl_strategy": {
"type": "BFSDeepCrawlStrategy",
"max_depth": 1,
"filter_chain": [
{
"type": "ContentTypeFilter",
"allowed_types": ["text/html"]
}
]
}
}
}
response = await client.post(
"http://localhost:11235/crawl",
json=request
)
if response.status_code == 200:
record_result("Docker ContentRelevanceFilter", "#1642", True,
"Filter deserializes correctly in Docker API")
else:
record_result("Docker ContentRelevanceFilter", "#1642", False,
f"Docker API returned {response.status_code}: {response.text[:100]}")
except ImportError:
record_result("Docker ContentRelevanceFilter", "#1642", True,
"ContentRelevanceFilter exportable (Docker test skipped - httpx not installed)",
skipped=True)
except Exception as e:
record_result("Docker ContentRelevanceFilter", "#1642", True,
f"ContentRelevanceFilter exportable (Docker test skipped: {e})",
skipped=True)
# =============================================================================
# TEST 9: Docker Cache Permissions (#1638)
# =============================================================================
async def test_docker_cache_permissions():
"""
Verify Docker image has correct .cache folder permissions.
This test requires Docker container to be running.
"""
print_test("Docker Cache Permissions", "#1638")
try:
import httpx
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get("http://localhost:11235/health")
if response.status_code != 200:
raise Exception("Docker not available")
# Test by making a crawl request with caching
async with httpx.AsyncClient(timeout=60.0) as client:
request = {
"urls": ["https://httpbin.org/html"],
"crawler_config": {
"cache_mode": "enabled"
}
}
response = await client.post(
"http://localhost:11235/crawl",
json=request
)
if response.status_code == 200:
result = response.json()
# Check if there were permission errors
if "permission" in str(result).lower() and "denied" in str(result).lower():
record_result("Docker Cache Permissions", "#1638", False,
"Permission denied error in response")
else:
record_result("Docker Cache Permissions", "#1638", True,
"Crawl with caching succeeded in Docker")
else:
error_text = response.text[:200]
if "permission" in error_text.lower():
record_result("Docker Cache Permissions", "#1638", False,
f"Permission error: {error_text}")
else:
record_result("Docker Cache Permissions", "#1638", False,
f"Request failed: {response.status_code}")
except ImportError:
record_result("Docker Cache Permissions", "#1638", True,
"Skipped - httpx not installed", skipped=True)
except Exception as e:
record_result("Docker Cache Permissions", "#1638", True,
f"Skipped - Docker not available: {e}", skipped=True)
# =============================================================================
# TEST 10: AdaptiveCrawler Query Expansion (#1621)
# =============================================================================
async def test_adaptive_crawler_embedding():
"""
Verify EmbeddingStrategy LLM code is uncommented and functional.
BEFORE: LLM call was commented out, using hardcoded mock data
AFTER: Actually calls LLM for query expansion
"""
print_test("AdaptiveCrawler Query Expansion", "#1621")
try:
# Read the source file to verify the fix
import crawl4ai.adaptive_crawler as adaptive_module
source_file = adaptive_module.__file__
with open(source_file, 'r') as f:
source_code = f.read()
# Check that the LLM call is NOT commented out
# Look for the perform_completion_with_backoff call
# Find the EmbeddingStrategy section
if 'class EmbeddingStrategy' not in source_code:
record_result("AdaptiveCrawler Query Expansion", "#1621", True,
"EmbeddingStrategy not in adaptive_crawler (may have moved)",
skipped=True)
return
# Check if the mock data line is commented out
# and the actual LLM call is NOT commented out
lines = source_code.split('\n')
in_embedding_strategy = False
found_llm_call = False
mock_data_commented = False
for i, line in enumerate(lines):
if 'class EmbeddingStrategy' in line:
in_embedding_strategy = True
elif in_embedding_strategy and line.strip().startswith('class '):
in_embedding_strategy = False
if in_embedding_strategy:
# Check for uncommented LLM call
if 'perform_completion_with_backoff' in line and not line.strip().startswith('#'):
found_llm_call = True
# Check for commented mock data
if "variations ={'queries'" in line or 'variations = {\'queries\'' in line:
if line.strip().startswith('#'):
mock_data_commented = True
if found_llm_call:
record_result("AdaptiveCrawler Query Expansion", "#1621", True,
"LLM call is active in EmbeddingStrategy")
else:
# Check if the entire embedding strategy exists but might be structured differently
if 'perform_completion_with_backoff' in source_code:
record_result("AdaptiveCrawler Query Expansion", "#1621", True,
"perform_completion_with_backoff found in module")
else:
record_result("AdaptiveCrawler Query Expansion", "#1621", False,
"LLM call not found or still commented out")
except Exception as e:
record_result("AdaptiveCrawler Query Expansion", "#1621", False, f"Exception: {e}")
# =============================================================================
# TEST 11: Import Statement Formatting (#1181)
# =============================================================================
async def test_import_formatting():
"""
Verify code extraction properly formats import statements.
BEFORE: Import statements were concatenated without newlines
AFTER: Import statements have proper newline separation
"""
print_test("Import Statement Formatting", "#1181")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
# Create HTML with code containing imports
html_with_code = """
<html>
<body>
<pre><code>
import os
import sys
from pathlib import Path
from typing import List, Dict
def main():
pass
</code></pre>
</body>
</html>
"""
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url=f"raw:{html_with_code}",
config=CrawlerRunConfig()
)
markdown = result.markdown.raw_markdown if result.markdown else ""
# Check that imports are not concatenated on the same line
# Bad: "import osimport sys" (no newline between statements)
# This is the actual bug - statements getting merged on same line
bad_patterns = [
"import os import sys", # Space but no newline
"import osimport sys", # No space or newline
"import os from pathlib", # Space but no newline
"import osfrom pathlib", # No space or newline
]
markdown_single_line = markdown.replace('\n', ' ') # Convert newlines to spaces
for pattern in bad_patterns:
# Check if pattern exists without proper line separation
if pattern.replace(' ', '') in markdown_single_line.replace(' ', ''):
# Verify it's actually on same line (not just adjacent after newline removal)
lines = markdown.split('\n')
for line in lines:
if 'import' in line.lower():
# Count import statements on this line
import_count = line.lower().count('import ')
if import_count > 1:
record_result("Import Formatting", "#1181", False,
f"Multiple imports on same line: {line[:60]}...")
return
# Verify imports are present
if "import" in markdown.lower():
record_result("Import Formatting", "#1181", True,
"Import statements are properly line-separated")
else:
record_result("Import Formatting", "#1181", True,
"No import statements found to verify (test HTML may have changed)")
except Exception as e:
record_result("Import Formatting", "#1181", False, f"Exception: {e}")
# =============================================================================
# COMPREHENSIVE CRAWL TEST
# =============================================================================
async def test_comprehensive_crawl():
"""
Run a comprehensive crawl to verify overall stability.
"""
print_test("Comprehensive Crawl Test", "Overall")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
async with AsyncWebCrawler(config=BrowserConfig(headless=True)) as crawler:
result = await crawler.arun(
url="https://httpbin.org/html",
config=CrawlerRunConfig()
)
# Verify result
checks = []
if result.success:
checks.append("success=True")
else:
record_result("Comprehensive Crawl", "Overall", False,
f"Crawl failed: {result.error_message}")
return
if result.html and len(result.html) > 100:
checks.append(f"html={len(result.html)} chars")
if result.markdown and result.markdown.raw_markdown:
checks.append(f"markdown={len(result.markdown.raw_markdown)} chars")
if result.redirected_url:
checks.append("redirected_url present")
record_result("Comprehensive Crawl", "Overall", True,
f"All checks passed: {', '.join(checks)}")
except Exception as e:
record_result("Comprehensive Crawl", "Overall", False, f"Exception: {e}")
# =============================================================================
# MAIN
# =============================================================================
def print_summary():
"""Print test results summary"""
print_header("TEST RESULTS SUMMARY")
passed = sum(1 for r in results if r.passed and not r.skipped)
failed = sum(1 for r in results if not r.passed and not r.skipped)
skipped = sum(1 for r in results if r.skipped)
print(f"\nTotal: {len(results)} tests")
print(f" Passed: {passed}")
print(f" Failed: {failed}")
print(f" Skipped: {skipped}")
if failed > 0:
print("\nFailed Tests:")
for r in results:
if not r.passed and not r.skipped:
print(f" - {r.name} ({r.issue}): {r.message}")
if skipped > 0:
print("\nSkipped Tests:")
for r in results:
if r.skipped:
print(f" - {r.name} ({r.issue}): {r.message}")
print("\n" + "=" * 70)
if failed == 0:
print("All tests passed! v0.7.8 bug fixes verified.")
else:
print(f"WARNING: {failed} test(s) failed!")
print("=" * 70)
return failed == 0
async def main():
"""Run all verification tests"""
print_header("Crawl4AI v0.7.8 - Bug Fix Verification Tests")
print("Running actual tests to verify bug fixes...")
# Run all tests
tests = [
test_proxy_config_serialization, # #1629
test_configurable_backoff, # #1269
test_llm_input_format, # #1178
test_raw_html_url_variable, # #1116
test_redirect_url_handling, # #1268
test_pypdf_migration, # #1412
test_pydantic_configdict, # #678
test_docker_content_filter, # #1642
test_docker_cache_permissions, # #1638
test_adaptive_crawler_embedding, # #1621
test_import_formatting, # #1181
test_comprehensive_crawl, # Overall
]
for test_func in tests:
try:
await test_func()
except Exception as e:
print(f"\nTest {test_func.__name__} crashed: {e}")
results.append(TestResult(
test_func.__name__,
"Unknown",
False,
f"Crashed: {e}"
))
# Print summary
all_passed = print_summary()
return 0 if all_passed else 1
if __name__ == "__main__":
try:
exit_code = asyncio.run(main())
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\nTests interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\n\nTest suite failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+633
View File
@@ -0,0 +1,633 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.8.0 Release Demo - Feature Verification Tests
==========================================================
This demo ACTUALLY RUNS and VERIFIES the new features in v0.8.0.
Each test executes real code and validates the feature is working.
New Features Verified:
1. Crash Recovery - on_state_change callback for real-time state persistence
2. Crash Recovery - resume_state for resuming from checkpoint
3. Crash Recovery - State is JSON serializable
4. Prefetch Mode - Returns HTML and links only
5. Prefetch Mode - Skips heavy processing (markdown, extraction)
6. Prefetch Mode - Two-phase crawl pattern
7. Security - Hooks disabled by default (Docker API)
Breaking Changes in v0.8.0:
- Docker API hooks disabled by default (CRAWL4AI_HOOKS_ENABLED=false)
- file:// URLs blocked on Docker API endpoints
Usage:
python docs/releases_review/demo_v0.8.0.py
"""
import asyncio
import json
import sys
import time
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
# Test results tracking
@dataclass
class TestResult:
name: str
feature: str
passed: bool
message: str
skipped: bool = False
results: list[TestResult] = []
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f"{title}")
print(f"{'=' * 70}")
def print_test(name: str, feature: str):
print(f"\n[TEST] {name} ({feature})")
print("-" * 50)
def record_result(name: str, feature: str, passed: bool, message: str, skipped: bool = False):
results.append(TestResult(name, feature, passed, message, skipped))
if skipped:
print(f" SKIPPED: {message}")
elif passed:
print(f" PASSED: {message}")
else:
print(f" FAILED: {message}")
# =============================================================================
# TEST 1: Crash Recovery - State Capture with on_state_change
# =============================================================================
async def test_crash_recovery_state_capture():
"""
Verify on_state_change callback is called after each URL is processed.
NEW in v0.8.0: Deep crawl strategies support on_state_change callback
for real-time state persistence (useful for cloud deployments).
"""
print_test("Crash Recovery - State Capture", "on_state_change")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
captured_states: List[Dict[str, Any]] = []
async def capture_state(state: Dict[str, Any]):
"""Callback that fires after each URL is processed."""
captured_states.append(state.copy())
strategy = BFSDeepCrawlStrategy(
max_depth=1,
max_pages=3,
on_state_change=capture_state,
)
config = CrawlerRunConfig(
deep_crawl_strategy=strategy,
verbose=False,
)
async with AsyncWebCrawler(verbose=False) as crawler:
await crawler.arun("https://books.toscrape.com", config=config)
# Verify states were captured
if len(captured_states) == 0:
record_result("State Capture", "on_state_change", False,
"No states captured - callback not called")
return
# Verify callback was called for each page
pages_crawled = captured_states[-1].get("pages_crawled", 0)
if pages_crawled != len(captured_states):
record_result("State Capture", "on_state_change", False,
f"Callback count {len(captured_states)} != pages_crawled {pages_crawled}")
return
record_result("State Capture", "on_state_change", True,
f"Callback fired {len(captured_states)} times (once per URL)")
except Exception as e:
record_result("State Capture", "on_state_change", False, f"Exception: {e}")
# =============================================================================
# TEST 2: Crash Recovery - Resume from Checkpoint
# =============================================================================
async def test_crash_recovery_resume():
"""
Verify crawl can resume from a saved checkpoint without re-crawling visited URLs.
NEW in v0.8.0: BFSDeepCrawlStrategy accepts resume_state parameter
to continue from a previously saved checkpoint.
"""
print_test("Crash Recovery - Resume from Checkpoint", "resume_state")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
# Phase 1: Start crawl and capture state after 2 pages
crash_after = 2
captured_states: List[Dict] = []
phase1_urls: List[str] = []
async def capture_until_crash(state: Dict[str, Any]):
captured_states.append(state.copy())
phase1_urls.clear()
phase1_urls.extend(state["visited"])
if state["pages_crawled"] >= crash_after:
raise Exception("Simulated crash")
strategy1 = BFSDeepCrawlStrategy(
max_depth=1,
max_pages=5,
on_state_change=capture_until_crash,
)
config1 = CrawlerRunConfig(
deep_crawl_strategy=strategy1,
verbose=False,
)
# Run until "crash"
try:
async with AsyncWebCrawler(verbose=False) as crawler:
await crawler.arun("https://books.toscrape.com", config=config1)
except Exception:
pass # Expected crash
if not captured_states:
record_result("Resume from Checkpoint", "resume_state", False,
"No state captured before crash")
return
saved_state = captured_states[-1]
print(f" Phase 1: Crawled {len(phase1_urls)} URLs before crash")
# Phase 2: Resume from checkpoint
phase2_urls: List[str] = []
async def track_phase2(state: Dict[str, Any]):
new_urls = set(state["visited"]) - set(saved_state["visited"])
for url in new_urls:
if url not in phase2_urls:
phase2_urls.append(url)
strategy2 = BFSDeepCrawlStrategy(
max_depth=1,
max_pages=5,
resume_state=saved_state, # Resume from checkpoint!
on_state_change=track_phase2,
)
config2 = CrawlerRunConfig(
deep_crawl_strategy=strategy2,
verbose=False,
)
async with AsyncWebCrawler(verbose=False) as crawler:
await crawler.arun("https://books.toscrape.com", config=config2)
print(f" Phase 2: Crawled {len(phase2_urls)} new URLs after resume")
# Verify no duplicates
duplicates = set(phase2_urls) & set(phase1_urls)
if duplicates:
record_result("Resume from Checkpoint", "resume_state", False,
f"Re-crawled {len(duplicates)} URLs: {list(duplicates)[:2]}")
return
record_result("Resume from Checkpoint", "resume_state", True,
f"Resumed successfully, no duplicate crawls")
except Exception as e:
record_result("Resume from Checkpoint", "resume_state", False, f"Exception: {e}")
# =============================================================================
# TEST 3: Crash Recovery - State is JSON Serializable
# =============================================================================
async def test_crash_recovery_json_serializable():
"""
Verify the state dictionary can be serialized to JSON (for Redis/DB storage).
NEW in v0.8.0: State dictionary is designed to be JSON-serializable
for easy storage in Redis, databases, or files.
"""
print_test("Crash Recovery - JSON Serializable", "State Structure")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
captured_state: Optional[Dict] = None
async def capture_state(state: Dict[str, Any]):
nonlocal captured_state
captured_state = state
strategy = BFSDeepCrawlStrategy(
max_depth=1,
max_pages=2,
on_state_change=capture_state,
)
config = CrawlerRunConfig(
deep_crawl_strategy=strategy,
verbose=False,
)
async with AsyncWebCrawler(verbose=False) as crawler:
await crawler.arun("https://books.toscrape.com", config=config)
if not captured_state:
record_result("JSON Serializable", "State Structure", False,
"No state captured")
return
# Test JSON serialization round-trip
try:
json_str = json.dumps(captured_state)
restored = json.loads(json_str)
except (TypeError, json.JSONDecodeError) as e:
record_result("JSON Serializable", "State Structure", False,
f"JSON serialization failed: {e}")
return
# Verify state structure
required_fields = ["strategy_type", "visited", "pending", "depths", "pages_crawled"]
missing = [f for f in required_fields if f not in restored]
if missing:
record_result("JSON Serializable", "State Structure", False,
f"Missing fields: {missing}")
return
# Verify types
if not isinstance(restored["visited"], list):
record_result("JSON Serializable", "State Structure", False,
"visited is not a list")
return
if not isinstance(restored["pages_crawled"], int):
record_result("JSON Serializable", "State Structure", False,
"pages_crawled is not an int")
return
record_result("JSON Serializable", "State Structure", True,
f"State serializes to {len(json_str)} bytes, all fields present")
except Exception as e:
record_result("JSON Serializable", "State Structure", False, f"Exception: {e}")
# =============================================================================
# TEST 4: Prefetch Mode - Returns HTML and Links Only
# =============================================================================
async def test_prefetch_returns_html_links():
"""
Verify prefetch mode returns HTML and links but skips markdown generation.
NEW in v0.8.0: CrawlerRunConfig accepts prefetch=True for fast
URL discovery without heavy processing.
"""
print_test("Prefetch Mode - HTML and Links", "prefetch=True")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
config = CrawlerRunConfig(prefetch=True)
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun("https://books.toscrape.com", config=config)
# Verify HTML is present
if not result.html or len(result.html) < 100:
record_result("Prefetch HTML/Links", "prefetch=True", False,
"HTML not returned or too short")
return
# Verify links are present
if not result.links:
record_result("Prefetch HTML/Links", "prefetch=True", False,
"Links not returned")
return
internal_count = len(result.links.get("internal", []))
external_count = len(result.links.get("external", []))
if internal_count == 0:
record_result("Prefetch HTML/Links", "prefetch=True", False,
"No internal links extracted")
return
record_result("Prefetch HTML/Links", "prefetch=True", True,
f"HTML: {len(result.html)} chars, Links: {internal_count} internal, {external_count} external")
except Exception as e:
record_result("Prefetch HTML/Links", "prefetch=True", False, f"Exception: {e}")
# =============================================================================
# TEST 5: Prefetch Mode - Skips Heavy Processing
# =============================================================================
async def test_prefetch_skips_processing():
"""
Verify prefetch mode skips markdown generation and content extraction.
NEW in v0.8.0: prefetch=True skips markdown generation, content scraping,
media extraction, and LLM extraction for maximum speed.
"""
print_test("Prefetch Mode - Skips Processing", "prefetch=True")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
config = CrawlerRunConfig(prefetch=True)
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun("https://books.toscrape.com", config=config)
# Check that heavy processing was skipped
checks = []
# Markdown should be None or empty
if result.markdown is None:
checks.append("markdown=None")
elif hasattr(result.markdown, 'raw_markdown') and result.markdown.raw_markdown is None:
checks.append("raw_markdown=None")
else:
record_result("Prefetch Skips Processing", "prefetch=True", False,
f"Markdown was generated (should be skipped)")
return
# cleaned_html should be None
if result.cleaned_html is None:
checks.append("cleaned_html=None")
else:
record_result("Prefetch Skips Processing", "prefetch=True", False,
"cleaned_html was generated (should be skipped)")
return
# extracted_content should be None
if result.extracted_content is None:
checks.append("extracted_content=None")
record_result("Prefetch Skips Processing", "prefetch=True", True,
f"Heavy processing skipped: {', '.join(checks)}")
except Exception as e:
record_result("Prefetch Skips Processing", "prefetch=True", False, f"Exception: {e}")
# =============================================================================
# TEST 6: Prefetch Mode - Two-Phase Crawl Pattern
# =============================================================================
async def test_prefetch_two_phase():
"""
Verify the two-phase crawl pattern: prefetch for discovery, then full processing.
NEW in v0.8.0: Prefetch mode enables efficient two-phase crawling where
you discover URLs quickly, then selectively process important ones.
"""
print_test("Prefetch Mode - Two-Phase Crawl", "Two-Phase Pattern")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
async with AsyncWebCrawler(verbose=False) as crawler:
# Phase 1: Fast discovery with prefetch
prefetch_config = CrawlerRunConfig(prefetch=True)
start = time.time()
discovery = await crawler.arun("https://books.toscrape.com", config=prefetch_config)
prefetch_time = time.time() - start
all_urls = [link["href"] for link in discovery.links.get("internal", [])]
# Filter to specific pages (e.g., book detail pages)
book_urls = [
url for url in all_urls
if "catalogue/" in url and "category/" not in url
][:2] # Just 2 for demo
print(f" Phase 1: Found {len(all_urls)} URLs in {prefetch_time:.2f}s")
print(f" Filtered to {len(book_urls)} book pages for full processing")
if len(book_urls) == 0:
record_result("Two-Phase Crawl", "Two-Phase Pattern", False,
"No book URLs found to process")
return
# Phase 2: Full processing on selected URLs
full_config = CrawlerRunConfig() # Normal mode
start = time.time()
processed = []
for url in book_urls:
result = await crawler.arun(url, config=full_config)
if result.success and result.markdown:
processed.append(result)
full_time = time.time() - start
print(f" Phase 2: Processed {len(processed)} pages in {full_time:.2f}s")
if len(processed) == 0:
record_result("Two-Phase Crawl", "Two-Phase Pattern", False,
"No pages successfully processed in phase 2")
return
# Verify full processing includes markdown
if not processed[0].markdown or not processed[0].markdown.raw_markdown:
record_result("Two-Phase Crawl", "Two-Phase Pattern", False,
"Full processing did not generate markdown")
return
record_result("Two-Phase Crawl", "Two-Phase Pattern", True,
f"Discovered {len(all_urls)} URLs (prefetch), processed {len(processed)} (full)")
except Exception as e:
record_result("Two-Phase Crawl", "Two-Phase Pattern", False, f"Exception: {e}")
# =============================================================================
# TEST 7: Security - Hooks Disabled by Default
# =============================================================================
async def test_security_hooks_disabled():
"""
Verify hooks are disabled by default in Docker API for security.
NEW in v0.8.0: Docker API hooks are disabled by default to prevent
Remote Code Execution. Set CRAWL4AI_HOOKS_ENABLED=true to enable.
"""
print_test("Security - Hooks Disabled", "CRAWL4AI_HOOKS_ENABLED")
try:
import os
# Check the default environment variable
hooks_enabled = os.environ.get("CRAWL4AI_HOOKS_ENABLED", "false").lower()
if hooks_enabled == "true":
record_result("Hooks Disabled Default", "Security", True,
"CRAWL4AI_HOOKS_ENABLED is explicitly set to 'true' (user override)",
skipped=True)
return
# Verify default is "false"
if hooks_enabled == "false":
record_result("Hooks Disabled Default", "Security", True,
"Hooks disabled by default (CRAWL4AI_HOOKS_ENABLED=false)")
else:
record_result("Hooks Disabled Default", "Security", True,
f"CRAWL4AI_HOOKS_ENABLED='{hooks_enabled}' (not 'true', hooks disabled)")
except Exception as e:
record_result("Hooks Disabled Default", "Security", False, f"Exception: {e}")
# =============================================================================
# TEST 8: Comprehensive Crawl Test
# =============================================================================
async def test_comprehensive_crawl():
"""
Run a comprehensive crawl to verify overall stability with new features.
"""
print_test("Comprehensive Crawl Test", "Overall")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
async with AsyncWebCrawler(config=BrowserConfig(headless=True), verbose=False) as crawler:
result = await crawler.arun(
url="https://httpbin.org/html",
config=CrawlerRunConfig()
)
checks = []
if result.success:
checks.append("success=True")
else:
record_result("Comprehensive Crawl", "Overall", False,
f"Crawl failed: {result.error_message}")
return
if result.html and len(result.html) > 100:
checks.append(f"html={len(result.html)} chars")
if result.markdown and result.markdown.raw_markdown:
checks.append(f"markdown={len(result.markdown.raw_markdown)} chars")
if result.links:
total_links = len(result.links.get("internal", [])) + len(result.links.get("external", []))
checks.append(f"links={total_links}")
record_result("Comprehensive Crawl", "Overall", True,
f"All checks passed: {', '.join(checks)}")
except Exception as e:
record_result("Comprehensive Crawl", "Overall", False, f"Exception: {e}")
# =============================================================================
# MAIN
# =============================================================================
def print_summary():
"""Print test results summary"""
print_header("TEST RESULTS SUMMARY")
passed = sum(1 for r in results if r.passed and not r.skipped)
failed = sum(1 for r in results if not r.passed and not r.skipped)
skipped = sum(1 for r in results if r.skipped)
print(f"\nTotal: {len(results)} tests")
print(f" Passed: {passed}")
print(f" Failed: {failed}")
print(f" Skipped: {skipped}")
if failed > 0:
print("\nFailed Tests:")
for r in results:
if not r.passed and not r.skipped:
print(f" - {r.name} ({r.feature}): {r.message}")
if skipped > 0:
print("\nSkipped Tests:")
for r in results:
if r.skipped:
print(f" - {r.name} ({r.feature}): {r.message}")
print("\n" + "=" * 70)
if failed == 0:
print("All tests passed! v0.8.0 features verified.")
else:
print(f"WARNING: {failed} test(s) failed!")
print("=" * 70)
return failed == 0
async def main():
"""Run all verification tests"""
print_header("Crawl4AI v0.8.0 - Feature Verification Tests")
print("Running actual tests to verify new features...")
print("\nKey Features in v0.8.0:")
print(" - Crash Recovery for Deep Crawl (resume_state, on_state_change)")
print(" - Prefetch Mode for Fast URL Discovery (prefetch=True)")
print(" - Security: Hooks disabled by default on Docker API")
# Run all tests
tests = [
test_crash_recovery_state_capture, # on_state_change
test_crash_recovery_resume, # resume_state
test_crash_recovery_json_serializable, # State structure
test_prefetch_returns_html_links, # prefetch=True basics
test_prefetch_skips_processing, # prefetch skips heavy work
test_prefetch_two_phase, # Two-phase pattern
test_security_hooks_disabled, # Security check
test_comprehensive_crawl, # Overall stability
]
for test_func in tests:
try:
await test_func()
except Exception as e:
print(f"\nTest {test_func.__name__} crashed: {e}")
results.append(TestResult(
test_func.__name__,
"Unknown",
False,
f"Crashed: {e}"
))
# Print summary
all_passed = print_summary()
return 0 if all_passed else 1
if __name__ == "__main__":
try:
exit_code = asyncio.run(main())
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\nTests interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\n\nTest suite failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+913
View File
@@ -0,0 +1,913 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.8.5 Release Demo - Feature Verification Tests
==========================================================
This demo ACTUALLY RUNS and VERIFIES the new features in v0.8.5.
Each test executes real crawls and validates the feature is working.
New Features Verified:
1. Anti-bot detection - Detects blocked pages and passes normal ones
2. Anti-bot + crawl_stats - Real crawl produces crawl_stats tracking
3. Proxy escalation chain - proxy_config accepts a list with DIRECT
4. Config defaults API - set_defaults affects real crawls
5. Shadow DOM flattening - Crawl a shadow-DOM site with/without flattening
6. Deep crawl cancellation - DFS crawl stops at callback limit
7. Consent popup removal - Crawl with remove_consent_popups enabled
8. Source/sibling selector - Extract from sibling elements via "source" field
9. GFM table compliance - Crawl a page with tables, verify pipe delimiters
10. avoid_ads / avoid_css - Crawl with resource filtering enabled
11. Browser recycling - Crawl multiple pages with memory_saving_mode
12. BM25 content filter dedup - fit_markdown has no duplicate chunks
13. cleaned_html preserves class/id - Verify attributes retained after crawl
Usage:
python docs/releases_review/demo_v0.8.5.py
"""
import asyncio
import json
import sys
import time
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
# Test results tracking
@dataclass
class TestResult:
name: str
feature: str
passed: bool
message: str
skipped: bool = False
results: list[TestResult] = []
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f"{title}")
print(f"{'=' * 70}")
def print_test(name: str, feature: str):
print(f"\n[TEST] {name} ({feature})")
print("-" * 50)
def record_result(name: str, feature: str, passed: bool, message: str, skipped: bool = False):
results.append(TestResult(name, feature, passed, message, skipped))
if skipped:
print(f" SKIPPED: {message}")
elif passed:
print(f" PASSED: {message}")
else:
print(f" FAILED: {message}")
# =============================================================================
# TEST 1: Anti-bot Detection - Unit + Live Crawl
# =============================================================================
async def test_antibot_detection():
"""
Verify is_blocked() detects blocked pages and a real crawl to a normal
site succeeds without false positives.
NEW in v0.8.5: 3-tier anti-bot detection (status codes, content patterns,
structural integrity) with automatic retry and fallback.
"""
print_test("Anti-bot Detection", "is_blocked() + live crawl")
try:
from crawl4ai.antibot_detector import is_blocked
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
# Unit: blocked page detected
blocked, reason = is_blocked(
403,
'<html><body><h1>Please verify you are human</h1>'
'<p>Checking your browser...</p></body></html>',
)
if not blocked:
record_result("Anti-bot Detection", "is_blocked()", False,
"Failed to detect challenge page")
return
# Unit: JSON response not flagged
blocked, _ = is_blocked(
200,
'<html><head></head><body><pre>{"status":"ok"}</pre></body></html>',
)
if blocked:
record_result("Anti-bot Detection", "is_blocked()", False,
"False positive on JSON response")
return
# Live: crawl a normal site, verify no false positive
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
"https://quotes.toscrape.com",
config=CrawlerRunConfig(),
)
if not result.success:
record_result("Anti-bot Detection", "live crawl", False,
f"Normal site crawl failed: {result.error_message}")
return
record_result("Anti-bot Detection", "is_blocked() + live crawl", True,
f"Detects blocks, no false positive on live crawl "
f"({len(result.html)} chars)")
except Exception as e:
record_result("Anti-bot Detection", "is_blocked()", False, f"Exception: {e}")
# =============================================================================
# TEST 2: Anti-bot crawl_stats Tracking
# =============================================================================
async def test_crawl_stats():
"""
Verify a real crawl produces crawl_stats with proxy/fallback tracking.
NEW in v0.8.5: CrawlResult includes crawl_stats dict tracking which
proxies were used, whether fallback was invoked, and how it resolved.
"""
print_test("Crawl Stats Tracking", "crawl_stats on CrawlResult")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
"https://example.com",
config=CrawlerRunConfig(),
)
if not result.success:
record_result("Crawl Stats", "crawl_stats", False,
f"Crawl failed: {result.error_message}")
return
stats = getattr(result, "crawl_stats", None)
if stats is None:
record_result("Crawl Stats", "crawl_stats", False,
"crawl_stats not present on CrawlResult")
return
# Check expected fields
has_proxies = "proxies_used" in stats
has_resolved = "resolved_by" in stats
if not has_proxies or not has_resolved:
record_result("Crawl Stats", "crawl_stats", False,
f"Missing fields. Keys: {list(stats.keys())}")
return
record_result("Crawl Stats", "crawl_stats", True,
f"Stats present: resolved_by={stats.get('resolved_by')}, "
f"proxies_used={len(stats.get('proxies_used', []))} entries")
except Exception as e:
record_result("Crawl Stats", "crawl_stats", False, f"Exception: {e}")
# =============================================================================
# TEST 3: Proxy Escalation Chain + DIRECT Sentinel
# =============================================================================
async def test_proxy_escalation():
"""
Verify proxy_config accepts a list and DIRECT sentinel, then crawl
with DIRECT-only to prove the escalation path works.
NEW in v0.8.5: proxy_config can be a list of ProxyConfig/None for
escalation. ProxyConfig.DIRECT normalizes to None (no proxy).
"""
print_test("Proxy Escalation Chain", "list proxy_config + DIRECT crawl")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.async_configs import ProxyConfig
# Verify DIRECT normalizes correctly in a list
config = CrawlerRunConfig(
proxy_config=[ProxyConfig.DIRECT],
)
if not isinstance(config.proxy_config, list):
record_result("Proxy Escalation", "list config", False,
f"proxy_config is {type(config.proxy_config)}, expected list")
return
# Live crawl with DIRECT (no proxy)
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
"https://example.com",
config=config,
)
if not result.success:
record_result("Proxy Escalation", "DIRECT crawl", False,
f"DIRECT crawl failed: {result.error_message}")
return
record_result("Proxy Escalation", "list + DIRECT crawl", True,
f"List config accepted, DIRECT crawl succeeded "
f"({len(result.html)} chars)")
except Exception as e:
record_result("Proxy Escalation", "proxy_config list", False, f"Exception: {e}")
# =============================================================================
# TEST 4: Config Defaults API — Real Crawl
# =============================================================================
async def test_config_defaults():
"""
Set text_mode=True as a default, then crawl and verify it took effect.
NEW in v0.8.5: BrowserConfig.set_defaults() / get_defaults() /
reset_defaults() persist across all new instances.
"""
print_test("Config Defaults API", "set_defaults → real crawl")
try:
from crawl4ai import AsyncWebCrawler
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
original = BrowserConfig.get_defaults()
try:
# Set text_mode as default (disables image loading)
BrowserConfig.set_defaults(text_mode=True, headless=True)
# Verify it applies
bc = BrowserConfig()
if not bc.text_mode:
record_result("Config Defaults", "set_defaults", False,
"text_mode default not applied")
return
# Verify explicit override wins
bc2 = BrowserConfig(text_mode=False)
if bc2.text_mode:
record_result("Config Defaults", "set_defaults", False,
"Explicit override didn't work")
return
# Real crawl with default text_mode
async with AsyncWebCrawler(config=BrowserConfig(), verbose=False) as crawler:
result = await crawler.arun(
"https://example.com",
config=CrawlerRunConfig(),
)
if not result.success:
record_result("Config Defaults", "crawl with defaults", False,
f"Crawl failed: {result.error_message}")
return
# Verify reset works
BrowserConfig.reset_defaults()
if BrowserConfig.get_defaults():
record_result("Config Defaults", "reset_defaults", False,
"Defaults not cleared after reset")
return
record_result("Config Defaults", "set/get/reset + crawl", True,
f"Defaults applied to crawl, override works, reset clears "
f"({len(result.markdown.raw_markdown)} chars markdown)")
finally:
BrowserConfig.reset_defaults()
if original:
BrowserConfig.set_defaults(**original)
except Exception as e:
record_result("Config Defaults", "set/get/reset_defaults", False, f"Exception: {e}")
# =============================================================================
# TEST 5: Shadow DOM Flattening — Comparison Crawl
# =============================================================================
async def test_shadow_dom_flattening():
"""
Crawl a page with and without flatten_shadow_dom and compare content.
NEW in v0.8.5: CrawlerRunConfig.flatten_shadow_dom serializes shadow DOM
into the light DOM, exposing hidden content to extraction.
"""
print_test("Shadow DOM Flattening", "comparison crawl")
try:
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
# Use a page known to use web components / shadow DOM
# (GitHub uses shadow DOM for some components)
url = "https://books.toscrape.com"
async with AsyncWebCrawler(
config=BrowserConfig(headless=True),
verbose=False,
) as crawler:
# Without flattening
result_normal = await crawler.arun(
url, config=CrawlerRunConfig(flatten_shadow_dom=False),
)
# With flattening
result_flat = await crawler.arun(
url, config=CrawlerRunConfig(flatten_shadow_dom=True),
)
if not result_normal.success or not result_flat.success:
record_result("Shadow DOM", "comparison crawl", False,
"One or both crawls failed")
return
normal_len = len(result_normal.html or "")
flat_len = len(result_flat.html or "")
# Both should succeed (this page may not have shadow DOM, but
# the flattening pipeline should run without error)
record_result("Shadow DOM", "flatten_shadow_dom", True,
f"Both crawls succeeded. Normal: {normal_len} chars, "
f"Flattened: {flat_len} chars. Pipeline runs cleanly.")
except Exception as e:
record_result("Shadow DOM", "flatten_shadow_dom", False, f"Exception: {e}")
# =============================================================================
# TEST 6: Deep Crawl Cancellation — DFS with should_cancel
# =============================================================================
async def test_deep_crawl_cancellation():
"""
Run a DFS deep crawl and cancel after 2 pages via should_cancel callback.
NEW in v0.8.5: All deep crawl strategies support cancel() method and
should_cancel callback for graceful cancellation.
"""
print_test("Deep Crawl Cancellation", "DFS cancel after 2 pages")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.deep_crawling import DFSDeepCrawlStrategy
pages_crawled = 0
def should_cancel():
return pages_crawled >= 2
async def track_state(state: Dict[str, Any]):
nonlocal pages_crawled
pages_crawled = state.get("pages_crawled", 0)
strategy = DFSDeepCrawlStrategy(
max_depth=1,
max_pages=10,
should_cancel=should_cancel,
on_state_change=track_state,
)
config = CrawlerRunConfig(
deep_crawl_strategy=strategy,
verbose=False,
)
async with AsyncWebCrawler(verbose=False) as crawler:
await crawler.arun("https://books.toscrape.com", config=config)
if strategy.cancelled:
record_result("Deep Crawl Cancel", "should_cancel", True,
f"Cancelled after {pages_crawled} pages (limit was 2)")
elif pages_crawled <= 3:
record_result("Deep Crawl Cancel", "should_cancel", True,
f"Stopped at {pages_crawled} pages (callback triggered)")
else:
record_result("Deep Crawl Cancel", "should_cancel", False,
f"Crawled {pages_crawled} pages — cancellation didn't work")
except Exception as e:
record_result("Deep Crawl Cancel", "should_cancel", False, f"Exception: {e}")
# =============================================================================
# TEST 7: Consent Popup Removal — Real Crawl
# =============================================================================
async def test_consent_popup_removal():
"""
Crawl a site with remove_consent_popups=True and verify the JS runs
without errors and content is still captured.
NEW in v0.8.5: CrawlerRunConfig.remove_consent_popups runs a JS snippet
that clicks "Accept All" on 40+ CMP platforms.
"""
print_test("Consent Popup Removal", "crawl with remove_consent_popups")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
"https://quotes.toscrape.com",
config=CrawlerRunConfig(remove_consent_popups=True),
)
if not result.success:
record_result("Consent Popup", "remove_consent_popups", False,
f"Crawl failed: {result.error_message}")
return
md = result.markdown.raw_markdown if result.markdown else ""
if len(md) < 50:
record_result("Consent Popup", "remove_consent_popups", False,
"Content too short — JS may have broken the page")
return
record_result("Consent Popup", "remove_consent_popups", True,
f"Crawl succeeded with consent popup removal "
f"({len(md)} chars markdown)")
except Exception as e:
record_result("Consent Popup", "remove_consent_popups", False, f"Exception: {e}")
# =============================================================================
# TEST 8: Source/Sibling Selector — Extract from Real Crawled HTML
# =============================================================================
async def test_source_sibling_selector():
"""
Crawl a page, then use JsonCssExtractionStrategy with "source" field
to extract data spanning sibling elements.
NEW in v0.8.5: "source": "+ selector" navigates to sibling elements
before applying the field selector. Works in CSS and XPath strategies.
"""
print_test("Source/Sibling Selector", "crawl + extract with source field")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
# Use a schema with source field on synthetic HTML first to verify
# the feature works, then also run it through a real crawl pipeline
schema = {
"name": "SiblingItems",
"baseSelector": "tr.athing",
"fields": [
{"name": "title", "selector": ".titleline > a", "type": "text"},
{"name": "score", "selector": ".score", "type": "text", "source": "+ tr"},
],
}
strategy = JsonCssExtractionStrategy(schema=schema)
# Test with sibling HTML structure
html = """
<html><body><table>
<tr class="athing" id="1">
<td><span class="titleline"><a href="http://ex.com">Article One</a></span></td>
</tr>
<tr>
<td><span class="score">250 points</span></td>
</tr>
<tr class="athing" id="2">
<td><span class="titleline"><a href="http://ex.com/2">Article Two</a></span></td>
</tr>
<tr>
<td><span class="score">180 points</span></td>
</tr>
</table></body></html>
"""
# Run through the full crawl pipeline with raw: URL
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
f"raw:{html}",
config=CrawlerRunConfig(
extraction_strategy=strategy,
),
)
if not result.extracted_content:
record_result("Sibling Selector", "source field", False,
"No extracted_content returned")
return
data = json.loads(result.extracted_content)
if len(data) < 2:
record_result("Sibling Selector", "source field", False,
f"Expected 2 items, got {len(data)}")
return
if data[0].get("title") != "Article One":
record_result("Sibling Selector", "source field", False,
f"Title mismatch: {data[0].get('title')}")
return
if data[0].get("score") != "250 points":
record_result("Sibling Selector", "source field", False,
f"Sibling score not extracted: {data[0].get('score')}")
return
if data[1].get("score") != "180 points":
record_result("Sibling Selector", "source field", False,
f"Second sibling score wrong: {data[1].get('score')}")
return
record_result("Sibling Selector", "source field via crawl pipeline", True,
f"Extracted {len(data)} items with sibling scores through "
f"full arun() pipeline")
except Exception as e:
record_result("Sibling Selector", "source field", False, f"Exception: {e}")
# =============================================================================
# TEST 9: GFM Table Compliance — Crawl Page with Tables
# =============================================================================
async def test_gfm_tables():
"""
Crawl a page containing HTML tables and verify the markdown output
has proper GFM pipe delimiters.
NEW in v0.8.5: html2text now generates | col1 | col2 | with proper
leading/trailing pipes instead of col1 | col2.
"""
print_test("GFM Table Compliance", "crawl page with tables")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
# Use raw HTML with a table
html = """
<html><body>
<h1>Product Comparison</h1>
<table>
<tr><th>Product</th><th>Price</th><th>Rating</th></tr>
<tr><td>Widget A</td><td>$9.99</td><td>4.5</td></tr>
<tr><td>Widget B</td><td>$14.99</td><td>4.8</td></tr>
</table>
</body></html>
"""
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
f"raw:{html}",
config=CrawlerRunConfig(),
)
if not result.success or not result.markdown:
record_result("GFM Tables", "table crawl", False,
"Crawl failed or no markdown")
return
md = result.markdown.raw_markdown
table_lines = [
l.strip() for l in md.split("\n")
if l.strip() and "|" in l
]
if not table_lines:
record_result("GFM Tables", "pipe delimiters", False,
f"No table lines found in markdown:\n{md}")
return
all_have_pipes = all(
l.startswith("|") and l.endswith("|")
for l in table_lines
)
if not all_have_pipes:
record_result("GFM Tables", "pipe delimiters", False,
f"Missing leading/trailing pipes:\n" +
"\n".join(table_lines))
return
record_result("GFM Tables", "pipe delimiters via crawl", True,
f"Table has proper GFM pipes ({len(table_lines)} rows)")
except Exception as e:
record_result("GFM Tables", "pipe delimiters", False, f"Exception: {e}")
# =============================================================================
# TEST 10: avoid_ads / avoid_css — Real Crawl with Filtering
# =============================================================================
async def test_avoid_ads():
"""
Crawl a real page with avoid_ads=True and verify content is still captured.
NEW in v0.8.5: BrowserConfig.avoid_ads blocks ad/tracker domains,
BrowserConfig.avoid_css blocks CSS resources at the network level.
"""
print_test("Resource Filtering", "crawl with avoid_ads + avoid_css")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
# Crawl with ad blocking enabled
async with AsyncWebCrawler(
config=BrowserConfig(
headless=True,
avoid_ads=True,
avoid_css=True,
),
verbose=False,
) as crawler:
result = await crawler.arun(
"https://quotes.toscrape.com",
config=CrawlerRunConfig(),
)
if not result.success:
record_result("Resource Filtering", "avoid_ads crawl", False,
f"Crawl failed: {result.error_message}")
return
md = result.markdown.raw_markdown if result.markdown else ""
# Verify actual content was captured (quotes should be there)
has_quotes = "quote" in md.lower() or "albert einstein" in md.lower()
if not has_quotes and len(md) < 100:
record_result("Resource Filtering", "avoid_ads crawl", False,
"Content missing — filtering may have broken the page")
return
record_result("Resource Filtering", "avoid_ads + avoid_css crawl", True,
f"Content captured with ad/CSS blocking "
f"({len(md)} chars markdown)")
except Exception as e:
record_result("Resource Filtering", "avoid_ads/css", False, f"Exception: {e}")
# =============================================================================
# TEST 11: Browser Recycling — Multi-page Crawl with memory_saving_mode
# =============================================================================
async def test_browser_recycling():
"""
Crawl multiple pages with memory_saving_mode enabled and verify
all succeed without browser crashes.
NEW in v0.8.5: BrowserConfig.memory_saving_mode adds aggressive cache/V8
flags. max_pages_before_recycle triggers automatic browser restart.
"""
print_test("Browser Recycling", "multi-page crawl with memory_saving_mode")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
urls = [
"https://example.com",
"https://quotes.toscrape.com",
"https://httpbin.org/html",
]
async with AsyncWebCrawler(
config=BrowserConfig(
headless=True,
memory_saving_mode=True,
),
verbose=False,
) as crawler:
succeeded = 0
for url in urls:
result = await crawler.arun(url, config=CrawlerRunConfig())
if result.success:
succeeded += 1
if succeeded == len(urls):
record_result("Browser Recycling", "memory_saving_mode", True,
f"All {succeeded}/{len(urls)} crawls succeeded with "
f"memory_saving_mode")
else:
record_result("Browser Recycling", "memory_saving_mode", False,
f"Only {succeeded}/{len(urls)} crawls succeeded")
except Exception as e:
record_result("Browser Recycling", "memory_saving_mode", False, f"Exception: {e}")
# =============================================================================
# TEST 12: BM25 Content Filter Deduplication
# =============================================================================
async def test_bm25_dedup():
"""
Crawl a page using BM25ContentFilter and verify no duplicate chunks
in fit_markdown.
NEW in v0.8.5: BM25ContentFilter.filter_content() deduplicates output
chunks, keeping the first occurrence in document order.
"""
print_test("BM25 Deduplication", "fit_markdown has no duplicates")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from crawl4ai.content_filter_strategy import BM25ContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
"https://quotes.toscrape.com",
config=CrawlerRunConfig(
markdown_generator=DefaultMarkdownGenerator(
content_filter=BM25ContentFilter(
user_query="famous quotes about life",
),
),
),
)
if not result.success:
record_result("BM25 Dedup", "fit_markdown", False,
f"Crawl failed: {result.error_message}")
return
fit_md = result.markdown.fit_markdown if result.markdown else ""
if not fit_md:
record_result("BM25 Dedup", "fit_markdown", False,
"No fit_markdown produced")
return
# Check for duplicate lines (non-empty, non-header)
lines = [l.strip() for l in fit_md.split("\n") if l.strip() and not l.startswith("#")]
unique_lines = list(dict.fromkeys(lines)) # preserves order
dupes = len(lines) - len(unique_lines)
if dupes > 0:
record_result("BM25 Dedup", "fit_markdown", False,
f"{dupes} duplicate lines found in fit_markdown")
return
record_result("BM25 Dedup", "fit_markdown dedup", True,
f"No duplicates in fit_markdown ({len(unique_lines)} unique lines)")
except Exception as e:
record_result("BM25 Dedup", "fit_markdown", False, f"Exception: {e}")
# =============================================================================
# TEST 13: cleaned_html Preserves class and id Attributes
# =============================================================================
async def test_cleaned_html_attrs():
"""
Crawl a page and verify cleaned_html retains class and id attributes.
NEW in v0.8.5: 'class' and 'id' are now in IMPORTANT_ATTRS, so they
survive HTML cleaning. Previously they were stripped.
"""
print_test("cleaned_html Attributes", "class and id preserved")
try:
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
html = """
<html><body>
<div id="main-content" class="container wide">
<h1 class="page-title">Hello World</h1>
<p id="intro" class="lead text-muted">Introduction paragraph.</p>
</div>
</body></html>
"""
async with AsyncWebCrawler(verbose=False) as crawler:
result = await crawler.arun(
f"raw:{html}",
config=CrawlerRunConfig(),
)
if not result.success or not result.cleaned_html:
record_result("cleaned_html Attrs", "class/id", False,
"Crawl failed or no cleaned_html")
return
cleaned = result.cleaned_html
checks = []
if 'id="main-content"' in cleaned:
checks.append("id=main-content")
if 'class="container wide"' in cleaned or 'class="container' in cleaned:
checks.append("class=container")
if 'class="page-title"' in cleaned:
checks.append("class=page-title")
if 'id="intro"' in cleaned:
checks.append("id=intro")
if len(checks) < 2:
record_result("cleaned_html Attrs", "class/id", False,
f"Only found {len(checks)} attrs: {checks}. "
f"cleaned_html snippet: {cleaned[:200]}")
return
record_result("cleaned_html Attrs", "class/id preserved", True,
f"Found {len(checks)} preserved attributes: {', '.join(checks)}")
except Exception as e:
record_result("cleaned_html Attrs", "class/id", False, f"Exception: {e}")
# =============================================================================
# MAIN
# =============================================================================
def print_summary():
"""Print test results summary"""
print_header("TEST RESULTS SUMMARY")
passed = sum(1 for r in results if r.passed and not r.skipped)
failed = sum(1 for r in results if not r.passed and not r.skipped)
skipped = sum(1 for r in results if r.skipped)
print(f"\nTotal: {len(results)} tests")
print(f" Passed: {passed}")
print(f" Failed: {failed}")
print(f" Skipped: {skipped}")
if failed > 0:
print("\nFailed Tests:")
for r in results:
if not r.passed and not r.skipped:
print(f" - {r.name} ({r.feature}): {r.message}")
if skipped > 0:
print("\nSkipped Tests:")
for r in results:
if r.skipped:
print(f" - {r.name} ({r.feature}): {r.message}")
print("\n" + "=" * 70)
if failed == 0:
print("All tests passed! v0.8.5 features verified.")
else:
print(f"WARNING: {failed} test(s) failed!")
print("=" * 70)
return failed == 0
async def main():
"""Run all verification tests"""
print_header("Crawl4AI v0.8.5 - Feature Verification Tests")
print("Running actual tests to verify new features...")
print("\nKey Features in v0.8.5:")
print(" - Anti-bot detection + retry + proxy escalation + fallback")
print(" - Shadow DOM flattening (flatten_shadow_dom)")
print(" - Deep crawl cancellation (cancel / should_cancel)")
print(" - Config defaults API (set_defaults / get_defaults / reset_defaults)")
print(" - Source/sibling selector in JSON extraction")
print(" - Consent popup removal (40+ CMP platforms)")
print(" - avoid_ads / avoid_css resource filtering")
print(" - Browser recycling + memory-saving mode")
print(" - GFM table compliance")
print(" - BM25 content filter deduplication")
print(" - cleaned_html preserves class/id attributes")
print(" - 49+ bug fixes including critical RCE and CVE patches")
tests = [
test_antibot_detection, # Anti-bot + live crawl
test_crawl_stats, # crawl_stats tracking
test_proxy_escalation, # Proxy chain + DIRECT crawl
test_config_defaults, # set_defaults → real crawl
test_shadow_dom_flattening, # Shadow DOM comparison crawl
test_deep_crawl_cancellation, # DFS cancel at 2 pages
test_consent_popup_removal, # Crawl with consent removal
test_source_sibling_selector, # Sibling extraction via pipeline
test_gfm_tables, # Table crawl with pipe check
test_avoid_ads, # Crawl with ad/CSS blocking
test_browser_recycling, # Multi-page memory_saving crawl
test_bm25_dedup, # BM25 fit_markdown dedup
test_cleaned_html_attrs, # class/id preserved
]
for test_func in tests:
try:
await test_func()
except Exception as e:
print(f"\nTest {test_func.__name__} crashed: {e}")
results.append(TestResult(
test_func.__name__,
"Unknown",
False,
f"Crashed: {e}"
))
all_passed = print_summary()
return 0 if all_passed else 1
if __name__ == "__main__":
try:
exit_code = asyncio.run(main())
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\nTests interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\n\nTest suite failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+272
View File
@@ -0,0 +1,272 @@
#!/usr/bin/env python3
"""
Crawl4AI v0.9.1 Release Demo - Feature Verification Tests
==========================================================
This demo ACTUALLY RUNS and VERIFIES the key changes in v0.9.1.
Each test executes real crawls or exercises the fix path end-to-end.
Features / Fixes Verified:
1. PruningContentFilter preserve_classes / preserve_tags whitelist
2. Windows channel='chromium' fix (channel not passed for default)
3. page_timeout ms-to-seconds conversion for HTTP mode
4. html2text bypass_tables preserves table attributes
5. Best-first batch ordering stability
Usage:
python docs/releases_review/demo_v0.9.1.py
"""
import asyncio
import sys
from dataclasses import dataclass
@dataclass
class TestResult:
name: str
feature: str
passed: bool
message: str
skipped: bool = False
results: list[TestResult] = []
def print_header(title: str):
print(f"\n{'=' * 70}")
print(f"{title}")
print(f"{'=' * 70}")
def print_test(name: str, feature: str):
print(f"\n[TEST] {name} ({feature})")
print("-" * 50)
def record_result(name: str, feature: str, passed: bool, message: str, skipped: bool = False):
results.append(TestResult(name, feature, passed, message, skipped))
status = "SKIP" if skipped else ("PASS" if passed else "FAIL")
print(f" [{status}] {message}")
# ── Test 1: PruningContentFilter preserve_classes / preserve_tags ────
async def test_preserve_whitelist():
"""Verify that whitelisted classes/tags survive pruning."""
print_test("PruningContentFilter Whitelist", "preserve_classes / preserve_tags")
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
from crawl4ai.content_filter_strategy import PruningContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
# HTML where short metadata elements would normally be pruned
html = """
<html><body>
<div class="article">
<h1>Main Article Title</h1>
<p>This is a long paragraph with enough content that the density-based
scoring will keep it. It contains multiple sentences and substantial text
that makes it clearly content rather than boilerplate navigation.</p>
<span class="author">By John Doe</span>
<time datetime="2026-07-08">July 8, 2026</time>
<p>Another substantial paragraph with enough text to be kept by the
pruning filter. This ensures the article has real content around the
short metadata elements we want to test preservation for.</p>
</div>
</body></html>
"""
# Without whitelist — author/time may be pruned
filter_no_wl = PruningContentFilter(threshold=0.48)
gen_no_wl = DefaultMarkdownGenerator(content_filter=filter_no_wl)
# With whitelist — author class and time tag protected
filter_wl = PruningContentFilter(
threshold=0.48,
preserve_classes=["author"],
preserve_tags=["time"],
)
gen_wl = DefaultMarkdownGenerator(content_filter=filter_wl)
config_no_wl = CrawlerRunConfig(
markdown_generator=gen_no_wl,
)
config_wl = CrawlerRunConfig(
markdown_generator=gen_wl,
)
async with AsyncWebCrawler(config=BrowserConfig()) as crawler:
result_wl = await crawler.arun(url=f"raw://{html}", config=config_wl)
fit = result_wl.markdown.fit_markdown or ""
has_author = "John Doe" in fit
has_time = "July 8, 2026" in fit
if has_author and has_time:
record_result("preserve_whitelist", "PruningContentFilter", True,
"Whitelisted class and tag preserved in fit_markdown")
else:
record_result("preserve_whitelist", "PruningContentFilter", False,
f"author={'found' if has_author else 'MISSING'}, "
f"time={'found' if has_time else 'MISSING'} in fit_markdown")
# ── Test 2: channel='chromium' not passed to Playwright ──────────────
async def test_channel_chromium_skipped():
"""Verify that the default 'chromium' channel is not passed to launch args."""
print_test("Channel Chromium Skip", "Windows TargetClosedError fix")
from crawl4ai.browser_manager import BrowserManager
from crawl4ai.async_configs import BrowserConfig
config = BrowserConfig() # default chrome_channel='chromium'
mgr = BrowserManager(config)
args = mgr._build_browser_args()
if "channel" not in args:
record_result("channel_skip", "browser_manager", True,
"Default 'chromium' channel correctly omitted from launch args")
else:
record_result("channel_skip", "browser_manager", False,
f"channel='{args['channel']}' should not be in launch args")
# Verify explicit non-default channel IS passed
config2 = BrowserConfig(chrome_channel="chrome")
mgr2 = BrowserManager(config2)
args2 = mgr2._build_browser_args()
if args2.get("channel") == "chrome":
record_result("channel_explicit", "browser_manager", True,
"Explicit 'chrome' channel correctly passed")
else:
record_result("channel_explicit", "browser_manager", False,
f"Expected channel='chrome', got {args2.get('channel')}")
# ── Test 3: page_timeout ms→s conversion ─────────────────────────────
async def test_page_timeout_conversion():
"""Verify page_timeout is converted from ms to seconds for aiohttp."""
print_test("HTTP Timeout Conversion", "page_timeout ms to seconds")
from crawl4ai.async_configs import CrawlerRunConfig
config = CrawlerRunConfig()
# Default page_timeout is 60000 (ms). aiohttp timeout should be 60s, not 60000s.
import aiohttp
timeout = aiohttp.ClientTimeout(total=config.page_timeout / 1000)
if timeout.total == 60.0:
record_result("timeout_conversion", "HTTP mode", True,
f"page_timeout {config.page_timeout}ms → {timeout.total}s correctly")
else:
record_result("timeout_conversion", "HTTP mode", False,
f"Expected 60.0s, got {timeout.total}s")
# ── Test 4: html2text bypass_tables preserves attributes ──────────────
async def test_bypass_tables_attributes():
"""Verify table tag attributes are preserved when bypass_tables is enabled."""
print_test("Table Attribute Preservation", "html2text bypass_tables fix")
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, BrowserConfig
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
html = """
<html><body>
<table class="data-table" id="results" border="1">
<tr><th>Name</th><th>Value</th></tr>
<tr><td>Alpha</td><td>100</td></tr>
<tr><td>Beta</td><td>200</td></tr>
</table>
</body></html>
"""
config = CrawlerRunConfig(
markdown_generator=DefaultMarkdownGenerator(),
)
async with AsyncWebCrawler(config=BrowserConfig()) as crawler:
result = await crawler.arun(url=f"raw://{html}", config=config)
md = result.markdown.raw_markdown or ""
# Tables should render and contain content
has_content = "Alpha" in md and "Beta" in md
if has_content:
record_result("bypass_tables", "html2text", True,
"Table content preserved in markdown output")
else:
record_result("bypass_tables", "html2text", False,
"Table content missing from markdown")
# ── Test 5: Best-first batch ordering stability ──────────────────────
async def test_bestfirst_ordering():
"""Verify best-first scorer produces deterministic ordering."""
print_test("Best-First Ordering", "Stable batch ordering fix")
from crawl4ai.deep_crawling.scorers import KeywordRelevanceScorer, CompositeScorer
scorer = CompositeScorer([
KeywordRelevanceScorer(["python", "crawl"], weight=1.0),
])
urls = [
"https://example.com/python-guide",
"https://example.com/crawl-tutorial",
"https://example.com/about",
"https://example.com/python-crawl-tips",
"https://example.com/contact",
]
# Score multiple times — should be deterministic
scores_run1 = [scorer.score(u) for u in urls]
scores_run2 = [scorer.score(u) for u in urls]
if scores_run1 == scores_run2:
record_result("bestfirst_stable", "deep_crawling", True,
"Scorer produces deterministic results across runs")
else:
record_result("bestfirst_stable", "deep_crawling", False,
"Scorer results differ between runs")
# ── Main ──────────────────────────────────────────────────────────────
async def main():
print_header("Crawl4AI v0.9.1 Release Verification")
await test_preserve_whitelist()
await test_channel_chromium_skipped()
await test_page_timeout_conversion()
await test_bypass_tables_attributes()
await test_bestfirst_ordering()
# Summary
print_header("RESULTS SUMMARY")
total = len(results)
passed = sum(1 for r in results if r.passed and not r.skipped)
failed = sum(1 for r in results if not r.passed and not r.skipped)
skipped = sum(1 for r in results if r.skipped)
for r in results:
status = "SKIP" if r.skipped else ("PASS" if r.passed else "FAIL")
print(f" [{status}] {r.name}: {r.message}")
print(f"\nTotal: {total} | Passed: {passed} | Failed: {failed} | Skipped: {skipped}")
if failed > 0:
print("\n⚠️ Some tests FAILED. Review before release.")
sys.exit(1)
else:
print("\n✅ All tests passed!")
if __name__ == "__main__":
asyncio.run(main())
+276
View File
@@ -0,0 +1,276 @@
import os, sys
# append the parent directory to the sys.path
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(parent_dir)
parent_parent_dir = os.path.dirname(parent_dir)
sys.path.append(parent_parent_dir)
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
__data__ = os.path.join(__location__, "__data")
import asyncio
from pathlib import Path
import aiohttp
import json
from crawl4ai import AsyncWebCrawler, CacheMode
from crawl4ai.content_filter_strategy import BM25ContentFilter
# 1. File Download Processing Example
async def download_example():
"""Example of downloading files from Python.org"""
# downloads_path = os.path.join(os.getcwd(), "downloads")
downloads_path = os.path.join(Path.home(), ".crawl4ai", "downloads")
os.makedirs(downloads_path, exist_ok=True)
print(f"Downloads will be saved to: {downloads_path}")
async with AsyncWebCrawler(
accept_downloads=True, downloads_path=downloads_path, verbose=True
) as crawler:
result = await crawler.arun(
url="https://www.python.org/downloads/",
js_code="""
// Find and click the first Windows installer link
const downloadLink = document.querySelector('a[href$=".exe"]');
if (downloadLink) {
console.log('Found download link:', downloadLink.href);
downloadLink.click();
} else {
console.log('No .exe download link found');
}
""",
delay_before_return_html=1, # Wait 5 seconds to ensure download starts
cache_mode=CacheMode.BYPASS,
)
if result.downloaded_files:
print("\nDownload successful!")
print("Downloaded files:")
for file_path in result.downloaded_files:
print(f"- {file_path}")
print(f" File size: {os.path.getsize(file_path) / (1024*1024):.2f} MB")
else:
print("\nNo files were downloaded")
# 2. Local File and Raw HTML Processing Example
async def local_and_raw_html_example():
"""Example of processing local files and raw HTML"""
# Create a sample HTML file
sample_file = os.path.join(__data__, "sample.html")
with open(sample_file, "w") as f:
f.write(
"""
<html><body>
<h1>Test Content</h1>
<p>This is a test paragraph.</p>
</body></html>
"""
)
async with AsyncWebCrawler(verbose=True) as crawler:
# Process local file
local_result = await crawler.arun(url=f"file://{os.path.abspath(sample_file)}")
# Process raw HTML
raw_html = """
<html><body>
<h1>Raw HTML Test</h1>
<p>This is a test of raw HTML processing.</p>
</body></html>
"""
raw_result = await crawler.arun(url=f"raw:{raw_html}")
# Clean up
os.remove(sample_file)
print("Local file content:", local_result.markdown)
print("\nRaw HTML content:", raw_result.markdown)
# 3. Enhanced Markdown Generation Example
async def markdown_generation_example():
"""Example of enhanced markdown generation with citations and LLM-friendly features"""
async with AsyncWebCrawler(verbose=True) as crawler:
# Create a content filter (optional)
content_filter = BM25ContentFilter(
# user_query="History and cultivation",
bm25_threshold=1.0
)
result = await crawler.arun(
url="https://en.wikipedia.org/wiki/Apple",
css_selector="main div#bodyContent",
content_filter=content_filter,
cache_mode=CacheMode.BYPASS,
)
from crawl4ai.content_filter_strategy import BM25ContentFilter
result = await crawler.arun(
url="https://en.wikipedia.org/wiki/Apple",
css_selector="main div#bodyContent",
content_filter=BM25ContentFilter(),
)
print(result.markdown_v2.fit_markdown)
print("\nMarkdown Generation Results:")
print(f"1. Original markdown length: {len(result.markdown)}")
print("2. New markdown versions (markdown_v2):")
print(f" - Raw markdown length: {len(result.markdown_v2.raw_markdown)}")
print(
f" - Citations markdown length: {len(result.markdown_v2.markdown_with_citations)}"
)
print(
f" - References section length: {len(result.markdown_v2.references_markdown)}"
)
if result.markdown_v2.fit_markdown:
print(
f" - Filtered markdown length: {len(result.markdown_v2.fit_markdown)}"
)
# Save examples to files
output_dir = os.path.join(__data__, "markdown_examples")
os.makedirs(output_dir, exist_ok=True)
# Save different versions
with open(os.path.join(output_dir, "1_raw_markdown.md"), "w") as f:
f.write(result.markdown_v2.raw_markdown)
with open(os.path.join(output_dir, "2_citations_markdown.md"), "w") as f:
f.write(result.markdown_v2.markdown_with_citations)
with open(os.path.join(output_dir, "3_references.md"), "w") as f:
f.write(result.markdown_v2.references_markdown)
if result.markdown_v2.fit_markdown:
with open(os.path.join(output_dir, "4_filtered_markdown.md"), "w") as f:
f.write(result.markdown_v2.fit_markdown)
print(f"\nMarkdown examples saved to: {output_dir}")
# Show a sample of citations and references
print("\nSample of markdown with citations:")
print(result.markdown_v2.markdown_with_citations[:500] + "...\n")
print("Sample of references:")
print(
"\n".join(result.markdown_v2.references_markdown.split("\n")[:10]) + "..."
)
# 4. Browser Management Example
async def browser_management_example():
"""Example of using enhanced browser management features"""
# Use the specified user directory path
user_data_dir = os.path.join(Path.home(), ".crawl4ai", "browser_profile")
os.makedirs(user_data_dir, exist_ok=True)
print(f"Browser profile will be saved to: {user_data_dir}")
async with AsyncWebCrawler(
use_managed_browser=True,
user_data_dir=user_data_dir,
headless=False,
verbose=True,
) as crawler:
result = await crawler.arun(
url="https://crawl4ai.com",
# session_id="persistent_session_1",
cache_mode=CacheMode.BYPASS,
)
# Use GitHub as an example - it's a good test for browser management
# because it requires proper browser handling
result = await crawler.arun(
url="https://github.com/trending",
# session_id="persistent_session_1",
cache_mode=CacheMode.BYPASS,
)
print("\nBrowser session result:", result.success)
if result.success:
print("Page title:", result.metadata.get("title", "No title found"))
# 5. API Usage Example
async def api_example():
"""Example of using the new API endpoints"""
api_token = os.getenv("CRAWL4AI_API_TOKEN") or "test_api_code"
headers = {"Authorization": f"Bearer {api_token}"}
async with aiohttp.ClientSession() as session:
# Submit crawl job
crawl_request = {
"urls": ["https://news.ycombinator.com"], # Hacker News as an example
"extraction_config": {
"type": "json_css",
"params": {
"schema": {
"name": "Hacker News Articles",
"baseSelector": ".athing",
"fields": [
{"name": "title", "selector": ".title a", "type": "text"},
{"name": "score", "selector": ".score", "type": "text"},
{
"name": "url",
"selector": ".title a",
"type": "attribute",
"attribute": "href",
},
],
}
},
},
"crawler_params": {
"headless": True,
# "use_managed_browser": True
},
"cache_mode": "bypass",
# "screenshot": True,
# "magic": True
}
async with session.post(
"http://localhost:11235/crawl", json=crawl_request, headers=headers
) as response:
task_data = await response.json()
task_id = task_data["task_id"]
# Check task status
while True:
async with session.get(
f"http://localhost:11235/task/{task_id}", headers=headers
) as status_response:
result = await status_response.json()
print(f"Task status: {result['status']}")
if result["status"] == "completed":
print("Task completed!")
print("Results:")
news = json.loads(result["results"][0]["extracted_content"])
print(json.dumps(news[:4], indent=2))
break
else:
await asyncio.sleep(1)
# Main execution
async def main():
# print("Running Crawl4AI feature examples...")
# print("\n1. Running Download Example:")
# await download_example()
# print("\n2. Running Markdown Generation Example:")
# await markdown_generation_example()
# # print("\n3. Running Local and Raw HTML Example:")
# await local_and_raw_html_example()
# # print("\n4. Running Browser Management Example:")
await browser_management_example()
# print("\n5. Running API Example:")
await api_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,655 @@
#!/usr/bin/env python3
"""
🚀 Crawl4AI v0.7.5 - Docker Hooks System Complete Demonstration
================================================================
This file demonstrates the NEW Docker Hooks System introduced in v0.7.5.
The Docker Hooks System is a completely NEW feature that provides pipeline
customization through user-provided Python functions. It offers three approaches:
1. String-based hooks for REST API
2. hooks_to_string() utility to convert functions
3. Docker Client with automatic conversion (most convenient)
All three approaches are part of this NEW v0.7.5 feature!
Perfect for video recording and demonstration purposes.
Requirements:
- Docker container running: docker run -p 11235:11235 unclecode/crawl4ai:latest
- crawl4ai v0.7.5 installed: pip install crawl4ai==0.7.5
"""
import asyncio
import requests
import json
import time
from typing import Dict, Any
# Import Crawl4AI components
from crawl4ai import hooks_to_string
from crawl4ai.docker_client import Crawl4aiDockerClient
# Configuration
DOCKER_URL = "http://localhost:11235"
# DOCKER_URL = "http://localhost:11234"
TEST_URLS = [
# "https://httpbin.org/html",
"https://www.kidocode.com",
"https://quotes.toscrape.com",
]
def print_section(title: str, description: str = ""):
"""Print a formatted section header"""
print("\n" + "=" * 70)
print(f" {title}")
if description:
print(f" {description}")
print("=" * 70 + "\n")
def check_docker_service() -> bool:
"""Check if Docker service is running"""
try:
response = requests.get(f"{DOCKER_URL}/health", timeout=3)
return response.status_code == 200
except:
return False
# ============================================================================
# REUSABLE HOOK LIBRARY (NEW in v0.7.5)
# ============================================================================
async def performance_optimization_hook(page, context, **kwargs):
"""
Performance Hook: Block unnecessary resources to speed up crawling
"""
print(" [Hook] 🚀 Optimizing performance - blocking images and ads...")
# Block images
await context.route(
"**/*.{png,jpg,jpeg,gif,webp,svg,ico}",
lambda route: route.abort()
)
# Block ads and analytics
await context.route("**/analytics/*", lambda route: route.abort())
await context.route("**/ads/*", lambda route: route.abort())
await context.route("**/google-analytics.com/*", lambda route: route.abort())
print(" [Hook] ✓ Performance optimization applied")
return page
async def viewport_setup_hook(page, context, **kwargs):
"""
Viewport Hook: Set consistent viewport size for rendering
"""
print(" [Hook] 🖥️ Setting viewport to 1920x1080...")
await page.set_viewport_size({"width": 1920, "height": 1080})
print(" [Hook] ✓ Viewport configured")
return page
async def authentication_headers_hook(page, context, url, **kwargs):
"""
Headers Hook: Add custom authentication and tracking headers
"""
print(f" [Hook] 🔐 Adding custom headers for {url[:50]}...")
await page.set_extra_http_headers({
'X-Crawl4AI-Version': '0.7.5',
'X-Custom-Hook': 'function-based-demo',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': 'Crawl4AI/0.7.5 (Educational Demo)'
})
print(" [Hook] ✓ Custom headers added")
return page
async def lazy_loading_handler_hook(page, context, **kwargs):
"""
Content Hook: Handle lazy-loaded content by scrolling
"""
print(" [Hook] 📜 Scrolling to load lazy content...")
# Scroll to bottom
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(1000)
# Scroll to middle
await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")
await page.wait_for_timeout(500)
# Scroll back to top
await page.evaluate("window.scrollTo(0, 0)")
await page.wait_for_timeout(500)
print(" [Hook] ✓ Lazy content loaded")
return page
async def page_analytics_hook(page, context, **kwargs):
"""
Analytics Hook: Log page metrics before extraction
"""
print(" [Hook] 📊 Collecting page analytics...")
metrics = await page.evaluate('''
() => ({
title: document.title,
images: document.images.length,
links: document.links.length,
scripts: document.scripts.length,
headings: document.querySelectorAll('h1, h2, h3').length,
paragraphs: document.querySelectorAll('p').length
})
''')
print(f" [Hook] 📈 Page: {metrics['title'][:50]}...")
print(f" Links: {metrics['links']}, Images: {metrics['images']}, "
f"Headings: {metrics['headings']}, Paragraphs: {metrics['paragraphs']}")
return page
# ============================================================================
# DEMO 1: String-Based Hooks (NEW Docker Hooks System)
# ============================================================================
def demo_1_string_based_hooks():
"""
Demonstrate string-based hooks with REST API (part of NEW Docker Hooks System)
"""
print_section(
"DEMO 1: String-Based Hooks (REST API)",
"Part of the NEW Docker Hooks System - hooks as strings"
)
# Define hooks as strings
hooks_config = {
"on_page_context_created": """
async def hook(page, context, **kwargs):
print(" [String Hook] Setting up page context...")
# Block images for performance
await context.route("**/*.{png,jpg,jpeg,gif,webp}", lambda route: route.abort())
await page.set_viewport_size({"width": 1920, "height": 1080})
return page
""",
"before_goto": """
async def hook(page, context, url, **kwargs):
print(f" [String Hook] Navigating to {url[:50]}...")
await page.set_extra_http_headers({
'X-Crawl4AI': 'string-based-hooks',
'X-Demo': 'v0.7.5'
})
return page
""",
"before_retrieve_html": """
async def hook(page, context, **kwargs):
print(" [String Hook] Scrolling page...")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(1000)
return page
"""
}
# Prepare request payload
payload = {
"urls": [TEST_URLS[0]],
"hooks": {
"code": hooks_config,
"timeout": 30
},
"crawler_config": {
"cache_mode": "bypass"
}
}
print(f"🎯 Target URL: {TEST_URLS[0]}")
print(f"🔧 Configured {len(hooks_config)} string-based hooks")
print(f"📡 Sending request to Docker API...\n")
try:
start_time = time.time()
response = requests.post(f"{DOCKER_URL}/crawl", json=payload, timeout=60)
execution_time = time.time() - start_time
if response.status_code == 200:
result = response.json()
print(f"\n✅ Request successful! (took {execution_time:.2f}s)")
# Display results
if result.get('results') and result['results'][0].get('success'):
crawl_result = result['results'][0]
html_length = len(crawl_result.get('html', ''))
markdown_length = len(crawl_result.get('markdown', ''))
print(f"\n📊 Results:")
print(f" • HTML length: {html_length:,} characters")
print(f" • Markdown length: {markdown_length:,} characters")
print(f" • URL: {crawl_result.get('url')}")
# Check hooks execution
if 'hooks' in result:
hooks_info = result['hooks']
print(f"\n🎣 Hooks Execution:")
print(f" • Status: {hooks_info['status']['status']}")
print(f" • Attached hooks: {len(hooks_info['status']['attached_hooks'])}")
if 'summary' in hooks_info:
summary = hooks_info['summary']
print(f" • Total executions: {summary['total_executions']}")
print(f" • Successful: {summary['successful']}")
print(f" • Success rate: {summary['success_rate']:.1f}%")
else:
print(f"⚠️ Crawl completed but no results")
else:
print(f"❌ Request failed with status {response.status_code}")
print(f" Error: {response.text[:200]}")
except requests.exceptions.Timeout:
print("⏰ Request timed out after 60 seconds")
except Exception as e:
print(f"❌ Error: {str(e)}")
print("\n" + "" * 70)
print("✓ String-based hooks demo complete\n")
# ============================================================================
# DEMO 2: Function-Based Hooks with hooks_to_string() Utility
# ============================================================================
def demo_2_hooks_to_string_utility():
"""
Demonstrate the new hooks_to_string() utility for converting functions
"""
print_section(
"DEMO 2: hooks_to_string() Utility (NEW! ✨)",
"Convert Python functions to strings for REST API"
)
print("📦 Creating hook functions...")
print(" • performance_optimization_hook")
print(" • viewport_setup_hook")
print(" • authentication_headers_hook")
print(" • lazy_loading_handler_hook")
# Convert function objects to strings using the NEW utility
print("\n🔄 Converting functions to strings with hooks_to_string()...")
hooks_dict = {
"on_page_context_created": performance_optimization_hook,
"before_goto": authentication_headers_hook,
"before_retrieve_html": lazy_loading_handler_hook,
}
hooks_as_strings = hooks_to_string(hooks_dict)
print(f"✅ Successfully converted {len(hooks_as_strings)} functions to strings")
# Show a preview
print("\n📝 Sample converted hook (first 250 characters):")
print("" * 70)
sample_hook = list(hooks_as_strings.values())[0]
print(sample_hook[:250] + "...")
print("" * 70)
# Use the converted hooks with REST API
print("\n📡 Using converted hooks with REST API...")
payload = {
"urls": [TEST_URLS[0]],
"hooks": {
"code": hooks_as_strings,
"timeout": 30
}
}
try:
start_time = time.time()
response = requests.post(f"{DOCKER_URL}/crawl", json=payload, timeout=60)
execution_time = time.time() - start_time
if response.status_code == 200:
result = response.json()
print(f"\n✅ Request successful! (took {execution_time:.2f}s)")
if result.get('results') and result['results'][0].get('success'):
crawl_result = result['results'][0]
print(f" • HTML length: {len(crawl_result.get('html', '')):,} characters")
print(f" • Hooks executed successfully!")
else:
print(f"❌ Request failed: {response.status_code}")
except Exception as e:
print(f"❌ Error: {str(e)}")
print("\n💡 Benefits of hooks_to_string():")
print(" ✓ Write hooks as regular Python functions")
print(" ✓ Full IDE support (autocomplete, syntax highlighting)")
print(" ✓ Type checking and linting")
print(" ✓ Easy to test and debug")
print(" ✓ Reusable across projects")
print(" ✓ Works with any REST API client")
print("\n" + "" * 70)
print("✓ hooks_to_string() utility demo complete\n")
# ============================================================================
# DEMO 3: Docker Client with Automatic Conversion (RECOMMENDED! 🌟)
# ============================================================================
async def demo_3_docker_client_auto_conversion():
"""
Demonstrate Docker Client with automatic hook conversion (RECOMMENDED)
"""
print_section(
"DEMO 3: Docker Client with Auto-Conversion (RECOMMENDED! 🌟)",
"Pass function objects directly - conversion happens automatically!"
)
print("🐳 Initializing Crawl4AI Docker Client...")
client = Crawl4aiDockerClient(base_url=DOCKER_URL)
print("✅ Client ready!\n")
# Use our reusable hook library - just pass the function objects!
print("📚 Using reusable hook library:")
print(" • performance_optimization_hook")
print(" • viewport_setup_hook")
print(" • authentication_headers_hook")
print(" • lazy_loading_handler_hook")
print(" • page_analytics_hook")
print("\n🎯 Target URL: " + TEST_URLS[1])
print("🚀 Starting crawl with automatic hook conversion...\n")
try:
start_time = time.time()
# Pass function objects directly - NO manual conversion needed! ✨
results = await client.crawl(
urls=[TEST_URLS[0]],
hooks={
"on_page_context_created": performance_optimization_hook,
"before_goto": authentication_headers_hook,
"before_retrieve_html": lazy_loading_handler_hook,
"before_return_html": page_analytics_hook,
},
hooks_timeout=30
)
execution_time = time.time() - start_time
print(f"\n✅ Crawl completed! (took {execution_time:.2f}s)\n")
# Display results
if results and results.success:
result = results
print(f"📊 Results:")
print(f" • URL: {result.url}")
print(f" • Success: {result.success}")
print(f" • HTML length: {len(result.html):,} characters")
print(f" • Markdown length: {len(result.markdown):,} characters")
# Show metadata
if result.metadata:
print(f"\n📋 Metadata:")
print(f" • Title: {result.metadata.get('title', 'N/A')}")
print(f" • Description: {result.metadata.get('description', 'N/A')}")
# Show links
if result.links:
internal_count = len(result.links.get('internal', []))
external_count = len(result.links.get('external', []))
print(f"\n🔗 Links Found:")
print(f" • Internal: {internal_count}")
print(f" • External: {external_count}")
else:
print(f"⚠️ Crawl completed but no successful results")
if results:
print(f" Error: {results.error_message}")
except Exception as e:
print(f"❌ Error: {str(e)}")
import traceback
traceback.print_exc()
print("\n🌟 Why Docker Client is RECOMMENDED:")
print(" ✓ Automatic function-to-string conversion")
print(" ✓ No manual hooks_to_string() calls needed")
print(" ✓ Cleaner, more Pythonic code")
print(" ✓ Full type hints and IDE support")
print(" ✓ Built-in error handling")
print(" ✓ Async/await support")
print("\n" + "" * 70)
print("✓ Docker Client auto-conversion demo complete\n")
# ============================================================================
# DEMO 4: Advanced Use Case - Complete Hook Pipeline
# ============================================================================
async def demo_4_complete_hook_pipeline():
"""
Demonstrate a complete hook pipeline using all 8 hook points
"""
print_section(
"DEMO 4: Complete Hook Pipeline",
"Using all 8 available hook points for comprehensive control"
)
# Define all 8 hooks
async def on_browser_created_hook(browser, **kwargs):
"""Hook 1: Called after browser is created"""
print(" [Pipeline] 1/8 Browser created")
return browser
async def on_page_context_created_hook(page, context, **kwargs):
"""Hook 2: Called after page context is created"""
print(" [Pipeline] 2/8 Page context created - setting up...")
await page.set_viewport_size({"width": 1920, "height": 1080})
return page
async def on_user_agent_updated_hook(page, context, user_agent, **kwargs):
"""Hook 3: Called when user agent is updated"""
print(f" [Pipeline] 3/8 User agent updated: {user_agent[:50]}...")
return page
async def before_goto_hook(page, context, url, **kwargs):
"""Hook 4: Called before navigating to URL"""
print(f" [Pipeline] 4/8 Before navigation to: {url[:60]}...")
return page
async def after_goto_hook(page, context, url, response, **kwargs):
"""Hook 5: Called after navigation completes"""
print(f" [Pipeline] 5/8 After navigation - Status: {response.status if response else 'N/A'}")
await page.wait_for_timeout(1000)
return page
async def on_execution_started_hook(page, context, **kwargs):
"""Hook 6: Called when JavaScript execution starts"""
print(" [Pipeline] 6/8 JavaScript execution started")
return page
async def before_retrieve_html_hook(page, context, **kwargs):
"""Hook 7: Called before retrieving HTML"""
print(" [Pipeline] 7/8 Before HTML retrieval - scrolling...")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
return page
async def before_return_html_hook(page, context, html, **kwargs):
"""Hook 8: Called before returning HTML"""
print(f" [Pipeline] 8/8 Before return - HTML length: {len(html):,} chars")
return page
print("🎯 Target URL: " + TEST_URLS[0])
print("🔧 Configured ALL 8 hook points for complete pipeline control\n")
client = Crawl4aiDockerClient(base_url=DOCKER_URL)
try:
print("🚀 Starting complete pipeline crawl...\n")
start_time = time.time()
results = await client.crawl(
urls=[TEST_URLS[0]],
hooks={
"on_browser_created": on_browser_created_hook,
"on_page_context_created": on_page_context_created_hook,
"on_user_agent_updated": on_user_agent_updated_hook,
"before_goto": before_goto_hook,
"after_goto": after_goto_hook,
"on_execution_started": on_execution_started_hook,
"before_retrieve_html": before_retrieve_html_hook,
"before_return_html": before_return_html_hook,
},
hooks_timeout=45
)
execution_time = time.time() - start_time
if results and results.success:
print(f"\n✅ Complete pipeline executed successfully! (took {execution_time:.2f}s)")
print(f" • All 8 hooks executed in sequence")
print(f" • HTML length: {len(results.html):,} characters")
else:
print(f"⚠️ Pipeline completed with warnings")
except Exception as e:
print(f"❌ Error: {str(e)}")
print("\n📚 Available Hook Points:")
print(" 1. on_browser_created - Browser initialization")
print(" 2. on_page_context_created - Page context setup")
print(" 3. on_user_agent_updated - User agent configuration")
print(" 4. before_goto - Pre-navigation setup")
print(" 5. after_goto - Post-navigation processing")
print(" 6. on_execution_started - JavaScript execution start")
print(" 7. before_retrieve_html - Pre-extraction processing")
print(" 8. before_return_html - Final HTML processing")
print("\n" + "" * 70)
print("✓ Complete hook pipeline demo complete\n")
# ============================================================================
# MAIN EXECUTION
# ============================================================================
async def main():
"""
Run all demonstrations
"""
print("\n" + "=" * 70)
print(" 🚀 Crawl4AI v0.7.5 - Docker Hooks Complete Demonstration")
print("=" * 70)
# Check Docker service
print("\n🔍 Checking Docker service status...")
if not check_docker_service():
print("❌ Docker service is not running!")
print("\n📋 To start the Docker service:")
print(" docker run -p 11235:11235 unclecode/crawl4ai:latest")
print("\nPlease start the service and run this demo again.")
return
print("✅ Docker service is running!\n")
# Run all demos
demos = [
("String-Based Hooks (REST API)", demo_1_string_based_hooks, False),
("hooks_to_string() Utility", demo_2_hooks_to_string_utility, False),
("Docker Client Auto-Conversion", demo_3_docker_client_auto_conversion, True),
# ("Complete Hook Pipeline", demo_4_complete_hook_pipeline, True),
]
for i, (name, demo_func, is_async) in enumerate(demos, 1):
print(f"\n{'🔷' * 35}")
print(f"Starting Demo {i}/{len(demos)}: {name}")
print(f"{'🔷' * 35}\n")
try:
if is_async:
await demo_func()
else:
demo_func()
print(f"✅ Demo {i} completed successfully!")
# Pause between demos (except the last one)
if i < len(demos):
print("\n⏸️ Press Enter to continue to next demo...")
# input()
except KeyboardInterrupt:
print(f"\n⏹️ Demo interrupted by user")
break
except Exception as e:
print(f"\n❌ Demo {i} failed: {str(e)}")
import traceback
traceback.print_exc()
print("\nContinuing to next demo...\n")
continue
# Final summary
print("\n" + "=" * 70)
print(" 🎉 All Demonstrations Complete!")
print("=" * 70)
print("\n📊 Summary of v0.7.5 Docker Hooks System:")
print("\n🆕 COMPLETELY NEW FEATURE in v0.7.5:")
print(" The Docker Hooks System lets you customize the crawling pipeline")
print(" with user-provided Python functions at 8 strategic points.")
print("\n✨ Three Ways to Use Docker Hooks (All NEW!):")
print(" 1. String-based - Write hooks as strings for REST API")
print(" 2. hooks_to_string() - Convert Python functions to strings")
print(" 3. Docker Client - Automatic conversion (RECOMMENDED)")
print("\n💡 Key Benefits:")
print(" ✓ Full IDE support (autocomplete, syntax highlighting)")
print(" ✓ Type checking and linting")
print(" ✓ Easy to test and debug")
print(" ✓ Reusable across projects")
print(" ✓ Complete pipeline control")
print("\n🎯 8 Hook Points Available:")
print(" • on_browser_created, on_page_context_created")
print(" • on_user_agent_updated, before_goto, after_goto")
print(" • on_execution_started, before_retrieve_html, before_return_html")
print("\n📚 Resources:")
print(" • Docs: https://docs.crawl4ai.com")
print(" • GitHub: https://github.com/unclecode/crawl4ai")
print(" • Discord: https://discord.gg/jP8KfhDhyN")
print("\n" + "=" * 70)
print(" Happy Crawling with v0.7.5! 🕷️")
print("=" * 70 + "\n")
if __name__ == "__main__":
print("\n🎬 Starting Crawl4AI v0.7.5 Docker Hooks Demonstration...")
print("Press Ctrl+C anytime to exit\n")
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n👋 Demo stopped by user. Thanks for exploring Crawl4AI v0.7.5!")
except Exception as e:
print(f"\n\n❌ Demo error: {str(e)}")
import traceback
traceback.print_exc()
File diff suppressed because it is too large Load Diff
+464
View File
@@ -0,0 +1,464 @@
"""
Crawl4AI v0.4.24 Feature Walkthrough
===================================
This script demonstrates the new features introduced in Crawl4AI v0.4.24.
Each section includes detailed examples and explanations of the new capabilities.
"""
import asyncio
import os
import json
import re
from typing import List
from crawl4ai import (
AsyncWebCrawler,
BrowserConfig,
CrawlerRunConfig,
CacheMode,
LLMExtractionStrategy,
JsonCssExtractionStrategy,
)
from crawl4ai.content_filter_strategy import RelevantContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
from bs4 import BeautifulSoup
# Sample HTML for demonstrations
SAMPLE_HTML = """
<div class="article-list">
<article class="post" data-category="tech" data-author="john">
<h2 class="title"><a href="/post-1">First Post</a></h2>
<div class="meta">
<a href="/author/john" class="author">John Doe</a>
<span class="date">2023-12-31</span>
</div>
<div class="content">
<p>First post content...</p>
<a href="/read-more-1" class="read-more">Read More</a>
</div>
</article>
<article class="post" data-category="science" data-author="jane">
<h2 class="title"><a href="/post-2">Second Post</a></h2>
<div class="meta">
<a href="/author/jane" class="author">Jane Smith</a>
<span class="date">2023-12-30</span>
</div>
<div class="content">
<p>Second post content...</p>
<a href="/read-more-2" class="read-more">Read More</a>
</div>
</article>
</div>
"""
async def demo_ssl_features():
"""
Enhanced SSL & Security Features Demo
-----------------------------------
This example demonstrates the new SSL certificate handling and security features:
1. Custom certificate paths
2. SSL verification options
3. HTTPS error handling
4. Certificate validation configurations
These features are particularly useful when:
- Working with self-signed certificates
- Dealing with corporate proxies
- Handling mixed content websites
- Managing different SSL security levels
"""
print("\n1. Enhanced SSL & Security Demo")
print("--------------------------------")
browser_config = BrowserConfig()
run_config = CrawlerRunConfig(
cache_mode=CacheMode.BYPASS,
fetch_ssl_certificate=True, # Enable SSL certificate fetching
)
async with AsyncWebCrawler(config=browser_config) as crawler:
result = await crawler.arun(url="https://example.com", config=run_config)
print(f"SSL Crawl Success: {result.success}")
result.ssl_certificate.to_json(
os.path.join(os.getcwd(), "ssl_certificate.json")
)
if not result.success:
print(f"SSL Error: {result.error_message}")
async def demo_content_filtering():
"""
Smart Content Filtering Demo
----------------------
Demonstrates advanced content filtering capabilities:
1. Custom filter to identify and extract specific content
2. Integration with markdown generation
3. Flexible pruning rules
"""
print("\n2. Smart Content Filtering Demo")
print("--------------------------------")
# Create a custom content filter
class CustomNewsFilter(RelevantContentFilter):
def __init__(self):
super().__init__()
# Add news-specific patterns
self.negative_patterns = re.compile(
r"nav|footer|header|sidebar|ads|comment|share|related|recommended|popular|trending",
re.I,
)
self.min_word_count = 30 # Higher threshold for news content
def filter_content(
self, html: str, min_word_threshold: int = None
) -> List[str]:
"""
Implements news-specific content filtering logic.
Args:
html (str): HTML content to be filtered
min_word_threshold (int, optional): Minimum word count threshold
Returns:
List[str]: List of filtered HTML content blocks
"""
if not html or not isinstance(html, str):
return []
soup = BeautifulSoup(html, "lxml")
if not soup.body:
soup = BeautifulSoup(f"<body>{html}</body>", "lxml")
body = soup.find("body")
# Extract chunks with metadata
chunks = self.extract_text_chunks(
body, min_word_threshold or self.min_word_count
)
# Filter chunks based on news-specific criteria
filtered_chunks = []
for _, text, tag_type, element in chunks:
# Skip if element has negative class/id
if self.is_excluded(element):
continue
# Headers are important in news articles
if tag_type == "header":
filtered_chunks.append(self.clean_element(element))
continue
# For content, check word count and link density
text = element.get_text(strip=True)
if len(text.split()) >= (min_word_threshold or self.min_word_count):
# Calculate link density
links_text = " ".join(
a.get_text(strip=True) for a in element.find_all("a")
)
link_density = len(links_text) / len(text) if text else 1
# Accept if link density is reasonable
if link_density < 0.5:
filtered_chunks.append(self.clean_element(element))
return filtered_chunks
# Create markdown generator with custom filter
markdown_gen = DefaultMarkdownGenerator(content_filter=CustomNewsFilter())
run_config = CrawlerRunConfig(
markdown_generator=markdown_gen, cache_mode=CacheMode.BYPASS
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://news.ycombinator.com", config=run_config
)
print("Filtered Content Sample:")
print(result.markdown[:500]) # Show first 500 chars
async def demo_json_extraction():
"""
Improved JSON Extraction Demo
---------------------------
Demonstrates the enhanced JSON extraction capabilities:
1. Base element attributes extraction
2. Complex nested structures
3. Multiple extraction patterns
Key features shown:
- Extracting attributes from base elements (href, data-* attributes)
- Processing repeated patterns
- Handling optional fields
"""
print("\n3. Improved JSON Extraction Demo")
print("--------------------------------")
# Define the extraction schema with base element attributes
json_strategy = JsonCssExtractionStrategy(
schema={
"name": "Blog Posts",
"baseSelector": "div.article-list",
"baseFields": [
{"name": "list_id", "type": "attribute", "attribute": "data-list-id"},
{"name": "category", "type": "attribute", "attribute": "data-category"},
],
"fields": [
{
"name": "posts",
"selector": "article.post",
"type": "nested_list",
"baseFields": [
{
"name": "post_id",
"type": "attribute",
"attribute": "data-post-id",
},
{
"name": "author_id",
"type": "attribute",
"attribute": "data-author",
},
],
"fields": [
{
"name": "title",
"selector": "h2.title a",
"type": "text",
"baseFields": [
{
"name": "url",
"type": "attribute",
"attribute": "href",
}
],
},
{
"name": "author",
"selector": "div.meta a.author",
"type": "text",
"baseFields": [
{
"name": "profile_url",
"type": "attribute",
"attribute": "href",
}
],
},
{"name": "date", "selector": "span.date", "type": "text"},
{
"name": "read_more",
"selector": "a.read-more",
"type": "nested",
"fields": [
{"name": "text", "type": "text"},
{
"name": "url",
"type": "attribute",
"attribute": "href",
},
],
},
],
}
],
}
)
# Demonstrate extraction from raw HTML
run_config = CrawlerRunConfig(
extraction_strategy=json_strategy, cache_mode=CacheMode.BYPASS
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="raw:" + SAMPLE_HTML, # Use raw: prefix for raw HTML
config=run_config,
)
print("Extracted Content:")
print(result.extracted_content)
async def demo_input_formats():
"""
Input Format Handling Demo
----------------------
Demonstrates how LLM extraction can work with different input formats:
1. Markdown (default) - Good for simple text extraction
2. HTML - Better when you need structure and attributes
This example shows how HTML input can be beneficial when:
- You need to understand the DOM structure
- You want to extract both visible text and HTML attributes
- The content has complex layouts like tables or forms
"""
print("\n4. Input Format Handling Demo")
print("---------------------------")
# Create a dummy HTML with rich structure
dummy_html = """
<div class="job-posting" data-post-id="12345">
<header class="job-header">
<h1 class="job-title">Senior AI/ML Engineer</h1>
<div class="job-meta">
<span class="department">AI Research Division</span>
<span class="location" data-remote="hybrid">San Francisco (Hybrid)</span>
</div>
<div class="salary-info" data-currency="USD">
<span class="range">$150,000 - $220,000</span>
<span class="period">per year</span>
</div>
</header>
<section class="requirements">
<div class="technical-skills">
<h3>Technical Requirements</h3>
<ul class="required-skills">
<li class="skill required" data-priority="must-have">
5+ years experience in Machine Learning
</li>
<li class="skill required" data-priority="must-have">
Proficiency in Python and PyTorch/TensorFlow
</li>
<li class="skill preferred" data-priority="nice-to-have">
Experience with distributed training systems
</li>
</ul>
</div>
<div class="soft-skills">
<h3>Professional Skills</h3>
<ul class="required-skills">
<li class="skill required" data-priority="must-have">
Strong problem-solving abilities
</li>
<li class="skill preferred" data-priority="nice-to-have">
Experience leading technical teams
</li>
</ul>
</div>
</section>
<section class="timeline">
<time class="deadline" datetime="2024-02-28">
Application Deadline: February 28, 2024
</time>
</section>
<footer class="contact-section">
<div class="hiring-manager">
<h4>Hiring Manager</h4>
<div class="contact-info">
<span class="name">Dr. Sarah Chen</span>
<span class="title">Director of AI Research</span>
<span class="email">ai.hiring@example.com</span>
</div>
</div>
<div class="team-info">
<p>Join our team of 50+ researchers working on cutting-edge AI applications</p>
</div>
</footer>
</div>
"""
# Use raw:// prefix to pass HTML content directly
url = f"raw://{dummy_html}"
from pydantic import BaseModel, Field
from typing import List, Optional
# Define our schema using Pydantic
class JobRequirement(BaseModel):
category: str = Field(
description="Category of the requirement (e.g., Technical, Soft Skills)"
)
items: List[str] = Field(
description="List of specific requirements in this category"
)
priority: str = Field(
description="Priority level (Required/Preferred) based on the HTML class or context"
)
class JobPosting(BaseModel):
title: str = Field(description="Job title")
department: str = Field(description="Department or team")
location: str = Field(description="Job location, including remote options")
salary_range: Optional[str] = Field(description="Salary range if specified")
requirements: List[JobRequirement] = Field(
description="Categorized job requirements"
)
application_deadline: Optional[str] = Field(
description="Application deadline if specified"
)
contact_info: Optional[dict] = Field(
description="Contact information from footer or contact section"
)
# First try with markdown (default)
markdown_strategy = LLMExtractionStrategy(
provider="openai/gpt-4o",
api_token=os.getenv("OPENAI_API_KEY"),
schema=JobPosting.model_json_schema(),
extraction_type="schema",
instruction="""
Extract job posting details into structured data. Focus on the visible text content
and organize requirements into categories.
""",
input_format="markdown", # default
)
# Then with HTML for better structure understanding
html_strategy = LLMExtractionStrategy(
provider="openai/gpt-4",
api_token=os.getenv("OPENAI_API_KEY"),
schema=JobPosting.model_json_schema(),
extraction_type="schema",
instruction="""
Extract job posting details, using HTML structure to:
1. Identify requirement priorities from CSS classes (e.g., 'required' vs 'preferred')
2. Extract contact info from the page footer or dedicated contact section
3. Parse salary information from specially formatted elements
4. Determine application deadline from timestamp or date elements
Use HTML attributes and classes to enhance extraction accuracy.
""",
input_format="html", # explicitly use HTML
)
async with AsyncWebCrawler() as crawler:
# Try with markdown first
markdown_config = CrawlerRunConfig(extraction_strategy=markdown_strategy)
markdown_result = await crawler.arun(url=url, config=markdown_config)
print("\nMarkdown-based Extraction Result:")
items = json.loads(markdown_result.extracted_content)
print(json.dumps(items, indent=2))
# Then with HTML for better structure understanding
html_config = CrawlerRunConfig(extraction_strategy=html_strategy)
html_result = await crawler.arun(url=url, config=html_config)
print("\nHTML-based Extraction Result:")
items = json.loads(html_result.extracted_content)
print(json.dumps(items, indent=2))
# Main execution
async def main():
print("Crawl4AI v0.4.24 Feature Walkthrough")
print("====================================")
# Run all demos
await demo_ssl_features()
await demo_content_filtering()
await demo_json_extraction()
# await demo_input_formats()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,349 @@
"""
Crawl4ai v0.4.3b2 Features Demo
============================
This demonstration showcases three major categories of new features in Crawl4ai v0.4.3:
1. Efficiency & Speed:
- Memory-efficient dispatcher strategies
- New scraping algorithm
- Streaming support for batch crawling
2. LLM Integration:
- Automatic schema generation
- LLM-powered content filtering
- Smart markdown generation
3. Core Improvements:
- Robots.txt compliance
- Proxy rotation
- Enhanced URL handling
- Shared data among hooks
- add page routes
Each demo function can be run independently or as part of the full suite.
"""
import asyncio
import os
import json
import re
import random
from typing import Optional, Dict
from dotenv import load_dotenv
from crawl4ai import (
AsyncWebCrawler,
BrowserConfig,
CrawlerRunConfig,
CacheMode,
DisplayMode,
MemoryAdaptiveDispatcher,
CrawlerMonitor,
DefaultMarkdownGenerator,
LXMLWebScrapingStrategy,
JsonCssExtractionStrategy,
LLMContentFilter
)
load_dotenv()
async def demo_memory_dispatcher():
"""Demonstrates the new memory-efficient dispatcher system.
Key Features:
- Adaptive memory management
- Real-time performance monitoring
- Concurrent session control
"""
print("\n=== Memory Dispatcher Demo ===")
try:
# Configuration
browser_config = BrowserConfig(headless=True, verbose=False)
crawler_config = CrawlerRunConfig(
cache_mode=CacheMode.BYPASS,
markdown_generator=DefaultMarkdownGenerator()
)
# Test URLs
urls = ["http://example.com", "http://example.org", "http://example.net"] * 3
print("\n📈 Initializing crawler with memory monitoring...")
async with AsyncWebCrawler(config=browser_config) as crawler:
monitor = CrawlerMonitor(
max_visible_rows=10,
display_mode=DisplayMode.DETAILED
)
dispatcher = MemoryAdaptiveDispatcher(
memory_threshold_percent=80.0,
check_interval=0.5,
max_session_permit=5,
monitor=monitor
)
print("\n🚀 Starting batch crawl...")
results = await crawler.arun_many(
urls=urls,
config=crawler_config,
dispatcher=dispatcher
)
print(f"\n✅ Completed {len(results)} URLs successfully")
except Exception as e:
print(f"\n❌ Error in memory dispatcher demo: {str(e)}")
async def demo_streaming_support():
"""
2. Streaming Support Demo
======================
Shows how to process URLs as they complete using streaming
"""
print("\n=== 2. Streaming Support Demo ===")
browser_config = BrowserConfig(headless=True, verbose=False)
crawler_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS, stream=True)
# Test URLs
urls = ["http://example.com", "http://example.org", "http://example.net"] * 2
async with AsyncWebCrawler(config=browser_config) as crawler:
# Initialize dispatcher for streaming
dispatcher = MemoryAdaptiveDispatcher(max_session_permit=3, check_interval=0.5)
print("Starting streaming crawl...")
async for result in await crawler.arun_many(
urls=urls,
config=crawler_config,
dispatcher=dispatcher
):
# Process each result as it arrives
print(
f"Received result for {result.url} - Success: {result.success}"
)
if result.success:
print(f"Content length: {len(result.markdown)}")
async def demo_content_scraping():
"""
3. Content Scraping Strategy Demo
==============================
Demonstrates the new LXMLWebScrapingStrategy for faster content scraping.
"""
print("\n=== 3. Content Scraping Strategy Demo ===")
crawler = AsyncWebCrawler()
url = "https://example.com/article"
# Configure with the new LXML strategy
config = CrawlerRunConfig(
scraping_strategy=LXMLWebScrapingStrategy(),
verbose=True
)
print("Scraping content with LXML strategy...")
async with crawler:
result = await crawler.arun(url, config=config)
if result.success:
print("Successfully scraped content using LXML strategy")
async def demo_llm_markdown():
"""
4. LLM-Powered Markdown Generation Demo
===================================
Shows how to use the new LLM-powered content filtering and markdown generation.
"""
print("\n=== 4. LLM-Powered Markdown Generation Demo ===")
crawler = AsyncWebCrawler()
url = "https://docs.python.org/3/tutorial/classes.html"
content_filter = LLMContentFilter(
provider="openai/gpt-4o",
api_token=os.getenv("OPENAI_API_KEY"),
instruction="""
Focus on extracting the core educational content about Python classes.
Include:
- Key concepts and their explanations
- Important code examples
- Essential technical details
Exclude:
- Navigation elements
- Sidebars
- Footer content
- Version information
- Any non-essential UI elements
Format the output as clean markdown with proper code blocks and headers.
""",
verbose=True,
)
# Configure LLM-powered markdown generation
config = CrawlerRunConfig(
markdown_generator=DefaultMarkdownGenerator(
content_filter=content_filter
),
cache_mode = CacheMode.BYPASS,
verbose=True
)
print("Generating focused markdown with LLM...")
async with crawler:
result = await crawler.arun(url, config=config)
if result.success and result.markdown_v2:
print("Successfully generated LLM-filtered markdown")
print("First 500 chars of filtered content:")
print(result.markdown_v2.fit_markdown[:500])
print("Successfully generated LLM-filtered markdown")
async def demo_robots_compliance():
"""
5. Robots.txt Compliance Demo
==========================
Demonstrates the new robots.txt compliance feature with SQLite caching.
"""
print("\n=== 5. Robots.txt Compliance Demo ===")
crawler = AsyncWebCrawler()
urls = ["https://example.com", "https://facebook.com", "https://twitter.com"]
# Enable robots.txt checking
config = CrawlerRunConfig(check_robots_txt=True, verbose=True)
print("Crawling with robots.txt compliance...")
async with crawler:
results = await crawler.arun_many(urls, config=config)
for result in results:
if result.status_code == 403:
print(f"Access blocked by robots.txt: {result.url}")
elif result.success:
print(f"Successfully crawled: {result.url}")
async def demo_json_schema_generation():
"""
7. LLM-Powered Schema Generation Demo
=================================
Demonstrates automatic CSS and XPath schema generation using LLM models.
"""
print("\n=== 7. LLM-Powered Schema Generation Demo ===")
# Example HTML content for a job listing
html_content = """
<div class="job-listing">
<h1 class="job-title">Senior Software Engineer</h1>
<div class="job-details">
<span class="location">San Francisco, CA</span>
<span class="salary">$150,000 - $200,000</span>
<div class="requirements">
<h2>Requirements</h2>
<ul>
<li>5+ years Python experience</li>
<li>Strong background in web crawling</li>
</ul>
</div>
</div>
</div>
"""
print("Generating CSS selectors schema...")
# Generate CSS selectors with a specific query
css_schema = JsonCssExtractionStrategy.generate_schema(
html_content,
schema_type="CSS",
query="Extract job title, location, and salary information",
provider="openai/gpt-4o", # or use other providers like "ollama"
)
print("\nGenerated CSS Schema:")
print(css_schema)
# Example of using the generated schema with crawler
crawler = AsyncWebCrawler()
url = "https://example.com/job-listing"
# Create an extraction strategy with the generated schema
extraction_strategy = JsonCssExtractionStrategy(schema=css_schema)
config = CrawlerRunConfig(extraction_strategy=extraction_strategy, verbose=True)
print("\nTesting generated schema with crawler...")
async with crawler:
result = await crawler.arun(url, config=config)
if result.success:
print(json.dumps(result.extracted_content, indent=2) if result.extracted_content else None)
print("Successfully used generated schema for crawling")
async def demo_proxy_rotation():
"""
8. Proxy Rotation Demo
===================
Demonstrates how to rotate proxies for each request using Crawl4ai.
"""
print("\n=== 8. Proxy Rotation Demo ===")
async def get_next_proxy(proxy_file: str = "proxies.txt") -> Optional[Dict]:
"""Get next proxy from local file"""
try:
proxies = os.getenv("PROXIES", "").split(",")
ip, port, username, password = random.choice(proxies).split(":")
return {
"server": f"http://{ip}:{port}",
"username": username,
"password": password,
"ip": ip # Store original IP for verification
}
except Exception as e:
print(f"Error loading proxy: {e}")
return None
# Create 10 test requests to httpbin
urls = ["https://httpbin.org/ip"] * 2
browser_config = BrowserConfig(headless=True, verbose=False)
run_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
async with AsyncWebCrawler(config=browser_config) as crawler:
for url in urls:
proxy = await get_next_proxy()
if not proxy:
print("No proxy available, skipping...")
continue
# Create new config with proxy
current_config = run_config.clone(proxy_config=proxy, user_agent="")
result = await crawler.arun(url=url, config=current_config)
if result.success:
ip_match = re.search(r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}', result.html)
print(f"Proxy {proxy['ip']} -> Response IP: {ip_match.group(0) if ip_match else 'Not found'}")
verified = ip_match.group(0) == proxy['ip']
if verified:
print(f"✅ Proxy working! IP matches: {proxy['ip']}")
else:
print("❌ Proxy failed or IP mismatch!")
else:
print(f"Failed with proxy {proxy['ip']}")
async def main():
"""Run all feature demonstrations."""
print("\n📊 Running Crawl4ai v0.4.3 Feature Demos\n")
# Efficiency & Speed Demos
print("\n🚀 EFFICIENCY & SPEED DEMOS")
await demo_memory_dispatcher()
await demo_streaming_support()
await demo_content_scraping()
# # LLM Integration Demos
print("\n🤖 LLM INTEGRATION DEMOS")
await demo_json_schema_generation()
await demo_llm_markdown()
# # Core Improvements
print("\n🔧 CORE IMPROVEMENT DEMOS")
await demo_robots_compliance()
await demo_proxy_rotation()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,280 @@
"""
🚀 Crawl4AI v0.7.0 Feature Demo
================================
This file demonstrates the major features introduced in v0.7.0 with practical examples.
"""
import asyncio
import json
from pathlib import Path
from crawl4ai import (
AsyncWebCrawler,
CrawlerRunConfig,
BrowserConfig,
CacheMode,
# New imports for v0.7.0
VirtualScrollConfig,
LinkPreviewConfig,
AdaptiveCrawler,
AdaptiveConfig,
AsyncUrlSeeder,
SeedingConfig,
c4a_compile,
)
async def demo_link_preview():
"""
Demo 1: Link Preview with 3-Layer Scoring
Shows how to analyze links with intrinsic quality scores,
contextual relevance, and combined total scores.
"""
print("\n" + "="*60)
print("🔗 DEMO 1: Link Preview & Intelligent Scoring")
print("="*60)
# Configure link preview with contextual scoring
config = CrawlerRunConfig(
link_preview_config=LinkPreviewConfig(
include_internal=True,
include_external=False,
max_links=10,
concurrency=5,
query="machine learning tutorials", # For contextual scoring
score_threshold=0.3, # Minimum relevance
verbose=True
),
score_links=True, # Enable intrinsic scoring
cache_mode=CacheMode.BYPASS
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://scikit-learn.org/stable/", config=config)
if result.success:
# Get scored links
internal_links = result.links.get("internal", [])
scored_links = [l for l in internal_links if l.get("total_score")]
scored_links.sort(key=lambda x: x.get("total_score", 0), reverse=True)
print(f"\nTop 5 Most Relevant Links:")
for i, link in enumerate(scored_links[:5], 1):
print(f"\n{i}. {link.get('text', 'No text')[:50]}...")
print(f" URL: {link['href']}")
print(f" Intrinsic Score: {link.get('intrinsic_score', 0):.2f}/10")
print(f" Contextual Score: {link.get('contextual_score', 0):.3f}")
print(f" Total Score: {link.get('total_score', 0):.3f}")
# Show metadata if available
if link.get('head_data'):
title = link['head_data'].get('title', 'No title')
print(f" Title: {title[:60]}...")
async def demo_adaptive_crawling():
"""
Demo 2: Adaptive Crawling
Shows intelligent crawling that stops when enough information
is gathered, with confidence tracking.
"""
print("\n" + "="*60)
print("🎯 DEMO 2: Adaptive Crawling with Confidence Tracking")
print("="*60)
# Configure adaptive crawler
config = AdaptiveConfig(
strategy="statistical", # or "embedding" for semantic understanding
max_pages=10,
confidence_threshold=0.7, # Stop at 70% confidence
top_k_links=3, # Follow top 3 links per page
min_gain_threshold=0.05 # Need 5% information gain to continue
)
async with AsyncWebCrawler(verbose=False) as crawler:
adaptive = AdaptiveCrawler(crawler, config)
print("Starting adaptive crawl about Python decorators...")
result = await adaptive.digest(
start_url="https://docs.python.org/3/glossary.html",
query="python decorators functions wrapping"
)
print(f"\n✅ Crawling Complete!")
print(f"• Confidence Level: {adaptive.confidence:.0%}")
print(f"• Pages Crawled: {len(result.crawled_urls)}")
print(f"• Knowledge Base: {len(adaptive.state.knowledge_base)} documents")
# Get most relevant content
relevant = adaptive.get_relevant_content(top_k=3)
print(f"\nMost Relevant Pages:")
for i, page in enumerate(relevant, 1):
print(f"{i}. {page['url']} (relevance: {page['score']:.2%})")
async def demo_virtual_scroll():
"""
Demo 3: Virtual Scroll for Modern Web Pages
Shows how to capture content from pages with DOM recycling
(Twitter, Instagram, infinite scroll).
"""
print("\n" + "="*60)
print("📜 DEMO 3: Virtual Scroll Support")
print("="*60)
# Configure virtual scroll for a news site
virtual_config = VirtualScrollConfig(
container_selector="main, article, .content", # Common containers
scroll_count=20, # Scroll up to 20 times
scroll_by="container_height", # Scroll by container height
wait_after_scroll=0.5 # Wait 500ms after each scroll
)
config = CrawlerRunConfig(
virtual_scroll_config=virtual_config,
cache_mode=CacheMode.BYPASS,
wait_for="css:article" # Wait for articles to load
)
# Example with a real news site
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
"https://news.ycombinator.com/",
config=config
)
if result.success:
# Count items captured
import re
items = len(re.findall(r'class="athing"', result.html))
print(f"\n✅ Captured {items} news items")
print(f"• HTML size: {len(result.html):,} bytes")
print(f"• Without virtual scroll, would only capture ~30 items")
async def demo_url_seeder():
"""
Demo 4: URL Seeder for Intelligent Discovery
Shows how to discover and filter URLs before crawling,
with relevance scoring.
"""
print("\n" + "="*60)
print("🌱 DEMO 4: URL Seeder - Smart URL Discovery")
print("="*60)
async with AsyncUrlSeeder() as seeder:
# Discover Python tutorial URLs
config = SeedingConfig(
source="sitemap", # Use sitemap
pattern="*python*", # URL pattern filter
extract_head=True, # Get metadata
query="python tutorial", # For relevance scoring
scoring_method="bm25",
score_threshold=0.2,
max_urls=10
)
print("Discovering Python async tutorial URLs...")
urls = await seeder.urls("https://www.geeksforgeeks.org/", config)
print(f"\n✅ Found {len(urls)} relevant URLs:")
for i, url_info in enumerate(urls[:5], 1):
print(f"\n{i}. {url_info['url']}")
if url_info.get('relevance_score'):
print(f" Relevance: {url_info['relevance_score']:.3f}")
if url_info.get('head_data', {}).get('title'):
print(f" Title: {url_info['head_data']['title'][:60]}...")
async def demo_c4a_script():
"""
Demo 5: C4A Script Language
Shows the domain-specific language for web automation
with JavaScript transpilation.
"""
print("\n" + "="*60)
print("🎭 DEMO 5: C4A Script - Web Automation Language")
print("="*60)
# Example C4A script
c4a_script = """
# E-commerce automation script
WAIT `body` 3
# Handle cookie banner
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
# Search for product
CLICK `.search-box`
TYPE "wireless headphones"
PRESS Enter
# Wait for results
WAIT `.product-grid` 10
# Load more products
REPEAT (SCROLL DOWN 500, `document.querySelectorAll('.product').length < 50`)
# Apply filter
IF (EXISTS `.price-filter`) THEN CLICK `input[data-max-price="100"]`
"""
# Compile the script
print("Compiling C4A script...")
result = c4a_compile(c4a_script)
if result.success:
print(f"✅ Successfully compiled to {len(result.js_code)} JavaScript statements!")
print("\nFirst 3 JS statements:")
for stmt in result.js_code[:3]:
print(f"{stmt}")
# Use with crawler
config = CrawlerRunConfig(
c4a_script=c4a_script, # Pass C4A script directly
cache_mode=CacheMode.BYPASS
)
print("\n✅ Script ready for use with AsyncWebCrawler!")
else:
print(f"❌ Compilation error: {result.first_error.message}")
async def main():
"""Run all demos"""
print("\n🚀 Crawl4AI v0.7.0 Feature Demonstrations")
print("=" * 60)
demos = [
("Link Preview & Scoring", demo_link_preview),
("Adaptive Crawling", demo_adaptive_crawling),
("Virtual Scroll", demo_virtual_scroll),
("URL Seeder", demo_url_seeder),
("C4A Script", demo_c4a_script),
]
for name, demo_func in demos:
try:
await demo_func()
except Exception as e:
print(f"\n❌ Error in {name} demo: {str(e)}")
# Pause between demos
await asyncio.sleep(1)
print("\n" + "="*60)
print("✅ All demos completed!")
print("\nKey Takeaways:")
print("• Link Preview: 3-layer scoring for intelligent link analysis")
print("• Adaptive Crawling: Stop when you have enough information")
print("• Virtual Scroll: Capture all content from modern web pages")
print("• URL Seeder: Pre-discover and filter URLs efficiently")
print("• C4A Script: Simple language for complex automations")
if __name__ == "__main__":
asyncio.run(main())