chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
# Adaptive Web Crawling
|
||||
|
||||
## Introduction
|
||||
|
||||
Traditional web crawlers follow predetermined patterns, crawling pages blindly without knowing when they've gathered enough information. **Adaptive Crawling** changes this paradigm by introducing intelligence into the crawling process.
|
||||
|
||||
Think of it like research: when you're looking for information, you don't read every book in the library. You stop when you've found sufficient information to answer your question. That's exactly what Adaptive Crawling does for web scraping.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### The Problem It Solves
|
||||
|
||||
When crawling websites for specific information, you face two challenges:
|
||||
1. **Under-crawling**: Stopping too early and missing crucial information
|
||||
2. **Over-crawling**: Wasting resources by crawling irrelevant pages
|
||||
|
||||
Adaptive Crawling solves both by using a three-layer scoring system that determines when you have "enough" information.
|
||||
|
||||
### How It Works
|
||||
|
||||
The AdaptiveCrawler uses three metrics to measure information sufficiency:
|
||||
|
||||
- **Coverage**: How well your collected pages cover the query terms
|
||||
- **Consistency**: Whether the information is coherent across pages
|
||||
- **Saturation**: Detecting when new pages aren't adding new information
|
||||
|
||||
When these metrics indicate sufficient information has been gathered, crawling stops automatically.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, AdaptiveCrawler
|
||||
|
||||
async def main():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Create an adaptive crawler (config is optional)
|
||||
adaptive = AdaptiveCrawler(crawler)
|
||||
|
||||
# Start crawling with a query
|
||||
result = await adaptive.digest(
|
||||
start_url="https://docs.python.org/3/",
|
||||
query="async context managers"
|
||||
)
|
||||
|
||||
# View statistics
|
||||
adaptive.print_stats()
|
||||
|
||||
# Get the most relevant content
|
||||
relevant_pages = adaptive.get_relevant_content(top_k=5)
|
||||
for page in relevant_pages:
|
||||
print(f"- {page['url']} (score: {page['score']:.2f})")
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```python
|
||||
from crawl4ai import AdaptiveConfig
|
||||
|
||||
config = AdaptiveConfig(
|
||||
confidence_threshold=0.8, # Stop when 80% confident (default: 0.7)
|
||||
max_pages=30, # Maximum pages to crawl (default: 20)
|
||||
top_k_links=5, # Links to follow per page (default: 3)
|
||||
min_gain_threshold=0.05 # Minimum expected gain to continue (default: 0.1)
|
||||
)
|
||||
|
||||
adaptive = AdaptiveCrawler(crawler, config)
|
||||
```
|
||||
|
||||
## Crawling Strategies
|
||||
|
||||
Adaptive Crawling supports two distinct strategies for determining information sufficiency:
|
||||
|
||||
### Statistical Strategy (Default)
|
||||
|
||||
The statistical strategy uses pure information theory and term-based analysis:
|
||||
|
||||
- **Fast and efficient** - No API calls or model loading
|
||||
- **Term-based coverage** - Analyzes query term presence and distribution
|
||||
- **No external dependencies** - Works offline
|
||||
- **Best for**: Well-defined queries with specific terminology
|
||||
|
||||
```python
|
||||
# Default configuration uses statistical strategy
|
||||
config = AdaptiveConfig(
|
||||
strategy="statistical", # This is the default
|
||||
confidence_threshold=0.8
|
||||
)
|
||||
```
|
||||
|
||||
### Embedding Strategy
|
||||
|
||||
The embedding strategy uses semantic embeddings for deeper understanding:
|
||||
|
||||
- **Semantic understanding** - Captures meaning beyond exact term matches
|
||||
- **Query expansion** - Automatically generates query variations
|
||||
- **Gap-driven selection** - Identifies semantic gaps in knowledge
|
||||
- **Validation-based stopping** - Uses held-out queries to validate coverage
|
||||
- **Best for**: Complex queries, ambiguous topics, conceptual understanding
|
||||
|
||||
```python
|
||||
# Configure embedding strategy with local embeddings
|
||||
config = AdaptiveConfig(
|
||||
strategy="embedding",
|
||||
embedding_model="sentence-transformers/all-MiniLM-L6-v2", # Default
|
||||
n_query_variations=10, # Generate 10 query variations
|
||||
embedding_min_confidence_threshold=0.1 # Stop if completely irrelevant
|
||||
)
|
||||
|
||||
# With separate LLM configs for embeddings and query expansion (recommended)
|
||||
from crawl4ai import LLMConfig
|
||||
|
||||
config = AdaptiveConfig(
|
||||
strategy="embedding",
|
||||
# Embedding model — used for text-to-vector calls
|
||||
embedding_llm_config=LLMConfig(
|
||||
provider='openai/text-embedding-3-small',
|
||||
api_token='your-api-key'
|
||||
),
|
||||
# Query model — used for chat completion (query expansion)
|
||||
query_llm_config=LLMConfig(
|
||||
provider='openai/gpt-4o-mini',
|
||||
api_token='your-api-key'
|
||||
)
|
||||
)
|
||||
|
||||
# Alternative: Dictionary format (backward compatible)
|
||||
config = AdaptiveConfig(
|
||||
strategy="embedding",
|
||||
embedding_llm_config={
|
||||
'provider': 'openai/text-embedding-3-small',
|
||||
'api_token': 'your-api-key'
|
||||
},
|
||||
query_llm_config={
|
||||
'provider': 'openai/gpt-4o-mini',
|
||||
'api_token': 'your-api-key'
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> **Note:** The embedding strategy makes two types of API calls that need different model types:
|
||||
> - **Embedding calls** (text → vector) require an embedding model like `text-embedding-3-small`
|
||||
> - **Query expansion** (chat completion) requires a chat model like `gpt-4o-mini`
|
||||
>
|
||||
> Use `embedding_llm_config` for the embedding model and `query_llm_config` for the chat model. If `query_llm_config` is not set, it falls back to `embedding_llm_config` for backward compatibility.
|
||||
|
||||
### Strategy Comparison
|
||||
|
||||
| Feature | Statistical | Embedding |
|
||||
|---------|------------|-----------|
|
||||
| **Speed** | Very fast | Moderate (API calls) |
|
||||
| **Cost** | Free | Depends on provider |
|
||||
| **Accuracy** | Good for exact terms | Excellent for concepts |
|
||||
| **Dependencies** | None | Embedding model/API |
|
||||
| **Query Understanding** | Literal | Semantic |
|
||||
| **Best Use Case** | Technical docs, specific terms | Research, broad topics |
|
||||
|
||||
### Embedding Strategy Configuration
|
||||
|
||||
The embedding strategy offers fine-tuned control through several parameters:
|
||||
|
||||
```python
|
||||
config = AdaptiveConfig(
|
||||
strategy="embedding",
|
||||
|
||||
# Model configuration
|
||||
embedding_model="sentence-transformers/all-MiniLM-L6-v2",
|
||||
embedding_llm_config=None, # Use for API-based embeddings (embedding model)
|
||||
query_llm_config=None, # Use for query expansion (chat completion model)
|
||||
|
||||
# Query expansion
|
||||
n_query_variations=10, # Number of query variations to generate
|
||||
|
||||
# Coverage parameters
|
||||
embedding_coverage_radius=0.2, # Distance threshold for coverage
|
||||
embedding_k_exp=3.0, # Exponential decay factor (higher = stricter)
|
||||
|
||||
# Stopping criteria
|
||||
embedding_min_relative_improvement=0.1, # Min improvement to continue
|
||||
embedding_validation_min_score=0.3, # Min validation score
|
||||
embedding_min_confidence_threshold=0.1, # Below this = irrelevant
|
||||
|
||||
# Link selection
|
||||
embedding_overlap_threshold=0.85, # Similarity for deduplication
|
||||
|
||||
# Display confidence mapping
|
||||
embedding_quality_min_confidence=0.7, # Min displayed confidence
|
||||
embedding_quality_max_confidence=0.95 # Max displayed confidence
|
||||
)
|
||||
```
|
||||
|
||||
### Handling Irrelevant Queries
|
||||
|
||||
The embedding strategy can detect when a query is completely unrelated to the content:
|
||||
|
||||
```python
|
||||
# This will stop quickly with low confidence
|
||||
result = await adaptive.digest(
|
||||
start_url="https://docs.python.org/3/",
|
||||
query="how to cook pasta" # Irrelevant to Python docs
|
||||
)
|
||||
|
||||
# Check if query was irrelevant
|
||||
if result.metrics.get('is_irrelevant', False):
|
||||
print("Query is unrelated to the content!")
|
||||
```
|
||||
|
||||
## When to Use Adaptive Crawling
|
||||
|
||||
### Perfect For:
|
||||
- **Research Tasks**: Finding comprehensive information about a topic
|
||||
- **Question Answering**: Gathering sufficient context to answer specific queries
|
||||
- **Knowledge Base Building**: Creating focused datasets for AI/ML applications
|
||||
- **Competitive Intelligence**: Collecting complete information about specific products/features
|
||||
|
||||
### Not Recommended For:
|
||||
- **Full Site Archiving**: When you need every page regardless of content
|
||||
- **Structured Data Extraction**: When targeting specific, known page patterns
|
||||
- **Real-time Monitoring**: When you need continuous updates
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
### Confidence Score
|
||||
|
||||
The confidence score (0-1) indicates how sufficient the gathered information is:
|
||||
- **0.0-0.3**: Insufficient information, needs more crawling
|
||||
- **0.3-0.6**: Partial information, may answer basic queries
|
||||
- **0.6-0.7**: Good coverage, can answer most queries
|
||||
- **0.7-1.0**: Excellent coverage, comprehensive information
|
||||
|
||||
### Statistics Display
|
||||
|
||||
```python
|
||||
adaptive.print_stats(detailed=False) # Summary table
|
||||
adaptive.print_stats(detailed=True) # Detailed metrics
|
||||
```
|
||||
|
||||
The summary shows:
|
||||
- Pages crawled vs. confidence achieved
|
||||
- Coverage, consistency, and saturation scores
|
||||
- Crawling efficiency metrics
|
||||
|
||||
## Persistence and Resumption
|
||||
|
||||
### Saving Progress
|
||||
|
||||
```python
|
||||
config = AdaptiveConfig(
|
||||
save_state=True,
|
||||
state_path="my_crawl_state.json"
|
||||
)
|
||||
|
||||
# Crawl will auto-save progress
|
||||
result = await adaptive.digest(start_url, query)
|
||||
```
|
||||
|
||||
### Resuming a Crawl
|
||||
|
||||
```python
|
||||
# Resume from saved state
|
||||
result = await adaptive.digest(
|
||||
start_url,
|
||||
query,
|
||||
resume_from="my_crawl_state.json"
|
||||
)
|
||||
```
|
||||
|
||||
### Exporting Knowledge Base
|
||||
|
||||
```python
|
||||
# Export collected pages to JSONL
|
||||
adaptive.export_knowledge_base("knowledge_base.jsonl")
|
||||
|
||||
# Import into another session
|
||||
new_adaptive = AdaptiveCrawler(crawler)
|
||||
await new_adaptive.import_knowledge_base("knowledge_base.jsonl")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Query Formulation
|
||||
- Use specific, descriptive queries
|
||||
- Include key terms you expect to find
|
||||
- Avoid overly broad queries
|
||||
|
||||
### 2. Threshold Tuning
|
||||
- Start with default (0.7) for general use
|
||||
- Lower to 0.5-0.6 for exploratory crawling
|
||||
- Raise to 0.8+ for exhaustive coverage
|
||||
|
||||
### 3. Performance Optimization
|
||||
- Use appropriate `max_pages` limits
|
||||
- Adjust `top_k_links` based on site structure
|
||||
- Enable caching for repeat crawls
|
||||
|
||||
### 4. Link Selection
|
||||
- The crawler prioritizes links based on:
|
||||
- Relevance to query
|
||||
- Expected information gain
|
||||
- URL structure and depth
|
||||
|
||||
## Examples
|
||||
|
||||
### Research Assistant
|
||||
|
||||
```python
|
||||
# Gather information about a programming concept
|
||||
result = await adaptive.digest(
|
||||
start_url="https://realpython.com",
|
||||
query="python decorators implementation patterns"
|
||||
)
|
||||
|
||||
# Get the most relevant excerpts
|
||||
for doc in adaptive.get_relevant_content(top_k=3):
|
||||
print(f"\nFrom: {doc['url']}")
|
||||
print(f"Relevance: {doc['score']:.2%}")
|
||||
print(doc['content'][:500] + "...")
|
||||
```
|
||||
|
||||
### Knowledge Base Builder
|
||||
|
||||
```python
|
||||
# Build a focused knowledge base about machine learning
|
||||
queries = [
|
||||
"supervised learning algorithms",
|
||||
"neural network architectures",
|
||||
"model evaluation metrics"
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
await adaptive.digest(
|
||||
start_url="https://scikit-learn.org/stable/",
|
||||
query=query
|
||||
)
|
||||
|
||||
# Export combined knowledge base
|
||||
adaptive.export_knowledge_base("ml_knowledge.jsonl")
|
||||
```
|
||||
|
||||
### API Documentation Crawler
|
||||
|
||||
```python
|
||||
# Intelligently crawl API documentation
|
||||
config = AdaptiveConfig(
|
||||
confidence_threshold=0.85, # Higher threshold for completeness
|
||||
max_pages=30
|
||||
)
|
||||
|
||||
adaptive = AdaptiveCrawler(crawler, config)
|
||||
result = await adaptive.digest(
|
||||
start_url="https://api.example.com/docs",
|
||||
query="authentication endpoints rate limits"
|
||||
)
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [Advanced Adaptive Strategies](../advanced/adaptive-strategies.md)
|
||||
- Explore the [AdaptiveCrawler API Reference](../api/adaptive-crawler.md)
|
||||
- See more [Examples](https://github.com/unclecode/crawl4ai/tree/main/docs/examples/adaptive_crawling)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: How is this different from traditional crawling?**
|
||||
A: Traditional crawling follows fixed patterns (BFS/DFS). Adaptive crawling makes intelligent decisions about which links to follow and when to stop based on information gain.
|
||||
|
||||
**Q: Can I use this with JavaScript-heavy sites?**
|
||||
A: Yes! AdaptiveCrawler inherits all capabilities from AsyncWebCrawler, including JavaScript execution.
|
||||
|
||||
**Q: How does it handle large websites?**
|
||||
A: The algorithm naturally limits crawling to relevant sections. Use `max_pages` as a safety limit.
|
||||
|
||||
**Q: Can I customize the scoring algorithms?**
|
||||
A: Advanced users can implement custom strategies. See [Adaptive Strategies](../advanced/adaptive-strategies.md).
|
||||
@@ -0,0 +1,74 @@
|
||||
<div class="ask-ai-container">
|
||||
<iframe id="ask-ai-frame" src="../../ask_ai/index.html" width="100%" style="border:none; display: block;" title="Crawl4AI Assistant"></iframe>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Iframe height adjustment
|
||||
function resizeAskAiIframe() {
|
||||
const iframe = document.getElementById('ask-ai-frame');
|
||||
if (iframe) {
|
||||
const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height') || '55');
|
||||
// Footer is removed by JS below, so calculate height based on header + small buffer
|
||||
const topOffset = headerHeight + 20; // Header + buffer/margin
|
||||
|
||||
const availableHeight = window.innerHeight - topOffset;
|
||||
iframe.style.height = Math.max(600, availableHeight) + 'px'; // Min height 600px
|
||||
}
|
||||
}
|
||||
|
||||
// Run immediately and on resize/load
|
||||
resizeAskAiIframe(); // Initial call
|
||||
let resizeTimer;
|
||||
window.addEventListener('load', resizeAskAiIframe);
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(resizeAskAiIframe, 150);
|
||||
});
|
||||
|
||||
// Remove Footer & HR from parent page (DOM Ready might be safer)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => { // Add slight delay just in case elements render slowly
|
||||
const footer = window.parent.document.querySelector('footer'); // Target parent document
|
||||
if (footer) {
|
||||
const hrBeforeFooter = footer.previousElementSibling;
|
||||
if (hrBeforeFooter && hrBeforeFooter.tagName === 'HR') {
|
||||
hrBeforeFooter.remove();
|
||||
}
|
||||
footer.remove();
|
||||
// Trigger resize again after removing footer
|
||||
resizeAskAiIframe();
|
||||
} else {
|
||||
console.warn("Ask AI Page: Could not find footer in parent document to remove.");
|
||||
}
|
||||
}, 100); // Shorter delay
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#terminal-mkdocs-main-content {
|
||||
padding: 0 !important;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden; /* Prevent body scrollbars, panels handle scroll */
|
||||
}
|
||||
|
||||
/* Ensure iframe container takes full space */
|
||||
#terminal-mkdocs-main-content .ask-ai-container {
|
||||
/* Remove negative margins if footer removal handles space */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
/* Let the JS set the height */
|
||||
/* height: 600px; Initial fallback height */
|
||||
overflow: hidden; /* Hide potential overflow before JS resize */
|
||||
}
|
||||
|
||||
/* Hide title/paragraph if they were part of the markdown */
|
||||
/* Alternatively, just remove them from the .md file directly */
|
||||
/* #terminal-mkdocs-main-content > h1,
|
||||
#terminal-mkdocs-main-content > p:first-of-type {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
# Browser, Crawler & LLM Configuration (Quick Overview)
|
||||
|
||||
Crawl4AI's flexibility stems from two key classes:
|
||||
|
||||
1. **`BrowserConfig`** – Dictates **how** the browser is launched and behaves (e.g., headless or visible, proxy, user agent).
|
||||
2. **`CrawlerRunConfig`** – Dictates **how** each **crawl** operates (e.g., caching, extraction, timeouts, JavaScript code to run, etc.).
|
||||
3. **`LLMConfig`** - Dictates **how** LLM providers are configured. (model, api token, base url, temperature etc.)
|
||||
|
||||
In most examples, you create **one** `BrowserConfig` for the entire crawler session, then pass a **fresh** or re-used `CrawlerRunConfig` whenever you call `arun()`. This tutorial shows the most commonly used parameters. If you need advanced or rarely used fields, see the [Configuration Parameters](../api/parameters.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. BrowserConfig Essentials
|
||||
|
||||
```python
|
||||
class BrowserConfig:
|
||||
def __init__(
|
||||
browser_type="chromium",
|
||||
headless=True,
|
||||
browser_mode="dedicated",
|
||||
use_managed_browser=False,
|
||||
cdp_url=None,
|
||||
debugging_port=9222,
|
||||
host="localhost",
|
||||
proxy_config=None,
|
||||
viewport_width=1080,
|
||||
viewport_height=600,
|
||||
verbose=True,
|
||||
use_persistent_context=False,
|
||||
user_data_dir=None,
|
||||
cookies=None,
|
||||
headers=None,
|
||||
user_agent=(
|
||||
# "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) AppleWebKit/537.36 "
|
||||
# "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
# "(KHTML, like Gecko) Chrome/116.0.5845.187 Safari/604.1 Edg/117.0.2045.47"
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116.0.0.0 Safari/537.36"
|
||||
),
|
||||
user_agent_mode="",
|
||||
text_mode=False,
|
||||
light_mode=False,
|
||||
extra_args=None,
|
||||
enable_stealth=False,
|
||||
# ... other advanced parameters omitted here
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
### Key Fields to Note
|
||||
|
||||
1.⠀**`browser_type`**
|
||||
- Options: `"chromium"`, `"firefox"`, or `"webkit"`.
|
||||
- Defaults to `"chromium"`.
|
||||
- If you need a different engine, specify it here.
|
||||
|
||||
2.⠀**`headless`**
|
||||
- `True`: Runs the browser in headless mode (invisible browser).
|
||||
- `False`: Runs the browser in visible mode, which helps with debugging.
|
||||
|
||||
3.⠀**`browser_mode`**
|
||||
- Determines how the browser should be initialized:
|
||||
- `"dedicated"` (default): Creates a new browser instance each time
|
||||
- `"builtin"`: Uses the builtin CDP browser running in background
|
||||
- `"custom"`: Uses explicit CDP settings provided in `cdp_url`
|
||||
- `"docker"`: Runs browser in Docker container with isolation
|
||||
|
||||
4.⠀**`use_managed_browser`** & **`cdp_url`**
|
||||
- `use_managed_browser=True`: Launch browser using Chrome DevTools Protocol (CDP) for advanced control
|
||||
- `cdp_url`: URL for CDP endpoint (e.g., `"ws://localhost:9222/devtools/browser/"`)
|
||||
- Automatically set based on `browser_mode`
|
||||
|
||||
5.⠀**`debugging_port`** & **`host`**
|
||||
- `debugging_port`: Port for browser debugging protocol (default: 9222)
|
||||
- `host`: Host for browser connection (default: "localhost")
|
||||
|
||||
6.⠀**`proxy_config`**
|
||||
- A `ProxyConfig` object or dictionary with fields like:
|
||||
```json
|
||||
{
|
||||
"server": "http://proxy.example.com:8080",
|
||||
"username": "...",
|
||||
"password": "..."
|
||||
}
|
||||
```
|
||||
- Leave as `None` if a proxy is not required.
|
||||
|
||||
7.⠀**`viewport_width` & `viewport_height`**
|
||||
- The initial window size.
|
||||
- Some sites behave differently with smaller or bigger viewports.
|
||||
|
||||
8.⠀**`device_scale_factor`**
|
||||
- Controls the device pixel ratio (DPR) for rendering. Default is `1.0`.
|
||||
- Set to `2.0` for Retina-quality screenshots (e.g., a 1920×1080 viewport produces 3840×2160 images).
|
||||
- Higher values increase screenshot size and rendering time proportionally.
|
||||
|
||||
9.⠀**`verbose`**
|
||||
- If `True`, prints extra logs.
|
||||
- Handy for debugging.
|
||||
|
||||
9.⠀**`use_persistent_context`**
|
||||
- If `True`, uses a **persistent** browser profile, storing cookies/local storage across runs.
|
||||
- Typically also set `user_data_dir` to point to a folder.
|
||||
|
||||
10.⠀**`cookies`** & **`headers`**
|
||||
- If you want to start with specific cookies or add universal HTTP headers to the browser context, set them here.
|
||||
- E.g. `cookies=[{"name": "session", "value": "abc123", "domain": "example.com"}]`.
|
||||
|
||||
11.⠀**`user_agent`** & **`user_agent_mode`**
|
||||
- `user_agent`: Custom User-Agent string. If `None`, a default is used.
|
||||
- `user_agent_mode`: Set to `"random"` for randomization (helps fight bot detection).
|
||||
|
||||
12.⠀**`text_mode`** & **`light_mode`**
|
||||
- `text_mode=True` disables images, possibly speeding up text-only crawls.
|
||||
- `light_mode=True` turns off certain background features for performance.
|
||||
|
||||
13.⠀**`avoid_ads`** & **`avoid_css`**
|
||||
- `avoid_ads=True` blocks requests to common ad and tracker domains (Google Analytics, DoubleClick, Facebook, Hotjar, etc.) at the browser context level. Reduces network overhead and memory usage.
|
||||
- `avoid_css=True` blocks loading of CSS files (`.css`, `.less`, `.scss`, `.sass`), useful when you only need text content and want faster, leaner crawls.
|
||||
- Both default to `False` (opt-in). Can be combined with each other and with `text_mode`.
|
||||
|
||||
14.⠀**`extra_args`**
|
||||
- Additional flags for the underlying browser.
|
||||
- E.g. `["--disable-extensions"]`.
|
||||
|
||||
15.⠀**`enable_stealth`**
|
||||
- If `True`, enables stealth mode using playwright-stealth.
|
||||
- Modifies browser fingerprints to avoid basic bot detection.
|
||||
- Default is `False`. Recommended for sites with bot protection.
|
||||
|
||||
### Helper Methods
|
||||
|
||||
Both configuration classes provide a `clone()` method to create modified copies:
|
||||
|
||||
```python
|
||||
# Create a base browser config
|
||||
base_browser = BrowserConfig(
|
||||
browser_type="chromium",
|
||||
headless=True,
|
||||
text_mode=True
|
||||
)
|
||||
|
||||
# Create a visible browser config for debugging
|
||||
debug_browser = base_browser.clone(
|
||||
headless=False,
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
### Class-Level Defaults
|
||||
|
||||
Both `BrowserConfig` and `CrawlerRunConfig` support **class-level default overrides** via `set_defaults()`. This is useful in server/cloud deployments where every config instance needs the same base settings — set them once at startup instead of repeating at every call site.
|
||||
|
||||
```python
|
||||
from crawl4ai import BrowserConfig, CrawlerRunConfig
|
||||
|
||||
# At application startup — one time
|
||||
BrowserConfig.set_defaults(
|
||||
cache_cdp_connection=True,
|
||||
cdp_close_delay=0,
|
||||
create_isolated_context=True,
|
||||
)
|
||||
CrawlerRunConfig.set_defaults(verbose=False)
|
||||
|
||||
# Every new instance automatically inherits those defaults
|
||||
cfg = BrowserConfig(cdp_url="ws://localhost:9222")
|
||||
# → cache_cdp_connection=True, cdp_close_delay=0, create_isolated_context=True
|
||||
|
||||
# Explicit values still win
|
||||
cfg = BrowserConfig(cdp_url="ws://localhost:9222", cache_cdp_connection=False)
|
||||
# → cache_cdp_connection=False (explicit overrides the class default)
|
||||
```
|
||||
|
||||
**Available methods** (on both `BrowserConfig` and `CrawlerRunConfig`):
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `set_defaults(**kwargs)` | Set class-level defaults. Invalid parameter names raise `ValueError`. |
|
||||
| `get_defaults()` | Return a copy of the current class-level defaults. |
|
||||
| `reset_defaults()` | Clear all class-level defaults. |
|
||||
| `reset_defaults("param1", "param2")` | Clear only the named defaults. |
|
||||
|
||||
> **Note:** Class defaults are independent per class — `BrowserConfig.set_defaults()` does not affect `CrawlerRunConfig`, and vice versa. Defaults are stored in memory and apply for the lifetime of the process.
|
||||
|
||||
**Minimal Example**:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig
|
||||
|
||||
browser_conf = BrowserConfig(
|
||||
browser_type="firefox",
|
||||
headless=False,
|
||||
text_mode=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_conf) as crawler:
|
||||
result = await crawler.arun("https://example.com")
|
||||
print(result.markdown[:300])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. CrawlerRunConfig Essentials
|
||||
|
||||
```python
|
||||
class CrawlerRunConfig:
|
||||
def __init__(
|
||||
word_count_threshold=200,
|
||||
extraction_strategy=None,
|
||||
chunking_strategy=RegexChunking(),
|
||||
markdown_generator=None,
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
js_code=None,
|
||||
c4a_script=None,
|
||||
wait_for=None,
|
||||
screenshot=False,
|
||||
pdf=False,
|
||||
capture_mhtml=False,
|
||||
# Location and Identity Parameters
|
||||
locale=None, # e.g. "en-US", "fr-FR"
|
||||
timezone_id=None, # e.g. "America/New_York"
|
||||
geolocation=None, # GeolocationConfig object
|
||||
# Proxy Configuration
|
||||
proxy_config=None,
|
||||
proxy_rotation_strategy=None,
|
||||
# Page Interaction Parameters
|
||||
scan_full_page=False,
|
||||
scroll_delay=0.2,
|
||||
wait_until="domcontentloaded",
|
||||
page_timeout=60000,
|
||||
delay_before_return_html=0.1,
|
||||
# URL Matching Parameters
|
||||
url_matcher=None, # For URL-specific configurations
|
||||
match_mode=MatchMode.OR,
|
||||
verbose=True,
|
||||
stream=False, # Enable streaming for arun_many()
|
||||
# ... other advanced parameters omitted
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
### Key Fields to Note
|
||||
|
||||
1.⠀**`word_count_threshold`**:
|
||||
- The minimum word count before a block is considered.
|
||||
- If your site has lots of short paragraphs or items, you can lower it.
|
||||
|
||||
2.⠀**`extraction_strategy`**:
|
||||
- Where you plug in JSON-based extraction (CSS, LLM, etc.).
|
||||
- If `None`, no structured extraction is done (only raw/cleaned HTML + markdown).
|
||||
|
||||
3.⠀**`chunking_strategy`**:
|
||||
- Strategy to chunk content before extraction.
|
||||
- Defaults to `RegexChunking()`. Can be customized for different chunking approaches.
|
||||
|
||||
4.⠀**`markdown_generator`**:
|
||||
- E.g., `DefaultMarkdownGenerator(...)`, controlling how HTML→Markdown conversion is done.
|
||||
- If `None`, a default approach is used.
|
||||
|
||||
5.⠀**`cache_mode`**:
|
||||
- Controls caching behavior (`ENABLED`, `BYPASS`, `DISABLED`, etc.).
|
||||
- Defaults to `CacheMode.BYPASS`.
|
||||
|
||||
6.⠀**`js_code`**, **`js_code_before_wait`**, & **`c4a_script`**:
|
||||
- `js_code`: JavaScript to run **after** `wait_for` completes — on the fully-loaded page.
|
||||
- `js_code_before_wait`: JavaScript to run **before** `wait_for` — for triggering loading that `wait_for` then checks.
|
||||
- `c4a_script`: C4A script that compiles to JavaScript.
|
||||
- Great for "Load More" buttons or user interactions.
|
||||
|
||||
7.⠀**`wait_for`**:
|
||||
- A CSS or JS expression to wait for before extracting content.
|
||||
- Common usage: `wait_for="css:.main-loaded"` or `wait_for="js:() => window.loaded === true"`.
|
||||
|
||||
8.⠀**`flatten_shadow_dom`**:
|
||||
- If `True`, flattens Shadow DOM content into the light DOM before HTML capture.
|
||||
- Essential for sites built with Web Components (Stencil, Lit, Shoelace, etc.).
|
||||
- Also force-opens closed shadow roots. See [Flattening Shadow DOM](content-selection.md#31-flattening-shadow-dom).
|
||||
|
||||
9.⠀**`screenshot`**, **`pdf`**, & **`capture_mhtml`**:
|
||||
- If `True`, captures a screenshot, PDF, or MHTML snapshot after the page is fully loaded.
|
||||
- The results go to `result.screenshot` (base64), `result.pdf` (bytes), or `result.mhtml` (string).
|
||||
- Use `force_viewport_screenshot=True` to capture only the visible viewport instead of the full page. This is faster and produces smaller images when you don't need a full-page screenshot.
|
||||
|
||||
9.⠀**Location Parameters**:
|
||||
- **`locale`**: Browser's locale (e.g., `"en-US"`, `"fr-FR"`) for language preferences
|
||||
- **`timezone_id`**: Browser's timezone (e.g., `"America/New_York"`, `"Europe/Paris"`)
|
||||
- **`geolocation`**: GPS coordinates via `GeolocationConfig(latitude=48.8566, longitude=2.3522)`
|
||||
- See [Identity Based Crawling](../advanced/identity-based-crawling.md#7-locale-timezone-and-geolocation-control)
|
||||
|
||||
10.⠀**Proxy Configuration**:
|
||||
- **`proxy_config`**: Single `ProxyConfig` or `list[ProxyConfig]` — proxies tried in order. Pass a list for automatic escalation.
|
||||
- **`proxy_rotation_strategy`**: Strategy for rotating proxies during crawls
|
||||
|
||||
11.⠀**Anti-Bot Retry & Fallback** (see [Anti-Bot & Fallback](../advanced/anti-bot-and-fallback.md)):
|
||||
- **`max_retries`**: Number of retry rounds when blocking is detected (default: 0). Each round tries all proxies in `proxy_config`.
|
||||
- **`fallback_fetch_function`**: Async function called as last resort — takes URL, returns raw HTML
|
||||
|
||||
12.⠀**Page Interaction Parameters**:
|
||||
- **`scan_full_page`**: If `True`, scroll through the entire page to load all content
|
||||
- **`wait_until`**: Condition to wait for when navigating (e.g., "domcontentloaded", "networkidle")
|
||||
- **`page_timeout`**: Timeout in milliseconds for page operations (default: 60000)
|
||||
- **`delay_before_return_html`**: Delay in seconds before retrieving final HTML.
|
||||
|
||||
13.⠀**`url_matcher`** & **`match_mode`**:
|
||||
- Enable URL-specific configurations when used with `arun_many()`.
|
||||
- Set `url_matcher` to patterns (glob, function, or list) to match specific URLs.
|
||||
- Use `match_mode` (OR/AND) to control how multiple patterns combine.
|
||||
- See [URL-Specific Configurations](../api/arun_many.md#url-specific-configurations) for examples.
|
||||
|
||||
13.⠀**`verbose`**:
|
||||
- Logs additional runtime details.
|
||||
- Overlaps with the browser's verbosity if also set to `True` in `BrowserConfig`.
|
||||
|
||||
14.⠀**`stream`**:
|
||||
- If `True`, enables streaming mode for `arun_many()` to process URLs as they complete.
|
||||
- Allows handling results incrementally instead of waiting for all URLs to finish.
|
||||
|
||||
|
||||
### Helper Methods
|
||||
|
||||
The `clone()` method is particularly useful for creating variations of your crawler configuration:
|
||||
|
||||
```python
|
||||
# Create a base configuration
|
||||
base_config = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.ENABLED,
|
||||
word_count_threshold=200,
|
||||
wait_until="networkidle"
|
||||
)
|
||||
|
||||
# Create variations for different use cases
|
||||
stream_config = base_config.clone(
|
||||
stream=True, # Enable streaming mode
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
|
||||
debug_config = base_config.clone(
|
||||
page_timeout=120000, # Longer timeout for debugging
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
The `clone()` method:
|
||||
- Creates a new instance with all the same settings
|
||||
- Updates only the specified parameters
|
||||
- Leaves the original configuration unchanged
|
||||
- Perfect for creating variations without repeating all parameters
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 3. LLMConfig Essentials
|
||||
|
||||
### Key fields to note
|
||||
|
||||
1.⠀**`provider`**:
|
||||
- Which LLM provider to use.
|
||||
- Possible values are `"ollama/llama3","groq/llama3-70b-8192","groq/llama3-8b-8192", "openai/gpt-4o-mini" ,"openai/gpt-4o","openai/o1-mini","openai/o1-preview","openai/o3-mini","openai/o3-mini-high","anthropic/claude-3-haiku-20240307","anthropic/claude-3-opus-20240229","anthropic/claude-3-sonnet-20240229","anthropic/claude-3-5-sonnet-20240620","gemini/gemini-pro","gemini/gemini-1.5-pro","gemini/gemini-2.0-flash","gemini/gemini-2.0-flash-exp","gemini/gemini-2.0-flash-lite-preview-02-05","deepseek/deepseek-chat"`<br/>*(default: `"openai/gpt-4o-mini"`)*
|
||||
|
||||
2.⠀**`api_token`**:
|
||||
- Optional. When not provided explicitly, api_token will be read from environment variables based on provider. For example: If a gemini model is passed as provider then,`"GEMINI_API_KEY"` will be read from environment variables
|
||||
- API token of LLM provider <br/> eg: `api_token = "gsk_1ClHGGJ7Lpn4WGybR7vNWGdyb3FY7zXEw3SCiy0BAVM9lL8CQv"`
|
||||
- Environment variable - use with prefix "env:" <br/> eg:`api_token = "env: GROQ_API_KEY"`
|
||||
|
||||
3.⠀**`base_url`**:
|
||||
- If your provider has a custom endpoint
|
||||
|
||||
4.⠀**Retry/backoff controls** *(optional)*:
|
||||
- `backoff_base_delay` *(default `2` seconds)* – base delay inserted before the first retry when the provider returns a rate-limit response.
|
||||
- `backoff_max_attempts` *(default `3`)* – total number of attempts (initial call plus retries) before the request is surfaced as an error.
|
||||
- `backoff_exponential_factor` *(default `2`)* – growth rate for the retry delay (`delay = base_delay * factor^attempt`).
|
||||
- These values are forwarded to the shared `perform_completion_with_backoff` helper, ensuring every strategy that consumes your `LLMConfig` honors the same throttling policy.
|
||||
|
||||
```python
|
||||
llm_config = LLMConfig(
|
||||
provider="openai/gpt-4o-mini",
|
||||
api_token=os.getenv("OPENAI_API_KEY"),
|
||||
backoff_base_delay=1, # optional
|
||||
backoff_max_attempts=5, # optional
|
||||
backoff_exponential_factor=3, #optional
|
||||
)
|
||||
```
|
||||
|
||||
## 4. Putting It All Together
|
||||
|
||||
In a typical scenario, you define **one** `BrowserConfig` for your crawler session, then create **one or more** `CrawlerRunConfig` & `LLMConfig` depending on each call's needs:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode, LLMConfig, LLMContentFilter, DefaultMarkdownGenerator
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def main():
|
||||
# 1) Browser config: headless, bigger viewport, no proxy
|
||||
browser_conf = BrowserConfig(
|
||||
headless=True,
|
||||
viewport_width=1280,
|
||||
viewport_height=720
|
||||
)
|
||||
|
||||
# 2) Example extraction strategy
|
||||
schema = {
|
||||
"name": "Articles",
|
||||
"baseSelector": "div.article",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2", "type": "text"},
|
||||
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
|
||||
]
|
||||
}
|
||||
extraction = JsonCssExtractionStrategy(schema)
|
||||
|
||||
# 3) Example LLM content filtering
|
||||
|
||||
gemini_config = LLMConfig(
|
||||
provider="gemini/gemini-1.5-pro",
|
||||
api_token = "env:GEMINI_API_TOKEN"
|
||||
)
|
||||
|
||||
# Initialize LLM filter with specific instruction
|
||||
filter = LLMContentFilter(
|
||||
llm_config=gemini_config, # or your preferred provider
|
||||
instruction="""
|
||||
Focus on extracting the core educational content.
|
||||
Include:
|
||||
- Key concepts and explanations
|
||||
- Important code examples
|
||||
- Essential technical details
|
||||
Exclude:
|
||||
- Navigation elements
|
||||
- Sidebars
|
||||
- Footer content
|
||||
Format the output as clean markdown with proper code blocks and headers.
|
||||
""",
|
||||
chunk_token_threshold=500, # Adjust based on your needs
|
||||
verbose=True
|
||||
)
|
||||
|
||||
md_generator = DefaultMarkdownGenerator(
|
||||
content_filter=filter,
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
|
||||
# 4) Crawler run config: skip cache, use extraction
|
||||
run_conf = CrawlerRunConfig(
|
||||
markdown_generator=md_generator,
|
||||
extraction_strategy=extraction,
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_conf) as crawler:
|
||||
# 4) Execute the crawl
|
||||
result = await crawler.arun(url="https://example.com/news", config=run_conf)
|
||||
|
||||
if result.success:
|
||||
print("Extracted content:", result.extracted_content)
|
||||
else:
|
||||
print("Error:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Next Steps
|
||||
|
||||
For a **detailed list** of available parameters (including advanced ones), see:
|
||||
|
||||
- [BrowserConfig, CrawlerRunConfig & LLMConfig Reference](../api/parameters.md)
|
||||
|
||||
You can explore topics like:
|
||||
|
||||
- **Custom Hooks & Auth** (Inject JavaScript or handle login forms).
|
||||
- **Session Management** (Re-use pages, preserve state across multiple calls).
|
||||
- **Magic Mode** or **Identity-based Crawling** (Fight bot detection by simulating user behavior).
|
||||
- **Advanced Caching** (Fine-tune read/write cache modes).
|
||||
|
||||
---
|
||||
|
||||
## 6. Conclusion
|
||||
|
||||
**BrowserConfig**, **CrawlerRunConfig** and **LLMConfig** give you straightforward ways to define:
|
||||
|
||||
- **Which** browser to launch, how it should run, and any proxy or user agent needs.
|
||||
- **How** each crawl should behave—caching, timeouts, JavaScript code, extraction strategies, etc.
|
||||
- **Which** LLM provider to use, api token, temperature and base url for custom endpoints
|
||||
|
||||
Use them together for **clear, maintainable** code, and when you need more specialized behavior, check out the advanced parameters in the [reference docs](../api/parameters.md). Happy crawling!
|
||||
@@ -0,0 +1,393 @@
|
||||
# C4A-Script: Visual Web Automation Made Simple
|
||||
|
||||
## What is C4A-Script?
|
||||
|
||||
C4A-Script is a powerful, human-readable domain-specific language (DSL) designed for web automation and interaction. Think of it as a simplified programming language that anyone can read and write, perfect for automating repetitive web tasks, testing user interfaces, or creating interactive demos.
|
||||
|
||||
### Why C4A-Script?
|
||||
|
||||
**Simple Syntax, Powerful Results**
|
||||
```c4a
|
||||
# Navigate and interact in plain English
|
||||
GO https://example.com
|
||||
WAIT `#search-box` 5
|
||||
TYPE "Hello World"
|
||||
CLICK `button[type="submit"]`
|
||||
```
|
||||
|
||||
**Visual Programming Support**
|
||||
C4A-Script comes with a built-in Blockly visual editor, allowing you to create scripts by dragging and dropping blocks - no coding experience required!
|
||||
|
||||
**Perfect for:**
|
||||
- **UI Testing**: Automate user interaction flows
|
||||
- **Demo Creation**: Build interactive product demonstrations
|
||||
- **Data Entry**: Automate form filling and submissions
|
||||
- **Testing Workflows**: Validate complex user journeys
|
||||
- **Training**: Teach web automation without code complexity
|
||||
|
||||
## Getting Started: Your First Script
|
||||
|
||||
Let's create a simple script that searches for something on a website:
|
||||
|
||||
```c4a
|
||||
# My first C4A-Script
|
||||
GO https://duckduckgo.com
|
||||
|
||||
# Wait for the search box to appear
|
||||
WAIT `input[name="q"]` 10
|
||||
|
||||
# Type our search query
|
||||
TYPE "Crawl4AI"
|
||||
|
||||
# Press Enter to search
|
||||
PRESS Enter
|
||||
|
||||
# Wait for results
|
||||
WAIT `.results` 5
|
||||
```
|
||||
|
||||
That's it! In just a few lines, you've automated a complete search workflow.
|
||||
|
||||
## Interactive Tutorial & Live Demo
|
||||
|
||||
Want to learn by doing? We've got you covered:
|
||||
|
||||
**🚀 [Live Demo](https://docs.crawl4ai.com/apps/c4a-script/)** - Try C4A-Script in your browser right now!
|
||||
|
||||
**📁 [Tutorial Examples](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/c4a_script/)** - Complete examples with source code
|
||||
|
||||
### Running the Tutorial Locally
|
||||
|
||||
The tutorial includes a Flask-based web interface with:
|
||||
- **Live Code Editor** with syntax highlighting
|
||||
- **Visual Blockly Editor** for drag-and-drop programming
|
||||
- **Recording Mode** to capture your actions and generate scripts
|
||||
- **Timeline View** to see and edit your automation steps
|
||||
|
||||
```bash
|
||||
# Clone and navigate to the tutorial
|
||||
cd docs/examples/c4a_script/tutorial/
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Launch the tutorial server
|
||||
python server.py
|
||||
|
||||
# Open http://localhost:8000 in your browser
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Commands and Syntax
|
||||
|
||||
C4A-Script uses simple, English-like commands. Each command does one specific thing:
|
||||
|
||||
```c4a
|
||||
# Comments start with #
|
||||
COMMAND parameter1 parameter2
|
||||
|
||||
# Most commands use CSS selectors in backticks
|
||||
CLICK `#submit-button`
|
||||
|
||||
# Text content goes in quotes
|
||||
TYPE "Hello, World!"
|
||||
|
||||
# Numbers are used directly
|
||||
WAIT 3
|
||||
```
|
||||
|
||||
### Selectors: Finding Elements
|
||||
|
||||
C4A-Script uses CSS selectors to identify elements on the page:
|
||||
|
||||
```c4a
|
||||
# By ID
|
||||
CLICK `#login-button`
|
||||
|
||||
# By class
|
||||
CLICK `.submit-btn`
|
||||
|
||||
# By attribute
|
||||
CLICK `button[type="submit"]`
|
||||
|
||||
# By accessible attributes
|
||||
CLICK `button[aria-label="Search"][title="Search"]`
|
||||
|
||||
# Complex selectors
|
||||
CLICK `.form-container input[name="email"]`
|
||||
```
|
||||
|
||||
### Variables and Dynamic Content
|
||||
|
||||
Store and reuse values with variables:
|
||||
|
||||
```c4a
|
||||
# Set a variable
|
||||
SETVAR username = "john@example.com"
|
||||
SETVAR password = "secret123"
|
||||
|
||||
# Use variables (prefix with $)
|
||||
TYPE $username
|
||||
PRESS Tab
|
||||
TYPE $password
|
||||
```
|
||||
|
||||
## Command Categories
|
||||
|
||||
### 🧭 Navigation Commands
|
||||
Move around the web like a user would:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `GO` | Navigate to URL | `GO https://example.com` |
|
||||
| `RELOAD` | Refresh current page | `RELOAD` |
|
||||
| `BACK` | Go back in history | `BACK` |
|
||||
| `FORWARD` | Go forward in history | `FORWARD` |
|
||||
|
||||
### ⏱️ Wait Commands
|
||||
Ensure elements are ready before interacting:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `WAIT` | Wait for time/element/text | `WAIT 3` or `WAIT \`#element\` 10` |
|
||||
|
||||
### 🖱️ Mouse Commands
|
||||
Click, drag, and move like a human:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `CLICK` | Click element or coordinates | `CLICK \`button\`` or `CLICK 100 200` |
|
||||
| `DOUBLE_CLICK` | Double-click element | `DOUBLE_CLICK \`.item\`` |
|
||||
| `RIGHT_CLICK` | Right-click element | `RIGHT_CLICK \`#menu\`` |
|
||||
| `SCROLL` | Scroll in direction | `SCROLL DOWN 500` |
|
||||
| `DRAG` | Drag from point to point | `DRAG 100 100 500 300` |
|
||||
|
||||
### ⌨️ Keyboard Commands
|
||||
Type text and press keys naturally:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `TYPE` | Type text or variable | `TYPE "Hello"` or `TYPE $username` |
|
||||
| `PRESS` | Press special keys | `PRESS Tab` or `PRESS Enter` |
|
||||
| `CLEAR` | Clear input field | `CLEAR \`#search\`` |
|
||||
| `SET` | Set input value directly | `SET \`#email\` "user@example.com"` |
|
||||
|
||||
### 🔀 Control Flow
|
||||
Add logic and repetition to your scripts:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `IF` | Conditional execution | `IF (EXISTS \`#popup\`) THEN CLICK \`#close\`` |
|
||||
| `REPEAT` | Loop commands | `REPEAT (SCROLL DOWN 300, 5)` |
|
||||
|
||||
### 💾 Variables & Advanced
|
||||
Store data and execute custom code:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `SETVAR` | Create variable | `SETVAR email = "test@example.com"` |
|
||||
| `EVAL` | Execute JavaScript | `EVAL \`console.log('Hello')\`` |
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Login Flow
|
||||
```c4a
|
||||
# Complete login automation
|
||||
GO https://myapp.com/login
|
||||
|
||||
# Wait for page to load
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Fill credentials
|
||||
CLICK `#email`
|
||||
TYPE "user@example.com"
|
||||
PRESS Tab
|
||||
TYPE "mypassword"
|
||||
|
||||
# Submit form
|
||||
CLICK `button[type="submit"]`
|
||||
|
||||
# Wait for dashboard
|
||||
WAIT `.dashboard` 10
|
||||
```
|
||||
|
||||
### Example 2: E-commerce Shopping
|
||||
```c4a
|
||||
# Shopping automation with variables
|
||||
SETVAR product = "laptop"
|
||||
SETVAR budget = "1000"
|
||||
|
||||
GO https://shop.example.com
|
||||
WAIT `#search-box` 3
|
||||
|
||||
# Search for product
|
||||
TYPE $product
|
||||
PRESS Enter
|
||||
WAIT `.product-list` 5
|
||||
|
||||
# Filter by price
|
||||
CLICK `.price-filter`
|
||||
SET `#max-price` $budget
|
||||
CLICK `.apply-filters`
|
||||
|
||||
# Select first result
|
||||
WAIT `.product-item` 3
|
||||
CLICK `.product-item:first-child`
|
||||
```
|
||||
|
||||
### Example 3: Form Automation with Conditions
|
||||
```c4a
|
||||
# Smart form filling with error handling
|
||||
GO https://forms.example.com
|
||||
|
||||
# Check if user is already logged in
|
||||
IF (EXISTS `.user-menu`) THEN GO https://forms.example.com/new
|
||||
IF (NOT EXISTS `.user-menu`) THEN CLICK `#login-link`
|
||||
|
||||
# Fill form
|
||||
WAIT `#contact-form` 5
|
||||
SET `#name` "John Doe"
|
||||
SET `#email` "john@example.com"
|
||||
SET `#message` "Hello from C4A-Script!"
|
||||
|
||||
# Handle popup if it appears
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
|
||||
# Submit
|
||||
CLICK `#submit-button`
|
||||
WAIT `.success-message` 10
|
||||
```
|
||||
|
||||
## Visual Programming with Blockly
|
||||
|
||||
C4A-Script includes a powerful visual programming interface built on Google Blockly. Perfect for:
|
||||
|
||||
- **Non-programmers** who want to create automation
|
||||
- **Rapid prototyping** of automation workflows
|
||||
- **Educational environments** for teaching automation concepts
|
||||
- **Collaborative development** where visual representation helps communication
|
||||
|
||||
### Features:
|
||||
- **Drag & Drop Interface**: Build scripts by connecting blocks
|
||||
- **Real-time Sync**: Changes in visual mode instantly update the text script
|
||||
- **Smart Block Types**: Blocks are categorized by function (Navigation, Actions, etc.)
|
||||
- **Error Prevention**: Visual connections prevent syntax errors
|
||||
- **Comment Support**: Add visual comment blocks for documentation
|
||||
|
||||
Try the visual editor in our [live demo](https://docs.crawl4ai.com/c4a-script/demo) or [local tutorial](/examples/c4a_script/tutorial/).
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Recording Mode
|
||||
The tutorial interface includes a recording feature that watches your browser interactions and automatically generates C4A-Script commands:
|
||||
|
||||
1. Click "Record" in the tutorial interface
|
||||
2. Perform actions in the browser preview
|
||||
3. Watch as C4A-Script commands are generated in real-time
|
||||
4. Edit and refine the generated script
|
||||
|
||||
### Error Handling and Debugging
|
||||
C4A-Script provides clear error messages and debugging information:
|
||||
|
||||
```c4a
|
||||
# Use comments for debugging
|
||||
# This will wait up to 10 seconds for the element
|
||||
WAIT `#slow-loading-element` 10
|
||||
|
||||
# Check if element exists before clicking
|
||||
IF (EXISTS `#optional-button`) THEN CLICK `#optional-button`
|
||||
|
||||
# Use EVAL for custom debugging
|
||||
EVAL `console.log("Current page title:", document.title)`
|
||||
```
|
||||
|
||||
### Integration with Crawl4AI
|
||||
C4A-Script integrates seamlessly with Crawl4AI's web crawling capabilities:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
# Use C4A-Script for interaction before crawling
|
||||
script = """
|
||||
GO https://example.com
|
||||
CLICK `#load-more-content`
|
||||
WAIT `.dynamic-content` 5
|
||||
"""
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
js_code=script,
|
||||
wait_for=".dynamic-content"
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
print(result.markdown)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Wait for Elements
|
||||
```c4a
|
||||
# Bad: Clicking immediately
|
||||
CLICK `#button`
|
||||
|
||||
# Good: Wait for element to appear
|
||||
WAIT `#button` 5
|
||||
CLICK `#button`
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Comments
|
||||
```c4a
|
||||
# Login to user account
|
||||
GO https://myapp.com/login
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Enter credentials
|
||||
TYPE "user@example.com"
|
||||
PRESS Tab
|
||||
TYPE "password123"
|
||||
|
||||
# Submit and wait for redirect
|
||||
CLICK `#submit-button`
|
||||
WAIT `.dashboard` 10
|
||||
```
|
||||
|
||||
### 3. Handle Variable Conditions
|
||||
```c4a
|
||||
# Handle different page states
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
IF (EXISTS `.popup-modal`) THEN CLICK `.close-modal`
|
||||
|
||||
# Proceed with main workflow
|
||||
CLICK `#main-action`
|
||||
```
|
||||
|
||||
### 4. Use Variables for Reusability
|
||||
```c4a
|
||||
# Define once, use everywhere
|
||||
SETVAR base_url = "https://myapp.com"
|
||||
SETVAR test_email = "test@example.com"
|
||||
|
||||
GO $base_url/login
|
||||
SET `#email` $test_email
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **📖 [Complete Examples](/examples/c4a_script/)** - Real-world automation scripts
|
||||
- **🎮 [Interactive Tutorial](/examples/c4a_script/tutorial/)** - Hands-on learning environment
|
||||
- **📋 [API Reference](/api/c4a-script-reference/)** - Detailed command documentation
|
||||
- **🌐 [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try it in your browser
|
||||
|
||||
## What's Next?
|
||||
|
||||
Ready to dive deeper? Check out:
|
||||
|
||||
1. **[API Reference](/api/c4a-script-reference/)** - Complete command documentation
|
||||
2. **[Tutorial Examples](/examples/c4a_script/)** - Copy-paste ready scripts
|
||||
3. **[Local Tutorial Setup](/examples/c4a_script/tutorial/)** - Run the full development environment
|
||||
|
||||
C4A-Script makes web automation accessible to everyone. Whether you're a developer automating tests, a designer creating interactive demos, or a business user streamlining repetitive tasks, C4A-Script has the tools you need.
|
||||
|
||||
*Start automating today - your future self will thank you!* 🚀
|
||||
@@ -0,0 +1,69 @@
|
||||
# Crawl4AI Cache System and Migration Guide
|
||||
|
||||
## Overview
|
||||
Starting from version 0.5.0, Crawl4AI introduces a new caching system that replaces the old boolean flags with a more intuitive `CacheMode` enum. This change simplifies cache control and makes the behavior more predictable.
|
||||
|
||||
## Old vs New Approach
|
||||
|
||||
### Old Way (Deprecated)
|
||||
The old system used multiple boolean flags:
|
||||
- `bypass_cache`: Skip cache entirely
|
||||
- `disable_cache`: Disable all caching
|
||||
- `no_cache_read`: Don't read from cache
|
||||
- `no_cache_write`: Don't write to cache
|
||||
|
||||
### New Way (Recommended)
|
||||
The new system uses a single `CacheMode` enum:
|
||||
- `CacheMode.ENABLED`: Normal caching (read/write)
|
||||
- `CacheMode.DISABLED`: No caching at all
|
||||
- `CacheMode.READ_ONLY`: Only read from cache
|
||||
- `CacheMode.WRITE_ONLY`: Only write to cache
|
||||
- `CacheMode.BYPASS`: Skip cache for this operation
|
||||
|
||||
## Migration Example
|
||||
|
||||
### Old Code (Deprecated)
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
|
||||
async def old_code(crawler: AsyncWebCrawler):
|
||||
# Legacy `bypass_cache` / `disable_cache` / `no_cache_read` / `no_cache_write`
|
||||
# were removed in v0.5+. This example no longer applies:
|
||||
result = await crawler.arun(
|
||||
url="https://www.nbcnews.com/business",
|
||||
# cache_mode is the only cache option now.
|
||||
)
|
||||
print(len(result.markdown))
|
||||
```
|
||||
|
||||
### New Code (Recommended)
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CacheMode
|
||||
from crawl4ai.async_configs import CrawlerRunConfig
|
||||
|
||||
async def use_proxy():
|
||||
# Use CacheMode in CrawlerRunConfig
|
||||
config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
async with AsyncWebCrawler(verbose=True) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://www.nbcnews.com/business",
|
||||
config=config # Pass the configuration object
|
||||
)
|
||||
print(len(result.markdown))
|
||||
|
||||
async def main():
|
||||
await use_proxy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Common Migration Patterns
|
||||
|
||||
| Legacy Flag | Replacement |
|
||||
|------------------------|----------------------------|
|
||||
| `bypass_cache` | `cache_mode=CacheMode.BYPASS` |
|
||||
| `disable_cache` | `cache_mode=CacheMode.DISABLED` |
|
||||
| `no_cache_read` | `cache_mode=CacheMode.READ_ONLY` |
|
||||
| `no_cache_write` | `cache_mode=CacheMode.WRITE_ONLY`|
|
||||
@@ -0,0 +1,307 @@
|
||||
# Crawl4AI CLI Guide
|
||||
|
||||
## Table of Contents
|
||||
- [Installation](#installation)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Configuration](#configuration)
|
||||
- [Browser Configuration](#browser-configuration)
|
||||
- [Crawler Configuration](#crawler-configuration)
|
||||
- [Extraction Configuration](#extraction-configuration)
|
||||
- [Content Filtering](#content-filtering)
|
||||
- [Advanced Features](#advanced-features)
|
||||
- [LLM Q&A](#llm-qa)
|
||||
- [Structured Data Extraction](#structured-data-extraction)
|
||||
- [Content Filtering](#content-filtering-1)
|
||||
- [Output Formats](#output-formats)
|
||||
- [Examples](#examples)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [Best Practices & Tips](#best-practices--tips)
|
||||
|
||||
## Installation
|
||||
The Crawl4AI CLI will be installed automatically when you install the library.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The Crawl4AI CLI (`crwl`) provides a simple interface to the Crawl4AI library:
|
||||
|
||||
```bash
|
||||
# Basic crawling
|
||||
crwl https://example.com
|
||||
|
||||
# Get markdown output
|
||||
crwl https://example.com -o markdown
|
||||
|
||||
# Verbose JSON output with cache bypass
|
||||
crwl https://example.com -o json -v --bypass-cache
|
||||
|
||||
# See usage examples
|
||||
crwl --example
|
||||
```
|
||||
|
||||
## Quick Example of Advanced Usage
|
||||
|
||||
If you clone the repository and run the following command, you will receive the content of the page in JSON format according to a JSON-CSS schema:
|
||||
|
||||
```bash
|
||||
crwl "https://www.infoq.com/ai-ml-data-eng/" -e docs/examples/cli/extract_css.yml -s docs/examples/cli/css_schema.json -o json;
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Browser Configuration
|
||||
|
||||
Browser settings can be configured via YAML file or command line parameters:
|
||||
|
||||
```yaml
|
||||
# browser.yml
|
||||
headless: true
|
||||
viewport_width: 1280
|
||||
user_agent_mode: "random"
|
||||
verbose: true
|
||||
ignore_https_errors: true
|
||||
```
|
||||
|
||||
```bash
|
||||
# Using config file
|
||||
crwl https://example.com -B browser.yml
|
||||
|
||||
# Using direct parameters
|
||||
crwl https://example.com -b "headless=true,viewport_width=1280,user_agent_mode=random"
|
||||
```
|
||||
|
||||
### Crawler Configuration
|
||||
|
||||
Control crawling behavior:
|
||||
|
||||
```yaml
|
||||
# crawler.yml
|
||||
cache_mode: "bypass"
|
||||
wait_until: "networkidle"
|
||||
page_timeout: 30000
|
||||
delay_before_return_html: 0.5
|
||||
word_count_threshold: 100
|
||||
scan_full_page: true
|
||||
scroll_delay: 0.3
|
||||
process_iframes: false
|
||||
remove_overlay_elements: true
|
||||
magic: true
|
||||
verbose: true
|
||||
```
|
||||
|
||||
```bash
|
||||
# Using config file
|
||||
crwl https://example.com -C crawler.yml
|
||||
|
||||
# Using direct parameters
|
||||
crwl https://example.com -c "css_selector=#main,delay_before_return_html=2,scan_full_page=true"
|
||||
```
|
||||
|
||||
### Extraction Configuration
|
||||
|
||||
Two types of extraction are supported:
|
||||
|
||||
1. CSS/XPath-based extraction:
|
||||
```yaml
|
||||
# extract_css.yml
|
||||
type: "json-css"
|
||||
params:
|
||||
verbose: true
|
||||
```
|
||||
|
||||
```json
|
||||
// css_schema.json
|
||||
{
|
||||
"name": "ArticleExtractor",
|
||||
"baseSelector": ".article",
|
||||
"fields": [
|
||||
{
|
||||
"name": "title",
|
||||
"selector": "h1.title",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "link",
|
||||
"selector": "a.read-more",
|
||||
"type": "attribute",
|
||||
"attribute": "href"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. LLM-based extraction:
|
||||
```yaml
|
||||
# extract_llm.yml
|
||||
type: "llm"
|
||||
provider: "openai/gpt-4"
|
||||
instruction: "Extract all articles with their titles and links"
|
||||
api_token: "your-token"
|
||||
params:
|
||||
temperature: 0.3
|
||||
max_tokens: 1000
|
||||
```
|
||||
|
||||
```json
|
||||
// llm_schema.json
|
||||
{
|
||||
"title": "Article",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the article"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"description": "URL to the full article"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### LLM Q&A
|
||||
|
||||
Ask questions about crawled content:
|
||||
|
||||
```bash
|
||||
# Simple question
|
||||
crwl https://example.com -q "What is the main topic discussed?"
|
||||
|
||||
# View content then ask questions
|
||||
crwl https://example.com -o markdown # See content first
|
||||
crwl https://example.com -q "Summarize the key points"
|
||||
crwl https://example.com -q "What are the conclusions?"
|
||||
|
||||
# Combined with advanced crawling
|
||||
crwl https://example.com \
|
||||
-B browser.yml \
|
||||
-c "css_selector=article,scan_full_page=true" \
|
||||
-q "What are the pros and cons mentioned?"
|
||||
```
|
||||
|
||||
First-time setup:
|
||||
- Prompts for LLM provider and API token
|
||||
- Saves configuration in `~/.crawl4ai/global.yml`
|
||||
- Supports various providers (openai/gpt-4, anthropic/claude-3-sonnet, etc.)
|
||||
- For case of `ollama` you do not need to provide API token.
|
||||
- See [LiteLLM Providers](https://docs.litellm.ai/docs/providers) for full list
|
||||
|
||||
### Structured Data Extraction
|
||||
|
||||
Extract structured data using CSS selectors:
|
||||
|
||||
```bash
|
||||
crwl https://example.com \
|
||||
-e extract_css.yml \
|
||||
-s css_schema.json \
|
||||
-o json
|
||||
```
|
||||
|
||||
Or using LLM-based extraction:
|
||||
|
||||
```bash
|
||||
crwl https://example.com \
|
||||
-e extract_llm.yml \
|
||||
-s llm_schema.json \
|
||||
-o json
|
||||
```
|
||||
|
||||
### Content Filtering
|
||||
|
||||
Filter content for relevance:
|
||||
|
||||
```yaml
|
||||
# filter_bm25.yml
|
||||
type: "bm25"
|
||||
query: "target content"
|
||||
threshold: 1.0
|
||||
|
||||
# filter_pruning.yml
|
||||
type: "pruning"
|
||||
query: "focus topic"
|
||||
threshold: 0.48
|
||||
```
|
||||
|
||||
```bash
|
||||
crwl https://example.com -f filter_bm25.yml -o markdown-fit
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
- `all` - Full crawl result including metadata
|
||||
- `json` - Extracted structured data (when using extraction)
|
||||
- `markdown` / `md` - Raw markdown output
|
||||
- `markdown-fit` / `md-fit` - Filtered markdown for better readability
|
||||
|
||||
## Complete Examples
|
||||
|
||||
1. Basic Extraction:
|
||||
```bash
|
||||
crwl https://example.com \
|
||||
-B browser.yml \
|
||||
-C crawler.yml \
|
||||
-o json
|
||||
```
|
||||
|
||||
2. Structured Data Extraction:
|
||||
```bash
|
||||
crwl https://example.com \
|
||||
-e extract_css.yml \
|
||||
-s css_schema.json \
|
||||
-o json \
|
||||
-v
|
||||
```
|
||||
|
||||
3. LLM Extraction with Filtering:
|
||||
```bash
|
||||
crwl https://example.com \
|
||||
-B browser.yml \
|
||||
-e extract_llm.yml \
|
||||
-s llm_schema.json \
|
||||
-f filter_bm25.yml \
|
||||
-o json
|
||||
```
|
||||
|
||||
4. Interactive Q&A:
|
||||
```bash
|
||||
# First crawl and view
|
||||
crwl https://example.com -o markdown
|
||||
|
||||
# Then ask questions
|
||||
crwl https://example.com -q "What are the main points?"
|
||||
crwl https://example.com -q "Summarize the conclusions"
|
||||
```
|
||||
|
||||
## Best Practices & Tips
|
||||
|
||||
1. **Configuration Management**:
|
||||
- Keep common configurations in YAML files
|
||||
- Use CLI parameters for quick overrides
|
||||
- Store sensitive data (API tokens) in `~/.crawl4ai/global.yml`
|
||||
|
||||
2. **Performance Optimization**:
|
||||
- Use `--bypass-cache` for fresh content
|
||||
- Enable `scan_full_page` for infinite scroll pages
|
||||
- Adjust `delay_before_return_html` for dynamic content
|
||||
|
||||
3. **Content Extraction**:
|
||||
- Use CSS extraction for structured content
|
||||
- Use LLM extraction for unstructured content
|
||||
- Combine with filters for focused results
|
||||
|
||||
4. **Q&A Workflow**:
|
||||
- View content first with `-o markdown`
|
||||
- Ask specific questions
|
||||
- Use broader context with appropriate selectors
|
||||
|
||||
## Recap
|
||||
|
||||
The Crawl4AI CLI provides:
|
||||
- Flexible configuration via files and parameters
|
||||
- Multiple extraction strategies (CSS, XPath, LLM)
|
||||
- Content filtering and optimization
|
||||
- Interactive Q&A capabilities
|
||||
- Various output formats
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
# Content Selection
|
||||
|
||||
Crawl4AI provides multiple ways to **select**, **filter**, and **refine** the content from your crawls. Whether you need to target a specific CSS region, exclude entire tags, filter out external links, or remove certain domains and images, **`CrawlerRunConfig`** offers a wide range of parameters.
|
||||
|
||||
Below, we show how to configure these parameters and combine them for precise control.
|
||||
|
||||
---
|
||||
|
||||
## 1. CSS-Based Selection
|
||||
|
||||
There are two ways to select content from a page: using `css_selector` or the more flexible `target_elements`.
|
||||
|
||||
### 1.1 Using `css_selector`
|
||||
|
||||
A straightforward way to **limit** your crawl results to a certain region of the page is **`css_selector`** in **`CrawlerRunConfig`**:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
# e.g., first 30 items from Hacker News
|
||||
css_selector=".athing:nth-child(-n+30)"
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com/newest",
|
||||
config=config
|
||||
)
|
||||
print("Partial HTML length:", len(result.cleaned_html))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Result**: Only elements matching that selector remain in `result.cleaned_html`.
|
||||
|
||||
### 1.2 Using `target_elements`
|
||||
|
||||
The `target_elements` parameter provides more flexibility by allowing you to target **multiple elements** for content extraction while preserving the entire page context for other features:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
# Target article body and sidebar, but not other content
|
||||
target_elements=["article.main-content", "aside.sidebar"]
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com/blog-post",
|
||||
config=config
|
||||
)
|
||||
print("Markdown focused on target elements")
|
||||
print("Links from entire page still available:", len(result.links.get("internal", [])))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Key difference**: With `target_elements`, the markdown generation and structural data extraction focus on those elements, but other page elements (like links, images, and tables) are still extracted from the entire page. This gives you fine-grained control over what appears in your markdown content while preserving full page context for link analysis and media collection.
|
||||
|
||||
---
|
||||
|
||||
## 2. Content Filtering & Exclusions
|
||||
|
||||
### 2.1 Basic Overview
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
# Content thresholds
|
||||
word_count_threshold=10, # Minimum words per block
|
||||
|
||||
# Tag exclusions
|
||||
excluded_tags=['form', 'header', 'footer', 'nav'],
|
||||
|
||||
# Link filtering
|
||||
exclude_external_links=True,
|
||||
exclude_social_media_links=True,
|
||||
# Block entire domains
|
||||
exclude_domains=["adtrackers.com", "spammynews.org"],
|
||||
exclude_social_media_domains=["facebook.com", "twitter.com"],
|
||||
|
||||
# Media filtering
|
||||
exclude_external_images=True
|
||||
)
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
- **`word_count_threshold`**: Ignores text blocks under X words. Helps skip trivial blocks like short nav or disclaimers.
|
||||
- **`excluded_tags`**: Removes entire tags (`<form>`, `<header>`, `<footer>`, etc.).
|
||||
- **Link Filtering**:
|
||||
- `exclude_external_links`: Strips out external links and may remove them from `result.links`.
|
||||
- `exclude_social_media_links`: Removes links pointing to known social media domains.
|
||||
- `exclude_domains`: A custom list of domains to block if discovered in links.
|
||||
- `exclude_social_media_domains`: A curated list (override or add to it) for social media sites.
|
||||
- **Media Filtering**:
|
||||
- `exclude_external_images`: Discards images not hosted on the same domain as the main page (or its subdomains).
|
||||
|
||||
By default in case you set `exclude_social_media_links=True`, the following social media domains are excluded:
|
||||
```python
|
||||
[
|
||||
'facebook.com',
|
||||
'twitter.com',
|
||||
'x.com',
|
||||
'linkedin.com',
|
||||
'instagram.com',
|
||||
'pinterest.com',
|
||||
'tiktok.com',
|
||||
'snapchat.com',
|
||||
'reddit.com',
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
### 2.2 Example Usage
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
css_selector="main.content",
|
||||
word_count_threshold=10,
|
||||
excluded_tags=["nav", "footer"],
|
||||
exclude_external_links=True,
|
||||
exclude_social_media_links=True,
|
||||
exclude_domains=["ads.com", "spammytrackers.net"],
|
||||
exclude_external_images=True,
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url="https://news.ycombinator.com", config=config)
|
||||
print("Cleaned HTML length:", len(result.cleaned_html))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Note**: If these parameters remove too much, reduce or disable them accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Handling Iframes
|
||||
|
||||
Some sites embed content in `<iframe>` tags. If you want that inline:
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
# Merge iframe content into the final output
|
||||
process_iframes=True,
|
||||
remove_overlay_elements=True,
|
||||
# Remove GDPR/cookie consent popups (OneTrust, Cookiebot, etc.)
|
||||
remove_consent_popups=True
|
||||
)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
process_iframes=True,
|
||||
remove_overlay_elements=True
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.org/iframe-demo",
|
||||
config=config
|
||||
)
|
||||
print("Iframe-merged length:", len(result.cleaned_html))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Flattening Shadow DOM
|
||||
|
||||
Sites built with **Web Components** (Stencil, Lit, Shoelace, Angular Elements, etc.) render content inside [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) — an encapsulated sub-tree that is invisible to normal page serialization. The browser renders it on screen, but `page.content()` never includes it.
|
||||
|
||||
Set `flatten_shadow_dom=True` to walk all shadow trees, resolve `<slot>` projections, and produce a single flat HTML document:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
# Flatten shadow DOM into the main document
|
||||
flatten_shadow_dom=True,
|
||||
# Give web components time to hydrate
|
||||
wait_until="load",
|
||||
delay_before_return_html=3.0,
|
||||
)
|
||||
```
|
||||
|
||||
**Full example** — crawling a product page where specs live inside shadow roots:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
flatten_shadow_dom=True,
|
||||
wait_until="load",
|
||||
delay_before_return_html=3.0,
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://store.boschrexroth.com/en/us/p/hydraulic-cylinder-r900999011",
|
||||
config=config,
|
||||
)
|
||||
# Without flatten_shadow_dom: ~1 KB of markdown (breadcrumbs only)
|
||||
# With flatten_shadow_dom: ~33 KB (full product specs, downloads, etc.)
|
||||
print(len(result.markdown.raw_markdown))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When `flatten_shadow_dom=True` is set, Crawl4AI also injects an init script that force-opens **closed** shadow roots (by patching `Element.prototype.attachShadow`), so even components that use `mode: 'closed'` become accessible.
|
||||
|
||||
> **Tip**: Web components need JavaScript to run before they render content (a process called *hydration*). Use `wait_until="load"` and a `delay_before_return_html` of 2–5 seconds to ensure components are fully hydrated before flattening.
|
||||
|
||||
For a complete runnable example, see [`shadow_dom_crawling.py`](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/shadow_dom_crawling.py).
|
||||
|
||||
---
|
||||
|
||||
## 4. Structured Extraction Examples
|
||||
|
||||
You can combine content selection with a more advanced extraction strategy. For instance, a **CSS-based** or **LLM-based** extraction strategy can run on the filtered HTML.
|
||||
|
||||
### 4.1 Pattern-Based with `JsonCssExtractionStrategy`
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def main():
|
||||
# Minimal schema for repeated items
|
||||
schema = {
|
||||
"name": "News Items",
|
||||
"baseSelector": "tr.athing",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "span.titleline a", "type": "text"},
|
||||
{
|
||||
"name": "link",
|
||||
"selector": "span.titleline a",
|
||||
"type": "attribute",
|
||||
"attribute": "href"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
# Content filtering
|
||||
excluded_tags=["form", "header"],
|
||||
exclude_domains=["adsite.com"],
|
||||
|
||||
# CSS selection or entire page
|
||||
css_selector="table.itemlist",
|
||||
|
||||
# No caching for demonstration
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
|
||||
# Extraction strategy
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema)
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com/newest",
|
||||
config=config
|
||||
)
|
||||
data = json.loads(result.extracted_content)
|
||||
print("Sample extracted item:", data[:1]) # Show first item
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 4.2 LLM-Based Extraction
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from pydantic import BaseModel, Field
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, LLMConfig
|
||||
from crawl4ai import LLMExtractionStrategy
|
||||
|
||||
class ArticleData(BaseModel):
|
||||
headline: str
|
||||
summary: str
|
||||
|
||||
async def main():
|
||||
llm_strategy = LLMExtractionStrategy(
|
||||
llm_config = LLMConfig(provider="openai/gpt-4",api_token="sk-YOUR_API_KEY")
|
||||
schema=ArticleData.schema(),
|
||||
extraction_type="schema",
|
||||
instruction="Extract 'headline' and a short 'summary' from the content."
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
exclude_external_links=True,
|
||||
word_count_threshold=20,
|
||||
extraction_strategy=llm_strategy
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url="https://news.ycombinator.com", config=config)
|
||||
article = json.loads(result.extracted_content)
|
||||
print(article)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Here, the crawler:
|
||||
|
||||
- Filters out external links (`exclude_external_links=True`).
|
||||
- Ignores very short text blocks (`word_count_threshold=20`).
|
||||
- Passes the final HTML to your LLM strategy for an AI-driven parse.
|
||||
|
||||
---
|
||||
|
||||
## 5. Comprehensive Example
|
||||
|
||||
Below is a short function that unifies **CSS selection**, **exclusion** logic, and a pattern-based extraction, demonstrating how you can fine-tune your final data:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def extract_main_articles(url: str):
|
||||
schema = {
|
||||
"name": "ArticleBlock",
|
||||
"baseSelector": "div.article-block",
|
||||
"fields": [
|
||||
{"name": "headline", "selector": "h2", "type": "text"},
|
||||
{"name": "summary", "selector": ".summary", "type": "text"},
|
||||
{
|
||||
"name": "metadata",
|
||||
"type": "nested",
|
||||
"fields": [
|
||||
{"name": "author", "selector": ".author", "type": "text"},
|
||||
{"name": "date", "selector": ".date", "type": "text"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
# Keep only #main-content
|
||||
css_selector="#main-content",
|
||||
|
||||
# Filtering
|
||||
word_count_threshold=10,
|
||||
excluded_tags=["nav", "footer"],
|
||||
exclude_external_links=True,
|
||||
exclude_domains=["somebadsite.com"],
|
||||
exclude_external_images=True,
|
||||
|
||||
# Extraction
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema),
|
||||
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url=url, config=config)
|
||||
if not result.success:
|
||||
print(f"Error: {result.error_message}")
|
||||
return None
|
||||
return json.loads(result.extracted_content)
|
||||
|
||||
async def main():
|
||||
articles = await extract_main_articles("https://news.ycombinator.com/newest")
|
||||
if articles:
|
||||
print("Extracted Articles:", articles[:2]) # Show first 2
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Why This Works**:
|
||||
- **CSS** scoping with `#main-content`.
|
||||
- Multiple **exclude_** parameters to remove domains, external images, etc.
|
||||
- A **JsonCssExtractionStrategy** to parse repeated article blocks.
|
||||
|
||||
---
|
||||
|
||||
## 6. Scraping Modes
|
||||
|
||||
Crawl4AI uses `LXMLWebScrapingStrategy` (LXML-based) as the default scraping strategy for HTML content processing. This strategy offers excellent performance, especially for large HTML documents.
|
||||
|
||||
**Note:** For backward compatibility, `WebScrapingStrategy` is still available as an alias for `LXMLWebScrapingStrategy`.
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, LXMLWebScrapingStrategy
|
||||
|
||||
async def main():
|
||||
# Default configuration already uses LXMLWebScrapingStrategy
|
||||
config = CrawlerRunConfig()
|
||||
|
||||
# Or explicitly specify it if desired
|
||||
config_explicit = CrawlerRunConfig(
|
||||
scraping_strategy=LXMLWebScrapingStrategy()
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=config
|
||||
)
|
||||
```
|
||||
|
||||
You can also create your own custom scraping strategy by inheriting from `ContentScrapingStrategy`. The strategy must return a `ScrapingResult` object with the following structure:
|
||||
|
||||
```python
|
||||
from crawl4ai import ContentScrapingStrategy, ScrapingResult, MediaItem, Media, Link, Links
|
||||
|
||||
class CustomScrapingStrategy(ContentScrapingStrategy):
|
||||
def scrap(self, url: str, html: str, **kwargs) -> ScrapingResult:
|
||||
# Implement your custom scraping logic here
|
||||
return ScrapingResult(
|
||||
cleaned_html="<html>...</html>", # Cleaned HTML content
|
||||
success=True, # Whether scraping was successful
|
||||
media=Media(
|
||||
images=[ # List of images found
|
||||
MediaItem(
|
||||
src="https://example.com/image.jpg",
|
||||
alt="Image description",
|
||||
desc="Surrounding text",
|
||||
score=1,
|
||||
type="image",
|
||||
group_id=1,
|
||||
format="jpg",
|
||||
width=800
|
||||
)
|
||||
],
|
||||
videos=[], # List of videos (same structure as images)
|
||||
audios=[] # List of audio files (same structure as images)
|
||||
),
|
||||
links=Links(
|
||||
internal=[ # List of internal links
|
||||
Link(
|
||||
href="https://example.com/page",
|
||||
text="Link text",
|
||||
title="Link title",
|
||||
base_domain="example.com"
|
||||
)
|
||||
],
|
||||
external=[] # List of external links (same structure)
|
||||
),
|
||||
metadata={ # Additional metadata
|
||||
"title": "Page Title",
|
||||
"description": "Page description"
|
||||
}
|
||||
)
|
||||
|
||||
async def ascrap(self, url: str, html: str, **kwargs) -> ScrapingResult:
|
||||
# For simple cases, you can use the sync version
|
||||
return await asyncio.to_thread(self.scrap, url, html, **kwargs)
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
The LXML strategy provides excellent performance, particularly when processing large HTML documents, offering up to 10-20x faster processing compared to BeautifulSoup-based approaches.
|
||||
|
||||
Benefits of LXML strategy:
|
||||
- Fast processing of large HTML documents (especially >100KB)
|
||||
- Efficient memory usage
|
||||
- Good handling of well-formed HTML
|
||||
- Robust table detection and extraction
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
For users upgrading from earlier versions:
|
||||
- `WebScrapingStrategy` is now an alias for `LXMLWebScrapingStrategy`
|
||||
- Existing code using `WebScrapingStrategy` will continue to work without modification
|
||||
- No changes are required to your existing code
|
||||
|
||||
---
|
||||
|
||||
## 7. Combining CSS Selection Methods
|
||||
|
||||
You can combine `css_selector` and `target_elements` in powerful ways to achieve fine-grained control over your output:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def main():
|
||||
# Target specific content but preserve page context
|
||||
config = CrawlerRunConfig(
|
||||
# Focus markdown on main content and sidebar
|
||||
target_elements=["#main-content", ".sidebar"],
|
||||
|
||||
# Global filters applied to entire page
|
||||
excluded_tags=["nav", "footer", "header"],
|
||||
exclude_external_links=True,
|
||||
|
||||
# Use basic content thresholds
|
||||
word_count_threshold=15,
|
||||
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com/article",
|
||||
config=config
|
||||
)
|
||||
|
||||
print(f"Content focuses on specific elements, but all links still analyzed")
|
||||
print(f"Internal links: {len(result.links.get('internal', []))}")
|
||||
print(f"External links: {len(result.links.get('external', []))}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
This approach gives you the best of both worlds:
|
||||
- Markdown generation and content extraction focus on the elements you care about
|
||||
- Links, images and other page data still give you the full context of the page
|
||||
- Content filtering still applies globally
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
By mixing **target_elements** or **css_selector** scoping, **content filtering** parameters, and advanced **extraction strategies**, you can precisely **choose** which data to keep. Key parameters in **`CrawlerRunConfig`** for content selection include:
|
||||
|
||||
1. **`target_elements`** – Array of CSS selectors to focus markdown generation and data extraction, while preserving full page context for links and media.
|
||||
2. **`css_selector`** – Basic scoping to an element or region for all extraction processes.
|
||||
3. **`word_count_threshold`** – Skip short blocks.
|
||||
4. **`excluded_tags`** – Remove entire HTML tags.
|
||||
5. **`exclude_external_links`**, **`exclude_social_media_links`**, **`exclude_domains`** – Filter out unwanted links or domains.
|
||||
6. **`exclude_external_images`** – Remove images from external sources.
|
||||
7. **`process_iframes`** – Merge iframe content if needed.
|
||||
|
||||
Combine these with structured extraction (CSS, LLM-based, or others) to build powerful crawls that yield exactly the content you want, from raw or cleaned HTML up to sophisticated JSON structures. For more detail, see [Configuration Reference](../api/parameters.md). Enjoy curating your data to the max!
|
||||
@@ -0,0 +1,343 @@
|
||||
# Crawl Result and Output
|
||||
|
||||
When you call `arun()` on a page, Crawl4AI returns a **`CrawlResult`** object containing everything you might need—raw HTML, a cleaned version, optional screenshots or PDFs, structured extraction results, and more. This document explains those fields and how they map to different output types.
|
||||
|
||||
---
|
||||
|
||||
## 1. The `CrawlResult` Model
|
||||
|
||||
Below is the core schema. Each field captures a different aspect of the crawl’s result:
|
||||
|
||||
```python
|
||||
class MarkdownGenerationResult(BaseModel):
|
||||
raw_markdown: str
|
||||
markdown_with_citations: str
|
||||
references_markdown: str
|
||||
fit_markdown: Optional[str] = None
|
||||
fit_html: Optional[str] = None
|
||||
|
||||
class CrawlResult(BaseModel):
|
||||
url: str
|
||||
html: str
|
||||
fit_html: Optional[str] = None
|
||||
success: bool
|
||||
cleaned_html: Optional[str] = None
|
||||
media: Dict[str, List[Dict]] = {}
|
||||
links: Dict[str, List[Dict]] = {}
|
||||
downloaded_files: Optional[List[str]] = None
|
||||
js_execution_result: Optional[Dict[str, Any]] = None
|
||||
screenshot: Optional[str] = None
|
||||
pdf: Optional[bytes] = None
|
||||
mhtml: Optional[str] = None
|
||||
markdown: Optional[Union[str, MarkdownGenerationResult]] = None
|
||||
extracted_content: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
error_message: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
response_headers: Optional[dict] = None
|
||||
status_code: Optional[int] = None
|
||||
ssl_certificate: Optional[SSLCertificate] = None
|
||||
dispatch_result: Optional[DispatchResult] = None
|
||||
redirected_url: Optional[str] = None
|
||||
redirected_status_code: Optional[int] = None
|
||||
network_requests: Optional[List[Dict[str, Any]]] = None
|
||||
console_messages: Optional[List[Dict[str, Any]]] = None
|
||||
tables: List[Dict] = Field(default_factory=list)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
```
|
||||
|
||||
### Table: Key Fields in `CrawlResult`
|
||||
|
||||
| Field (Name & Type) | Description |
|
||||
|-------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
| **url (`str`)** | The final or actual URL crawled (in case of redirects). |
|
||||
| **html (`str`)** | Original, unmodified page HTML. Good for debugging or custom processing. |
|
||||
| **fit_html (`Optional[str]`)** | Preprocessed HTML optimized for extraction and content filtering. |
|
||||
| **success (`bool`)** | `True` if the crawl completed without major errors, else `False`. |
|
||||
| **cleaned_html (`Optional[str]`)** | Sanitized HTML with scripts/styles removed; can exclude tags if configured via `excluded_tags` etc. |
|
||||
| **media (`Dict[str, List[Dict]]`)** | Extracted media info (images, audio, etc.), each with attributes like `src`, `alt`, `score`, etc. |
|
||||
| **links (`Dict[str, List[Dict]]`)** | Extracted link data, split by `internal` and `external`. Each link usually has `href`, `text`, etc. |
|
||||
| **downloaded_files (`Optional[List[str]]`)** | If `accept_downloads=True` in `BrowserConfig`, this lists the filepaths of saved downloads. |
|
||||
| **js_execution_result (`Optional[Dict[str, Any]]`)** | Results from JavaScript execution during crawling. |
|
||||
| **screenshot (`Optional[str]`)** | Screenshot of the page (base64-encoded) if `screenshot=True`. |
|
||||
| **pdf (`Optional[bytes]`)** | PDF of the page if `pdf=True`. |
|
||||
| **mhtml (`Optional[str]`)** | MHTML snapshot of the page if `capture_mhtml=True`. Contains the full page with all resources. |
|
||||
| **markdown (`Optional[str or MarkdownGenerationResult]`)** | It holds a `MarkdownGenerationResult`. Over time, this will be consolidated into `markdown`. The generator can provide raw markdown, citations, references, and optionally `fit_markdown`. |
|
||||
| **extracted_content (`Optional[str]`)** | The output of a structured extraction (CSS/LLM-based) stored as JSON string or other text. |
|
||||
| **metadata (`Optional[dict]`)** | Additional info about the crawl or extracted data. |
|
||||
| **error_message (`Optional[str]`)** | If `success=False`, contains a short description of what went wrong. |
|
||||
| **session_id (`Optional[str]`)** | The ID of the session used for multi-page or persistent crawling. |
|
||||
| **response_headers (`Optional[dict]`)** | HTTP response headers, if captured. |
|
||||
| **status_code (`Optional[int]`)** | HTTP status code (e.g., 200 for OK). |
|
||||
| **ssl_certificate (`Optional[SSLCertificate]`)** | SSL certificate info if `fetch_ssl_certificate=True`. |
|
||||
| **dispatch_result (`Optional[DispatchResult]`)** | Additional concurrency and resource usage information when crawling URLs in parallel. |
|
||||
| **redirected_url (`Optional[str]`)** | The URL after any redirects (different from `url` which is the final URL). |
|
||||
| **redirected_status_code (`Optional[int]`)** | HTTP status code of the final redirect destination (e.g., 200). `None` for non-HTTP requests (raw HTML, local files). |
|
||||
| **network_requests (`Optional[List[Dict[str, Any]]]`)** | List of network requests, responses, and failures captured during the crawl if `capture_network_requests=True`. |
|
||||
| **console_messages (`Optional[List[Dict[str, Any]]]`)** | List of browser console messages captured during the crawl if `capture_console_messages=True`. |
|
||||
| **tables (`List[Dict]`)** | Table data extracted from HTML tables with structure `[{headers, rows, caption, summary}]`. |
|
||||
|
||||
---
|
||||
|
||||
## 2. HTML Variants
|
||||
|
||||
### `html`: Raw HTML
|
||||
|
||||
Crawl4AI preserves the exact HTML as `result.html`. Useful for:
|
||||
|
||||
- Debugging page issues or checking the original content.
|
||||
- Performing your own specialized parse if needed.
|
||||
|
||||
### `cleaned_html`: Sanitized
|
||||
|
||||
If you specify any cleanup or exclusion parameters in `CrawlerRunConfig` (like `excluded_tags`, `remove_forms`, etc.), you’ll see the result here:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
excluded_tags=["form", "header", "footer"],
|
||||
keep_data_attributes=False
|
||||
)
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
print(result.cleaned_html) # Freed of forms, header, footer, data-* attributes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Markdown Generation
|
||||
|
||||
### 3.1 `markdown`
|
||||
|
||||
- **`markdown`**: The current location for detailed markdown output, returning a **`MarkdownGenerationResult`** object.
|
||||
- **`markdown_v2`**: Removed in v0.5. Accessing it now raises `AttributeError`; use `markdown`.
|
||||
|
||||
**`MarkdownGenerationResult`** Fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------------------------|--------------------------------------------------------------------------------|
|
||||
| **raw_markdown** | The basic HTML→Markdown conversion. |
|
||||
| **markdown_with_citations** | Markdown including inline citations that reference links at the end. |
|
||||
| **references_markdown** | The references/citations themselves (if `citations=True`). |
|
||||
| **fit_markdown** | The filtered/“fit” markdown if a content filter was used. |
|
||||
| **fit_html** | The filtered HTML that generated `fit_markdown`. |
|
||||
|
||||
### 3.2 Basic Example with a Markdown Generator
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
options={"citations": True, "body_width": 80} # e.g. pass html2text style options
|
||||
)
|
||||
)
|
||||
result = await crawler.arun(url="https://example.com", config=config)
|
||||
|
||||
md_res = result.markdown # or eventually 'result.markdown'
|
||||
print(md_res.raw_markdown[:500])
|
||||
print(md_res.markdown_with_citations)
|
||||
print(md_res.references_markdown)
|
||||
```
|
||||
|
||||
**Note**: If you use a filter like `PruningContentFilter`, you’ll get `fit_markdown` and `fit_html` as well.
|
||||
|
||||
---
|
||||
|
||||
## 4. Structured Extraction: `extracted_content`
|
||||
|
||||
If you run a JSON-based extraction strategy (CSS, XPath, LLM, etc.), the structured data is **not** stored in `markdown`—it’s placed in **`result.extracted_content`** as a JSON string (or sometimes plain text).
|
||||
|
||||
### Example: CSS Extraction with `raw://` HTML
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def main():
|
||||
schema = {
|
||||
"name": "Example Items",
|
||||
"baseSelector": "div.item",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2", "type": "text"},
|
||||
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
|
||||
]
|
||||
}
|
||||
raw_html = "<div class='item'><h2>Item 1</h2><a href='https://example.com/item1'>Link 1</a></div>"
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="raw://" + raw_html,
|
||||
config=CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema)
|
||||
)
|
||||
)
|
||||
data = json.loads(result.extracted_content)
|
||||
print(data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Here:
|
||||
- `url="raw://..."` passes the HTML content directly, no network requests.
|
||||
- The **CSS** extraction strategy populates `result.extracted_content` with the JSON array `[{"title": "...", "link": "..."}]`.
|
||||
|
||||
---
|
||||
|
||||
## 5. More Fields: Links, Media, Tables and More
|
||||
|
||||
### 5.1 `links`
|
||||
|
||||
A dictionary, typically with `"internal"` and `"external"` lists. Each entry might have `href`, `text`, `title`, etc. This is automatically captured if you haven’t disabled link extraction.
|
||||
|
||||
```python
|
||||
print(result.links["internal"][:3]) # Show first 3 internal links
|
||||
```
|
||||
|
||||
### 5.2 `media`
|
||||
|
||||
Similarly, a dictionary with `"images"`, `"audio"`, `"video"`, etc. Each item could include `src`, `alt`, `score`, and more, if your crawler is set to gather them.
|
||||
|
||||
```python
|
||||
images = result.media.get("images", [])
|
||||
for img in images:
|
||||
print("Image URL:", img["src"], "Alt:", img.get("alt"))
|
||||
```
|
||||
|
||||
### 5.3 `tables`
|
||||
|
||||
The `tables` field contains structured data extracted from HTML tables found on the crawled page. Tables are analyzed based on various criteria to determine if they are actual data tables (as opposed to layout tables), including:
|
||||
|
||||
- Presence of thead and tbody sections
|
||||
- Use of th elements for headers
|
||||
- Column consistency
|
||||
- Text density
|
||||
- And other factors
|
||||
|
||||
Tables that score above the threshold (default: 7) are extracted and stored in result.tables.
|
||||
|
||||
### Accessing Table data:
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://www.w3schools.com/html/html_tables.asp",
|
||||
config=CrawlerRunConfig(
|
||||
table_score_threshold=7 # Minimum score for table detection
|
||||
)
|
||||
)
|
||||
|
||||
if result.success and result.tables:
|
||||
print(f"Found {len(result.tables)} tables")
|
||||
|
||||
for i, table in enumerate(result.tables):
|
||||
print(f"\nTable {i+1}:")
|
||||
print(f"Caption: {table.get('caption', 'No caption')}")
|
||||
print(f"Headers: {table['headers']}")
|
||||
print(f"Rows: {len(table['rows'])}")
|
||||
|
||||
# Print first few rows as example
|
||||
for j, row in enumerate(table['rows'][:3]):
|
||||
print(f" Row {j+1}: {row}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
### Configuring Table Extraction:
|
||||
|
||||
You can adjust the sensitivity of the table detection algorithm with:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
table_score_threshold=5 # Lower value = more tables detected (default: 7)
|
||||
)
|
||||
```
|
||||
|
||||
Each extracted table contains:
|
||||
|
||||
- `headers`: Column header names
|
||||
- `rows`: List of rows, each containing cell values
|
||||
- `caption`: Table caption text (if available)
|
||||
- `summary`: Table summary attribute (if specified)
|
||||
|
||||
### Table Extraction Tips
|
||||
|
||||
- Not all HTML tables are extracted - only those detected as "data tables" vs. layout tables.
|
||||
- Tables with inconsistent cell counts, nested tables, or those used purely for layout may be skipped.
|
||||
- If you're missing tables, try adjusting the `table_score_threshold` to a lower value (default is 7).
|
||||
|
||||
The table detection algorithm scores tables based on features like consistent columns, presence of headers, text density, and more. Tables scoring above the threshold are considered data tables worth extracting.
|
||||
|
||||
|
||||
### 5.4 `screenshot`, `pdf`, and `mhtml`
|
||||
|
||||
If you set `screenshot=True`, `pdf=True`, or `capture_mhtml=True` in **`CrawlerRunConfig`**, then:
|
||||
|
||||
- `result.screenshot` contains a base64-encoded PNG string.
|
||||
- `result.pdf` contains raw PDF bytes (you can write them to a file).
|
||||
- `result.mhtml` contains the MHTML snapshot of the page as a string (you can write it to a .mhtml file).
|
||||
|
||||
```python
|
||||
# Save the PDF
|
||||
with open("page.pdf", "wb") as f:
|
||||
f.write(result.pdf)
|
||||
|
||||
# Save the MHTML
|
||||
if result.mhtml:
|
||||
with open("page.mhtml", "w", encoding="utf-8") as f:
|
||||
f.write(result.mhtml)
|
||||
```
|
||||
|
||||
The MHTML (MIME HTML) format is particularly useful as it captures the entire web page including all of its resources (CSS, images, scripts, etc.) in a single file, making it perfect for archiving or offline viewing.
|
||||
|
||||
### 5.5 `ssl_certificate`
|
||||
|
||||
If `fetch_ssl_certificate=True`, `result.ssl_certificate` holds details about the site’s SSL cert, such as issuer, validity dates, etc.
|
||||
|
||||
---
|
||||
|
||||
## 6. Accessing These Fields
|
||||
|
||||
After you run:
|
||||
|
||||
```python
|
||||
result = await crawler.arun(url="https://example.com", config=some_config)
|
||||
```
|
||||
|
||||
Check any field:
|
||||
|
||||
```python
|
||||
if result.success:
|
||||
print(result.status_code, result.response_headers)
|
||||
print("Links found:", len(result.links.get("internal", [])))
|
||||
if result.markdown:
|
||||
print("Markdown snippet:", result.markdown.raw_markdown[:200])
|
||||
if result.extracted_content:
|
||||
print("Structured JSON:", result.extracted_content)
|
||||
else:
|
||||
print("Error:", result.error_message)
|
||||
```
|
||||
|
||||
**Deprecation**: Since v0.5 `markdown_v2`, `fit_markdown`, and `fit_html` are removed from `CrawlResult`. Use `result.markdown` for markdown output. It holds `MarkdownGenerationResult`, including `fit_html` and `fit_markdown`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. Next Steps
|
||||
|
||||
- **Markdown Generation**: Dive deeper into how to configure `DefaultMarkdownGenerator` and various filters.
|
||||
- **Content Filtering**: Learn how to use `BM25ContentFilter` and `PruningContentFilter`.
|
||||
- **Session & Hooks**: If you want to manipulate the page or preserve state across multiple `arun()` calls, see the hooking or session docs.
|
||||
- **LLM Extraction**: For complex or unstructured content requiring AI-driven parsing, check the LLM-based strategies doc.
|
||||
|
||||
**Enjoy** exploring all that `CrawlResult` offers—whether you need raw HTML, sanitized output, markdown, or fully structured data, Crawl4AI has you covered!
|
||||
@@ -0,0 +1,907 @@
|
||||
# Deep Crawling
|
||||
|
||||
One of Crawl4AI's most powerful features is its ability to perform **configurable deep crawling** that can explore websites beyond a single page. With fine-tuned control over crawl depth, domain boundaries, and content filtering, Crawl4AI gives you the tools to extract precisely the content you need.
|
||||
|
||||
In this tutorial, you'll learn:
|
||||
|
||||
1. How to set up a **Basic Deep Crawler** with BFS strategy
|
||||
2. Understanding the difference between **streamed and non-streamed** output
|
||||
3. Implementing **filters and scorers** to target specific content
|
||||
4. Creating **advanced filtering chains** for sophisticated crawls
|
||||
5. Using **BestFirstCrawling** for intelligent exploration prioritization
|
||||
6. **Crash recovery** for long-running production crawls
|
||||
7. **Prefetch mode** for fast URL discovery
|
||||
|
||||
> **Prerequisites**
|
||||
> - You’ve completed or read [AsyncWebCrawler Basics](../core/simple-crawling.md) to understand how to run a simple crawl.
|
||||
> - You know how to configure `CrawlerRunConfig`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Quick Example
|
||||
|
||||
Here's a minimal code snippet that implements a basic deep crawl using the **BFSDeepCrawlStrategy**:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
from crawl4ai.content_scraping_strategy import LXMLWebScrapingStrategy
|
||||
|
||||
async def main():
|
||||
# Configure a 2-level deep crawl
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(
|
||||
max_depth=2,
|
||||
include_external=False
|
||||
),
|
||||
scraping_strategy=LXMLWebScrapingStrategy(),
|
||||
verbose=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = await crawler.arun("https://example.com", config=config)
|
||||
|
||||
print(f"Crawled {len(results)} pages in total")
|
||||
|
||||
# Access individual results
|
||||
for result in results[:3]: # Show first 3 results
|
||||
print(f"URL: {result.url}")
|
||||
print(f"Depth: {result.metadata.get('depth', 0)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**What's happening?**
|
||||
- `BFSDeepCrawlStrategy(max_depth=2, include_external=False)` instructs Crawl4AI to:
|
||||
- Crawl the starting page (depth 0) plus 2 more levels
|
||||
- Stay within the same domain (don't follow external links)
|
||||
- Each result contains metadata like the crawl depth
|
||||
- Results are returned as a list after all crawling is complete
|
||||
|
||||
---
|
||||
|
||||
## 2. Understanding Deep Crawling Strategy Options
|
||||
|
||||
### 2.1 BFSDeepCrawlStrategy (Breadth-First Search)
|
||||
|
||||
The **BFSDeepCrawlStrategy** uses a breadth-first approach, exploring all links at one depth before moving deeper:
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
|
||||
# Basic configuration
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=2, # Crawl initial page + 2 levels deep
|
||||
include_external=False, # Stay within the same domain
|
||||
max_pages=50, # Maximum number of pages to crawl (optional)
|
||||
score_threshold=0.3, # Minimum score for URLs to be crawled (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**Key parameters:**
|
||||
- **`max_depth`**: Number of levels to crawl beyond the starting page
|
||||
- **`include_external`**: Whether to follow links to other domains
|
||||
- **`max_pages`**: Maximum number of pages to crawl (default: infinite)
|
||||
- **`score_threshold`**: Minimum score for URLs to be crawled (default: -inf)
|
||||
- **`filter_chain`**: FilterChain instance for URL filtering
|
||||
- **`url_scorer`**: Scorer instance for evaluating URLs
|
||||
|
||||
### 2.2 DFSDeepCrawlStrategy (Depth-First Search)
|
||||
|
||||
The **DFSDeepCrawlStrategy** uses a depth-first approach, explores as far down a branch as possible before backtracking.
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling import DFSDeepCrawlStrategy
|
||||
|
||||
# Basic configuration
|
||||
strategy = DFSDeepCrawlStrategy(
|
||||
max_depth=2, # Crawl initial page + 2 levels deep
|
||||
include_external=False, # Stay within the same domain
|
||||
max_pages=30, # Maximum number of pages to crawl (optional)
|
||||
score_threshold=0.5, # Minimum score for URLs to be crawled (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**Key parameters:**
|
||||
- **`max_depth`**: Number of levels to crawl beyond the starting page
|
||||
- **`include_external`**: Whether to follow links to other domains
|
||||
- **`max_pages`**: Maximum number of pages to crawl (default: infinite)
|
||||
- **`score_threshold`**: Minimum score for URLs to be crawled (default: -inf)
|
||||
- **`filter_chain`**: FilterChain instance for URL filtering
|
||||
- **`url_scorer`**: Scorer instance for evaluating URLs
|
||||
|
||||
### 2.3 BestFirstCrawlingStrategy (⭐️ - Recommended Deep crawl strategy)
|
||||
|
||||
For more intelligent crawling, use **BestFirstCrawlingStrategy** with scorers to prioritize the most relevant pages:
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling import BestFirstCrawlingStrategy
|
||||
from crawl4ai.deep_crawling.scorers import KeywordRelevanceScorer
|
||||
|
||||
# Create a scorer
|
||||
scorer = KeywordRelevanceScorer(
|
||||
keywords=["crawl", "example", "async", "configuration"],
|
||||
weight=0.7
|
||||
)
|
||||
|
||||
# Configure the strategy
|
||||
strategy = BestFirstCrawlingStrategy(
|
||||
max_depth=2,
|
||||
include_external=False,
|
||||
url_scorer=scorer,
|
||||
max_pages=25, # Maximum number of pages to crawl (optional)
|
||||
)
|
||||
```
|
||||
|
||||
This crawling approach:
|
||||
- Evaluates each discovered URL based on scorer criteria
|
||||
- Visits higher-scoring pages first
|
||||
- Helps focus crawl resources on the most relevant content
|
||||
- Can limit total pages crawled with `max_pages`
|
||||
- Does not need `score_threshold` as it naturally prioritizes by score
|
||||
|
||||
---
|
||||
|
||||
## 3. Streaming vs. Non-Streaming Results
|
||||
|
||||
Crawl4AI can return results in two modes:
|
||||
|
||||
### 3.1 Non-Streaming Mode (Default)
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(max_depth=1),
|
||||
stream=False # Default behavior
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Wait for ALL results to be collected before returning
|
||||
results = await crawler.arun("https://example.com", config=config)
|
||||
|
||||
for result in results:
|
||||
process_result(result)
|
||||
```
|
||||
|
||||
**When to use non-streaming mode:**
|
||||
- You need the complete dataset before processing
|
||||
- You're performing batch operations on all results together
|
||||
- Crawl time isn't a critical factor
|
||||
|
||||
### 3.2 Streaming Mode
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(max_depth=1),
|
||||
stream=True # Enable streaming
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Returns an async iterator
|
||||
async for result in await crawler.arun("https://example.com", config=config):
|
||||
# Process each result as it becomes available
|
||||
process_result(result)
|
||||
```
|
||||
|
||||
**Benefits of streaming mode:**
|
||||
- Process results immediately as they're discovered
|
||||
- Start working with early results while crawling continues
|
||||
- Better for real-time applications or progressive display
|
||||
- Reduces memory pressure when handling many pages
|
||||
|
||||
---
|
||||
|
||||
## 4. Filtering Content with Filter Chains
|
||||
|
||||
Filters help you narrow down which pages to crawl. Combine multiple filters using **FilterChain** for powerful targeting.
|
||||
|
||||
### 4.1 Basic URL Pattern Filter
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling.filters import FilterChain, URLPatternFilter
|
||||
|
||||
# Only follow URLs containing "blog" or "docs"
|
||||
url_filter = URLPatternFilter(patterns=["*blog*", "*docs*"])
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(
|
||||
max_depth=1,
|
||||
filter_chain=FilterChain([url_filter])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 4.2 Combining Multiple Filters
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling.filters import (
|
||||
FilterChain,
|
||||
URLPatternFilter,
|
||||
DomainFilter,
|
||||
ContentTypeFilter
|
||||
)
|
||||
|
||||
# Create a chain of filters
|
||||
filter_chain = FilterChain([
|
||||
# Only follow URLs with specific patterns
|
||||
URLPatternFilter(patterns=["*guide*", "*tutorial*"]),
|
||||
|
||||
# Only crawl specific domains
|
||||
DomainFilter(
|
||||
allowed_domains=["docs.example.com"],
|
||||
blocked_domains=["old.docs.example.com"]
|
||||
),
|
||||
|
||||
# Only include specific content types
|
||||
ContentTypeFilter(allowed_types=["text/html"])
|
||||
])
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(
|
||||
max_depth=2,
|
||||
filter_chain=filter_chain
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 4.3 Available Filter Types
|
||||
|
||||
Crawl4AI includes several specialized filters:
|
||||
|
||||
- **`URLPatternFilter`**: Matches URL patterns using wildcard syntax
|
||||
- **`DomainFilter`**: Controls which domains to include or exclude
|
||||
- **`ContentTypeFilter`**: Filters based on HTTP Content-Type
|
||||
- **`ContentRelevanceFilter`**: Uses similarity to a text query
|
||||
- **`SEOFilter`**: Evaluates SEO elements (meta tags, headers, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Using Scorers for Prioritized Crawling
|
||||
|
||||
Scorers assign priority values to discovered URLs, helping the crawler focus on the most relevant content first.
|
||||
|
||||
### 5.1 KeywordRelevanceScorer
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling.scorers import KeywordRelevanceScorer
|
||||
from crawl4ai.deep_crawling import BestFirstCrawlingStrategy
|
||||
|
||||
# Create a keyword relevance scorer
|
||||
keyword_scorer = KeywordRelevanceScorer(
|
||||
keywords=["crawl", "example", "async", "configuration"],
|
||||
weight=0.7 # Importance of this scorer (0.0 to 1.0)
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BestFirstCrawlingStrategy(
|
||||
max_depth=2,
|
||||
url_scorer=keyword_scorer
|
||||
),
|
||||
stream=True # Recommended with BestFirstCrawling
|
||||
)
|
||||
|
||||
# Results will come in order of relevance score
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async for result in await crawler.arun("https://example.com", config=config):
|
||||
score = result.metadata.get("score", 0)
|
||||
print(f"Score: {score:.2f} | {result.url}")
|
||||
```
|
||||
|
||||
**How scorers work:**
|
||||
- Evaluate each discovered URL before crawling
|
||||
- Calculate relevance based on various signals
|
||||
- Help the crawler make intelligent choices about traversal order
|
||||
|
||||
---
|
||||
|
||||
## 6. Advanced Filtering Techniques
|
||||
|
||||
### 6.1 SEO Filter for Quality Assessment
|
||||
|
||||
The **SEOFilter** helps you identify pages with strong SEO characteristics:
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling.filters import FilterChain, SEOFilter
|
||||
|
||||
# Create an SEO filter that looks for specific keywords in page metadata
|
||||
seo_filter = SEOFilter(
|
||||
threshold=0.5, # Minimum score (0.0 to 1.0)
|
||||
keywords=["tutorial", "guide", "documentation"]
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(
|
||||
max_depth=1,
|
||||
filter_chain=FilterChain([seo_filter])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 6.2 Content Relevance Filter
|
||||
|
||||
The **ContentRelevanceFilter** analyzes the actual content of pages:
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling.filters import FilterChain, ContentRelevanceFilter
|
||||
|
||||
# Create a content relevance filter
|
||||
relevance_filter = ContentRelevanceFilter(
|
||||
query="Web crawling and data extraction with Python",
|
||||
threshold=0.7 # Minimum similarity score (0.0 to 1.0)
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(
|
||||
max_depth=1,
|
||||
filter_chain=FilterChain([relevance_filter])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
This filter:
|
||||
- Measures semantic similarity between query and page content
|
||||
- It's a BM25-based relevance filter using head section content
|
||||
|
||||
---
|
||||
|
||||
## 7. Building a Complete Advanced Crawler
|
||||
|
||||
This example combines multiple techniques for a sophisticated crawl:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.content_scraping_strategy import LXMLWebScrapingStrategy
|
||||
from crawl4ai.deep_crawling import BestFirstCrawlingStrategy
|
||||
from crawl4ai.deep_crawling.filters import (
|
||||
FilterChain,
|
||||
DomainFilter,
|
||||
URLPatternFilter,
|
||||
ContentTypeFilter
|
||||
)
|
||||
from crawl4ai.deep_crawling.scorers import KeywordRelevanceScorer
|
||||
|
||||
async def run_advanced_crawler():
|
||||
# Create a sophisticated filter chain
|
||||
filter_chain = FilterChain([
|
||||
# Domain boundaries
|
||||
DomainFilter(
|
||||
allowed_domains=["docs.example.com"],
|
||||
blocked_domains=["old.docs.example.com"]
|
||||
),
|
||||
|
||||
# URL patterns to include
|
||||
URLPatternFilter(patterns=["*guide*", "*tutorial*", "*blog*"]),
|
||||
|
||||
# Content type filtering
|
||||
ContentTypeFilter(allowed_types=["text/html"])
|
||||
])
|
||||
|
||||
# Create a relevance scorer
|
||||
keyword_scorer = KeywordRelevanceScorer(
|
||||
keywords=["crawl", "example", "async", "configuration"],
|
||||
weight=0.7
|
||||
)
|
||||
|
||||
# Set up the configuration
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BestFirstCrawlingStrategy(
|
||||
max_depth=2,
|
||||
include_external=False,
|
||||
filter_chain=filter_chain,
|
||||
url_scorer=keyword_scorer
|
||||
),
|
||||
scraping_strategy=LXMLWebScrapingStrategy(),
|
||||
stream=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Execute the crawl
|
||||
results = []
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async for result in await crawler.arun("https://docs.example.com", config=config):
|
||||
results.append(result)
|
||||
score = result.metadata.get("score", 0)
|
||||
depth = result.metadata.get("depth", 0)
|
||||
print(f"Depth: {depth} | Score: {score:.2f} | {result.url}")
|
||||
|
||||
# Analyze the results
|
||||
print(f"Crawled {len(results)} high-value pages")
|
||||
print(f"Average score: {sum(r.metadata.get('score', 0) for r in results) / len(results):.2f}")
|
||||
|
||||
# Group by depth
|
||||
depth_counts = {}
|
||||
for result in results:
|
||||
depth = result.metadata.get("depth", 0)
|
||||
depth_counts[depth] = depth_counts.get(depth, 0) + 1
|
||||
|
||||
print("Pages crawled by depth:")
|
||||
for depth, count in sorted(depth_counts.items()):
|
||||
print(f" Depth {depth}: {count} pages")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_advanced_crawler())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 8. Limiting and Controlling Crawl Size
|
||||
|
||||
### 8.1 Using max_pages
|
||||
|
||||
You can limit the total number of pages crawled with the `max_pages` parameter:
|
||||
|
||||
```python
|
||||
# Limit to exactly 20 pages regardless of depth
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
max_pages=20
|
||||
)
|
||||
```
|
||||
|
||||
This feature is useful for:
|
||||
- Controlling API costs
|
||||
- Setting predictable execution times
|
||||
- Focusing on the most important content
|
||||
- Testing crawl configurations before full execution
|
||||
|
||||
### 8.2 Using score_threshold
|
||||
|
||||
For BFS and DFS strategies, you can set a minimum score threshold to only crawl high-quality pages:
|
||||
|
||||
```python
|
||||
# Only follow links with scores above 0.4
|
||||
strategy = DFSDeepCrawlStrategy(
|
||||
max_depth=2,
|
||||
url_scorer=KeywordRelevanceScorer(keywords=["api", "guide", "reference"]),
|
||||
score_threshold=0.4 # Skip URLs with scores below this value
|
||||
)
|
||||
```
|
||||
|
||||
Note that for BestFirstCrawlingStrategy, score_threshold is not needed since pages are already processed in order of highest score first.
|
||||
|
||||
## 9. Common Pitfalls & Tips
|
||||
|
||||
1.**Set realistic limits.** Be cautious with `max_depth` values > 3, which can exponentially increase crawl size. Use `max_pages` to set hard limits.
|
||||
|
||||
2.**Don't neglect the scoring component.** BestFirstCrawling works best with well-tuned scorers. Experiment with keyword weights for optimal prioritization.
|
||||
|
||||
3.**Be a good web citizen.** Respect robots.txt. (disabled by default)
|
||||
|
||||
4.**Handle page errors gracefully.** Not all pages will be accessible. Check `result.status` when processing results.
|
||||
|
||||
5.**Balance breadth vs. depth.** Choose your strategy wisely - BFS for comprehensive coverage, DFS for deep exploration, BestFirst for focused relevance-based crawling.
|
||||
|
||||
6.**Preserve HTTPS for security.** If crawling HTTPS sites that redirect to HTTP, use `preserve_https_for_internal_links=True` to maintain secure connections:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=BFSDeepCrawlStrategy(max_depth=2),
|
||||
preserve_https_for_internal_links=True # Keep HTTPS even if server redirects to HTTP
|
||||
)
|
||||
```
|
||||
|
||||
This is especially useful for security-conscious crawling or when dealing with sites that support both protocols.
|
||||
|
||||
---
|
||||
|
||||
## 10. Crash Recovery for Long-Running Crawls
|
||||
|
||||
For production deployments, especially in cloud environments where instances can be terminated unexpectedly, Crawl4AI provides built-in crash recovery support for all deep crawl strategies.
|
||||
|
||||
### 10.1 Enabling State Persistence
|
||||
|
||||
All deep crawl strategies (BFS, DFS, Best-First) support two optional parameters:
|
||||
|
||||
- **`resume_state`**: Pass a previously saved state to resume from a checkpoint
|
||||
- **`on_state_change`**: Async callback fired after each URL is processed
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
import json
|
||||
|
||||
# Callback to save state after each URL
|
||||
async def save_state_to_redis(state: dict):
|
||||
await redis.set("crawl_state", json.dumps(state))
|
||||
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
on_state_change=save_state_to_redis, # Called after each URL
|
||||
)
|
||||
```
|
||||
|
||||
### 10.2 State Structure
|
||||
|
||||
The state dictionary is JSON-serializable and contains:
|
||||
|
||||
```python
|
||||
{
|
||||
"strategy_type": "bfs", # or "dfs", "best_first"
|
||||
"visited": ["url1", "url2", ...], # Already crawled URLs
|
||||
"pending": [{"url": "...", "parent_url": "..."}], # Queue/stack
|
||||
"depths": {"url1": 0, "url2": 1}, # Depth tracking
|
||||
"pages_crawled": 42 # Counter
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 Resuming from a Checkpoint
|
||||
|
||||
```python
|
||||
import json
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
|
||||
# Load saved state (e.g., from Redis, database, or file)
|
||||
saved_state = json.loads(await redis.get("crawl_state"))
|
||||
|
||||
# Resume crawling from where we left off
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
resume_state=saved_state, # Continue from checkpoint
|
||||
on_state_change=save_state_to_redis, # Keep saving progress
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(deep_crawl_strategy=strategy)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Will skip already-visited URLs and continue from pending queue
|
||||
results = await crawler.arun(start_url, config=config)
|
||||
```
|
||||
|
||||
### 10.4 Manual State Export
|
||||
|
||||
You can export the last captured state using `export_state()`. Note that this requires `on_state_change` to be set (state is captured in the callback):
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
captured_state = None
|
||||
|
||||
async def capture_state(state: dict):
|
||||
global captured_state
|
||||
captured_state = state
|
||||
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=2,
|
||||
on_state_change=capture_state, # Required for state capture
|
||||
)
|
||||
config = CrawlerRunConfig(deep_crawl_strategy=strategy)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = await crawler.arun(start_url, config=config)
|
||||
|
||||
# Get the last captured state
|
||||
state = strategy.export_state()
|
||||
if state:
|
||||
# Save to your preferred storage
|
||||
with open("crawl_checkpoint.json", "w") as f:
|
||||
json.dump(state, f)
|
||||
```
|
||||
|
||||
### 10.5 Complete Example: Redis-Based Recovery
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import redis.asyncio as redis
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
|
||||
REDIS_KEY = "crawl4ai:crawl_state"
|
||||
|
||||
async def main():
|
||||
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
# Check for existing state
|
||||
saved_state = None
|
||||
existing = await redis_client.get(REDIS_KEY)
|
||||
if existing:
|
||||
saved_state = json.loads(existing)
|
||||
print(f"Resuming from checkpoint: {saved_state['pages_crawled']} pages already crawled")
|
||||
|
||||
# State persistence callback
|
||||
async def persist_state(state: dict):
|
||||
await redis_client.set(REDIS_KEY, json.dumps(state))
|
||||
|
||||
# Create strategy with recovery support
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
max_pages=100,
|
||||
resume_state=saved_state,
|
||||
on_state_change=persist_state,
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(deep_crawl_strategy=strategy, stream=True)
|
||||
|
||||
try:
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async for result in await crawler.arun("https://example.com", config=config):
|
||||
print(f"Crawled: {result.url}")
|
||||
except Exception as e:
|
||||
print(f"Crawl interrupted: {e}")
|
||||
print("State saved - restart to resume")
|
||||
finally:
|
||||
await redis_client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 10.6 Zero Overhead
|
||||
|
||||
When `resume_state=None` and `on_state_change=None` (the defaults), there is no performance impact. State tracking only activates when you enable these features.
|
||||
|
||||
---
|
||||
|
||||
## 11. Cancellation Support for Deep Crawls
|
||||
|
||||
For production environments like cloud platforms, you often need to stop a running crawl mid-execution—whether the user changed their mind, specified the wrong URL, or wants to control costs. Crawl4AI provides built-in cancellation support for all deep crawl strategies.
|
||||
|
||||
### 11.1 Two Ways to Cancel
|
||||
|
||||
**Option A: Callback-based cancellation** (recommended for external systems)
|
||||
|
||||
Use `should_cancel` to check an external source (Redis, database, API) before each URL:
|
||||
|
||||
```python
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
|
||||
async def check_if_cancelled():
|
||||
# Check Redis, database, or any external source
|
||||
job = await redis.get(f"job:{job_id}")
|
||||
return job.get("status") == "cancelled"
|
||||
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
max_pages=1000,
|
||||
should_cancel=check_if_cancelled, # Called before each URL
|
||||
)
|
||||
```
|
||||
|
||||
**Option B: Direct cancellation** (for in-process control)
|
||||
|
||||
Call `cancel()` directly on the strategy instance:
|
||||
|
||||
```python
|
||||
strategy = BFSDeepCrawlStrategy(max_depth=3, max_pages=1000)
|
||||
|
||||
# In another coroutine or thread:
|
||||
strategy.cancel() # Thread-safe, stops before next URL
|
||||
```
|
||||
|
||||
### 11.2 Checking Cancellation Status
|
||||
|
||||
Use the `cancelled` property to check if a crawl was cancelled:
|
||||
|
||||
```python
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = await crawler.arun(url, config=config)
|
||||
|
||||
if strategy.cancelled:
|
||||
print(f"Crawl was cancelled after {len(results)} pages")
|
||||
else:
|
||||
print(f"Crawl completed with {len(results)} pages")
|
||||
```
|
||||
|
||||
### 11.3 State Notifications Include Cancelled Flag
|
||||
|
||||
When using `on_state_change`, the state dictionary includes a `cancelled` field:
|
||||
|
||||
```python
|
||||
async def handle_state(state: dict):
|
||||
if state.get("cancelled"):
|
||||
print("Crawl was cancelled!")
|
||||
print(f"Crawled {state['pages_crawled']} pages before cancellation")
|
||||
# Save state for potential resume
|
||||
await redis.set("crawl_state", json.dumps(state))
|
||||
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
should_cancel=check_cancelled,
|
||||
on_state_change=handle_state,
|
||||
)
|
||||
```
|
||||
|
||||
### 11.4 Key Behaviors
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Cancel before first URL | Returns empty results, `cancelled=True` |
|
||||
| Cancel during crawl | Completes current URL, then stops |
|
||||
| Callback raises exception | Logged as warning, crawl continues (fail-open) |
|
||||
| Strategy reuse after cancel | Works normally (cancel flag auto-resets) |
|
||||
| Sync callback function | Supported (auto-detected and handled) |
|
||||
|
||||
### 11.5 Complete Example: Cloud Platform Job Cancellation
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import redis.asyncio as redis
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.deep_crawling import BFSDeepCrawlStrategy
|
||||
|
||||
async def run_cancellable_crawl(job_id: str, start_url: str):
|
||||
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
# Check external cancellation source
|
||||
async def check_cancelled():
|
||||
status = await redis_client.get(f"job:{job_id}:status")
|
||||
return status == b"cancelled"
|
||||
|
||||
# Save progress for monitoring and recovery
|
||||
async def save_progress(state: dict):
|
||||
await redis_client.set(
|
||||
f"job:{job_id}:state",
|
||||
json.dumps(state)
|
||||
)
|
||||
# Update job progress
|
||||
await redis_client.set(
|
||||
f"job:{job_id}:pages_crawled",
|
||||
state["pages_crawled"]
|
||||
)
|
||||
|
||||
strategy = BFSDeepCrawlStrategy(
|
||||
max_depth=3,
|
||||
max_pages=500,
|
||||
should_cancel=check_cancelled,
|
||||
on_state_change=save_progress,
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
deep_crawl_strategy=strategy,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
results = []
|
||||
try:
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
async for result in await crawler.arun(start_url, config=config):
|
||||
results.append(result)
|
||||
print(f"Crawled: {result.url}")
|
||||
finally:
|
||||
# Report final status
|
||||
if strategy.cancelled:
|
||||
await redis_client.set(f"job:{job_id}:status", "cancelled")
|
||||
print(f"Job cancelled after {len(results)} pages")
|
||||
else:
|
||||
await redis_client.set(f"job:{job_id}:status", "completed")
|
||||
print(f"Job completed with {len(results)} pages")
|
||||
|
||||
await redis_client.close()
|
||||
|
||||
return results
|
||||
|
||||
# Usage
|
||||
# asyncio.run(run_cancellable_crawl("job-123", "https://example.com"))
|
||||
#
|
||||
# To cancel from another process:
|
||||
# redis_client.set("job:job-123:status", "cancelled")
|
||||
```
|
||||
|
||||
### 11.6 Supported Strategies
|
||||
|
||||
Cancellation works identically across all deep crawl strategies:
|
||||
|
||||
- **BFSDeepCrawlStrategy** - Breadth-first search
|
||||
- **DFSDeepCrawlStrategy** - Depth-first search
|
||||
- **BestFirstCrawlingStrategy** - Priority-based crawling
|
||||
|
||||
All strategies support:
|
||||
- `should_cancel` callback parameter
|
||||
- `cancel()` method
|
||||
- `cancelled` property
|
||||
|
||||
---
|
||||
|
||||
## 12. Prefetch Mode for Fast URL Discovery
|
||||
|
||||
When you need to quickly discover URLs without full page processing, use **prefetch mode**. This is ideal for two-phase crawling where you first map the site, then selectively process specific pages.
|
||||
|
||||
### 12.1 Enabling Prefetch Mode
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
config = CrawlerRunConfig(prefetch=True)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
|
||||
# Result contains only HTML and links - no markdown, no extraction
|
||||
print(f"Found {len(result.links['internal'])} internal links")
|
||||
print(f"Found {len(result.links['external'])} external links")
|
||||
```
|
||||
|
||||
### 12.2 What Gets Skipped
|
||||
|
||||
Prefetch mode uses a fast path that bypasses heavy processing:
|
||||
|
||||
| Processing Step | Normal Mode | Prefetch Mode |
|
||||
|----------------|-------------|---------------|
|
||||
| Fetch HTML | ✅ | ✅ |
|
||||
| Extract links | ✅ | ✅ (fast `quick_extract_links()`) |
|
||||
| Generate markdown | ✅ | ❌ Skipped |
|
||||
| Content scraping | ✅ | ❌ Skipped |
|
||||
| Media extraction | ✅ | ❌ Skipped |
|
||||
| LLM extraction | ✅ | ❌ Skipped |
|
||||
|
||||
### 12.3 Performance Benefit
|
||||
|
||||
- **Normal mode**: Full pipeline (~2-5 seconds per page)
|
||||
- **Prefetch mode**: HTML + links only (~200-500ms per page)
|
||||
|
||||
This makes prefetch mode **5-10x faster** for URL discovery.
|
||||
|
||||
### 12.4 Two-Phase Crawling Pattern
|
||||
|
||||
The most common use case is two-phase crawling:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def two_phase_crawl(start_url: str):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# ═══════════════════════════════════════════════
|
||||
# Phase 1: Fast discovery (prefetch mode)
|
||||
# ═══════════════════════════════════════════════
|
||||
prefetch_config = CrawlerRunConfig(prefetch=True)
|
||||
discovery = await crawler.arun(start_url, config=prefetch_config)
|
||||
|
||||
all_urls = [link["href"] for link in discovery.links.get("internal", [])]
|
||||
print(f"Discovered {len(all_urls)} URLs")
|
||||
|
||||
# Filter to URLs you care about
|
||||
blog_urls = [url for url in all_urls if "/blog/" in url]
|
||||
print(f"Found {len(blog_urls)} blog posts to process")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# Phase 2: Full processing on selected URLs only
|
||||
# ═══════════════════════════════════════════════
|
||||
full_config = CrawlerRunConfig(
|
||||
# Your normal extraction settings
|
||||
word_count_threshold=100,
|
||||
remove_overlay_elements=True,
|
||||
)
|
||||
|
||||
results = []
|
||||
for url in blog_urls:
|
||||
result = await crawler.arun(url, config=full_config)
|
||||
if result.success:
|
||||
results.append(result)
|
||||
print(f"Processed: {url}")
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = asyncio.run(two_phase_crawl("https://example.com"))
|
||||
print(f"Fully processed {len(results)} pages")
|
||||
```
|
||||
|
||||
### 12.5 Use Cases
|
||||
|
||||
- **Site mapping**: Quickly discover all URLs before deciding what to process
|
||||
- **Link validation**: Check which pages exist without heavy processing
|
||||
- **Selective deep crawl**: Prefetch to find URLs, filter by pattern, then full crawl
|
||||
- **Crawl planning**: Estimate crawl size before committing resources
|
||||
|
||||
---
|
||||
|
||||
## 13. Summary & Next Steps
|
||||
|
||||
In this **Deep Crawling with Crawl4AI** tutorial, you learned to:
|
||||
|
||||
- Configure **BFSDeepCrawlStrategy**, **DFSDeepCrawlStrategy**, and **BestFirstCrawlingStrategy**
|
||||
- Process results in streaming or non-streaming mode
|
||||
- Apply filters to target specific content
|
||||
- Use scorers to prioritize the most relevant pages
|
||||
- Limit crawls with `max_pages` and `score_threshold` parameters
|
||||
- Build a complete advanced crawler with combined techniques
|
||||
- **Implement crash recovery** with `resume_state` and `on_state_change` for production deployments
|
||||
- **Cancel running crawls** with `should_cancel` callback or `cancel()` method for cloud platform job management
|
||||
- **Use prefetch mode** for fast URL discovery and two-phase crawling
|
||||
|
||||
With these tools, you can efficiently extract structured data from websites at scale, focusing precisely on the content you need for your specific use case.
|
||||
@@ -0,0 +1,343 @@
|
||||
# Domain Mapping: Discover Every URL Under a Domain
|
||||
|
||||
## What Is Domain Mapping?
|
||||
|
||||
Domain mapping goes beyond URL seeding. Instead of checking a single sitemap or index, `DomainMapper` combines **8 discovery sources** to find every URL under a domain — including subdomains you didn't know existed.
|
||||
|
||||
### DomainMapper vs AsyncUrlSeeder
|
||||
|
||||
| Aspect | AsyncUrlSeeder | DomainMapper |
|
||||
|--------|---------------|--------------|
|
||||
| **Scope** | Single host, listed URLs only | Entire domain + all subdomains |
|
||||
| **Sources** | Sitemap + Common Crawl | 8 sources (sitemap, CC, Wayback, crt.sh, probe, robots.txt, feeds, homepage) |
|
||||
| **Subdomain discovery** | No | Yes (Certificate Transparency, DNS, Wayback) |
|
||||
| **Soft-404 detection** | No | Yes (fingerprints SPA sites) |
|
||||
| **Best for** | Known domains with good sitemaps | Full domain reconnaissance |
|
||||
|
||||
**Real-world example**: For `superdesign.dev`, AsyncUrlSeeder found 4 URLs. DomainMapper found **171 URLs across 11 hosts** — including docs, API servers, staging environments, and analytics dashboards that no sitemap listed.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import DomainMapper, DomainMapperConfig
|
||||
|
||||
async def main():
|
||||
async with DomainMapper() as mapper:
|
||||
results = await mapper.scan("example.com")
|
||||
|
||||
print(f"Found {len(results)} URLs")
|
||||
for r in results[:10]:
|
||||
print(f" [{r['source']}] {r['url']}")
|
||||
if r.get("head_data", {}).get("title"):
|
||||
print(f" Title: {r['head_data']['title']}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Or via `AsyncWebCrawler`:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, DomainMapperConfig
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
results = await crawler.amap_domain("example.com")
|
||||
```
|
||||
|
||||
## The 8 Discovery Sources
|
||||
|
||||
DomainMapper combines these sources, each catching URLs the others miss:
|
||||
|
||||
### 1. `sitemap` — Sitemap Discovery
|
||||
|
||||
Checks `/sitemap.xml`, `/sitemap_index.xml`, and `robots.txt` `Sitemap:` directives **on every discovered host** — not just the root domain.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="sitemap")
|
||||
```
|
||||
|
||||
### 2. `cc` — Common Crawl
|
||||
|
||||
Queries the Common Crawl CDX API for `*.domain.tld/*`, catching URLs and subdomains the web's largest public crawl has indexed.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="cc")
|
||||
```
|
||||
|
||||
### 3. `wayback` — Wayback Machine
|
||||
|
||||
Queries the Internet Archive's CDX API. Often has different coverage than Common Crawl — including historical pages that have since been removed.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="wayback")
|
||||
```
|
||||
|
||||
### 4. `crt` — Certificate Transparency
|
||||
|
||||
Queries [crt.sh](https://crt.sh) for SSL certificates issued to `*.domain.tld`. This is the single most effective subdomain discovery technique — it found 14 subdomains for `superdesign.dev` that no other source knew about.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="crt")
|
||||
```
|
||||
|
||||
### 5. `probe` — Common Path Probing
|
||||
|
||||
Tries ~25 well-known paths on each discovered host (`/docs`, `/api`, `/login`, `/dashboard`, `/openapi.json`, etc.). Combined with soft-404 detection to avoid false positives.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="probe")
|
||||
|
||||
# Add custom paths to probe
|
||||
config = DomainMapperConfig(
|
||||
source="probe",
|
||||
probe_paths=["/custom-api", "/internal/status"]
|
||||
)
|
||||
```
|
||||
|
||||
### 6. `robots` — robots.txt Path Mining
|
||||
|
||||
Parses `Disallow:` and `Allow:` lines from `robots.txt`. These are confirmed real paths the site acknowledges exist — often revealing admin panels, APIs, and internal tools that aren't linked anywhere.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="robots")
|
||||
```
|
||||
|
||||
### 7. `feed` — RSS/Atom Feed Parsing
|
||||
|
||||
Discovers and parses RSS/Atom feeds at common paths (`/feed`, `/rss`, `/atom.xml`, etc.). Feeds are curated lists of content URLs maintained by the site.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="feed")
|
||||
```
|
||||
|
||||
### 8. `homepage` — Homepage Link Extraction
|
||||
|
||||
Fetches each host's homepage via HTTP and extracts all internal links using `quick_extract_links()`. Also mines `<link rel="alternate|preload|prefetch">` tags from the `<head>` for additional URLs. No browser needed.
|
||||
|
||||
```python
|
||||
config = DomainMapperConfig(source="homepage")
|
||||
```
|
||||
|
||||
### Combining Sources
|
||||
|
||||
Sources are combined with `+`:
|
||||
|
||||
```python
|
||||
# Default: most useful combination
|
||||
config = DomainMapperConfig(source="sitemap+cc+crt+probe")
|
||||
|
||||
# Maximum coverage: all 8 sources
|
||||
config = DomainMapperConfig(
|
||||
source="sitemap+cc+wayback+crt+probe+robots+feed+homepage"
|
||||
)
|
||||
|
||||
# Lightweight: just sitemap + probing
|
||||
config = DomainMapperConfig(source="sitemap+probe")
|
||||
```
|
||||
|
||||
## How It Works: The Three Phases
|
||||
|
||||
### Phase 1: Host Discovery
|
||||
|
||||
DomainMapper first discovers all subdomains under your domain:
|
||||
|
||||
```
|
||||
superdesign.dev
|
||||
├── crt.sh → docs, app, cloud, insights, staging-api, ui2web, ...
|
||||
├── Wayback CDX → api, app, docs, www, ...
|
||||
├── Common Crawl → app, www, ...
|
||||
└── DNS guessing → www, app, api, docs, blog, admin, cloud, ...
|
||||
|
||||
Result: 13 validated hosts
|
||||
```
|
||||
|
||||
Each discovered host is validated with an HTTP HEAD request. Hosts that don't respond are dropped.
|
||||
|
||||
### Phase 2: Per-Host Scanning
|
||||
|
||||
For each validated host, DomainMapper runs all enabled sources in parallel:
|
||||
|
||||
```
|
||||
docs.superdesign.dev
|
||||
├── Soft-404 fingerprint → (404 returns proper error — no SPA issue)
|
||||
├── robots.txt → 1 sitemap URL, 1 disallow path
|
||||
├── Sitemap parsing → 19 URLs
|
||||
├── Path probing → 2 valid (/docs, /)
|
||||
├── Feed discovery → (no feeds found)
|
||||
└── Homepage extraction → 26 internal links
|
||||
```
|
||||
|
||||
### Phase 3: Post-Processing
|
||||
|
||||
All discovered URLs go through:
|
||||
|
||||
1. **URL normalization** — using `normalize_url()` to canonicalize
|
||||
2. **Deduplication** — by normalized URL, merging source attribution
|
||||
3. **Nonsense filtering** — removes static assets (JS, CSS, images, fonts), webpack chunks, Wayback garbage
|
||||
4. **Head extraction** — parallel `<head>` fetching for metadata (optional)
|
||||
5. **BM25 scoring** — relevance scoring against a query (optional)
|
||||
|
||||
## Soft-404 Detection
|
||||
|
||||
Many modern SPAs return HTTP 200 for every URL — even pages that don't exist. DomainMapper detects this:
|
||||
|
||||
1. **Fingerprinting**: Fetches a guaranteed-nonexistent URL (e.g., `/c4ai-probe-a1b2c3d4`) on each host
|
||||
2. **Recording**: Captures the response title and body hash
|
||||
3. **Filtering**: When probing real paths, compares against the fingerprint. If they match → soft-404, filtered out
|
||||
|
||||
For `superdesign.dev`, this correctly:
|
||||
- Blocked **all 25+ probe paths** on `app.superdesign.dev` (SPA that returns 200 for everything)
|
||||
- Blocked **476 sitemap URLs** from `app.superdesign.dev` (all rendering the same shell)
|
||||
- Kept all 19 legitimate URLs from `docs.superdesign.dev`
|
||||
|
||||
```python
|
||||
# Soft-404 detection is on by default
|
||||
config = DomainMapperConfig(soft_404_detection=True)
|
||||
|
||||
# Disable if you want raw results
|
||||
config = DomainMapperConfig(soft_404_detection=False)
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### DomainMapperConfig
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `source` | str | `"sitemap+cc+crt+probe"` | Discovery sources joined by `+` |
|
||||
| `max_urls` | int | `-1` | Maximum URLs to return (-1 = unlimited) |
|
||||
| `concurrency` | int | `50` | Max concurrent requests across all hosts |
|
||||
| `hits_per_sec` | int | `10` | Rate limit in requests/second |
|
||||
| `force` | bool | `False` | Bypass all caches |
|
||||
| `extract_head` | bool | `True` | Fetch and parse `<head>` metadata |
|
||||
| `filter_nonsense_urls` | bool | `True` | Filter static assets and utility URLs |
|
||||
| `soft_404_detection` | bool | `True` | Fingerprint and filter soft-404 pages |
|
||||
| `query` | str | `None` | BM25 relevance query (requires `extract_head=True`) |
|
||||
| `score_threshold` | float | `None` | Minimum relevance score (0.0-1.0) |
|
||||
| `scoring_method` | str | `"bm25"` | Scoring algorithm |
|
||||
| `probe_paths` | List[str] | `None` | Extra paths to probe on each host |
|
||||
| `common_subdomains` | List[str] | `None` | Extra subdomain prefixes to guess |
|
||||
| `use_browser_for_homepage` | bool | `False` | Use Playwright for JS-rendered homepages |
|
||||
| `verbose` | bool | `None` | Override logger verbose setting |
|
||||
| `cache_ttl_hours` | int | `24` | Hours before cached results expire |
|
||||
| `dns_timeout` | float | `3.0` | Timeout for DNS resolution (seconds) |
|
||||
| `http_timeout` | float | `10.0` | Timeout for HTTP requests (seconds) |
|
||||
|
||||
### Output Format
|
||||
|
||||
Each result is a dict:
|
||||
|
||||
```python
|
||||
{
|
||||
"url": "https://docs.superdesign.dev/quickstart",
|
||||
"host": "docs.superdesign.dev",
|
||||
"source": "homepage+sitemap", # which source(s) found it
|
||||
"status": "valid", # valid | not_valid | soft_404
|
||||
"head_data": { # if extract_head=True
|
||||
"title": "Quickstart",
|
||||
"meta": {"description": "..."},
|
||||
"link": {...},
|
||||
"jsonld": [...]
|
||||
},
|
||||
"relevance_score": 0.85, # if query provided
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Discover and Crawl Documentation
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, DomainMapperConfig, CrawlerRunConfig
|
||||
|
||||
async def crawl_all_docs():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Step 1: Discover all URLs
|
||||
pages = await crawler.amap_domain("example.com", DomainMapperConfig(
|
||||
source="sitemap+crt+probe+homepage",
|
||||
extract_head=True,
|
||||
query="documentation tutorial guide",
|
||||
))
|
||||
|
||||
# Step 2: Filter for docs
|
||||
doc_urls = [
|
||||
p["url"] for p in pages
|
||||
if p.get("relevance_score", 0) > 0.3
|
||||
]
|
||||
print(f"Found {len(doc_urls)} documentation pages")
|
||||
|
||||
# Step 3: Crawl them
|
||||
results = await crawler.arun_many(
|
||||
doc_urls[:50],
|
||||
config=CrawlerRunConfig(only_text=True)
|
||||
)
|
||||
for r in results:
|
||||
if r.success:
|
||||
print(f" Crawled: {r.url}")
|
||||
|
||||
asyncio.run(crawl_all_docs())
|
||||
```
|
||||
|
||||
### Security Audit: Find Exposed Services
|
||||
|
||||
```python
|
||||
async def audit_domain():
|
||||
async with DomainMapper() as mapper:
|
||||
results = await mapper.scan("company.com", DomainMapperConfig(
|
||||
source="crt+probe+robots",
|
||||
extract_head=True,
|
||||
probe_paths=[
|
||||
"/openapi.json", "/swagger.json", "/api-docs",
|
||||
"/graphql", "/.env", "/debug", "/admin",
|
||||
"/phpinfo.php", "/server-status",
|
||||
],
|
||||
))
|
||||
|
||||
# Flag exposed services
|
||||
for r in results:
|
||||
title = r.get("head_data", {}).get("title", "")
|
||||
if any(x in title.lower() for x in ["swagger", "api", "admin", "debug"]):
|
||||
print(f" EXPOSED: {r['url']} — {title}")
|
||||
```
|
||||
|
||||
### Compare Subdomains Across a Domain
|
||||
|
||||
```python
|
||||
async def map_infrastructure():
|
||||
async with DomainMapper() as mapper:
|
||||
results = await mapper.scan("company.com", DomainMapperConfig(
|
||||
source="crt+probe",
|
||||
extract_head=False,
|
||||
))
|
||||
|
||||
# Group by host
|
||||
from collections import defaultdict
|
||||
by_host = defaultdict(list)
|
||||
for r in results:
|
||||
by_host[r["host"]].append(r)
|
||||
|
||||
print(f"Discovered {len(by_host)} hosts:")
|
||||
for host, urls in sorted(by_host.items()):
|
||||
print(f" {host}: {len(urls)} URLs")
|
||||
```
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Start with the default sources** (`sitemap+cc+crt+probe`). Add `wayback`, `robots`, `feed`, and `homepage` if you need maximum coverage.
|
||||
|
||||
2. **Use `extract_head=False` for speed** when you just need URL lists. Head extraction makes ~1 HTTP request per URL.
|
||||
|
||||
3. **The `query` parameter is powerful** for finding specific content across a large domain without crawling anything.
|
||||
|
||||
4. **`probe_paths` is your extensibility hook** — add domain-specific paths you suspect exist.
|
||||
|
||||
5. **Rate limiting matters** — `hits_per_sec=10` is respectful. Lower it for smaller sites, raise it for your own infrastructure.
|
||||
|
||||
6. **Soft-404 detection is critical for SPAs** — without it, single-page apps flood your results with hundreds of identical shell pages.
|
||||
|
||||
## See Also
|
||||
|
||||
- [URL Seeding](url-seeding.md) — simpler, single-host URL discovery from sitemaps and Common Crawl
|
||||
- [Deep Crawling](deep-crawling.md) — follow links dynamically within pages
|
||||
- [Multi-URL Crawling](../advanced/multi-url-crawling.md) — crawl discovered URLs in bulk
|
||||
@@ -0,0 +1,134 @@
|
||||
# Code Examples
|
||||
|
||||
This page provides a comprehensive list of example scripts that demonstrate various features and capabilities of Crawl4AI. Each example is designed to showcase specific functionality, making it easier for you to understand how to implement these features in your own projects.
|
||||
|
||||
## Getting Started Examples
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Hello World | A simple introductory example demonstrating basic usage of AsyncWebCrawler with JavaScript execution and content filtering. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/hello_world.py) |
|
||||
| Quickstart | A comprehensive collection of examples showcasing various features including basic crawling, content cleaning, link analysis, JavaScript execution, CSS selectors, media handling, custom hooks, proxy configuration, screenshots, and multiple extraction strategies. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/quickstart.py) |
|
||||
| Quickstart Set 1 | Basic examples for getting started with Crawl4AI. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/quickstart_examples_set_1.py) |
|
||||
| Quickstart Set 2 | More advanced examples for working with Crawl4AI. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/quickstart_examples_set_2.py) |
|
||||
|
||||
## Proxies
|
||||
|
||||
| Example | Description | Link |
|
||||
|----------|--------------|------|
|
||||
| **NSTProxy** | [NSTProxy](https://www.nstproxy.com/?utm_source=crawl4ai) Seamlessly integrates with crawl4ai — no setup required. Access high-performance residential, datacenter, ISP, and IPv6 proxies with smart rotation and anti-blocking technology. Starts from $0.1/GB. Use code crawl4ai for 10% off. | [View Code](https://github.com/unclecode/crawl4ai/tree/main/docs/examples/proxy) |
|
||||
|
||||
## Browser & Crawling Features
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Built-in Browser | Demonstrates how to use the built-in browser capabilities. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/builtin_browser_example.py) |
|
||||
| Browser Optimization | Focuses on browser performance optimization techniques. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/browser_optimization_example.py) |
|
||||
| arun vs arun_many | Compares the `arun` and `arun_many` methods for single vs. multiple URL crawling. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/arun_vs_arun_many.py) |
|
||||
| Multiple URLs | Shows how to crawl multiple URLs asynchronously. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/async_webcrawler_multiple_urls_example.py) |
|
||||
| Page Interaction | Guide on interacting with dynamic elements through clicks. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/tutorial_dynamic_clicks.md) |
|
||||
| Crawler Monitor | Shows how to monitor the crawler's activities and status. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/crawler_monitor_example.py) |
|
||||
| Full Page Screenshot & PDF | Guide on capturing full-page screenshots and PDFs from massive webpages. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/full_page_screenshot_and_pdf_export.md) |
|
||||
|
||||
## Advanced Crawling & Deep Crawling
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Deep Crawling | An extensive tutorial on deep crawling capabilities, demonstrating BFS and BestFirst strategies, stream vs. non-stream execution, filters, scorers, and advanced configurations. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/deepcrawl_example.py) |
|
||||
| Virtual Scroll | Comprehensive examples for handling virtualized scrolling on sites like Twitter, Instagram. Demonstrates different scrolling scenarios with local test server. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/virtual_scroll_example.py) |
|
||||
| Adaptive Crawling | Demonstrates intelligent crawling that automatically determines when sufficient information has been gathered. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/adaptive_crawling/) |
|
||||
| Dispatcher | Shows how to use the crawl dispatcher for advanced workload management. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/dispatcher_example.py) |
|
||||
| Storage State | Tutorial on managing browser storage state for persistence. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/storage_state_tutorial.md) |
|
||||
| Network Console Capture | Demonstrates how to capture and analyze network requests and console logs. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/network_console_capture_example.py) |
|
||||
|
||||
## Extraction Strategies
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Extraction Strategies | Demonstrates different extraction strategies with various input formats (markdown, HTML, fit_markdown) and JSON-based extractors (CSS and XPath). | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/extraction_strategies_examples.py) |
|
||||
| Scraping Strategies | Compares the performance of different scraping strategies. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/scraping_strategies_performance.py) |
|
||||
| LLM Extraction | Demonstrates LLM-based extraction specifically for OpenAI pricing data. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/llm_extraction_openai_pricing.py) |
|
||||
| LLM Markdown | Shows how to use LLMs to generate markdown from crawled content. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/llm_markdown_generator.py) |
|
||||
| Summarize Page | Shows how to summarize web page content. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/summarize_page.py) |
|
||||
|
||||
## E-commerce & Specialized Crawling
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Amazon Product Extraction | Demonstrates how to extract structured product data from Amazon search results using CSS selectors. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/amazon_product_extraction_direct_url.py) |
|
||||
| Amazon with Hooks | Shows how to use hooks with Amazon product extraction. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/amazon_product_extraction_using_hooks.py) |
|
||||
| Amazon with JavaScript | Demonstrates using custom JavaScript for Amazon product extraction. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/amazon_product_extraction_using_use_javascript.py) |
|
||||
| Crypto Analysis | Demonstrates how to crawl and analyze cryptocurrency data. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/crypto_analysis_example.py) |
|
||||
| SERP API | Demonstrates using Crawl4AI with search engine result pages. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/serp_api_project_11_feb.py) |
|
||||
|
||||
## Anti-Bot & Stealth Features
|
||||
|
||||
| Example | Description | Link |
|
||||
|----------------------------|-------------|------|
|
||||
| Stealth Mode Quick Start | Five practical examples showing how to use stealth mode for bypassing basic bot detection. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/stealth_mode_quick_start.py) |
|
||||
| Stealth Mode Comprehensive | Comprehensive demonstration of stealth mode features with bot detection testing and comparisons. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/stealth_mode_example.py) |
|
||||
| Undetected Browser | Simple example showing how to use the undetected browser adapter. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/hello_world_undetected.py) |
|
||||
| Undetected Browser Demo | Basic demo comparing regular and undetected browser modes. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/undetected_simple_demo.py) |
|
||||
| Undetected Tests | Advanced tests comparing regular vs undetected browsers on various bot detection services. | [View Folder](https://github.com/unclecode/crawl4ai/tree/main/docs/examples/undetectability/) |
|
||||
| CapSolver Captcha Solver | Seamlessly integrate with [CapSolver](https://www.capsolver.com/?utm_source=crawl4ai&utm_medium=github_pr&utm_campaign=crawl4ai_integration) to automatically solve reCAPTCHA v2/v3, Cloudflare Turnstile / Challenges, AWS WAF and more for uninterrupted scraping and automation. | [View Folder](https://github.com/unclecode/crawl4ai/tree/main/docs/examples/capsolver_captcha_solver/) |
|
||||
|
||||
## Customization & Security
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Hooks | Illustrates how to use hooks at different stages of the crawling process for advanced customization. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/hooks_example.py) |
|
||||
| Identity-Based Browsing | Illustrates identity-based browsing configurations for authentic browsing experiences. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/identity_based_browsing.py) |
|
||||
| Proxy Rotation | Shows how to use proxy rotation for web scraping and avoiding IP blocks. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/proxy_rotation_demo.py) |
|
||||
| SSL Certificate | Illustrates SSL certificate handling and verification. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/ssl_example.py) |
|
||||
| Language Support | Shows how to handle different languages during crawling. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/language_support_example.py) |
|
||||
| Geolocation | Demonstrates how to use geolocation features. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/use_geo_location.py) |
|
||||
|
||||
## Docker & Deployment
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Docker Config | Demonstrates how to create and use Docker configuration objects. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/docker_config_obj.py) |
|
||||
| Docker Basic | A test suite for Docker deployment, showcasing various functionalities through the Docker API. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/docker_example.py) |
|
||||
| Docker REST API | Shows how to interact with Crawl4AI Docker using REST API calls. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/docker_python_rest_api.py) |
|
||||
| Docker SDK | Demonstrates using the Python SDK for Crawl4AI Docker. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/docker_python_sdk.py) |
|
||||
|
||||
## Application Examples
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Research Assistant | Demonstrates how to build a research assistant using Crawl4AI. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/research_assistant.py) |
|
||||
| REST Call | Shows how to make REST API calls with Crawl4AI. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/rest_call.py) |
|
||||
| Chainlit Integration | Shows how to integrate Crawl4AI with Chainlit. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/chainlit.md) |
|
||||
| Crawl4AI vs FireCrawl | Compares Crawl4AI with the FireCrawl library. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/crawlai_vs_firecrawl.py) |
|
||||
|
||||
## Content Generation & Markdown
|
||||
|
||||
| Example | Description | Link |
|
||||
|---------|-------------|------|
|
||||
| Content Source | Demonstrates how to work with different content sources in markdown generation. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/markdown/content_source_example.py) |
|
||||
| Content Source (Short) | A simplified version of content source usage. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/markdown/content_source_short_example.py) |
|
||||
| Built-in Browser Guide | Guide for using the built-in browser capabilities. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/README_BUILTIN_BROWSER.md) |
|
||||
|
||||
## Running the Examples
|
||||
|
||||
To run any of these examples, you'll need to have Crawl4AI installed:
|
||||
|
||||
```bash
|
||||
pip install crawl4ai
|
||||
```
|
||||
|
||||
Then, you can run an example script like this:
|
||||
|
||||
```bash
|
||||
python -m docs.examples.hello_world
|
||||
```
|
||||
|
||||
For examples that require additional dependencies or environment variables, refer to the comments at the top of each file.
|
||||
|
||||
Some examples may require:
|
||||
- API keys (for LLM-based examples)
|
||||
- Docker setup (for Docker-related examples)
|
||||
- Additional dependencies (specified in the example files)
|
||||
|
||||
## Contributing New Examples
|
||||
|
||||
If you've created an interesting example that demonstrates a unique use case or feature of Crawl4AI, we encourage you to contribute it to our examples collection. Please see our [contribution guidelines](https://github.com/unclecode/crawl4ai/blob/main/CONTRIBUTORS.md) for more information.
|
||||
@@ -0,0 +1,245 @@
|
||||
# Fit Markdown with Pruning & BM25
|
||||
|
||||
**Fit Markdown** is a specialized **filtered** version of your page’s markdown, focusing on the most relevant content. By default, Crawl4AI converts the entire HTML into a broad **raw_markdown**. With fit markdown, we apply a **content filter** algorithm (e.g., **Pruning** or **BM25**) to remove or rank low-value sections—such as repetitive sidebars, shallow text blocks, or irrelevancies—leaving a concise textual “core.”
|
||||
|
||||
---
|
||||
|
||||
## 1. How “Fit Markdown” Works
|
||||
|
||||
### 1.1 The `content_filter`
|
||||
|
||||
In **`CrawlerRunConfig`**, you can specify a **`content_filter`** to shape how content is pruned or ranked before final markdown generation. A filter’s logic is applied **before** or **during** the HTML→Markdown process, producing:
|
||||
|
||||
- **`result.markdown.raw_markdown`** (unfiltered)
|
||||
- **`result.markdown.fit_markdown`** (filtered or “fit” version)
|
||||
- **`result.markdown.fit_html`** (the corresponding HTML snippet that produced `fit_markdown`)
|
||||
|
||||
|
||||
### 1.2 Common Filters
|
||||
|
||||
1. **PruningContentFilter** – Scores each node by text density, link density, and tag importance, discarding those below a threshold.
|
||||
2. **BM25ContentFilter** – Focuses on textual relevance using BM25 ranking, especially useful if you have a specific user query (e.g., “machine learning” or “food nutrition”).
|
||||
|
||||
---
|
||||
|
||||
## 2. PruningContentFilter
|
||||
|
||||
**Pruning** discards less relevant nodes based on **text density, link density, and tag importance**. It’s a heuristic-based approach—if certain sections appear too “thin” or too “spammy,” they’re pruned.
|
||||
|
||||
### 2.1 Usage Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.content_filter_strategy import PruningContentFilter
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
|
||||
async def main():
|
||||
# Step 1: Create a pruning filter
|
||||
prune_filter = PruningContentFilter(
|
||||
# Lower → more content retained, higher → more content pruned
|
||||
threshold=0.45,
|
||||
# "fixed" or "dynamic"
|
||||
threshold_type="dynamic",
|
||||
# Ignore nodes with <5 words
|
||||
min_word_threshold=5
|
||||
)
|
||||
|
||||
# Step 2: Insert it into a Markdown Generator
|
||||
md_generator = DefaultMarkdownGenerator(content_filter=prune_filter)
|
||||
|
||||
# Step 3: Pass it to CrawlerRunConfig
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=md_generator
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com",
|
||||
config=config
|
||||
)
|
||||
|
||||
if result.success:
|
||||
# 'fit_markdown' is your pruned content, focusing on "denser" text
|
||||
print("Raw Markdown length:", len(result.markdown.raw_markdown))
|
||||
print("Fit Markdown length:", len(result.markdown.fit_markdown))
|
||||
else:
|
||||
print("Error:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 2.2 Key Parameters
|
||||
|
||||
- **`min_word_threshold`** (int): If a block has fewer words than this, it’s pruned.
|
||||
- **`threshold_type`** (str):
|
||||
- `"fixed"` → each node must exceed `threshold` (0–1).
|
||||
- `"dynamic"` → node scoring adjusts according to tag type, text/link density, etc.
|
||||
- **`threshold`** (float, default ~0.48): The base or “anchor” cutoff.
|
||||
|
||||
**Algorithmic Factors**:
|
||||
|
||||
- **Text density** – Encourages blocks that have a higher ratio of text to overall content.
|
||||
- **Link density** – Penalizes sections that are mostly links.
|
||||
- **Tag importance** – e.g., an `<article>` or `<p>` might be more important than a `<div>`.
|
||||
- **Structural context** – If a node is deeply nested or in a suspected sidebar, it might be deprioritized.
|
||||
|
||||
---
|
||||
|
||||
## 3. BM25ContentFilter
|
||||
|
||||
**BM25** is a classical text ranking algorithm often used in search engines. If you have a **user query** or rely on page metadata to derive a query, BM25 can identify which text chunks best match that query.
|
||||
|
||||
### 3.1 Usage Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.content_filter_strategy import BM25ContentFilter
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
|
||||
async def main():
|
||||
# 1) A BM25 filter with a user query
|
||||
bm25_filter = BM25ContentFilter(
|
||||
user_query="startup fundraising tips",
|
||||
# Adjust for stricter or looser results
|
||||
bm25_threshold=1.2
|
||||
)
|
||||
|
||||
# 2) Insert into a Markdown Generator
|
||||
md_generator = DefaultMarkdownGenerator(content_filter=bm25_filter)
|
||||
|
||||
# 3) Pass to crawler config
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=md_generator
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com",
|
||||
config=config
|
||||
)
|
||||
if result.success:
|
||||
print("Fit Markdown (BM25 query-based):")
|
||||
print(result.markdown.fit_markdown)
|
||||
else:
|
||||
print("Error:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 3.2 Parameters
|
||||
|
||||
- **`user_query`** (str, optional): E.g. `"machine learning"`. If blank, the filter tries to glean a query from page metadata.
|
||||
- **`bm25_threshold`** (float, default 1.0):
|
||||
- Higher → fewer chunks but more relevant.
|
||||
- Lower → more inclusive.
|
||||
|
||||
> In more advanced scenarios, you might see parameters like `language`, `case_sensitive`, or `priority_tags` to refine how text is tokenized or weighted.
|
||||
|
||||
---
|
||||
|
||||
## 4. Accessing the “Fit” Output
|
||||
|
||||
After the crawl, your “fit” content is found in **`result.markdown.fit_markdown`**.
|
||||
|
||||
```python
|
||||
fit_md = result.markdown.fit_markdown
|
||||
fit_html = result.markdown.fit_html
|
||||
```
|
||||
|
||||
If the content filter is **BM25**, you might see additional logic or references in `fit_markdown` that highlight relevant segments. If it’s **Pruning**, the text is typically well-cleaned but not necessarily matched to a query.
|
||||
|
||||
---
|
||||
|
||||
## 5. Code Patterns Recap
|
||||
|
||||
### 5.1 Pruning
|
||||
|
||||
```python
|
||||
prune_filter = PruningContentFilter(
|
||||
threshold=0.5,
|
||||
threshold_type="fixed",
|
||||
min_word_threshold=10
|
||||
)
|
||||
md_generator = DefaultMarkdownGenerator(content_filter=prune_filter)
|
||||
config = CrawlerRunConfig(markdown_generator=md_generator)
|
||||
```
|
||||
|
||||
### 5.2 BM25
|
||||
|
||||
```python
|
||||
bm25_filter = BM25ContentFilter(
|
||||
user_query="health benefits fruit",
|
||||
bm25_threshold=1.2
|
||||
)
|
||||
md_generator = DefaultMarkdownGenerator(content_filter=bm25_filter)
|
||||
config = CrawlerRunConfig(markdown_generator=md_generator)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Combining with “word_count_threshold” & Exclusions
|
||||
|
||||
Remember you can also specify:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
word_count_threshold=10,
|
||||
excluded_tags=["nav", "footer", "header"],
|
||||
exclude_external_links=True,
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter(threshold=0.5)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Thus, **multi-level** filtering occurs:
|
||||
|
||||
1. The crawler’s `excluded_tags` are removed from the HTML first.
|
||||
2. The content filter (Pruning, BM25, or custom) prunes or ranks the remaining text blocks.
|
||||
3. The final “fit” content is generated in `result.markdown.fit_markdown`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Custom Filters
|
||||
|
||||
If you need a different approach (like a specialized ML model or site-specific heuristics), you can create a new class inheriting from `RelevantContentFilter` and implement `filter_content(html)`. Then inject it into your **markdown generator**:
|
||||
|
||||
```python
|
||||
from crawl4ai.content_filter_strategy import RelevantContentFilter
|
||||
|
||||
class MyCustomFilter(RelevantContentFilter):
|
||||
def filter_content(self, html, min_word_threshold=None):
|
||||
# parse HTML, implement custom logic
|
||||
return [block for block in ... if ... some condition...]
|
||||
|
||||
```
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. Subclass `RelevantContentFilter`.
|
||||
2. Implement `filter_content(...)`.
|
||||
3. Use it in your `DefaultMarkdownGenerator(content_filter=MyCustomFilter(...))`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Final Thoughts
|
||||
|
||||
**Fit Markdown** is a crucial feature for:
|
||||
|
||||
- **Summaries**: Quickly get the important text from a cluttered page.
|
||||
- **Search**: Combine with **BM25** to produce content relevant to a query.
|
||||
- **AI Pipelines**: Filter out boilerplate so LLM-based extraction or summarization runs on denser text.
|
||||
|
||||
**Key Points**:
|
||||
- **PruningContentFilter**: Great if you just want the “meatiest” text without a user query.
|
||||
- **BM25ContentFilter**: Perfect for query-based extraction or searching.
|
||||
- Combine with **`excluded_tags`, `exclude_external_links`, `word_count_threshold`** to refine your final “fit” text.
|
||||
- Fit markdown ends up in **`result.markdown.fit_markdown`**; eventually **`result.markdown.fit_markdown`** in future versions.
|
||||
|
||||
With these tools, you can **zero in** on the text that truly matters, ignoring spammy or boilerplate content, and produce a concise, relevant “fit markdown” for your AI or data pipelines. Happy pruning and searching!
|
||||
|
||||
- Last Updated: 2025-01-01
|
||||
@@ -0,0 +1,129 @@
|
||||
# Installation & Setup (2023 Edition)
|
||||
|
||||
## 1. Basic Installation
|
||||
|
||||
```bash
|
||||
pip install crawl4ai
|
||||
```
|
||||
|
||||
This installs the **core** Crawl4AI library along with essential dependencies. **No** advanced features (like transformers or PyTorch) are included yet.
|
||||
|
||||
## 2. Initial Setup & Diagnostics
|
||||
|
||||
### 2.1 Run the Setup Command
|
||||
After installing, call:
|
||||
|
||||
```bash
|
||||
crawl4ai-setup
|
||||
```
|
||||
|
||||
**What does it do?**
|
||||
- Installs or updates required browser dependencies for both regular and undetected modes
|
||||
- Performs OS-level checks (e.g., missing libs on Linux)
|
||||
- Confirms your environment is ready to crawl
|
||||
|
||||
### 2.2 Diagnostics
|
||||
Optionally, you can run **diagnostics** to confirm everything is functioning:
|
||||
|
||||
```bash
|
||||
crawl4ai-doctor
|
||||
```
|
||||
|
||||
This command attempts to:
|
||||
- Check Python version compatibility
|
||||
- Verify Playwright installation
|
||||
- Inspect environment variables or library conflicts
|
||||
|
||||
If any issues arise, follow its suggestions (e.g., installing additional system packages) and re-run `crawl4ai-setup`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Verifying Installation: A Simple Crawl (Skip this step if you already run `crawl4ai-doctor`)
|
||||
|
||||
Below is a minimal Python script demonstrating a **basic** crawl. It uses our new **`BrowserConfig`** and **`CrawlerRunConfig`** for clarity, though no custom settings are passed in this example:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://www.example.com",
|
||||
)
|
||||
print(result.markdown[:300]) # Show the first 300 characters of extracted text
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Expected** outcome:
|
||||
- A headless browser session loads `example.com`
|
||||
- Crawl4AI returns ~300 characters of markdown.
|
||||
If errors occur, rerun `crawl4ai-doctor` or manually ensure Playwright is installed correctly.
|
||||
|
||||
---
|
||||
|
||||
## 4. Advanced Installation (Optional)
|
||||
|
||||
**Warning**: Only install these **if you truly need them**. They bring in larger dependencies, including big models, which can increase disk usage and memory load significantly.
|
||||
|
||||
### 4.1 Torch, Transformers, or All
|
||||
|
||||
- **Text Clustering (Torch)**
|
||||
```bash
|
||||
pip install crawl4ai[torch]
|
||||
crawl4ai-setup
|
||||
```
|
||||
Installs PyTorch-based features (e.g., cosine similarity or advanced semantic chunking).
|
||||
|
||||
- **Transformers**
|
||||
```bash
|
||||
pip install crawl4ai[transformer]
|
||||
crawl4ai-setup
|
||||
```
|
||||
Adds Hugging Face-based summarization or generation strategies.
|
||||
|
||||
- **All Features**
|
||||
```bash
|
||||
pip install crawl4ai[all]
|
||||
crawl4ai-setup
|
||||
```
|
||||
|
||||
#### (Optional) Pre-Fetching Models
|
||||
```bash
|
||||
crawl4ai-download-models
|
||||
```
|
||||
This step caches large models locally (if needed). **Only do this** if your workflow requires them.
|
||||
|
||||
---
|
||||
|
||||
## 5. Docker (Experimental)
|
||||
|
||||
We provide a **temporary** Docker approach for testing. **It’s not stable and may break** with future releases. We plan a major Docker revamp in a future stable version, 2025 Q1. If you still want to try:
|
||||
|
||||
```bash
|
||||
docker pull unclecode/crawl4ai:basic
|
||||
docker run -p 11235:11235 unclecode/crawl4ai:basic
|
||||
```
|
||||
|
||||
You can then make POST requests to `http://localhost:11235/crawl` to perform crawls. **Production usage** is discouraged until our new Docker approach is ready (planned in Jan or Feb 2025).
|
||||
|
||||
---
|
||||
|
||||
## 6. Local Server Mode (Legacy)
|
||||
|
||||
Some older docs mention running Crawl4AI as a local server. This approach has been **partially replaced** by the new Docker-based prototype and upcoming stable server release. You can experiment, but expect major changes. Official local server instructions will arrive once the new Docker architecture is finalized.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. **Install** with `pip install crawl4ai` and run `crawl4ai-setup`.
|
||||
2. **Diagnose** with `crawl4ai-doctor` if you see errors.
|
||||
3. **Verify** by crawling `example.com` with minimal `BrowserConfig` + `CrawlerRunConfig`.
|
||||
4. **Advanced** features (Torch, Transformers) are **optional**—avoid them if you don’t need them (they significantly increase resource usage).
|
||||
5. **Docker** is **experimental**—use at your own risk until the stable version is released.
|
||||
6. **Local server** references in older docs are largely deprecated; a new solution is in progress.
|
||||
|
||||
**Got questions?** Check [GitHub issues](https://github.com/unclecode/crawl4ai/issues) for updates or ask the community!
|
||||
@@ -0,0 +1,698 @@
|
||||
# Link & Media
|
||||
|
||||
In this tutorial, you’ll learn how to:
|
||||
|
||||
1. Extract links (internal, external) from crawled pages
|
||||
2. Filter or exclude specific domains (e.g., social media or custom domains)
|
||||
3. Access and ma### 3.2 Excluding Images
|
||||
|
||||
#### Excluding External Images
|
||||
|
||||
If you're dealing with heavy pages or want to skip third-party images (advertisements, for example), you can turn on:
|
||||
|
||||
```python
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_external_images=True
|
||||
)
|
||||
```
|
||||
|
||||
This setting attempts to discard images from outside the primary domain, keeping only those from the site you're crawling.
|
||||
|
||||
#### Excluding All Images
|
||||
|
||||
If you want to completely remove all images from the page to maximize performance and reduce memory usage, use:
|
||||
|
||||
```python
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_all_images=True
|
||||
)
|
||||
```
|
||||
|
||||
This setting removes all images very early in the processing pipeline, which significantly improves memory efficiency and processing speed. This is particularly useful when:
|
||||
- You don't need image data in your results
|
||||
- You're crawling image-heavy pages that cause memory issues
|
||||
- You want to focus only on text content
|
||||
- You need to maximize crawling speeddata (especially images) in the crawl result
|
||||
4. Configure your crawler to exclude or prioritize certain images
|
||||
|
||||
> **Prerequisites**
|
||||
> - You have completed or are familiar with the [AsyncWebCrawler Basics](../core/simple-crawling.md) tutorial.
|
||||
> - You can run Crawl4AI in your environment (Playwright, Python, etc.).
|
||||
|
||||
---
|
||||
|
||||
Below is a revised version of the **Link Extraction** and **Media Extraction** sections that includes example data structures showing how links and media items are stored in `CrawlResult`. Feel free to adjust any field names or descriptions to match your actual output.
|
||||
|
||||
---
|
||||
|
||||
## 1. Link Extraction
|
||||
|
||||
### 1.1 `result.links`
|
||||
|
||||
When you call `arun()` or `arun_many()` on a URL, Crawl4AI automatically extracts links and stores them in the `links` field of `CrawlResult`. By default, the crawler tries to distinguish **internal** links (same domain) from **external** links (different domains).
|
||||
|
||||
**Basic Example**:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://www.example.com")
|
||||
if result.success:
|
||||
internal_links = result.links.get("internal", [])
|
||||
external_links = result.links.get("external", [])
|
||||
print(f"Found {len(internal_links)} internal links.")
|
||||
print(f"Found {len(internal_links)} external links.")
|
||||
print(f"Found {len(result.media)} media items.")
|
||||
|
||||
# Each link is typically a dictionary with fields like:
|
||||
# { "href": "...", "text": "...", "title": "...", "base_domain": "..." }
|
||||
if internal_links:
|
||||
print("Sample Internal Link:", internal_links[0])
|
||||
else:
|
||||
print("Crawl failed:", result.error_message)
|
||||
```
|
||||
|
||||
**Structure Example**:
|
||||
|
||||
```python
|
||||
result.links = {
|
||||
"internal": [
|
||||
{
|
||||
"href": "https://kidocode.com/",
|
||||
"text": "",
|
||||
"title": "",
|
||||
"base_domain": "kidocode.com"
|
||||
},
|
||||
{
|
||||
"href": "https://kidocode.com/degrees/technology",
|
||||
"text": "Technology Degree",
|
||||
"title": "KidoCode Tech Program",
|
||||
"base_domain": "kidocode.com"
|
||||
},
|
||||
# ...
|
||||
],
|
||||
"external": [
|
||||
# possibly other links leading to third-party sites
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **`href`**: The raw hyperlink URL.
|
||||
- **`text`**: The link text (if any) within the `<a>` tag.
|
||||
- **`title`**: The `title` attribute of the link (if present).
|
||||
- **`base_domain`**: The domain extracted from `href`. Helpful for filtering or grouping by domain.
|
||||
|
||||
---
|
||||
|
||||
## 2. Advanced Link Head Extraction & Scoring
|
||||
|
||||
Ever wanted to not just extract links, but also get the actual content (title, description, metadata) from those linked pages? And score them for relevance? This is exactly what Link Head Extraction does - it fetches the `<head>` section from each discovered link and scores them using multiple algorithms.
|
||||
|
||||
### 2.1 Why Link Head Extraction?
|
||||
|
||||
When you crawl a page, you get hundreds of links. But which ones are actually valuable? Link Head Extraction solves this by:
|
||||
|
||||
1. **Fetching head content** from each link (title, description, meta tags)
|
||||
2. **Scoring links intrinsically** based on URL quality, text relevance, and context
|
||||
3. **Scoring links contextually** using BM25 algorithm when you provide a search query
|
||||
4. **Combining scores intelligently** to give you a final relevance ranking
|
||||
|
||||
### 2.2 Complete Working Example
|
||||
|
||||
Here's a full example you can copy, paste, and run immediately:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai import LinkPreviewConfig
|
||||
|
||||
async def extract_link_heads_example():
|
||||
"""
|
||||
Complete example showing link head extraction with scoring.
|
||||
This will crawl a documentation site and extract head content from internal links.
|
||||
"""
|
||||
|
||||
# Configure link head extraction
|
||||
config = CrawlerRunConfig(
|
||||
# Enable link head extraction with detailed configuration
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
include_internal=True, # Extract from internal links
|
||||
include_external=False, # Skip external links for this example
|
||||
max_links=10, # Limit to 10 links for demo
|
||||
concurrency=5, # Process 5 links simultaneously
|
||||
timeout=10, # 10 second timeout per link
|
||||
query="API documentation guide", # Query for contextual scoring
|
||||
score_threshold=0.3, # Only include links scoring above 0.3
|
||||
verbose=True # Show detailed progress
|
||||
),
|
||||
# Enable intrinsic scoring (URL quality, text relevance)
|
||||
score_links=True,
|
||||
# Keep output clean
|
||||
only_text=True,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Crawl a documentation site (great for testing)
|
||||
result = await crawler.arun("https://docs.python.org/3/", config=config)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ Successfully crawled: {result.url}")
|
||||
print(f"📄 Page title: {result.metadata.get('title', 'No title')}")
|
||||
|
||||
# Access links (now enhanced with head data and scores)
|
||||
internal_links = result.links.get("internal", [])
|
||||
external_links = result.links.get("external", [])
|
||||
|
||||
print(f"\n🔗 Found {len(internal_links)} internal links")
|
||||
print(f"🌍 Found {len(external_links)} external links")
|
||||
|
||||
# Count links with head data
|
||||
links_with_head = [link for link in internal_links
|
||||
if link.get("head_data") is not None]
|
||||
print(f"🧠 Links with head data extracted: {len(links_with_head)}")
|
||||
|
||||
# Show the top 3 scoring links
|
||||
print(f"\n🏆 Top 3 Links with Full Scoring:")
|
||||
for i, link in enumerate(links_with_head[:3]):
|
||||
print(f"\n{i+1}. {link['href']}")
|
||||
print(f" Link Text: '{link.get('text', 'No text')[:50]}...'")
|
||||
|
||||
# Show all three score types
|
||||
intrinsic = link.get('intrinsic_score')
|
||||
contextual = link.get('contextual_score')
|
||||
total = link.get('total_score')
|
||||
|
||||
if intrinsic is not None:
|
||||
print(f" 📊 Intrinsic Score: {intrinsic:.2f}/10.0 (URL quality & context)")
|
||||
if contextual is not None:
|
||||
print(f" 🎯 Contextual Score: {contextual:.3f} (BM25 relevance to query)")
|
||||
if total is not None:
|
||||
print(f" ⭐ Total Score: {total:.3f} (combined final score)")
|
||||
|
||||
# Show extracted head data
|
||||
head_data = link.get("head_data", {})
|
||||
if head_data:
|
||||
title = head_data.get("title", "No title")
|
||||
description = head_data.get("meta", {}).get("description", "No description")
|
||||
|
||||
print(f" 📰 Title: {title[:60]}...")
|
||||
if description:
|
||||
print(f" 📝 Description: {description[:80]}...")
|
||||
|
||||
# Show extraction status
|
||||
status = link.get("head_extraction_status", "unknown")
|
||||
print(f" ✅ Extraction Status: {status}")
|
||||
else:
|
||||
print(f"❌ Crawl failed: {result.error_message}")
|
||||
|
||||
# Run the example
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(extract_link_heads_example())
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
✅ Successfully crawled: https://docs.python.org/3/
|
||||
📄 Page title: 3.13.5 Documentation
|
||||
🔗 Found 53 internal links
|
||||
🌍 Found 1 external links
|
||||
🧠 Links with head data extracted: 10
|
||||
|
||||
🏆 Top 3 Links with Full Scoring:
|
||||
|
||||
1. https://docs.python.org/3.15/
|
||||
Link Text: 'Python 3.15 (in development)...'
|
||||
📊 Intrinsic Score: 4.17/10.0 (URL quality & context)
|
||||
🎯 Contextual Score: 1.000 (BM25 relevance to query)
|
||||
⭐ Total Score: 5.917 (combined final score)
|
||||
📰 Title: 3.15.0a0 Documentation...
|
||||
📝 Description: The official Python documentation...
|
||||
✅ Extraction Status: valid
|
||||
```
|
||||
|
||||
### 2.3 Configuration Deep Dive
|
||||
|
||||
The `LinkPreviewConfig` class supports these options:
|
||||
|
||||
```python
|
||||
from crawl4ai import LinkPreviewConfig
|
||||
|
||||
link_preview_config = LinkPreviewConfig(
|
||||
# BASIC SETTINGS
|
||||
verbose=True, # Show detailed logs (recommended for learning)
|
||||
|
||||
# LINK FILTERING
|
||||
include_internal=True, # Include same-domain links
|
||||
include_external=True, # Include different-domain links
|
||||
max_links=50, # Maximum links to process (prevents overload)
|
||||
|
||||
# PATTERN FILTERING
|
||||
include_patterns=[ # Only process links matching these patterns
|
||||
"*/docs/*",
|
||||
"*/api/*",
|
||||
"*/reference/*"
|
||||
],
|
||||
exclude_patterns=[ # Skip links matching these patterns
|
||||
"*/login*",
|
||||
"*/admin*"
|
||||
],
|
||||
|
||||
# PERFORMANCE SETTINGS
|
||||
concurrency=10, # How many links to process simultaneously
|
||||
timeout=5, # Seconds to wait per link
|
||||
|
||||
# RELEVANCE SCORING
|
||||
query="machine learning API", # Query for BM25 contextual scoring
|
||||
score_threshold=0.3, # Only include links above this score
|
||||
)
|
||||
```
|
||||
|
||||
### 2.4 Understanding the Three Score Types
|
||||
|
||||
Each extracted link gets three different scores:
|
||||
|
||||
#### 1. **Intrinsic Score (0-10)** - URL and Content Quality
|
||||
Based on URL structure, link text quality, and page context:
|
||||
|
||||
```python
|
||||
# High intrinsic score indicators:
|
||||
# ✅ Clean URL structure (docs.python.org/api/reference)
|
||||
# ✅ Meaningful link text ("API Reference Guide")
|
||||
# ✅ Relevant to page context
|
||||
# ✅ Not buried deep in navigation
|
||||
|
||||
# Low intrinsic score indicators:
|
||||
# ❌ Random URLs (site.com/x7f9g2h)
|
||||
# ❌ No link text or generic text ("Click here")
|
||||
# ❌ Unrelated to page content
|
||||
```
|
||||
|
||||
#### 2. **Contextual Score (0-1)** - BM25 Relevance to Query
|
||||
Only available when you provide a `query`. Uses BM25 algorithm against head content:
|
||||
|
||||
```python
|
||||
# Example: query = "machine learning tutorial"
|
||||
# High contextual score: Link to "Complete Machine Learning Guide"
|
||||
# Low contextual score: Link to "Privacy Policy"
|
||||
```
|
||||
|
||||
#### 3. **Total Score** - Smart Combination
|
||||
Intelligently combines intrinsic and contextual scores with fallbacks:
|
||||
|
||||
```python
|
||||
# When both scores available: (intrinsic * 0.3) + (contextual * 0.7)
|
||||
# When only intrinsic: uses intrinsic score
|
||||
# When only contextual: uses contextual score
|
||||
# When neither: not calculated
|
||||
```
|
||||
|
||||
### 2.5 Practical Use Cases
|
||||
|
||||
#### Use Case 1: Research Assistant
|
||||
Find the most relevant documentation pages:
|
||||
|
||||
```python
|
||||
async def research_assistant():
|
||||
config = CrawlerRunConfig(
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
include_internal=True,
|
||||
include_external=True,
|
||||
include_patterns=["*/docs/*", "*/tutorial/*", "*/guide/*"],
|
||||
query="machine learning neural networks",
|
||||
max_links=20,
|
||||
score_threshold=0.5, # Only high-relevance links
|
||||
verbose=True
|
||||
),
|
||||
score_links=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://scikit-learn.org/", config=config)
|
||||
|
||||
if result.success:
|
||||
# Get high-scoring links
|
||||
good_links = [link for link in result.links.get("internal", [])
|
||||
if link.get("total_score", 0) > 0.7]
|
||||
|
||||
print(f"🎯 Found {len(good_links)} highly relevant links:")
|
||||
for link in good_links[:5]:
|
||||
print(f"⭐ {link['total_score']:.3f} - {link['href']}")
|
||||
print(f" {link.get('head_data', {}).get('title', 'No title')}")
|
||||
```
|
||||
|
||||
#### Use Case 2: Content Discovery
|
||||
Find all API endpoints and references:
|
||||
|
||||
```python
|
||||
async def api_discovery():
|
||||
config = CrawlerRunConfig(
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
include_internal=True,
|
||||
include_patterns=["*/api/*", "*/reference/*"],
|
||||
exclude_patterns=["*/deprecated/*"],
|
||||
max_links=100,
|
||||
concurrency=15,
|
||||
verbose=False # Clean output
|
||||
),
|
||||
score_links=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://docs.example-api.com/", config=config)
|
||||
|
||||
if result.success:
|
||||
api_links = result.links.get("internal", [])
|
||||
|
||||
# Group by endpoint type
|
||||
endpoints = {}
|
||||
for link in api_links:
|
||||
if link.get("head_data"):
|
||||
title = link["head_data"].get("title", "")
|
||||
if "GET" in title:
|
||||
endpoints.setdefault("GET", []).append(link)
|
||||
elif "POST" in title:
|
||||
endpoints.setdefault("POST", []).append(link)
|
||||
|
||||
for method, links in endpoints.items():
|
||||
print(f"\n{method} Endpoints ({len(links)}):")
|
||||
for link in links[:3]:
|
||||
print(f" • {link['href']}")
|
||||
```
|
||||
|
||||
#### Use Case 3: Link Quality Analysis
|
||||
Analyze website structure and content quality:
|
||||
|
||||
```python
|
||||
async def quality_analysis():
|
||||
config = CrawlerRunConfig(
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
include_internal=True,
|
||||
max_links=200,
|
||||
concurrency=20,
|
||||
),
|
||||
score_links=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://your-website.com/", config=config)
|
||||
|
||||
if result.success:
|
||||
links = result.links.get("internal", [])
|
||||
|
||||
# Analyze intrinsic scores
|
||||
scores = [link.get('intrinsic_score', 0) for link in links]
|
||||
avg_score = sum(scores) / len(scores) if scores else 0
|
||||
|
||||
print(f"📊 Link Quality Analysis:")
|
||||
print(f" Average intrinsic score: {avg_score:.2f}/10.0")
|
||||
print(f" High quality links (>7.0): {len([s for s in scores if s > 7.0])}")
|
||||
print(f" Low quality links (<3.0): {len([s for s in scores if s < 3.0])}")
|
||||
|
||||
# Find problematic links
|
||||
bad_links = [link for link in links
|
||||
if link.get('intrinsic_score', 0) < 2.0]
|
||||
|
||||
if bad_links:
|
||||
print(f"\n⚠️ Links needing attention:")
|
||||
for link in bad_links[:5]:
|
||||
print(f" {link['href']} (score: {link.get('intrinsic_score', 0):.1f})")
|
||||
```
|
||||
|
||||
### 2.6 Performance Tips
|
||||
|
||||
1. **Start Small**: Begin with `max_links: 10` to understand the feature
|
||||
2. **Use Patterns**: Filter with `include_patterns` to focus on relevant sections
|
||||
3. **Adjust Concurrency**: Higher concurrency = faster but more resource usage
|
||||
4. **Set Timeouts**: Use `timeout: 5` to prevent hanging on slow sites
|
||||
5. **Use Score Thresholds**: Filter out low-quality links with `score_threshold`
|
||||
|
||||
### 2.7 Troubleshooting
|
||||
|
||||
**No head data extracted?**
|
||||
```python
|
||||
# Check your configuration:
|
||||
config = CrawlerRunConfig(
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
verbose=True # ← Enable to see what's happening
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Scores showing as None?**
|
||||
```python
|
||||
# Make sure scoring is enabled:
|
||||
config = CrawlerRunConfig(
|
||||
score_links=True, # ← Enable intrinsic scoring
|
||||
link_preview_config=LinkPreviewConfig(
|
||||
query="your search terms" # ← For contextual scoring
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Process taking too long?**
|
||||
```python
|
||||
# Optimize performance:
|
||||
link_preview_config = LinkPreviewConfig(
|
||||
max_links=20, # ← Reduce number
|
||||
concurrency=10, # ← Increase parallelism
|
||||
timeout=3, # ← Shorter timeout
|
||||
include_patterns=["*/important/*"] # ← Focus on key areas
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Domain Filtering
|
||||
|
||||
Some websites contain hundreds of third-party or affiliate links. You can filter out certain domains at **crawl time** by configuring the crawler. The most relevant parameters in `CrawlerRunConfig` are:
|
||||
|
||||
- **`exclude_external_links`**: If `True`, discard any link pointing outside the root domain.
|
||||
- **`exclude_social_media_domains`**: Provide a list of social media platforms (e.g., `["facebook.com", "twitter.com"]`) to exclude from your crawl.
|
||||
- **`exclude_social_media_links`**: If `True`, automatically skip known social platforms.
|
||||
- **`exclude_domains`**: Provide a list of custom domains you want to exclude (e.g., `["spammyads.com", "tracker.net"]`).
|
||||
|
||||
### 3.1 Example: Excluding External & Social Media Links
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_external_links=True, # No links outside primary domain
|
||||
exclude_social_media_links=True # Skip recognized social media domains
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
"https://www.example.com",
|
||||
config=crawler_cfg
|
||||
)
|
||||
if result.success:
|
||||
print("[OK] Crawled:", result.url)
|
||||
print("Internal links count:", len(result.links.get("internal", [])))
|
||||
print("External links count:", len(result.links.get("external", [])))
|
||||
# Likely zero external links in this scenario
|
||||
else:
|
||||
print("[ERROR]", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 3.2 Example: Excluding Specific Domains
|
||||
|
||||
If you want to let external links in, but specifically exclude a domain (e.g., `suspiciousads.com`), do this:
|
||||
|
||||
```python
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_domains=["suspiciousads.com"]
|
||||
)
|
||||
```
|
||||
|
||||
This approach is handy when you still want external links but need to block certain sites you consider spammy.
|
||||
|
||||
---
|
||||
|
||||
## 4. Media Extraction
|
||||
|
||||
### 4.1 Accessing `result.media`
|
||||
|
||||
By default, Crawl4AI collects images, audio and video URLs it finds on the page. These are stored in `result.media`, a dictionary keyed by media type (e.g., `images`, `videos`, `audio`).
|
||||
**Note: Tables have been moved from `result.media["tables"]` to the new `result.tables` format for better organization and direct access.**
|
||||
|
||||
**Basic Example**:
|
||||
|
||||
```python
|
||||
if result.success:
|
||||
# Get images
|
||||
images_info = result.media.get("images", [])
|
||||
print(f"Found {len(images_info)} images in total.")
|
||||
for i, img in enumerate(images_info[:3]): # Inspect just the first 3
|
||||
print(f"[Image {i}] URL: {img['src']}")
|
||||
print(f" Alt text: {img.get('alt', '')}")
|
||||
print(f" Score: {img.get('score')}")
|
||||
print(f" Description: {img.get('desc', '')}\n")
|
||||
```
|
||||
|
||||
**Structure Example**:
|
||||
|
||||
```python
|
||||
result.media = {
|
||||
"images": [
|
||||
{
|
||||
"src": "https://cdn.prod.website-files.com/.../Group%2089.svg",
|
||||
"alt": "coding school for kids",
|
||||
"desc": "Trial Class Degrees degrees All Degrees AI Degree Technology ...",
|
||||
"score": 3,
|
||||
"type": "image",
|
||||
"group_id": 0,
|
||||
"format": None,
|
||||
"width": None,
|
||||
"height": None
|
||||
},
|
||||
# ...
|
||||
],
|
||||
"videos": [
|
||||
# Similar structure but with video-specific fields
|
||||
],
|
||||
"audio": [
|
||||
# Similar structure but with audio-specific fields
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Depending on your Crawl4AI version or scraping strategy, these dictionaries can include fields like:
|
||||
|
||||
- **`src`**: The media URL (e.g., image source)
|
||||
- **`alt`**: The alt text for images (if present)
|
||||
- **`desc`**: A snippet of nearby text or a short description (optional)
|
||||
- **`score`**: A heuristic relevance score if you’re using content-scoring features
|
||||
- **`width`**, **`height`**: If the crawler detects dimensions for the image/video
|
||||
- **`type`**: Usually `"image"`, `"video"`, or `"audio"`
|
||||
- **`group_id`**: If you’re grouping related media items, the crawler might assign an ID
|
||||
|
||||
With these details, you can easily filter out or focus on certain images (for instance, ignoring images with very low scores or a different domain), or gather metadata for analytics.
|
||||
|
||||
### 4.2 Excluding External Images
|
||||
|
||||
If you’re dealing with heavy pages or want to skip third-party images (advertisements, for example), you can turn on:
|
||||
|
||||
```python
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_external_images=True
|
||||
)
|
||||
```
|
||||
|
||||
This setting attempts to discard images from outside the primary domain, keeping only those from the site you’re crawling.
|
||||
|
||||
### 4.3 Additional Media Config
|
||||
|
||||
- **`screenshot`**: Set to `True` if you want a full-page screenshot stored as `base64` in `result.screenshot`.
|
||||
- **`pdf`**: Set to `True` if you want a PDF version of the page in `result.pdf`.
|
||||
- **`capture_mhtml`**: Set to `True` if you want an MHTML snapshot of the page in `result.mhtml`. This format preserves the entire web page with all its resources (CSS, images, scripts) in a single file, making it perfect for archiving or offline viewing.
|
||||
- **`wait_for_images`**: If `True`, attempts to wait until images are fully loaded before final extraction.
|
||||
|
||||
#### Example: Capturing Page as MHTML
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
capture_mhtml=True # Enable MHTML capture
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=crawler_cfg)
|
||||
|
||||
if result.success and result.mhtml:
|
||||
# Save the MHTML snapshot to a file
|
||||
with open("example.mhtml", "w", encoding="utf-8") as f:
|
||||
f.write(result.mhtml)
|
||||
print("MHTML snapshot saved to example.mhtml")
|
||||
else:
|
||||
print("Failed to capture MHTML:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
The MHTML format is particularly useful because:
|
||||
- It captures the complete page state including all resources
|
||||
- It can be opened in most modern browsers for offline viewing
|
||||
- It preserves the page exactly as it appeared during crawling
|
||||
- It's a single file, making it easy to store and transfer
|
||||
|
||||
---
|
||||
|
||||
## 5. Putting It All Together: Link & Media Filtering
|
||||
|
||||
Here’s a combined example demonstrating how to filter out external links, skip certain domains, and exclude external images:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
# Suppose we want to keep only internal links, remove certain domains,
|
||||
# and discard external images from the final crawl data.
|
||||
crawler_cfg = CrawlerRunConfig(
|
||||
exclude_external_links=True,
|
||||
exclude_domains=["spammyads.com"],
|
||||
exclude_social_media_links=True, # skip Twitter, Facebook, etc.
|
||||
exclude_external_images=True, # keep only images from main domain
|
||||
wait_for_images=True, # ensure images are loaded
|
||||
verbose=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://www.example.com", config=crawler_cfg)
|
||||
|
||||
if result.success:
|
||||
print("[OK] Crawled:", result.url)
|
||||
|
||||
# 1. Links
|
||||
in_links = result.links.get("internal", [])
|
||||
ext_links = result.links.get("external", [])
|
||||
print("Internal link count:", len(in_links))
|
||||
print("External link count:", len(ext_links)) # should be zero with exclude_external_links=True
|
||||
|
||||
# 2. Images
|
||||
images = result.media.get("images", [])
|
||||
print("Images found:", len(images))
|
||||
|
||||
# Let's see a snippet of these images
|
||||
for i, img in enumerate(images[:3]):
|
||||
print(f" - {img['src']} (alt={img.get('alt','')}, score={img.get('score','N/A')})")
|
||||
else:
|
||||
print("[ERROR] Failed to crawl. Reason:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Pitfalls & Tips
|
||||
|
||||
1. **Conflicting Flags**:
|
||||
- `exclude_external_links=True` but then also specifying `exclude_social_media_links=True` is typically fine, but understand that the first setting already discards *all* external links. The second becomes somewhat redundant.
|
||||
- `exclude_external_images=True` but want to keep some external images? Currently no partial domain-based setting for images, so you might need a custom approach or hook logic.
|
||||
|
||||
2. **Relevancy Scores**:
|
||||
- If your version of Crawl4AI or your scraping strategy includes an `img["score"]`, it’s typically a heuristic based on size, position, or content analysis. Evaluate carefully if you rely on it.
|
||||
|
||||
3. **Performance**:
|
||||
- Excluding certain domains or external images can speed up your crawl, especially for large, media-heavy pages.
|
||||
- If you want a “full” link map, do *not* exclude them. Instead, you can post-filter in your own code.
|
||||
|
||||
4. **Social Media Lists**:
|
||||
- `exclude_social_media_links=True` typically references an internal list of known social domains like Facebook, Twitter, LinkedIn, etc. If you need to add or remove from that list, look for library settings or a local config file (depending on your version).
|
||||
|
||||
---
|
||||
|
||||
**That’s it for Link & Media Analysis!** You’re now equipped to filter out unwanted sites and zero in on the images and videos that matter for your project.
|
||||
@@ -0,0 +1,61 @@
|
||||
I<div class="llmtxt-container">
|
||||
<iframe id="llmtxt-frame" src="../../llmtxt/index.html" width="100%" style="border:none; display: block;" title="Crawl4AI LLM Context Builder"></iframe>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Iframe height adjustment
|
||||
function resizeLLMtxtIframe() {
|
||||
const iframe = document.getElementById('llmtxt-frame');
|
||||
if (iframe) {
|
||||
const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height') || '55');
|
||||
const topOffset = headerHeight + 20;
|
||||
const availableHeight = window.innerHeight - topOffset;
|
||||
iframe.style.height = Math.max(800, availableHeight) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// Run immediately and on resize/load
|
||||
resizeLLMtxtIframe();
|
||||
let resizeTimer;
|
||||
window.addEventListener('load', resizeLLMtxtIframe);
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(resizeLLMtxtIframe, 150);
|
||||
});
|
||||
|
||||
// Remove Footer & HR from parent page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => {
|
||||
const footer = window.parent.document.querySelector('footer');
|
||||
if (footer) {
|
||||
const hrBeforeFooter = footer.previousElementSibling;
|
||||
if (hrBeforeFooter && hrBeforeFooter.tagName === 'HR') {
|
||||
hrBeforeFooter.remove();
|
||||
}
|
||||
footer.remove();
|
||||
resizeLLMtxtIframe();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#terminal-mkdocs-main-content {
|
||||
padding: 0 !important;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#terminal-mkdocs-main-content .llmtxt-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#terminal-mkdocs-toc-panel {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
# Prefix-Based Input Handling in Crawl4AI
|
||||
|
||||
This guide will walk you through using the Crawl4AI library to crawl web pages, local HTML files, and raw HTML strings. We'll demonstrate these capabilities using a Wikipedia page as an example.
|
||||
|
||||
## Crawling a Web URL
|
||||
|
||||
To crawl a live web page, provide the URL starting with `http://` or `https://`, using a `CrawlerRunConfig` object:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig
|
||||
|
||||
async def crawl_web():
|
||||
config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://en.wikipedia.org/wiki/apple",
|
||||
config=config
|
||||
)
|
||||
if result.success:
|
||||
print("Markdown Content:")
|
||||
print(result.markdown)
|
||||
else:
|
||||
print(f"Failed to crawl: {result.error_message}")
|
||||
|
||||
asyncio.run(crawl_web())
|
||||
```
|
||||
|
||||
## Crawling a Local HTML File
|
||||
|
||||
To crawl a local HTML file, prefix the file path with `file://`.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig
|
||||
|
||||
async def crawl_local_file():
|
||||
local_file_path = "/path/to/apple.html" # Replace with your file path
|
||||
file_url = f"file://{local_file_path}"
|
||||
config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url=file_url, config=config)
|
||||
if result.success:
|
||||
print("Markdown Content from Local File:")
|
||||
print(result.markdown)
|
||||
else:
|
||||
print(f"Failed to crawl local file: {result.error_message}")
|
||||
|
||||
asyncio.run(crawl_local_file())
|
||||
```
|
||||
|
||||
## Crawling Raw HTML Content
|
||||
|
||||
To crawl raw HTML content, prefix the HTML string with `raw:`.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CacheMode
|
||||
from crawl4ai.async_configs import CrawlerRunConfig
|
||||
|
||||
async def crawl_raw_html():
|
||||
raw_html = "<html><body><h1>Hello, World!</h1></body></html>"
|
||||
raw_html_url = f"raw:{raw_html}"
|
||||
config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url=raw_html_url, config=config)
|
||||
if result.success:
|
||||
print("Markdown Content from Raw HTML:")
|
||||
print(result.markdown)
|
||||
else:
|
||||
print(f"Failed to crawl raw HTML: {result.error_message}")
|
||||
|
||||
asyncio.run(crawl_raw_html())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Complete Example
|
||||
|
||||
Below is a comprehensive script that:
|
||||
|
||||
1. Crawls the Wikipedia page for "Apple."
|
||||
2. Saves the HTML content to a local file (`apple.html`).
|
||||
3. Crawls the local HTML file and verifies the markdown length matches the original crawl.
|
||||
4. Crawls the raw HTML content from the saved file and verifies consistency.
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
wikipedia_url = "https://en.wikipedia.org/wiki/apple"
|
||||
script_dir = Path(__file__).parent
|
||||
html_file_path = script_dir / "apple.html"
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Step 1: Crawl the Web URL
|
||||
print("\n=== Step 1: Crawling the Wikipedia URL ===")
|
||||
web_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
result = await crawler.arun(url=wikipedia_url, config=web_config)
|
||||
|
||||
if not result.success:
|
||||
print(f"Failed to crawl {wikipedia_url}: {result.error_message}")
|
||||
return
|
||||
|
||||
with open(html_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(result.html)
|
||||
web_crawl_length = len(result.markdown)
|
||||
print(f"Length of markdown from web crawl: {web_crawl_length}\n")
|
||||
|
||||
# Step 2: Crawl from the Local HTML File
|
||||
print("=== Step 2: Crawling from the Local HTML File ===")
|
||||
file_url = f"file://{html_file_path.resolve()}"
|
||||
file_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
local_result = await crawler.arun(url=file_url, config=file_config)
|
||||
|
||||
if not local_result.success:
|
||||
print(f"Failed to crawl local file {file_url}: {local_result.error_message}")
|
||||
return
|
||||
|
||||
local_crawl_length = len(local_result.markdown)
|
||||
assert web_crawl_length == local_crawl_length, "Markdown length mismatch"
|
||||
print("✅ Markdown length matches between web and local file crawl.\n")
|
||||
|
||||
# Step 3: Crawl Using Raw HTML Content
|
||||
print("=== Step 3: Crawling Using Raw HTML Content ===")
|
||||
with open(html_file_path, 'r', encoding='utf-8') as f:
|
||||
raw_html_content = f.read()
|
||||
raw_html_url = f"raw:{raw_html_content}"
|
||||
raw_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS)
|
||||
raw_result = await crawler.arun(url=raw_html_url, config=raw_config)
|
||||
|
||||
if not raw_result.success:
|
||||
print(f"Failed to crawl raw HTML content: {raw_result.error_message}")
|
||||
return
|
||||
|
||||
raw_crawl_length = len(raw_result.markdown)
|
||||
assert web_crawl_length == raw_crawl_length, "Markdown length mismatch"
|
||||
print("✅ Markdown length matches between web and raw HTML crawl.\n")
|
||||
|
||||
print("All tests passed successfully!")
|
||||
if html_file_path.exists():
|
||||
os.remove(html_file_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Conclusion
|
||||
|
||||
With the unified `url` parameter and prefix-based handling in **Crawl4AI**, you can seamlessly handle web URLs, local HTML files, and raw HTML content. Use `CrawlerRunConfig` for flexible and consistent configuration in all scenarios.
|
||||
@@ -0,0 +1,504 @@
|
||||
# Markdown Generation Basics
|
||||
|
||||
One of Crawl4AI’s core features is generating **clean, structured markdown** from web pages. Originally built to solve the problem of extracting only the “actual” content and discarding boilerplate or noise, Crawl4AI’s markdown system remains one of its biggest draws for AI workflows.
|
||||
|
||||
In this tutorial, you’ll learn:
|
||||
|
||||
1. How to configure the **Default Markdown Generator**
|
||||
2. How **content filters** (BM25 or Pruning) help you refine markdown and discard junk
|
||||
3. The difference between raw markdown (`result.markdown`) and filtered markdown (`fit_markdown`)
|
||||
|
||||
> **Prerequisites**
|
||||
> - You’ve completed or read [AsyncWebCrawler Basics](../core/simple-crawling.md) to understand how to run a simple crawl.
|
||||
> - You know how to configure `CrawlerRunConfig`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Quick Example
|
||||
|
||||
Here’s a minimal code snippet that uses the **DefaultMarkdownGenerator** with no additional filtering:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator()
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
|
||||
if result.success:
|
||||
print("Raw Markdown Output:\n")
|
||||
print(result.markdown) # The unfiltered markdown from the page
|
||||
else:
|
||||
print("Crawl failed:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**What’s happening?**
|
||||
- `CrawlerRunConfig( markdown_generator = DefaultMarkdownGenerator() )` instructs Crawl4AI to convert the final HTML into markdown at the end of each crawl.
|
||||
- The resulting markdown is accessible via `result.markdown`.
|
||||
|
||||
---
|
||||
|
||||
## 2. How Markdown Generation Works
|
||||
|
||||
### 2.1 HTML-to-Text Conversion (Forked & Modified)
|
||||
|
||||
Under the hood, **DefaultMarkdownGenerator** uses a specialized HTML-to-text approach that:
|
||||
|
||||
- Preserves headings, code blocks, bullet points, etc.
|
||||
- Removes extraneous tags (scripts, styles) that don’t add meaningful content.
|
||||
- Can optionally generate references for links or skip them altogether.
|
||||
|
||||
A set of **options** (passed as a dict) allows you to customize precisely how HTML converts to markdown. These map to standard html2text-like configuration plus your own enhancements (e.g., ignoring internal links, preserving certain tags verbatim, or adjusting line widths).
|
||||
|
||||
### 2.2 Link Citations & References
|
||||
|
||||
By default, the generator can convert `<a href="...">` elements into `[text][1]` citations, then place the actual links at the bottom of the document. This is handy for research workflows that demand references in a structured manner.
|
||||
|
||||
### 2.3 Optional Content Filters
|
||||
|
||||
Before or after the HTML-to-Markdown step, you can apply a **content filter** (like BM25 or Pruning) to reduce noise and produce a “fit_markdown”—a heavily pruned version focusing on the page’s main text. We’ll cover these filters shortly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuring the Default Markdown Generator
|
||||
|
||||
You can tweak the output by passing an `options` dict to `DefaultMarkdownGenerator`. For example:
|
||||
|
||||
```python
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
# Example: ignore all links, don't escape HTML, and wrap text at 80 characters
|
||||
md_generator = DefaultMarkdownGenerator(
|
||||
options={
|
||||
"ignore_links": True,
|
||||
"escape_html": False,
|
||||
"body_width": 80
|
||||
}
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=md_generator
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com/docs", config=config)
|
||||
if result.success:
|
||||
print("Markdown:\n", result.markdown[:500]) # Just a snippet
|
||||
else:
|
||||
print("Crawl failed:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Some commonly used `options`:
|
||||
|
||||
- **`ignore_links`** (bool): Whether to remove all hyperlinks in the final markdown.
|
||||
- **`ignore_images`** (bool): Remove all `![image]()` references.
|
||||
- **`escape_html`** (bool): Turn HTML entities into text (default is often `True`).
|
||||
- **`body_width`** (int): Wrap text at N characters. `0` or `None` means no wrapping.
|
||||
- **`skip_internal_links`** (bool): If `True`, omit `#localAnchors` or internal links referencing the same page.
|
||||
- **`include_sup_sub`** (bool): Attempt to handle `<sup>` / `<sub>` in a more readable way.
|
||||
|
||||
## 4. Selecting the HTML Source for Markdown Generation
|
||||
|
||||
The `content_source` parameter allows you to control which HTML content is used as input for markdown generation. This gives you flexibility in how the HTML is processed before conversion to markdown.
|
||||
|
||||
```python
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
# Option 1: Use the raw HTML directly from the webpage (before any processing)
|
||||
raw_md_generator = DefaultMarkdownGenerator(
|
||||
content_source="raw_html",
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
|
||||
# Option 2: Use the cleaned HTML (after scraping strategy processing - default)
|
||||
cleaned_md_generator = DefaultMarkdownGenerator(
|
||||
content_source="cleaned_html", # This is the default
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
|
||||
# Option 3: Use preprocessed HTML optimized for schema extraction
|
||||
fit_md_generator = DefaultMarkdownGenerator(
|
||||
content_source="fit_html",
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
|
||||
# Use one of the generators in your crawler config
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=raw_md_generator # Try each of the generators
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
if result.success:
|
||||
print("Markdown:\n", result.markdown.raw_markdown[:500])
|
||||
else:
|
||||
print("Crawl failed:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### HTML Source Options
|
||||
|
||||
- **`"cleaned_html"`** (default): Uses the HTML after it has been processed by the scraping strategy. This HTML is typically cleaner and more focused on content, with some boilerplate removed.
|
||||
|
||||
- **`"raw_html"`**: Uses the original HTML directly from the webpage, before any cleaning or processing. This preserves more of the original content, but may include navigation bars, ads, footers, and other elements that might not be relevant to the main content.
|
||||
|
||||
- **`"fit_html"`**: Uses HTML preprocessed for schema extraction. This HTML is optimized for structured data extraction and may have certain elements simplified or removed.
|
||||
|
||||
### When to Use Each Option
|
||||
|
||||
- Use **`"cleaned_html"`** (default) for most cases where you want a balance of content preservation and noise removal.
|
||||
- Use **`"raw_html"`** when you need to preserve all original content, or when the cleaning process is removing content you actually want to keep.
|
||||
- Use **`"fit_html"`** when working with structured data or when you need HTML that's optimized for schema extraction.
|
||||
|
||||
---
|
||||
|
||||
## 5. Content Filters
|
||||
|
||||
**Content filters** selectively remove or rank sections of text before turning them into Markdown. This is especially helpful if your page has ads, nav bars, or other clutter you don’t want.
|
||||
|
||||
### 5.1 BM25ContentFilter
|
||||
|
||||
If you have a **search query**, BM25 is a good choice:
|
||||
|
||||
```python
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
from crawl4ai.content_filter_strategy import BM25ContentFilter
|
||||
from crawl4ai import CrawlerRunConfig
|
||||
|
||||
bm25_filter = BM25ContentFilter(
|
||||
user_query="machine learning",
|
||||
bm25_threshold=1.2,
|
||||
language="english"
|
||||
)
|
||||
|
||||
md_generator = DefaultMarkdownGenerator(
|
||||
content_filter=bm25_filter,
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(markdown_generator=md_generator)
|
||||
```
|
||||
|
||||
- **`user_query`**: The term you want to focus on. BM25 tries to keep only content blocks relevant to that query.
|
||||
- **`bm25_threshold`**: Raise it to keep fewer blocks; lower it to keep more.
|
||||
- **`use_stemming`** *(default `True`)*: Whether to apply stemming to the query and content.
|
||||
- **`language (str)`**: Language for stemming (default: 'english').
|
||||
|
||||
**No query provided?** BM25 tries to glean a context from page metadata, or you can simply treat it as a scorched-earth approach that discards text with low generic score. Realistically, you want to supply a query for best results.
|
||||
|
||||
### 5.2 PruningContentFilter
|
||||
|
||||
If you **don’t** have a specific query, or if you just want a robust “junk remover,” use `PruningContentFilter`. It analyzes text density, link density, HTML structure, and known patterns (like “nav,” “footer”) to systematically prune extraneous or repetitive sections.
|
||||
|
||||
```python
|
||||
from crawl4ai.content_filter_strategy import PruningContentFilter
|
||||
|
||||
prune_filter = PruningContentFilter(
|
||||
threshold=0.5,
|
||||
threshold_type="fixed", # or "dynamic"
|
||||
min_word_threshold=50
|
||||
)
|
||||
```
|
||||
|
||||
- **`threshold`**: Score boundary. Blocks below this score get removed.
|
||||
- **`threshold_type`**:
|
||||
- `"fixed"`: Straight comparison (`score >= threshold` keeps the block).
|
||||
- `"dynamic"`: The filter adjusts threshold in a data-driven manner.
|
||||
- **`min_word_threshold`**: Discard blocks under N words as likely too short or unhelpful.
|
||||
|
||||
**When to Use PruningContentFilter**
|
||||
- You want a broad cleanup without a user query.
|
||||
- The page has lots of repeated sidebars, footers, or disclaimers that hamper text extraction.
|
||||
|
||||
### 5.3 LLMContentFilter
|
||||
|
||||
For intelligent content filtering and high-quality markdown generation, you can use the **LLMContentFilter**. This filter leverages LLMs to generate relevant markdown while preserving the original content's meaning and structure:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, LLMConfig, DefaultMarkdownGenerator
|
||||
from crawl4ai.content_filter_strategy import LLMContentFilter
|
||||
|
||||
async def main():
|
||||
# Initialize LLM filter with specific instruction
|
||||
filter = LLMContentFilter(
|
||||
llm_config = LLMConfig(provider="openai/gpt-4o",api_token="your-api-token"), #or use environment variable
|
||||
instruction="""
|
||||
Focus on extracting the core educational content.
|
||||
Include:
|
||||
- Key concepts and explanations
|
||||
- Important code examples
|
||||
- Essential technical details
|
||||
Exclude:
|
||||
- Navigation elements
|
||||
- Sidebars
|
||||
- Footer content
|
||||
Format the output as clean markdown with proper code blocks and headers.
|
||||
""",
|
||||
chunk_token_threshold=4096, # Adjust based on your needs
|
||||
verbose=True
|
||||
)
|
||||
md_generator = DefaultMarkdownGenerator(
|
||||
content_filter=filter,
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=md_generator,
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
print(result.markdown.fit_markdown) # Filtered markdown content
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **Intelligent Filtering**: Uses LLMs to understand and extract relevant content while maintaining context
|
||||
- **Customizable Instructions**: Tailor the filtering process with specific instructions
|
||||
- **Chunk Processing**: Handles large documents by processing them in chunks (controlled by `chunk_token_threshold`)
|
||||
- **Parallel Processing**: For better performance, use smaller `chunk_token_threshold` (e.g., 2048 or 4096) to enable parallel processing of content chunks
|
||||
|
||||
**Two Common Use Cases:**
|
||||
|
||||
1. **Exact Content Preservation**:
|
||||
```python
|
||||
filter = LLMContentFilter(
|
||||
instruction="""
|
||||
Extract the main educational content while preserving its original wording and substance completely.
|
||||
1. Maintain the exact language and terminology
|
||||
2. Keep all technical explanations and examples intact
|
||||
3. Preserve the original flow and structure
|
||||
4. Remove only clearly irrelevant elements like navigation menus and ads
|
||||
""",
|
||||
chunk_token_threshold=4096
|
||||
)
|
||||
```
|
||||
|
||||
2. **Focused Content Extraction**:
|
||||
```python
|
||||
filter = LLMContentFilter(
|
||||
instruction="""
|
||||
Focus on extracting specific types of content:
|
||||
- Technical documentation
|
||||
- Code examples
|
||||
- API references
|
||||
Reformat the content into clear, well-structured markdown
|
||||
""",
|
||||
chunk_token_threshold=4096
|
||||
)
|
||||
```
|
||||
|
||||
> **Performance Tip**: Set a smaller `chunk_token_threshold` (e.g., 2048 or 4096) to enable parallel processing of content chunks. The default value is infinity, which processes the entire content as a single chunk.
|
||||
|
||||
---
|
||||
|
||||
## 6. Using Fit Markdown
|
||||
|
||||
When a content filter is active, the library produces two forms of markdown inside `result.markdown`:
|
||||
|
||||
1. **`raw_markdown`**: The full unfiltered markdown.
|
||||
2. **`fit_markdown`**: A “fit” version where the filter has removed or trimmed noisy segments.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
from crawl4ai.content_filter_strategy import PruningContentFilter
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter(threshold=0.6),
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://news.example.com/tech", config=config)
|
||||
if result.success:
|
||||
print("Raw markdown:\n", result.markdown)
|
||||
|
||||
# If a filter is used, we also have .fit_markdown:
|
||||
md_object = result.markdown # or your equivalent
|
||||
print("Filtered markdown:\n", md_object.fit_markdown)
|
||||
else:
|
||||
print("Crawl failed:", result.error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. The `MarkdownGenerationResult` Object
|
||||
|
||||
If your library stores detailed markdown output in an object like `MarkdownGenerationResult`, you’ll see fields such as:
|
||||
|
||||
- **`raw_markdown`**: The direct HTML-to-markdown transformation (no filtering).
|
||||
- **`markdown_with_citations`**: A version that moves links to reference-style footnotes.
|
||||
- **`references_markdown`**: A separate string or section containing the gathered references.
|
||||
- **`fit_markdown`**: The filtered markdown if you used a content filter.
|
||||
- **`fit_html`**: The corresponding HTML snippet used to generate `fit_markdown` (helpful for debugging or advanced usage).
|
||||
|
||||
**Example**:
|
||||
|
||||
```python
|
||||
md_obj = result.markdown # your library’s naming may vary
|
||||
print("RAW:\n", md_obj.raw_markdown)
|
||||
print("CITED:\n", md_obj.markdown_with_citations)
|
||||
print("REFERENCES:\n", md_obj.references_markdown)
|
||||
print("FIT:\n", md_obj.fit_markdown)
|
||||
```
|
||||
|
||||
**Why Does This Matter?**
|
||||
- You can supply `raw_markdown` to an LLM if you want the entire text.
|
||||
- Or feed `fit_markdown` into a vector database to reduce token usage.
|
||||
- `references_markdown` can help you keep track of link provenance.
|
||||
|
||||
---
|
||||
|
||||
Below is a **revised section** under “Combining Filters (BM25 + Pruning)” that demonstrates how you can run **two** passes of content filtering without re-crawling, by taking the HTML (or text) from a first pass and feeding it into the second filter. It uses real code patterns from the snippet you provided for **BM25ContentFilter**, which directly accepts **HTML** strings (and can also handle plain text with minimal adaptation).
|
||||
|
||||
---
|
||||
|
||||
## 8. Combining Filters (BM25 + Pruning) in Two Passes
|
||||
|
||||
You might want to **prune out** noisy boilerplate first (with `PruningContentFilter`), and then **rank what’s left** against a user query (with `BM25ContentFilter`). You don’t have to crawl the page twice. Instead:
|
||||
|
||||
1. **First pass**: Apply `PruningContentFilter` directly to the raw HTML from `result.html` (the crawler’s downloaded HTML).
|
||||
2. **Second pass**: Take the pruned HTML (or text) from step 1, and feed it into `BM25ContentFilter`, focusing on a user query.
|
||||
|
||||
### Two-Pass Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
from crawl4ai.content_filter_strategy import PruningContentFilter, BM25ContentFilter
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
async def main():
|
||||
# 1. Crawl with minimal or no markdown generator, just get raw HTML
|
||||
config = CrawlerRunConfig(
|
||||
# If you only want raw HTML, you can skip passing a markdown_generator
|
||||
# or provide one but focus on .html in this example
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com/tech-article", config=config)
|
||||
|
||||
if not result.success or not result.html:
|
||||
print("Crawl failed or no HTML content.")
|
||||
return
|
||||
|
||||
raw_html = result.html
|
||||
|
||||
# 2. First pass: PruningContentFilter on raw HTML
|
||||
pruning_filter = PruningContentFilter(threshold=0.5, min_word_threshold=50)
|
||||
|
||||
# filter_content returns a list of "text chunks" or cleaned HTML sections
|
||||
pruned_chunks = pruning_filter.filter_content(raw_html)
|
||||
# This list is basically pruned content blocks, presumably in HTML or text form
|
||||
|
||||
# For demonstration, let's combine these chunks back into a single HTML-like string
|
||||
# or you could do further processing. It's up to your pipeline design.
|
||||
pruned_html = "\n".join(pruned_chunks)
|
||||
|
||||
# 3. Second pass: BM25ContentFilter with a user query
|
||||
bm25_filter = BM25ContentFilter(
|
||||
user_query="machine learning",
|
||||
bm25_threshold=1.2,
|
||||
language="english"
|
||||
)
|
||||
|
||||
# returns a list of text chunks
|
||||
bm25_chunks = bm25_filter.filter_content(pruned_html)
|
||||
|
||||
if not bm25_chunks:
|
||||
print("Nothing matched the BM25 query after pruning.")
|
||||
return
|
||||
|
||||
# 4. Combine or display final results
|
||||
final_text = "\n---\n".join(bm25_chunks)
|
||||
|
||||
print("==== PRUNED OUTPUT (first pass) ====")
|
||||
print(pruned_html[:500], "... (truncated)") # preview
|
||||
|
||||
print("\n==== BM25 OUTPUT (second pass) ====")
|
||||
print(final_text[:500], "... (truncated)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### What’s Happening?
|
||||
|
||||
1. **Raw HTML**: We crawl once and store the raw HTML in `result.html`.
|
||||
2. **PruningContentFilter**: Takes HTML + optional parameters. It extracts blocks of text or partial HTML, removing headings/sections deemed “noise.” It returns a **list of text chunks**.
|
||||
3. **Combine or Transform**: We join these pruned chunks back into a single HTML-like string. (Alternatively, you could store them in a list for further logic—whatever suits your pipeline.)
|
||||
4. **BM25ContentFilter**: We feed the pruned string into `BM25ContentFilter` with a user query. This second pass further narrows the content to chunks relevant to “machine learning.”
|
||||
|
||||
**No Re-Crawling**: We used `raw_html` from the first pass, so there’s no need to run `arun()` again—**no second network request**.
|
||||
|
||||
### Tips & Variations
|
||||
|
||||
- **Plain Text vs. HTML**: If your pruned output is mostly text, BM25 can still handle it; just keep in mind it expects a valid string input. If you supply partial HTML (like `"<p>some text</p>"`), it will parse it as HTML.
|
||||
- **Chaining in a Single Pipeline**: If your code supports it, you can chain multiple filters automatically. Otherwise, manual two-pass filtering (as shown) is straightforward.
|
||||
- **Adjust Thresholds**: If you see too much or too little text in step one, tweak `threshold=0.5` or `min_word_threshold=50`. Similarly, `bm25_threshold=1.2` can be raised/lowered for more or fewer chunks in step two.
|
||||
|
||||
### One-Pass Combination?
|
||||
|
||||
If your codebase or pipeline design allows applying multiple filters in one pass, you could do so. But often it’s simpler—and more transparent—to run them sequentially, analyzing each step’s result.
|
||||
|
||||
**Bottom Line**: By **manually chaining** your filtering logic in two passes, you get powerful incremental control over the final content. First, remove “global” clutter with Pruning, then refine further with BM25-based query relevance—without incurring a second network crawl.
|
||||
|
||||
---
|
||||
|
||||
## 9. Common Pitfalls & Tips
|
||||
|
||||
1. **No Markdown Output?**
|
||||
- Make sure the crawler actually retrieved HTML. If the site is heavily JS-based, you may need to enable dynamic rendering or wait for elements.
|
||||
- Check if your content filter is too aggressive. Lower thresholds or disable the filter to see if content reappears.
|
||||
|
||||
2. **Performance Considerations**
|
||||
- Very large pages with multiple filters can be slower. Consider `cache_mode` to avoid re-downloading.
|
||||
- If your final use case is LLM ingestion, consider summarizing further or chunking big texts.
|
||||
|
||||
3. **Take Advantage of `fit_markdown`**
|
||||
- Great for RAG pipelines, semantic search, or any scenario where extraneous boilerplate is unwanted.
|
||||
- Still verify the textual quality—some sites have crucial data in footers or sidebars.
|
||||
|
||||
4. **Adjusting `html2text` Options**
|
||||
- If you see lots of raw HTML slipping into the text, turn on `escape_html`.
|
||||
- If code blocks look messy, experiment with `mark_code` or `handle_code_in_pre`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary & Next Steps
|
||||
|
||||
In this **Markdown Generation Basics** tutorial, you learned to:
|
||||
|
||||
- Configure the **DefaultMarkdownGenerator** with HTML-to-text options.
|
||||
- Select different HTML sources using the `content_source` parameter.
|
||||
- Use **BM25ContentFilter** for query-specific extraction or **PruningContentFilter** for general noise removal.
|
||||
- Distinguish between raw and filtered markdown (`fit_markdown`).
|
||||
- Leverage the `MarkdownGenerationResult` object to handle different forms of output (citations, references, etc.).
|
||||
|
||||
Now you can produce high-quality Markdown from any website, focusing on exactly the content you need—an essential step for powering AI models, summarization pipelines, or knowledge-base queries.
|
||||
|
||||
**Last Updated**: 2025-01-01
|
||||
@@ -0,0 +1,432 @@
|
||||
# Page Interaction
|
||||
|
||||
Crawl4AI provides powerful features for interacting with **dynamic** webpages, handling JavaScript execution, waiting for conditions, and managing multi-step flows. By combining **js_code**, **wait_for**, and certain **CrawlerRunConfig** parameters, you can:
|
||||
|
||||
1. Click “Load More” buttons
|
||||
2. Fill forms and submit them
|
||||
3. Wait for elements or data to appear
|
||||
4. Reuse sessions across multiple steps
|
||||
|
||||
Below is a quick overview of how to do it.
|
||||
|
||||
---
|
||||
|
||||
## 1. JavaScript Execution
|
||||
|
||||
### Basic Execution
|
||||
|
||||
**`js_code`** in **`CrawlerRunConfig`** accepts either a single JS string or a list of JS snippets. It runs **after** `wait_for` and `delay_before_return_html` — so the page is fully loaded when your code executes.
|
||||
|
||||
**Example**: We'll scroll to the bottom of the page, then optionally click a "Load More" button.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
# Single JS command
|
||||
config = CrawlerRunConfig(
|
||||
js_code="window.scrollTo(0, document.body.scrollHeight);"
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com", # Example site
|
||||
config=config
|
||||
)
|
||||
print("Crawled length:", len(result.cleaned_html))
|
||||
|
||||
# Multiple commands
|
||||
js_commands = [
|
||||
"window.scrollTo(0, document.body.scrollHeight);",
|
||||
# 'More' link on Hacker News
|
||||
"document.querySelector('a.morelink')?.click();",
|
||||
]
|
||||
config = CrawlerRunConfig(js_code=js_commands)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com", # Another pass
|
||||
config=config
|
||||
)
|
||||
print("After scroll+click, length:", len(result.cleaned_html))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Relevant `CrawlerRunConfig` params**:
|
||||
- **`js_code`**: JavaScript to run **after** `wait_for` and `delay_before_return_html` complete. Runs on the fully-loaded page.
|
||||
- **`js_code_before_wait`**: JavaScript to run **before** `wait_for`. Use when you need to trigger loading that `wait_for` then checks.
|
||||
- **`js_only`**: If set to `True` on subsequent calls, indicates we're continuing an existing session without a new full navigation.
|
||||
- **`session_id`**: If you want to keep the same page across multiple calls, specify an ID.
|
||||
|
||||
### Execution Order
|
||||
|
||||
Understanding when your JavaScript runs relative to other pipeline steps:
|
||||
|
||||
```
|
||||
1. Page navigation (page.goto)
|
||||
2. js_code_before_wait ← triggers loading / clicks tabs
|
||||
3. wait_for ← waits for content to appear
|
||||
4. delay_before_return_html ← extra safety margin
|
||||
5. js_code ← runs on the fully-loaded page
|
||||
6. flatten_shadow_dom ← if enabled
|
||||
7. page.content() ← HTML capture
|
||||
```
|
||||
|
||||
If you need JS to trigger something and then wait for the result, use `js_code_before_wait` + `wait_for`:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
# Click a tab first
|
||||
js_code_before_wait="document.querySelector('#specs-tab')?.click();",
|
||||
# Then wait for the tab content to appear
|
||||
wait_for="css:#specs-panel .content",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Wait Conditions
|
||||
|
||||
### 2.1 CSS-Based Waiting
|
||||
|
||||
Sometimes, you just want to wait for a specific element to appear. For example:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
config = CrawlerRunConfig(
|
||||
# Wait for at least 30 items on Hacker News
|
||||
wait_for="css:.athing:nth-child(30)"
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com",
|
||||
config=config
|
||||
)
|
||||
print("We have at least 30 items loaded!")
|
||||
# Rough check
|
||||
print("Total items in HTML:", result.cleaned_html.count("athing"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Key param**:
|
||||
- **`wait_for="css:..."`**: Tells the crawler to wait until that CSS selector is present.
|
||||
|
||||
### 2.2 JavaScript-Based Waiting
|
||||
|
||||
For more complex conditions (e.g., waiting for content length to exceed a threshold), prefix `js:`:
|
||||
|
||||
```python
|
||||
wait_condition = """() => {
|
||||
const items = document.querySelectorAll('.athing');
|
||||
return items.length > 50; // Wait for at least 51 items
|
||||
}"""
|
||||
|
||||
config = CrawlerRunConfig(wait_for=f"js:{wait_condition}")
|
||||
```
|
||||
|
||||
**Behind the Scenes**: Crawl4AI keeps polling the JS function until it returns `true` or a timeout occurs.
|
||||
|
||||
---
|
||||
|
||||
## 3. Handling Dynamic Content
|
||||
|
||||
Many modern sites require **multiple steps**: scrolling, clicking “Load More,” or updating via JavaScript. Below are typical patterns.
|
||||
|
||||
### 3.1 Load More Example (Hacker News “More” Link)
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
# Step 1: Load initial Hacker News page
|
||||
config = CrawlerRunConfig(
|
||||
wait_for="css:.athing:nth-child(30)" # Wait for 30 items
|
||||
)
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://news.ycombinator.com",
|
||||
config=config
|
||||
)
|
||||
print("Initial items loaded.")
|
||||
|
||||
# Step 2: Let's scroll and click the "More" link
|
||||
load_more_js = [
|
||||
"window.scrollTo(0, document.body.scrollHeight);",
|
||||
# The "More" link at page bottom
|
||||
"document.querySelector('a.morelink')?.click();"
|
||||
]
|
||||
|
||||
next_page_conf = CrawlerRunConfig(
|
||||
js_code=load_more_js,
|
||||
wait_for="""js:() => {
|
||||
return document.querySelectorAll('.athing').length > 30;
|
||||
}""",
|
||||
# Mark that we do not re-navigate, but run JS in the same session:
|
||||
js_only=True,
|
||||
session_id="hn_session"
|
||||
)
|
||||
|
||||
# Re-use the same crawler session
|
||||
result2 = await crawler.arun(
|
||||
url="https://news.ycombinator.com", # same URL but continuing session
|
||||
config=next_page_conf
|
||||
)
|
||||
total_items = result2.cleaned_html.count("athing")
|
||||
print("Items after load-more:", total_items)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Key params**:
|
||||
- **`session_id="hn_session"`**: Keep the same page across multiple calls to `arun()`.
|
||||
- **`js_only=True`**: We’re not performing a full reload, just applying JS in the existing page.
|
||||
- **`wait_for`** with `js:`: Wait for item count to grow beyond 30.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Form Interaction
|
||||
|
||||
If the site has a search or login form, you can fill fields and submit them with **`js_code`**. For instance, if GitHub had a local search form:
|
||||
|
||||
```python
|
||||
js_form_interaction = """
|
||||
document.querySelector('#your-search').value = 'TypeScript commits';
|
||||
document.querySelector('form').submit();
|
||||
"""
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
js_code=js_form_interaction,
|
||||
wait_for="css:.commit"
|
||||
)
|
||||
result = await crawler.arun(url="https://github.com/search", config=config)
|
||||
```
|
||||
|
||||
**In reality**: Replace IDs or classes with the real site’s form selectors.
|
||||
|
||||
---
|
||||
|
||||
## 4. Timing Control
|
||||
|
||||
1. **`page_timeout`** (ms): Overall page load or script execution time limit.
|
||||
2. **`delay_before_return_html`** (seconds): Wait an extra moment before capturing the final HTML.
|
||||
3. **`mean_delay`** & **`max_range`**: If you call `arun_many()` with multiple URLs, these add a random pause between each request.
|
||||
|
||||
**Example**:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
page_timeout=60000, # 60s limit
|
||||
delay_before_return_html=2.5
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Multi-Step Interaction Example
|
||||
|
||||
Below is a simplified script that does multiple “Load More” clicks on GitHub’s TypeScript commits page. It **re-uses** the same session to accumulate new commits each time. The code includes the relevant **`CrawlerRunConfig`** parameters you’d rely on.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def multi_page_commits():
|
||||
browser_cfg = BrowserConfig(
|
||||
headless=False, # Visible for demonstration
|
||||
verbose=True
|
||||
)
|
||||
session_id = "github_ts_commits"
|
||||
|
||||
base_wait = """js:() => {
|
||||
const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4');
|
||||
return commits.length > 0;
|
||||
}"""
|
||||
|
||||
# Step 1: Load initial commits
|
||||
config1 = CrawlerRunConfig(
|
||||
wait_for=base_wait,
|
||||
session_id=session_id,
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
# Not using js_only yet since it's our first load
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_cfg) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://github.com/microsoft/TypeScript/commits/main",
|
||||
config=config1
|
||||
)
|
||||
print("Initial commits loaded. Count:", result.cleaned_html.count("commit"))
|
||||
|
||||
# Step 2: For subsequent pages, we run JS to click 'Next Page' if it exists
|
||||
js_next_page = """
|
||||
const selector = 'a[data-testid="pagination-next-button"]';
|
||||
const button = document.querySelector(selector);
|
||||
if (button) button.click();
|
||||
"""
|
||||
|
||||
# Wait until new commits appear
|
||||
wait_for_more = """js:() => {
|
||||
const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4');
|
||||
if (!window.firstCommit && commits.length>0) {
|
||||
window.firstCommit = commits[0].textContent;
|
||||
return false;
|
||||
}
|
||||
// If top commit changes, we have new commits
|
||||
const topNow = commits[0]?.textContent.trim();
|
||||
return topNow && topNow !== window.firstCommit;
|
||||
}"""
|
||||
|
||||
for page in range(2): # let's do 2 more "Next" pages
|
||||
config_next = CrawlerRunConfig(
|
||||
session_id=session_id,
|
||||
js_code=js_next_page,
|
||||
wait_for=wait_for_more,
|
||||
js_only=True, # We're continuing from the open tab
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
result2 = await crawler.arun(
|
||||
url="https://github.com/microsoft/TypeScript/commits/main",
|
||||
config=config_next
|
||||
)
|
||||
print(f"Page {page+2} commits count:", result2.cleaned_html.count("commit"))
|
||||
|
||||
# Optionally kill session
|
||||
await crawler.crawler_strategy.kill_session(session_id)
|
||||
|
||||
async def main():
|
||||
await multi_page_commits()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **`session_id`**: Keep the same page open.
|
||||
- **`js_code`** + **`wait_for`** + **`js_only=True`**: We do partial refreshes, waiting for new commits to appear.
|
||||
- **`cache_mode=CacheMode.BYPASS`** ensures we always see fresh data each step.
|
||||
|
||||
---
|
||||
|
||||
## 6. Combine Interaction with Extraction
|
||||
|
||||
Once dynamic content is loaded, you can attach an **`extraction_strategy`** (like `JsonCssExtractionStrategy` or `LLMExtractionStrategy`). For example:
|
||||
|
||||
```python
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
schema = {
|
||||
"name": "Commits",
|
||||
"baseSelector": "li.Box-sc-g0xbh4-0",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h4.markdown-title", "type": "text"}
|
||||
]
|
||||
}
|
||||
config = CrawlerRunConfig(
|
||||
session_id="ts_commits_session",
|
||||
js_code=js_next_page,
|
||||
wait_for=wait_for_more,
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema)
|
||||
)
|
||||
```
|
||||
|
||||
When done, check `result.extracted_content` for the JSON.
|
||||
|
||||
---
|
||||
|
||||
## 7. Shadow DOM Flattening
|
||||
|
||||
Sites built with **Web Components** (Stencil, Lit, Shoelace, etc.) render content inside Shadow DOM — an encapsulated sub-tree that is invisible to normal page serialization. Set `flatten_shadow_dom=True` to extract it:
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
flatten_shadow_dom=True,
|
||||
wait_until="load",
|
||||
delay_before_return_html=3.0, # give components time to hydrate
|
||||
)
|
||||
```
|
||||
|
||||
This walks all shadow trees, resolves `<slot>` projections, and produces flat HTML. It also force-opens closed shadow roots via an init script. For details and a full example, see [Flattening Shadow DOM](content-selection.md#31-flattening-shadow-dom) and [`shadow_dom_crawling.py`](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/shadow_dom_crawling.py).
|
||||
|
||||
---
|
||||
|
||||
## 8. Relevant `CrawlerRunConfig` Parameters
|
||||
|
||||
Below are the key interaction-related parameters in `CrawlerRunConfig`. For a full list, see [Configuration Parameters](../api/parameters.md).
|
||||
|
||||
- **`js_code`**: JavaScript to run after `wait_for` + `delay_before_return_html`, on the fully-loaded page.
|
||||
- **`js_code_before_wait`**: JavaScript to run before `wait_for`. For triggering loading that `wait_for` then checks.
|
||||
- **`js_only`**: If `True`, no new page navigation—only JS in the existing session.
|
||||
- **`wait_for`**: CSS (`"css:..."`) or JS (`"js:..."`) expression to wait for.
|
||||
- **`session_id`**: Reuse the same page across calls.
|
||||
- **`cache_mode`**: Whether to read/write from the cache or bypass.
|
||||
- **`flatten_shadow_dom`**: Flatten Shadow DOM content into the light DOM before capture.
|
||||
- **`process_iframes`**: Inline iframe content into the main document.
|
||||
- **`remove_overlay_elements`**: Remove certain popups automatically.
|
||||
- **`remove_consent_popups`**: Remove GDPR/cookie consent popups from known CMP providers (OneTrust, Cookiebot, Didomi, etc.).
|
||||
- **`simulate_user`, `override_navigator`, `magic`**: Anti-bot or "human-like" interactions.
|
||||
|
||||
---
|
||||
|
||||
## 9. Conclusion
|
||||
|
||||
Crawl4AI's **page interaction** features let you:
|
||||
|
||||
1. **Execute JavaScript** for scrolling, clicks, or form filling.
|
||||
2. **Wait** for CSS or custom JS conditions before capturing data.
|
||||
3. **Handle** multi-step flows (like “Load More”) with partial reloads or persistent sessions.
|
||||
4. **Flatten Shadow DOM** on Web Component sites to extract hidden content.
|
||||
5. Combine with **structured extraction** for dynamic sites.
|
||||
|
||||
With these tools, you can scrape modern, interactive webpages confidently. For advanced hooking, user simulation, or in-depth config, check the [API reference](../api/parameters.md) or related advanced docs. Happy scripting!
|
||||
|
||||
---
|
||||
|
||||
## 10. Virtual Scrolling
|
||||
|
||||
For sites that use **virtual scrolling** (where content is replaced rather than appended as you scroll, like Twitter or Instagram), Crawl4AI provides a dedicated `VirtualScrollConfig`:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, VirtualScrollConfig
|
||||
|
||||
async def crawl_twitter_timeline():
|
||||
# Configure virtual scroll for Twitter-like feeds
|
||||
virtual_config = VirtualScrollConfig(
|
||||
container_selector="[data-testid='primaryColumn']", # Twitter's main column
|
||||
scroll_count=30, # Scroll 30 times
|
||||
scroll_by="container_height", # Scroll by container height each time
|
||||
wait_after_scroll=1.0 # Wait 1 second after each scroll
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
virtual_scroll_config=virtual_config
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://twitter.com/search?q=AI",
|
||||
config=config
|
||||
)
|
||||
# result.html now contains ALL tweets from the virtual scroll
|
||||
```
|
||||
|
||||
### Virtual Scroll vs JavaScript Scrolling
|
||||
|
||||
| Feature | Virtual Scroll | JS Code Scrolling |
|
||||
|---------|---------------|-------------------|
|
||||
| **Use Case** | Content replaced during scroll | Content appended or simple scroll |
|
||||
| **Configuration** | `VirtualScrollConfig` object | `js_code` with scroll commands |
|
||||
| **Automatic Merging** | Yes - merges all unique content | No - captures final state only |
|
||||
| **Best For** | Twitter, Instagram, virtual tables | Traditional pages, load more buttons |
|
||||
|
||||
For detailed examples and configuration options, see the [Virtual Scroll documentation](../advanced/virtual-scroll.md).
|
||||
@@ -0,0 +1,465 @@
|
||||
# Getting Started with Crawl4AI
|
||||
|
||||
Welcome to **Crawl4AI**, an open-source LLM-friendly Web Crawler & Scraper. In this tutorial, you’ll:
|
||||
|
||||
1. Run your **first crawl** using minimal configuration.
|
||||
2. Generate **Markdown** output (and learn how it’s influenced by content filters).
|
||||
3. Experiment with a simple **CSS-based extraction** strategy.
|
||||
4. See a glimpse of **LLM-based extraction** (including open-source and closed-source model options).
|
||||
5. Crawl a **dynamic** page that loads content via JavaScript.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Crawl4AI provides:
|
||||
|
||||
- An asynchronous crawler, **`AsyncWebCrawler`**.
|
||||
- Configurable browser and run settings via **`BrowserConfig`** and **`CrawlerRunConfig`**.
|
||||
- Automatic HTML-to-Markdown conversion via **`DefaultMarkdownGenerator`** (supports optional filters).
|
||||
- Multiple extraction strategies (LLM-based or “traditional” CSS/XPath-based).
|
||||
|
||||
By the end of this guide, you’ll have performed a basic crawl, generated Markdown, tried out two extraction strategies, and crawled a dynamic page that uses “Load More” buttons or JavaScript updates.
|
||||
|
||||
---
|
||||
|
||||
## 2. Your First Crawl
|
||||
|
||||
Here’s a minimal Python script that creates an **`AsyncWebCrawler`**, fetches a webpage, and prints the first 300 characters of its Markdown output:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
|
||||
async def main():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com")
|
||||
print(result.markdown[:300]) # Print first 300 chars
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**What’s happening?**
|
||||
- **`AsyncWebCrawler`** launches a headless browser (Chromium by default).
|
||||
- It fetches `https://example.com`.
|
||||
- Crawl4AI automatically converts the HTML into Markdown.
|
||||
|
||||
You now have a simple, working crawl!
|
||||
|
||||
---
|
||||
|
||||
## 3. Basic Configuration (Light Introduction)
|
||||
|
||||
Crawl4AI’s crawler can be heavily customized using two main classes:
|
||||
|
||||
1. **`BrowserConfig`**: Controls browser behavior (headless or full UI, user agent, JavaScript toggles, etc.).
|
||||
2. **`CrawlerRunConfig`**: Controls how each crawl runs (caching, extraction, timeouts, hooking, etc.).
|
||||
|
||||
Below is an example with minimal usage:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def main():
|
||||
browser_conf = BrowserConfig(headless=True) # or False to see the browser
|
||||
run_conf = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_conf) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=run_conf
|
||||
)
|
||||
print(result.markdown)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
> IMPORTANT: By default cache mode is set to `CacheMode.BYPASS` to have fresh content. Set `CacheMode.ENABLED` to enable caching.
|
||||
|
||||
We’ll explore more advanced config in later tutorials (like enabling proxies, PDF output, multi-tab sessions, etc.). For now, just note how you pass these objects to manage crawling.
|
||||
|
||||
---
|
||||
|
||||
## 4. Generating Markdown Output
|
||||
|
||||
By default, Crawl4AI automatically generates Markdown from each crawled page. However, the exact output depends on whether you specify a **markdown generator** or **content filter**.
|
||||
|
||||
- **`result.markdown`**:
|
||||
The direct HTML-to-Markdown conversion.
|
||||
- **`result.markdown.fit_markdown`**:
|
||||
The same content after applying any configured **content filter** (e.g., `PruningContentFilter`).
|
||||
|
||||
### Example: Using a Filter with `DefaultMarkdownGenerator`
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai.content_filter_strategy import PruningContentFilter
|
||||
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
||||
|
||||
md_generator = DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter(threshold=0.4, threshold_type="fixed")
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
markdown_generator=md_generator
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://news.ycombinator.com", config=config)
|
||||
print("Raw Markdown length:", len(result.markdown.raw_markdown))
|
||||
print("Fit Markdown length:", len(result.markdown.fit_markdown))
|
||||
```
|
||||
|
||||
**Note**: If you do **not** specify a content filter or markdown generator, you’ll typically see only the raw Markdown. `PruningContentFilter` may adds around `50ms` in processing time. We’ll dive deeper into these strategies in a dedicated **Markdown Generation** tutorial.
|
||||
|
||||
---
|
||||
|
||||
## 5. Simple Data Extraction (CSS-based)
|
||||
|
||||
Crawl4AI can also extract structured data (JSON) using CSS or XPath selectors. Below is a minimal CSS-based example:
|
||||
|
||||
> **New!** Crawl4AI now provides a powerful utility to automatically generate extraction schemas using LLM. This is a one-time cost that gives you a reusable schema for fast, LLM-free extractions:
|
||||
|
||||
```python
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
from crawl4ai import LLMConfig
|
||||
|
||||
# Generate a schema (one-time cost)
|
||||
html = "<div class='product'><h2>Gaming Laptop</h2><span class='price'>$999.99</span></div>"
|
||||
|
||||
# Using OpenAI (requires API token)
|
||||
schema = JsonCssExtractionStrategy.generate_schema(
|
||||
html,
|
||||
llm_config = LLMConfig(provider="openai/gpt-4o",api_token="your-openai-token") # Required for OpenAI
|
||||
)
|
||||
|
||||
# Or using Ollama (open source, no token needed)
|
||||
schema = JsonCssExtractionStrategy.generate_schema(
|
||||
html,
|
||||
llm_config = LLMConfig(provider="ollama/llama3.3", api_token=None) # Not needed for Ollama
|
||||
)
|
||||
|
||||
# Use the schema for fast, repeated extractions
|
||||
strategy = JsonCssExtractionStrategy(schema)
|
||||
```
|
||||
|
||||
For a complete guide on schema generation and advanced usage, see [No-LLM Extraction Strategies](../extraction/no-llm-strategies.md).
|
||||
|
||||
Here's a basic extraction example:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def main():
|
||||
schema = {
|
||||
"name": "Example Items",
|
||||
"baseSelector": "div.item",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2", "type": "text"},
|
||||
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
|
||||
]
|
||||
}
|
||||
|
||||
raw_html = "<div class='item'><h2>Item 1</h2><a href='https://example.com/item1'>Link 1</a></div>"
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(
|
||||
url="raw://" + raw_html,
|
||||
config=CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema)
|
||||
)
|
||||
)
|
||||
# The JSON output is stored in 'extracted_content'
|
||||
data = json.loads(result.extracted_content)
|
||||
print(data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Why is this helpful?**
|
||||
- Great for repetitive page structures (e.g., item listings, articles).
|
||||
- No AI usage or costs.
|
||||
- The crawler returns a JSON string you can parse or store.
|
||||
|
||||
> Tips: You can pass raw HTML to the crawler instead of a URL. To do so, prefix the HTML with `raw://`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Simple Data Extraction (LLM-based)
|
||||
|
||||
For more complex or irregular pages, a language model can parse text intelligently into a structure you define. Crawl4AI supports **open-source** or **closed-source** providers:
|
||||
|
||||
- **Open-Source Models** (e.g., `ollama/llama3.3`, `no_token`)
|
||||
- **OpenAI Models** (e.g., `openai/gpt-4`, requires `api_token`)
|
||||
- Or any provider supported by the underlying library
|
||||
|
||||
Below is an example using **open-source** style (no token) and closed-source:
|
||||
|
||||
```python
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from pydantic import BaseModel, Field
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, LLMConfig
|
||||
from crawl4ai import LLMExtractionStrategy
|
||||
|
||||
class OpenAIModelFee(BaseModel):
|
||||
model_name: str = Field(..., description="Name of the OpenAI model.")
|
||||
input_fee: str = Field(..., description="Fee for input token for the OpenAI model.")
|
||||
output_fee: str = Field(
|
||||
..., description="Fee for output token for the OpenAI model."
|
||||
)
|
||||
|
||||
async def extract_structured_data_using_llm(
|
||||
provider: str, api_token: str = None, extra_headers: Dict[str, str] = None
|
||||
):
|
||||
print(f"\n--- Extracting Structured Data with {provider} ---")
|
||||
|
||||
if api_token is None and provider != "ollama":
|
||||
print(f"API token is required for {provider}. Skipping this example.")
|
||||
return
|
||||
|
||||
browser_config = BrowserConfig(headless=True)
|
||||
|
||||
extra_args = {"temperature": 0, "top_p": 0.9, "max_tokens": 2000}
|
||||
if extra_headers:
|
||||
extra_args["extra_headers"] = extra_headers
|
||||
|
||||
crawler_config = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
word_count_threshold=1,
|
||||
page_timeout=80000,
|
||||
extraction_strategy=LLMExtractionStrategy(
|
||||
llm_config = LLMConfig(provider=provider,api_token=api_token),
|
||||
schema=OpenAIModelFee.model_json_schema(),
|
||||
extraction_type="schema",
|
||||
instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens.
|
||||
Do not miss any models in the entire content.""",
|
||||
extra_args=extra_args,
|
||||
),
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://openai.com/api/pricing/", config=crawler_config
|
||||
)
|
||||
print(result.extracted_content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
asyncio.run(
|
||||
extract_structured_data_using_llm(
|
||||
provider="openai/gpt-4o", api_token=os.getenv("OPENAI_API_KEY")
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**What’s happening?**
|
||||
- We define a Pydantic schema (`PricingInfo`) describing the fields we want.
|
||||
- The LLM extraction strategy uses that schema and your instructions to transform raw text into structured JSON.
|
||||
- Depending on the **provider** and **api_token**, you can use local models or a remote API.
|
||||
|
||||
---
|
||||
|
||||
## 7. Adaptive Crawling (New!)
|
||||
|
||||
Crawl4AI now includes intelligent adaptive crawling that automatically determines when sufficient information has been gathered. Here's a quick example:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, AdaptiveCrawler
|
||||
|
||||
async def adaptive_example():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
adaptive = AdaptiveCrawler(crawler)
|
||||
|
||||
# Start adaptive crawling
|
||||
result = await adaptive.digest(
|
||||
start_url="https://docs.python.org/3/",
|
||||
query="async context managers"
|
||||
)
|
||||
|
||||
# View results
|
||||
adaptive.print_stats()
|
||||
print(f"Crawled {len(result.crawled_urls)} pages")
|
||||
print(f"Achieved {adaptive.confidence:.0%} confidence")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(adaptive_example())
|
||||
```
|
||||
|
||||
**What's special about adaptive crawling?**
|
||||
- **Automatic stopping**: Stops when sufficient information is gathered
|
||||
- **Intelligent link selection**: Follows only relevant links
|
||||
- **Confidence scoring**: Know how complete your information is
|
||||
|
||||
[Learn more about Adaptive Crawling →](adaptive-crawling.md)
|
||||
|
||||
---
|
||||
|
||||
## 8. Multi-URL Concurrency (Preview)
|
||||
|
||||
If you need to crawl multiple URLs in **parallel**, you can use `arun_many()`. By default, Crawl4AI employs a **MemoryAdaptiveDispatcher**, automatically adjusting concurrency based on system resources. Here’s a quick glimpse:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def quick_parallel_example():
|
||||
urls = [
|
||||
"https://example.com/page1",
|
||||
"https://example.com/page2",
|
||||
"https://example.com/page3"
|
||||
]
|
||||
|
||||
run_conf = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
stream=True # Enable streaming mode
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Stream results as they complete
|
||||
async for result in await crawler.arun_many(urls, config=run_conf):
|
||||
if result.success:
|
||||
print(f"[OK] {result.url}, length: {len(result.markdown.raw_markdown)}")
|
||||
else:
|
||||
print(f"[ERROR] {result.url} => {result.error_message}")
|
||||
|
||||
# Or get all results at once (default behavior)
|
||||
run_conf = run_conf.clone(stream=False)
|
||||
results = await crawler.arun_many(urls, config=run_conf)
|
||||
for res in results:
|
||||
if res.success:
|
||||
print(f"[OK] {res.url}, length: {len(res.markdown.raw_markdown)}")
|
||||
else:
|
||||
print(f"[ERROR] {res.url} => {res.error_message}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(quick_parallel_example())
|
||||
```
|
||||
|
||||
The example above shows two ways to handle multiple URLs:
|
||||
1. **Streaming mode** (`stream=True`): Process results as they become available using `async for`
|
||||
2. **Batch mode** (`stream=False`): Wait for all results to complete
|
||||
|
||||
For more advanced concurrency (e.g., a **semaphore-based** approach, **adaptive memory usage throttling**, or customized rate limiting), see [Advanced Multi-URL Crawling](../advanced/multi-url-crawling.md).
|
||||
|
||||
---
|
||||
|
||||
## 8. Dynamic Content Example
|
||||
|
||||
Some sites require multiple “page clicks” or dynamic JavaScript updates. Below is an example showing how to **click** a “Next Page” button and wait for new commits to load on GitHub, using **`BrowserConfig`** and **`CrawlerRunConfig`**:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai import JsonCssExtractionStrategy
|
||||
|
||||
async def extract_structured_data_using_css_extractor():
|
||||
print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---")
|
||||
schema = {
|
||||
"name": "KidoCode Courses",
|
||||
"baseSelector": "section.charge-methodology .w-tab-content > div",
|
||||
"fields": [
|
||||
{
|
||||
"name": "section_title",
|
||||
"selector": "h3.heading-50",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"name": "section_description",
|
||||
"selector": ".charge-content",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"name": "course_name",
|
||||
"selector": ".text-block-93",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"name": "course_description",
|
||||
"selector": ".course-content-text",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"name": "course_icon",
|
||||
"selector": ".image-92",
|
||||
"type": "attribute",
|
||||
"attribute": "src",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
browser_config = BrowserConfig(headless=True, java_script_enabled=True)
|
||||
|
||||
js_click_tabs = """
|
||||
(async () => {
|
||||
const tabs = document.querySelectorAll("section.charge-methodology .tabs-menu-3 > div");
|
||||
for(let tab of tabs) {
|
||||
tab.scrollIntoView();
|
||||
tab.click();
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
})();
|
||||
"""
|
||||
|
||||
crawler_config = CrawlerRunConfig(
|
||||
cache_mode=CacheMode.BYPASS,
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema),
|
||||
js_code=[js_click_tabs],
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://www.kidocode.com/degrees/technology", config=crawler_config
|
||||
)
|
||||
|
||||
companies = json.loads(result.extracted_content)
|
||||
print(f"Successfully extracted {len(companies)} companies")
|
||||
print(json.dumps(companies[0], indent=2))
|
||||
|
||||
async def main():
|
||||
await extract_structured_data_using_css_extractor()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **`BrowserConfig(headless=False)`**: We want to watch it click “Next Page.”
|
||||
- **`CrawlerRunConfig(...)`**: We specify the extraction strategy, pass `session_id` to reuse the same page.
|
||||
- **`js_code`** and **`wait_for`** are used for subsequent pages (`page > 0`) to click the “Next” button and wait for new commits to load.
|
||||
- **`js_only=True`** indicates we’re not re-navigating but continuing the existing session.
|
||||
- Finally, we call `kill_session()` to clean up the page and browser session.
|
||||
|
||||
---
|
||||
|
||||
## 9. Next Steps
|
||||
|
||||
Congratulations! You have:
|
||||
|
||||
1. Performed a basic crawl and printed Markdown.
|
||||
2. Used **content filters** with a markdown generator.
|
||||
3. Extracted JSON via **CSS** or **LLM** strategies.
|
||||
4. Handled **dynamic** pages with JavaScript triggers.
|
||||
|
||||
If you’re ready for more, check out:
|
||||
|
||||
- **Installation**: A deeper dive into advanced installs, Docker usage (experimental), or optional dependencies.
|
||||
- **Hooks & Auth**: Learn how to run custom JavaScript or handle logins with cookies, local storage, etc.
|
||||
- **Deployment**: Explore ephemeral testing in Docker or plan for the upcoming stable Docker release.
|
||||
- **Browser Management**: Delve into user simulation, stealth modes, and concurrency best practices.
|
||||
|
||||
Crawl4AI is a powerful, flexible tool. Enjoy building out your scrapers, data pipelines, or AI-driven extraction flows. Happy crawling!
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
# Simple Crawling
|
||||
|
||||
This guide covers the basics of web crawling with Crawl4AI. You'll learn how to set up a crawler, make your first request, and understand the response.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Set up a simple crawl using `BrowserConfig` and `CrawlerRunConfig`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
|
||||
|
||||
async def main():
|
||||
browser_config = BrowserConfig() # Default browser configuration
|
||||
run_config = CrawlerRunConfig() # Default crawl run configuration
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=run_config
|
||||
)
|
||||
print(result.markdown) # Print clean markdown content
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Understanding the Response
|
||||
|
||||
The `arun()` method returns a `CrawlResult` object with several useful properties. Here's a quick overview (see [CrawlResult](../api/crawl-result.md) for complete details):
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter(threshold=0.6),
|
||||
options={"ignore_links": True}
|
||||
)
|
||||
)
|
||||
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=config
|
||||
)
|
||||
|
||||
# Different content formats
|
||||
print(result.html) # Raw HTML
|
||||
print(result.cleaned_html) # Cleaned HTML
|
||||
print(result.markdown.raw_markdown) # Raw markdown from cleaned html
|
||||
print(result.markdown.fit_markdown) # Most relevant content in markdown
|
||||
|
||||
# Check success status
|
||||
print(result.success) # True if crawl succeeded
|
||||
print(result.status_code) # HTTP status code (e.g., 200, 404)
|
||||
|
||||
# Access extracted media and links
|
||||
print(result.media) # Dictionary of found media (images, videos, audio)
|
||||
print(result.links) # Dictionary of internal and external links
|
||||
```
|
||||
|
||||
## Adding Basic Options
|
||||
|
||||
Customize your crawl using `CrawlerRunConfig`:
|
||||
|
||||
```python
|
||||
run_config = CrawlerRunConfig(
|
||||
word_count_threshold=10, # Minimum words per content block
|
||||
exclude_external_links=True, # Remove external links
|
||||
remove_overlay_elements=True, # Remove popups/modals
|
||||
process_iframes=True # Process iframe content
|
||||
)
|
||||
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=run_config
|
||||
)
|
||||
```
|
||||
|
||||
## Handling Errors
|
||||
|
||||
Always check if the crawl was successful:
|
||||
|
||||
```python
|
||||
run_config = CrawlerRunConfig()
|
||||
result = await crawler.arun(url="https://example.com", config=run_config)
|
||||
|
||||
if not result.success:
|
||||
print(f"Crawl failed: {result.error_message}")
|
||||
print(f"Status code: {result.status_code}")
|
||||
```
|
||||
|
||||
## Logging and Debugging
|
||||
|
||||
Enable verbose logging in `BrowserConfig`:
|
||||
|
||||
```python
|
||||
browser_config = BrowserConfig(verbose=True)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
run_config = CrawlerRunConfig()
|
||||
result = await crawler.arun(url="https://example.com", config=run_config)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a more comprehensive example demonstrating common usage patterns:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
|
||||
async def main():
|
||||
browser_config = BrowserConfig(verbose=True)
|
||||
run_config = CrawlerRunConfig(
|
||||
# Content filtering
|
||||
word_count_threshold=10,
|
||||
excluded_tags=['form', 'header'],
|
||||
exclude_external_links=True,
|
||||
|
||||
# Content processing
|
||||
process_iframes=True,
|
||||
remove_overlay_elements=True,
|
||||
|
||||
# Cache control
|
||||
cache_mode=CacheMode.ENABLED # Use cache if available
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
result = await crawler.arun(
|
||||
url="https://example.com",
|
||||
config=run_config
|
||||
)
|
||||
|
||||
if result.success:
|
||||
# Print clean content
|
||||
print("Content:", result.markdown[:500]) # First 500 chars
|
||||
|
||||
# Process images
|
||||
for image in result.media["images"]:
|
||||
print(f"Found image: {image['src']}")
|
||||
|
||||
# Process links
|
||||
for link in result.links["internal"]:
|
||||
print(f"Internal link: {link['href']}")
|
||||
|
||||
else:
|
||||
print(f"Crawl failed: {result.error_message}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
@@ -0,0 +1,807 @@
|
||||
# Table Extraction Strategies
|
||||
|
||||
## Overview
|
||||
|
||||
**New in v0.7.3+**: Table extraction now follows the **Strategy Design Pattern**, providing unprecedented flexibility and power for handling different table structures. Don't worry - **your existing code still works!** We maintain full backward compatibility while offering new capabilities.
|
||||
|
||||
### What's Changed?
|
||||
- **Architecture**: Table extraction now uses pluggable strategies
|
||||
- **Backward Compatible**: Your existing code with `table_score_threshold` continues to work
|
||||
- **More Power**: Choose from multiple strategies or create your own
|
||||
- **Same Default Behavior**: By default, uses `DefaultTableExtraction` (same as before)
|
||||
|
||||
### Key Points
|
||||
✅ **Old code still works** - No breaking changes
|
||||
✅ **Same default behavior** - Uses the proven extraction algorithm
|
||||
✅ **New capabilities** - Add LLM extraction or custom strategies when needed
|
||||
✅ **Strategy pattern** - Clean, extensible architecture
|
||||
|
||||
## Quick Start
|
||||
|
||||
### The Simplest Way (Works Like Before)
|
||||
|
||||
If you're already using Crawl4AI, nothing changes:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
async def extract_tables():
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# This works exactly like before - uses DefaultTableExtraction internally
|
||||
result = await crawler.arun("https://example.com/data")
|
||||
|
||||
# Tables are automatically extracted and available in result.tables
|
||||
for table in result.tables:
|
||||
print(f"Table with {len(table['rows'])} rows and {len(table['headers'])} columns")
|
||||
print(f"Headers: {table['headers']}")
|
||||
print(f"First row: {table['rows'][0] if table['rows'] else 'No data'}")
|
||||
|
||||
asyncio.run(extract_tables())
|
||||
```
|
||||
|
||||
### Using the Old Configuration (Still Supported)
|
||||
|
||||
Your existing code with `table_score_threshold` continues to work:
|
||||
|
||||
```python
|
||||
# This old approach STILL WORKS - we maintain backward compatibility
|
||||
config = CrawlerRunConfig(
|
||||
table_score_threshold=7 # Internally creates DefaultTableExtraction(table_score_threshold=7)
|
||||
)
|
||||
result = await crawler.arun(url, config)
|
||||
```
|
||||
|
||||
## Table Extraction Strategies
|
||||
|
||||
### Understanding the Strategy Pattern
|
||||
|
||||
The strategy pattern allows you to choose different table extraction algorithms at runtime. Think of it as having different tools in a toolbox - you pick the right one for the job:
|
||||
|
||||
- **No explicit strategy?** → Uses `DefaultTableExtraction` automatically (same as v0.7.2 and earlier)
|
||||
- **Need complex table handling?** → Choose `LLMTableExtraction` (costs money, use sparingly)
|
||||
- **Want to disable tables?** → Use `NoTableExtraction`
|
||||
- **Have special requirements?** → Create a custom strategy
|
||||
|
||||
### Available Strategies
|
||||
|
||||
| Strategy | Description | Use Case | Cost | When to Use |
|
||||
|----------|-------------|----------|------|-------------|
|
||||
| `DefaultTableExtraction` | **RECOMMENDED**: Same algorithm as before v0.7.3 | General purpose (default) | Free | **Use this first - handles 95% of cases** |
|
||||
| `LLMTableExtraction` | AI-powered extraction for complex tables | Tables with complex rowspan/colspan | **$$$ Per API call** | Only when DefaultTableExtraction fails |
|
||||
| `NoTableExtraction` | Disables table extraction | When tables aren't needed | Free | For text-only extraction |
|
||||
| Custom strategies | User-defined extraction logic | Specialized requirements | Free | Domain-specific needs |
|
||||
|
||||
> **⚠️ CRITICAL COST WARNING for LLMTableExtraction**:
|
||||
>
|
||||
> **DO NOT USE `LLMTableExtraction` UNLESS ABSOLUTELY NECESSARY!**
|
||||
>
|
||||
> - **Always try `DefaultTableExtraction` first** - It's free and handles most tables perfectly
|
||||
> - LLM extraction **costs money** with every API call
|
||||
> - For large tables (100+ rows), LLM extraction can be **very slow**
|
||||
> - **For large tables**: If you must use LLM, choose fast providers:
|
||||
> - ✅ **Groq** (fastest inference)
|
||||
> - ✅ **Cerebras** (optimized for speed)
|
||||
> - ⚠️ Avoid: OpenAI, Anthropic for large tables (slower)
|
||||
>
|
||||
> **🚧 WORK IN PROGRESS**:
|
||||
> We are actively developing an **advanced non-LLM algorithm** that will handle complex table structures (rowspan, colspan, nested tables) for **FREE**. This will replace the need for costly LLM extraction in most cases. Coming soon!
|
||||
|
||||
### DefaultTableExtraction
|
||||
|
||||
The default strategy uses a sophisticated scoring system to identify data tables:
|
||||
|
||||
```python
|
||||
from crawl4ai import DefaultTableExtraction, CrawlerRunConfig
|
||||
|
||||
# Customize the default extraction
|
||||
table_strategy = DefaultTableExtraction(
|
||||
table_score_threshold=7, # Scoring threshold (default: 7)
|
||||
min_rows=2, # Minimum rows required
|
||||
min_cols=2, # Minimum columns required
|
||||
verbose=True # Enable detailed logging
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=table_strategy
|
||||
)
|
||||
```
|
||||
|
||||
#### Scoring System
|
||||
|
||||
The scoring system evaluates multiple factors:
|
||||
|
||||
| Factor | Score Impact | Description |
|
||||
|--------|--------------|-------------|
|
||||
| Has `<thead>` | +2 | Semantic table structure |
|
||||
| Has `<tbody>` | +1 | Organized table body |
|
||||
| Has `<th>` elements | +2 | Header cells present |
|
||||
| Headers in correct position | +1 | Proper semantic structure |
|
||||
| Consistent column count | +2 | Regular data structure |
|
||||
| Has caption | +2 | Descriptive caption |
|
||||
| Has summary | +1 | Summary attribute |
|
||||
| High text density | +2 to +3 | Content-rich cells |
|
||||
| Data attributes | +0.5 each | Data-* attributes |
|
||||
| Nested tables | -3 | Often indicates layout |
|
||||
| Role="presentation" | -3 | Explicitly non-data |
|
||||
| Too few rows | -2 | Insufficient data |
|
||||
|
||||
### LLMTableExtraction (Use Sparingly!)
|
||||
|
||||
**⚠️ WARNING**: Only use this when `DefaultTableExtraction` fails with complex tables!
|
||||
|
||||
LLMTableExtraction uses AI to understand complex table structures that traditional parsers struggle with. It automatically handles large tables through intelligent chunking and parallel processing:
|
||||
|
||||
```python
|
||||
from crawl4ai import LLMTableExtraction, LLMConfig, CrawlerRunConfig
|
||||
|
||||
# Configure LLM (costs money per call!)
|
||||
llm_config = LLMConfig(
|
||||
provider="groq/llama-3.3-70b-versatile", # Fast provider for large tables
|
||||
api_token="your_api_key",
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
# Create LLM extraction strategy with smart chunking
|
||||
table_strategy = LLMTableExtraction(
|
||||
llm_config=llm_config,
|
||||
max_tries=3, # Retry up to 3 times if extraction fails
|
||||
css_selector="table", # Optional: focus on specific tables
|
||||
enable_chunking=True, # Automatically chunk large tables (default: True)
|
||||
chunk_token_threshold=3000, # Split tables larger than this (default: 3000 tokens)
|
||||
min_rows_per_chunk=10, # Minimum rows per chunk (default: 10)
|
||||
max_parallel_chunks=5, # Process up to 5 chunks in parallel (default: 5)
|
||||
verbose=True
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=table_strategy
|
||||
)
|
||||
|
||||
result = await crawler.arun(url, config)
|
||||
```
|
||||
|
||||
#### When to Use LLMTableExtraction
|
||||
|
||||
✅ **Use ONLY when**:
|
||||
- Tables have complex merged cells (rowspan/colspan) that break DefaultTableExtraction
|
||||
- Nested tables that need semantic understanding
|
||||
- Tables with irregular structures
|
||||
- You've tried DefaultTableExtraction and it failed
|
||||
|
||||
❌ **Never use when**:
|
||||
- DefaultTableExtraction works (99% of cases)
|
||||
- Tables are simple or well-structured
|
||||
- You're processing many pages (costs add up!)
|
||||
- Tables have 100+ rows (very slow)
|
||||
|
||||
#### How Smart Chunking Works
|
||||
|
||||
LLMTableExtraction automatically handles large tables through intelligent chunking:
|
||||
|
||||
1. **Automatic Detection**: Tables exceeding the token threshold are automatically split
|
||||
2. **Smart Splitting**: Chunks are created at row boundaries, preserving table structure
|
||||
3. **Header Preservation**: Each chunk includes the original headers for context
|
||||
4. **Parallel Processing**: Multiple chunks are processed simultaneously for speed
|
||||
5. **Intelligent Merging**: Results are merged back into a single, complete table
|
||||
|
||||
**Chunking Parameters**:
|
||||
- `enable_chunking` (default: `True`): Automatically handle large tables
|
||||
- `chunk_token_threshold` (default: `3000`): When to split tables
|
||||
- `min_rows_per_chunk` (default: `10`): Ensures meaningful chunk sizes
|
||||
- `max_parallel_chunks` (default: `5`): Concurrent processing for speed
|
||||
|
||||
The chunking is completely transparent - you get the same output format whether the table was processed in one piece or multiple chunks.
|
||||
|
||||
#### Performance Optimization for LLMTableExtraction
|
||||
|
||||
**Provider Recommendations by Table Size**:
|
||||
|
||||
| Table Size | Recommended Providers | Why |
|
||||
|------------|----------------------|-----|
|
||||
| Small (<50 rows) | Any provider | Fast enough |
|
||||
| Medium (50-200 rows) | Groq, Cerebras | Optimized inference |
|
||||
| Large (200+ rows) | **Groq** (best), Cerebras | Fastest inference + automatic chunking |
|
||||
| Very Large (500+ rows) | Groq with chunking | Parallel processing keeps it fast |
|
||||
|
||||
### NoTableExtraction
|
||||
|
||||
Disable table extraction for better performance when tables aren't needed:
|
||||
|
||||
```python
|
||||
from crawl4ai import NoTableExtraction, CrawlerRunConfig
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=NoTableExtraction()
|
||||
)
|
||||
|
||||
# Tables won't be extracted, improving performance
|
||||
result = await crawler.arun(url, config)
|
||||
assert len(result.tables) == 0
|
||||
```
|
||||
|
||||
## Extracted Table Structure
|
||||
|
||||
Each extracted table contains:
|
||||
|
||||
```python
|
||||
{
|
||||
"headers": ["Column 1", "Column 2", ...], # Column headers
|
||||
"rows": [ # Data rows
|
||||
["Row 1 Col 1", "Row 1 Col 2", ...],
|
||||
["Row 2 Col 1", "Row 2 Col 2", ...],
|
||||
],
|
||||
"caption": "Table Caption", # If present
|
||||
"summary": "Table Summary", # If present
|
||||
"metadata": {
|
||||
"row_count": 10, # Number of rows
|
||||
"column_count": 3, # Number of columns
|
||||
"has_headers": True, # Headers detected
|
||||
"has_caption": True, # Caption exists
|
||||
"has_summary": False, # Summary exists
|
||||
"id": "data-table-1", # Table ID if present
|
||||
"class": "financial-data" # Table class if present
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```python
|
||||
config = CrawlerRunConfig(
|
||||
# Table extraction settings
|
||||
table_score_threshold=7, # Default threshold (backward compatible)
|
||||
table_extraction=strategy, # Optional: custom strategy
|
||||
|
||||
# Filter what to process
|
||||
css_selector="main", # Focus on specific area
|
||||
excluded_tags=["nav", "aside"] # Exclude page sections
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```python
|
||||
from crawl4ai import DefaultTableExtraction, CrawlerRunConfig
|
||||
|
||||
# Fine-tuned extraction
|
||||
strategy = DefaultTableExtraction(
|
||||
table_score_threshold=5, # Lower = more permissive
|
||||
min_rows=3, # Require at least 3 rows
|
||||
min_cols=2, # Require at least 2 columns
|
||||
verbose=True # Detailed logging
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=strategy,
|
||||
css_selector="article.content", # Target specific content
|
||||
exclude_domains=["ads.com"], # Exclude ad domains
|
||||
cache_mode=CacheMode.BYPASS # Fresh extraction
|
||||
)
|
||||
```
|
||||
|
||||
## Working with Extracted Tables
|
||||
|
||||
### Convert to Pandas DataFrame
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
async def tables_to_dataframes(url):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url)
|
||||
|
||||
dataframes = []
|
||||
for table_data in result.tables:
|
||||
# Create DataFrame
|
||||
if table_data['headers']:
|
||||
df = pd.DataFrame(
|
||||
table_data['rows'],
|
||||
columns=table_data['headers']
|
||||
)
|
||||
else:
|
||||
df = pd.DataFrame(table_data['rows'])
|
||||
|
||||
# Add metadata as DataFrame attributes
|
||||
df.attrs['caption'] = table_data.get('caption', '')
|
||||
df.attrs['metadata'] = table_data.get('metadata', {})
|
||||
|
||||
dataframes.append(df)
|
||||
|
||||
return dataframes
|
||||
```
|
||||
|
||||
### Filter Tables by Criteria
|
||||
|
||||
```python
|
||||
async def extract_large_tables(url):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
# Configure minimum size requirements
|
||||
strategy = DefaultTableExtraction(
|
||||
min_rows=10,
|
||||
min_cols=3,
|
||||
table_score_threshold=6
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=strategy
|
||||
)
|
||||
|
||||
result = await crawler.arun(url, config)
|
||||
|
||||
# Further filter results
|
||||
large_tables = [
|
||||
table for table in result.tables
|
||||
if table['metadata']['row_count'] > 10
|
||||
and table['metadata']['column_count'] > 3
|
||||
]
|
||||
|
||||
return large_tables
|
||||
```
|
||||
|
||||
### Export Tables to Different Formats
|
||||
|
||||
```python
|
||||
import json
|
||||
import csv
|
||||
|
||||
async def export_tables(url):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun(url)
|
||||
|
||||
for i, table in enumerate(result.tables):
|
||||
# Export as JSON
|
||||
with open(f'table_{i}.json', 'w') as f:
|
||||
json.dump(table, f, indent=2)
|
||||
|
||||
# Export as CSV
|
||||
with open(f'table_{i}.csv', 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
if table['headers']:
|
||||
writer.writerow(table['headers'])
|
||||
writer.writerows(table['rows'])
|
||||
|
||||
# Export as Markdown
|
||||
with open(f'table_{i}.md', 'w') as f:
|
||||
# Write headers
|
||||
if table['headers']:
|
||||
f.write('| ' + ' | '.join(table['headers']) + ' |\n')
|
||||
f.write('|' + '---|' * len(table['headers']) + '\n')
|
||||
|
||||
# Write rows
|
||||
for row in table['rows']:
|
||||
f.write('| ' + ' | '.join(str(cell) for cell in row) + ' |\n')
|
||||
```
|
||||
|
||||
## Creating Custom Strategies
|
||||
|
||||
Extend `TableExtractionStrategy` to create custom extraction logic:
|
||||
|
||||
### Example: Financial Table Extractor
|
||||
|
||||
```python
|
||||
from crawl4ai import TableExtractionStrategy
|
||||
from typing import List, Dict, Any
|
||||
import re
|
||||
|
||||
class FinancialTableExtractor(TableExtractionStrategy):
|
||||
"""Extract tables containing financial data."""
|
||||
|
||||
def __init__(self, currency_symbols=None, require_numbers=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.currency_symbols = currency_symbols or ['$', '€', '£', '¥']
|
||||
self.require_numbers = require_numbers
|
||||
self.number_pattern = re.compile(r'\d+[,.]?\d*')
|
||||
|
||||
def extract_tables(self, element, **kwargs):
|
||||
tables_data = []
|
||||
|
||||
for table in element.xpath(".//table"):
|
||||
# Check if table contains financial indicators
|
||||
table_text = ''.join(table.itertext())
|
||||
|
||||
# Must contain currency symbols
|
||||
has_currency = any(sym in table_text for sym in self.currency_symbols)
|
||||
if not has_currency:
|
||||
continue
|
||||
|
||||
# Must contain numbers if required
|
||||
if self.require_numbers:
|
||||
numbers = self.number_pattern.findall(table_text)
|
||||
if len(numbers) < 3: # Arbitrary minimum
|
||||
continue
|
||||
|
||||
# Extract the table data
|
||||
table_data = self._extract_financial_data(table)
|
||||
if table_data:
|
||||
tables_data.append(table_data)
|
||||
|
||||
return tables_data
|
||||
|
||||
def _extract_financial_data(self, table):
|
||||
"""Extract and clean financial data from table."""
|
||||
headers = []
|
||||
rows = []
|
||||
|
||||
# Extract headers
|
||||
for th in table.xpath(".//thead//th | .//tr[1]//th"):
|
||||
headers.append(th.text_content().strip())
|
||||
|
||||
# Extract and clean rows
|
||||
for tr in table.xpath(".//tbody//tr | .//tr[position()>1]"):
|
||||
row = []
|
||||
for td in tr.xpath(".//td"):
|
||||
text = td.text_content().strip()
|
||||
# Clean currency formatting
|
||||
text = re.sub(r'[$€£¥,]', '', text)
|
||||
row.append(text)
|
||||
if row:
|
||||
rows.append(row)
|
||||
|
||||
return {
|
||||
"headers": headers,
|
||||
"rows": rows,
|
||||
"caption": self._get_caption(table),
|
||||
"summary": table.get("summary", ""),
|
||||
"metadata": {
|
||||
"type": "financial",
|
||||
"row_count": len(rows),
|
||||
"column_count": len(headers) or len(rows[0]) if rows else 0
|
||||
}
|
||||
}
|
||||
|
||||
def _get_caption(self, table):
|
||||
caption = table.xpath(".//caption/text()")
|
||||
return caption[0].strip() if caption else ""
|
||||
|
||||
# Usage
|
||||
strategy = FinancialTableExtractor(
|
||||
currency_symbols=['$', 'EUR'],
|
||||
require_numbers=True
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=strategy
|
||||
)
|
||||
```
|
||||
|
||||
### Example: Specific Table Extractor
|
||||
|
||||
```python
|
||||
class SpecificTableExtractor(TableExtractionStrategy):
|
||||
"""Extract only tables matching specific criteria."""
|
||||
|
||||
def __init__(self,
|
||||
required_headers=None,
|
||||
id_pattern=None,
|
||||
class_pattern=None,
|
||||
**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.required_headers = required_headers or []
|
||||
self.id_pattern = id_pattern
|
||||
self.class_pattern = class_pattern
|
||||
|
||||
def extract_tables(self, element, **kwargs):
|
||||
tables_data = []
|
||||
|
||||
for table in element.xpath(".//table"):
|
||||
# Check ID pattern
|
||||
if self.id_pattern:
|
||||
table_id = table.get('id', '')
|
||||
if not re.match(self.id_pattern, table_id):
|
||||
continue
|
||||
|
||||
# Check class pattern
|
||||
if self.class_pattern:
|
||||
table_class = table.get('class', '')
|
||||
if not re.match(self.class_pattern, table_class):
|
||||
continue
|
||||
|
||||
# Extract headers to check requirements
|
||||
headers = self._extract_headers(table)
|
||||
|
||||
# Check if required headers are present
|
||||
if self.required_headers:
|
||||
if not all(req in headers for req in self.required_headers):
|
||||
continue
|
||||
|
||||
# Extract full table data
|
||||
table_data = self._extract_table_data(table, headers)
|
||||
tables_data.append(table_data)
|
||||
|
||||
return tables_data
|
||||
```
|
||||
|
||||
## Combining with Other Strategies
|
||||
|
||||
Table extraction works seamlessly with other Crawl4AI strategies:
|
||||
|
||||
```python
|
||||
from crawl4ai import (
|
||||
AsyncWebCrawler,
|
||||
CrawlerRunConfig,
|
||||
DefaultTableExtraction,
|
||||
LLMExtractionStrategy,
|
||||
JsonCssExtractionStrategy
|
||||
)
|
||||
|
||||
async def combined_extraction(url):
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
config = CrawlerRunConfig(
|
||||
# Table extraction
|
||||
table_extraction=DefaultTableExtraction(
|
||||
table_score_threshold=6,
|
||||
min_rows=2
|
||||
),
|
||||
|
||||
# CSS-based extraction for specific elements
|
||||
extraction_strategy=JsonCssExtractionStrategy({
|
||||
"title": "h1",
|
||||
"summary": "p.summary",
|
||||
"date": "time"
|
||||
}),
|
||||
|
||||
# Focus on main content
|
||||
css_selector="main.content"
|
||||
)
|
||||
|
||||
result = await crawler.arun(url, config)
|
||||
|
||||
# Access different extraction results
|
||||
tables = result.tables # Table data
|
||||
structured = json.loads(result.extracted_content) # CSS extraction
|
||||
|
||||
return {
|
||||
"tables": tables,
|
||||
"structured_data": structured,
|
||||
"markdown": result.markdown
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Disable when not needed**: Use `NoTableExtraction` if tables aren't required
|
||||
2. **Target specific areas**: Use `css_selector` to limit processing scope
|
||||
3. **Set minimum thresholds**: Filter out small/irrelevant tables early
|
||||
4. **Cache results**: Use appropriate cache modes for repeated extractions
|
||||
|
||||
```python
|
||||
# Optimized configuration for large pages
|
||||
config = CrawlerRunConfig(
|
||||
# Only process main content area
|
||||
css_selector="article.main-content",
|
||||
|
||||
# Exclude navigation and sidebars
|
||||
excluded_tags=["nav", "aside", "footer"],
|
||||
|
||||
# Higher threshold for stricter filtering
|
||||
table_extraction=DefaultTableExtraction(
|
||||
table_score_threshold=8,
|
||||
min_rows=5,
|
||||
min_cols=3
|
||||
),
|
||||
|
||||
# Enable caching for repeated access
|
||||
cache_mode=CacheMode.ENABLED
|
||||
)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Important: Your Code Still Works!
|
||||
|
||||
**No changes required!** The transition to the strategy pattern is **fully backward compatible**.
|
||||
|
||||
### How It Works Internally
|
||||
|
||||
#### v0.7.2 and Earlier
|
||||
```python
|
||||
# Old way - directly passing table_score_threshold
|
||||
config = CrawlerRunConfig(
|
||||
table_score_threshold=7
|
||||
)
|
||||
# Internally: No strategy pattern, direct implementation
|
||||
```
|
||||
|
||||
#### v0.7.3+ (Current)
|
||||
```python
|
||||
# Old way STILL WORKS - we handle it internally
|
||||
config = CrawlerRunConfig(
|
||||
table_score_threshold=7
|
||||
)
|
||||
# Internally: Automatically creates DefaultTableExtraction(table_score_threshold=7)
|
||||
```
|
||||
|
||||
### Taking Advantage of New Features
|
||||
|
||||
While your old code works, you can now use the strategy pattern for more control:
|
||||
|
||||
```python
|
||||
# Option 1: Keep using the old way (perfectly fine!)
|
||||
config = CrawlerRunConfig(
|
||||
table_score_threshold=7 # Still supported
|
||||
)
|
||||
|
||||
# Option 2: Use the new strategy pattern (more flexibility)
|
||||
from crawl4ai import DefaultTableExtraction
|
||||
|
||||
strategy = DefaultTableExtraction(
|
||||
table_score_threshold=7,
|
||||
min_rows=2, # New capability!
|
||||
min_cols=2 # New capability!
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=strategy
|
||||
)
|
||||
|
||||
# Option 3: Use advanced strategies when needed
|
||||
from crawl4ai import LLMTableExtraction, LLMConfig
|
||||
|
||||
# Only for complex tables that DefaultTableExtraction can't handle
|
||||
# Automatically handles large tables with smart chunking
|
||||
llm_strategy = LLMTableExtraction(
|
||||
llm_config=LLMConfig(
|
||||
provider="groq/llama-3.3-70b-versatile",
|
||||
api_token="your_key"
|
||||
),
|
||||
max_tries=3,
|
||||
enable_chunking=True, # Automatically chunk large tables
|
||||
chunk_token_threshold=3000, # Chunk when exceeding 3000 tokens
|
||||
max_parallel_chunks=5 # Process up to 5 chunks in parallel
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=llm_strategy # Advanced extraction with automatic chunking
|
||||
)
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
- ✅ **No breaking changes** - Old code works as-is
|
||||
- ✅ **Same defaults** - DefaultTableExtraction is automatically used
|
||||
- ✅ **Gradual adoption** - Use new features when you need them
|
||||
- ✅ **Full compatibility** - result.tables structure unchanged
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Choose the Right Strategy (Cost-Conscious Approach)
|
||||
|
||||
**Decision Flow**:
|
||||
```
|
||||
1. Do you need tables?
|
||||
→ No: Use NoTableExtraction
|
||||
→ Yes: Continue to #2
|
||||
|
||||
2. Try DefaultTableExtraction first (FREE)
|
||||
→ Works? Done! ✅
|
||||
→ Fails? Continue to #3
|
||||
|
||||
3. Is the table critical and complex?
|
||||
→ No: Accept DefaultTableExtraction results
|
||||
→ Yes: Continue to #4
|
||||
|
||||
4. Use LLMTableExtraction (COSTS MONEY)
|
||||
→ Small table (<50 rows): Any LLM provider
|
||||
→ Large table (50+ rows): Use Groq or Cerebras
|
||||
→ Very large (500+ rows): Reconsider - maybe chunk the page
|
||||
```
|
||||
|
||||
**Strategy Selection Guide**:
|
||||
- **DefaultTableExtraction**: Use for 99% of cases - it's free and effective
|
||||
- **LLMTableExtraction**: Only for complex tables with merged cells that break DefaultTableExtraction
|
||||
- **NoTableExtraction**: When you only need text/markdown content
|
||||
- **Custom Strategy**: For specialized requirements (financial, scientific, etc.)
|
||||
|
||||
### 2. Validate Extracted Data
|
||||
|
||||
```python
|
||||
def validate_table(table):
|
||||
"""Validate table data quality."""
|
||||
# Check structure
|
||||
if not table.get('rows'):
|
||||
return False
|
||||
|
||||
# Check consistency
|
||||
if table.get('headers'):
|
||||
expected_cols = len(table['headers'])
|
||||
for row in table['rows']:
|
||||
if len(row) != expected_cols:
|
||||
return False
|
||||
|
||||
# Check minimum content
|
||||
total_cells = sum(len(row) for row in table['rows'])
|
||||
non_empty = sum(1 for row in table['rows']
|
||||
for cell in row if cell.strip())
|
||||
|
||||
if non_empty / total_cells < 0.5: # Less than 50% non-empty
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Filter valid tables
|
||||
valid_tables = [t for t in result.tables if validate_table(t)]
|
||||
```
|
||||
|
||||
### 3. Handle Edge Cases
|
||||
|
||||
```python
|
||||
async def robust_table_extraction(url):
|
||||
"""Extract tables with error handling."""
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
try:
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=DefaultTableExtraction(
|
||||
table_score_threshold=6,
|
||||
verbose=True
|
||||
)
|
||||
)
|
||||
|
||||
result = await crawler.arun(url, config)
|
||||
|
||||
if not result.success:
|
||||
print(f"Crawl failed: {result.error}")
|
||||
return []
|
||||
|
||||
# Process tables safely
|
||||
processed_tables = []
|
||||
for table in result.tables:
|
||||
try:
|
||||
# Validate and process
|
||||
if validate_table(table):
|
||||
processed_tables.append(table)
|
||||
except Exception as e:
|
||||
print(f"Error processing table: {e}")
|
||||
continue
|
||||
|
||||
return processed_tables
|
||||
|
||||
except Exception as e:
|
||||
print(f"Extraction error: {e}")
|
||||
return []
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
| Issue | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| No tables extracted | Score too high | Lower `table_score_threshold` |
|
||||
| Layout tables included | Score too low | Increase `table_score_threshold` |
|
||||
| Missing tables | CSS selector too specific | Broaden or remove `css_selector` |
|
||||
| Incomplete data | Complex table structure | Create custom strategy |
|
||||
| Performance issues | Processing entire page | Use `css_selector` to limit scope |
|
||||
|
||||
### Debug Logging
|
||||
|
||||
Enable verbose logging to understand extraction decisions:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Enable verbose mode in strategy
|
||||
strategy = DefaultTableExtraction(
|
||||
table_score_threshold=7,
|
||||
verbose=True # Detailed extraction logs
|
||||
)
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
table_extraction=strategy,
|
||||
verbose=True # General crawler logs
|
||||
)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Extraction Strategies](extraction-strategies.md) - Overview of all extraction strategies
|
||||
- [Content Selection](content-selection.md) - Using CSS selectors and filters
|
||||
- [Performance Optimization](../optimization/performance-tuning.md) - Speed up extraction
|
||||
- [Examples](../examples/table_extraction_example.py) - Complete working examples
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user