Files
2026-07-13 12:12:13 +08:00

2528 lines
115 KiB
Python

import copy
import functools
import importlib
import os
import warnings
import requests
from .config import (
DEFAULT_PROVIDER,
DEFAULT_PROVIDER_API_KEY,
MIN_WORD_THRESHOLD,
IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD,
PROVIDER_MODELS,
PROVIDER_MODELS_PREFIXES,
SCREENSHOT_HEIGHT_TRESHOLD,
PAGE_TIMEOUT,
IMAGE_SCORE_THRESHOLD,
SOCIAL_MEDIA_DOMAINS,
)
from .user_agent_generator import UAGen, ValidUAGenerator # , OnlineUAGenerator
from .extraction_strategy import ExtractionStrategy, LLMExtractionStrategy
from .chunking_strategy import ChunkingStrategy, RegexChunking
from .markdown_generation_strategy import MarkdownGenerationStrategy, DefaultMarkdownGenerator
from .content_scraping_strategy import ContentScrapingStrategy, LXMLWebScrapingStrategy
from .deep_crawling import DeepCrawlStrategy
from .table_extraction import TableExtractionStrategy, DefaultTableExtraction
from .cache_context import CacheMode
from .proxy_strategy import ProxyRotationStrategy
import inspect
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
from enum import Enum
# Type alias for URL matching
UrlMatcher = Union[str, Callable[[str], bool], List[Union[str, Callable[[str], bool]]]]
def _with_defaults(cls):
"""Class decorator: adds set_defaults/get_defaults/reset_defaults classmethods.
After decorating, every new instance resolves parameters as:
explicit arg > class-level user defaults > hardcoded default
Usage::
BrowserConfig.set_defaults(headless=False, viewport_width=1920)
cfg = BrowserConfig() # headless=False, viewport_width=1920
cfg = BrowserConfig(headless=True) # explicit wins → headless=True
"""
original_init = cls.__init__
sig = inspect.signature(original_init)
param_names = [p for p in sig.parameters if p != "self"]
valid_params = frozenset(param_names)
@functools.wraps(original_init)
def wrapped_init(self, *args, **kwargs):
user_defaults = type(self)._user_defaults
if user_defaults:
# Determine which params the caller passed explicitly
explicit = set(kwargs.keys())
for i in range(len(args)):
if i < len(param_names):
explicit.add(param_names[i])
# Inject user defaults for non-explicit params
for key, value in user_defaults.items():
if key not in explicit:
kwargs[key] = copy.deepcopy(value)
original_init(self, *args, **kwargs)
cls.__init__ = wrapped_init
cls._user_defaults = {}
@classmethod
def set_defaults(klass, **kwargs):
"""Set class-level default overrides for new instances.
Args:
**kwargs: Parameter names and their default values.
Raises:
ValueError: If any key is not a valid ``__init__`` parameter.
"""
invalid = set(kwargs) - valid_params
if invalid:
raise ValueError(
f"Invalid parameter(s) for {klass.__name__}: {invalid}"
)
for k, v in kwargs.items():
klass._user_defaults[k] = copy.deepcopy(v)
@classmethod
def get_defaults(klass):
"""Return a deep copy of the current class-level defaults."""
return copy.deepcopy(klass._user_defaults)
@classmethod
def reset_defaults(klass, *names):
"""Clear class-level defaults.
With no arguments, removes all overrides.
With arguments, removes only the named overrides.
"""
if names:
for n in names:
klass._user_defaults.pop(n, None)
else:
klass._user_defaults.clear()
cls.set_defaults = set_defaults
cls.get_defaults = get_defaults
cls.reset_defaults = reset_defaults
return cls
class MatchMode(Enum):
OR = "or"
AND = "and"
# from .proxy_strategy import ProxyConfig
# Allowlist of types that can be deserialized via from_serializable_dict().
# This prevents arbitrary class instantiation from untrusted input (e.g. API requests).
ALLOWED_DESERIALIZE_TYPES = {
# Config classes
"BrowserConfig", "CrawlerRunConfig", "HTTPCrawlerConfig",
"LLMConfig", "ProxyConfig", "GeolocationConfig",
"SeedingConfig", "VirtualScrollConfig", "LinkPreviewConfig", "DomainMapperConfig",
# Extraction strategies
"JsonCssExtractionStrategy", "JsonXPathExtractionStrategy",
"JsonLxmlExtractionStrategy", "LLMExtractionStrategy",
"CosineStrategy", "RegexExtractionStrategy",
# Markdown / content
"DefaultMarkdownGenerator",
"PruningContentFilter", "BM25ContentFilter", "LLMContentFilter",
# Scraping
"LXMLWebScrapingStrategy", "PDFContentScrapingStrategy",
# Chunking
"RegexChunking",
# Deep crawl
"BFSDeepCrawlStrategy", "DFSDeepCrawlStrategy", "BestFirstCrawlingStrategy",
# Filters & scorers
"FilterChain", "URLPatternFilter", "DomainFilter",
"ContentTypeFilter", "URLFilter", "SEOFilter", "ContentRelevanceFilter",
"KeywordRelevanceScorer", "URLScorer", "CompositeScorer",
"DomainAuthorityScorer", "FreshnessScorer", "PathDepthScorer",
# Enums
"CacheMode", "MatchMode", "DisplayMode",
# Dispatchers
"MemoryAdaptiveDispatcher", "SemaphoreDispatcher",
# Table extraction
"DefaultTableExtraction", "NoTableExtraction", "LLMTableExtraction",
# Proxy
"RoundRobinProxyStrategy",
}
# ───────────────────────── untrusted-input trust boundary ─────────────────────────
# When a config is deserialized from an untrusted source (a network request body
# on the Docker server), it may only construct a strict subset of types and may
# only set scalar, non-power fields, which are then clamped. SDK / in-process use
# is TRUSTED by default, so this changes nothing for library callers.
class Provenance(Enum):
"""Where deserialized config data came from.
TRUSTED - SDK / in-process construction (unchanged behavior).
UNTRUSTED - a network request body; gated by the allowlists below.
"""
TRUSTED = "trusted"
UNTRUSTED = "untrusted"
class UntrustedConfigError(ValueError):
"""An untrusted request tried to construct a forbidden type or set a
forbidden power-field. The Docker layer maps this to HTTP 400."""
# Types an untrusted body may construct - a strict subset of
# ALLOWED_DESERIALIZE_TYPES. Deliberately EXCLUDES everything that can run code,
# read secrets, route traffic, or recurse the crawl: LLMConfig,
# LLMExtractionStrategy, LLMContentFilter, LLMTableExtraction, ProxyConfig,
# RoundRobinProxyStrategy, all DeepCrawl*/Filter/Scorer/Dispatcher classes,
# SeedingConfig and DomainMapperConfig.
UNTRUSTED_ALLOWED_TYPES = {
"CrawlerRunConfig", "BrowserConfig", "HTTPCrawlerConfig",
"GeolocationConfig", "VirtualScrollConfig", "LinkPreviewConfig",
# non-LLM extraction / markdown / scraping / chunking strategies
"JsonCssExtractionStrategy", "JsonXPathExtractionStrategy",
"JsonLxmlExtractionStrategy", "RegexExtractionStrategy", "CosineStrategy",
"DefaultMarkdownGenerator", "PruningContentFilter", "BM25ContentFilter",
"LXMLWebScrapingStrategy", "PDFContentScrapingStrategy",
"RegexChunking",
"DefaultTableExtraction", "NoTableExtraction",
# safe scalar enums
"CacheMode", "MatchMode", "DisplayMode",
}
# Fields that must NEVER come from an untrusted body. Presence => 400 (loud), so
# a client smuggling these gets an explicit error rather than a silent drop.
UNTRUSTED_FORBIDDEN_FIELDS = {
"BrowserConfig": {
"proxy", "proxy_config", "extra_args", "user_data_dir", "channel",
"chrome_channel", "cdp_url", "debugging_port", "host", "storage_state",
"cookies", "headers", "init_scripts", "browser_context_id", "target_id",
},
"CrawlerRunConfig": {
"js_code", "js_code_before_wait", "c4a_script", "deep_crawl_strategy",
"proxy_config", "proxy_rotation_strategy", "proxy_session_id",
"proxy_session_ttl", "proxy_session_auto_release",
"fallback_fetch_function", "experimental", "base_url", "simulate_user",
"override_navigator", "magic", "process_in_browser", "shared_data",
"session_id",
},
}
# Scalar knobs an untrusted body MAY set, per class. A field not listed here is
# dropped (forward-compatible: new SDK fields don't open a hole). Strategy-object
# fields are kept so the nested object is itself type-gated by the recursion.
UNTRUSTED_FIELD_ALLOWLIST = {
"BrowserConfig": {
"browser_type", "headless", "browser_mode", "viewport_width",
"viewport_height", "viewport", "device_scale_factor", "accept_downloads",
"java_script_enabled", "text_mode", "light_mode", "enable_stealth",
"avoid_ads", "avoid_css", "user_agent", "user_agent_mode",
"user_agent_generator_config", "verbose", "memory_saving_mode",
"max_pages_before_recycle",
},
"CrawlerRunConfig": {
# content selection / cleaning
"word_count_threshold", "only_text", "css_selector", "target_elements",
"excluded_tags", "excluded_selector", "keep_data_attributes", "keep_attrs",
"remove_forms", "prettiify", "parser_type",
# strategy objects (nested type is gated by the recursion)
"extraction_strategy", "chunking_strategy", "markdown_generator",
"scraping_strategy", "table_extraction",
# locale / geo
"locale", "timezone_id", "geolocation",
# cache
"cache_mode", "bypass_cache", "disable_cache", "no_cache_read",
"no_cache_write", "check_cache_freshness", "cache_validation_timeout",
"fetch_ssl_certificate",
# timing / waiting
"wait_until", "page_timeout", "wait_for", "wait_for_timeout",
"wait_for_images", "delay_before_return_html", "mean_delay", "max_range",
# scrolling / rendering
"ignore_body_visibility", "scan_full_page", "scroll_delay",
"max_scroll_steps", "process_iframes", "flatten_shadow_dom",
"remove_overlay_elements", "remove_consent_popups",
"adjust_viewport_to_content", "virtual_scroll_config",
# media / capture
"screenshot", "screenshot_wait_for", "screenshot_height_threshold",
"force_viewport_screenshot", "pdf", "capture_mhtml",
"image_description_min_word_threshold", "image_score_threshold",
"table_score_threshold",
# links / images filtering
"exclude_external_images", "exclude_all_images",
"exclude_social_media_domains", "exclude_external_links",
"exclude_social_media_links", "exclude_domains", "exclude_internal_links",
"score_links", "preserve_https_for_internal_links", "link_preview_config",
# misc safe knobs
"verbose", "log_console", "capture_network_requests",
"capture_console_messages", "method", "stream", "prefetch", "url",
"check_robots_txt", "user_agent", "user_agent_mode",
"user_agent_generator_config", "url_matcher", "match_mode", "max_retries",
},
}
# Upper bounds applied to attacker-influenced quantities after filtering.
_MAX_TIMEOUT_MS = 60_000
_MAX_SCROLL_STEPS = 1000
_MAX_VIEWPORT = 4000
def _filter_untrusted_fields(type_name: str, params: dict) -> dict:
"""Drop non-allowlisted fields and raise on forbidden (power) fields."""
forbidden = UNTRUSTED_FORBIDDEN_FIELDS.get(type_name, set())
allowlist = UNTRUSTED_FIELD_ALLOWLIST.get(type_name) # None => keep all non-forbidden
out = {}
for key, value in params.items():
if key in forbidden:
raise UntrustedConfigError(
f"field '{key}' is not permitted on {type_name} from an untrusted request"
)
if allowlist is not None and key not in allowlist:
continue # silently drop unknown/unsafe fields (forward-compatible)
out[key] = value
return out
def _clamp_untrusted(type_name: str, params: dict) -> dict:
"""Clamp attacker-influenced quantities to safe upper bounds."""
def _cap_timeout(v):
# 0 historically meant "no timeout"; treat as the cap, never unbounded.
if not isinstance(v, (int, float)) or v <= 0:
return _MAX_TIMEOUT_MS
return min(int(v), _MAX_TIMEOUT_MS)
if type_name == "CrawlerRunConfig":
for f in ("page_timeout", "wait_for_timeout"):
if f in params:
params[f] = _cap_timeout(params[f])
if isinstance(params.get("max_scroll_steps"), int):
params["max_scroll_steps"] = min(params["max_scroll_steps"], _MAX_SCROLL_STEPS)
elif type_name == "BrowserConfig":
for f in ("viewport_width", "viewport_height"):
if isinstance(params.get(f), int):
params[f] = max(1, min(params[f], _MAX_VIEWPORT))
return params
def _enforce_untrusted(type_name: str, params: dict) -> dict:
"""Filter then clamp a parameter dict for an untrusted construction."""
return _clamp_untrusted(type_name, _filter_untrusted_fields(type_name, params))
def to_serializable_dict(obj: Any, ignore_default_value : bool = False):
"""
Recursively convert an object to a serializable dictionary using {type, params} structure
for complex objects.
"""
if obj is None:
return None
# Handle basic types
if isinstance(obj, (str, int, float, bool)):
return obj
# Handle Enum
if isinstance(obj, Enum):
return {"type": obj.__class__.__name__, "params": obj.value}
# Handle datetime objects
if hasattr(obj, "isoformat"):
return obj.isoformat()
# Handle lists, tuples, and sets, and basically any iterable
if isinstance(obj, (list, tuple, set)) or hasattr(obj, '__iter__') and not isinstance(obj, dict):
return [to_serializable_dict(item) for item in obj]
# Handle frozensets, which are not iterable
if isinstance(obj, frozenset):
return [to_serializable_dict(item) for item in list(obj)]
# Handle dictionaries - preserve them as-is
if isinstance(obj, dict):
return {
"type": "dict", # Mark as plain dictionary
"value": {str(k): to_serializable_dict(v) for k, v in obj.items()},
}
_type = obj.__class__.__name__
# Handle class instances
if hasattr(obj, "__class__"):
# Skip types that cannot be deserialized (e.g. logging.Logger, callables).
# Only serialize objects whose type is in ALLOWED_DESERIALIZE_TYPES so that
# from_serializable_dict can reconstruct them on the other side.
if _type not in ALLOWED_DESERIALIZE_TYPES:
return None
# Get constructor signature
sig = inspect.signature(obj.__class__.__init__)
params = sig.parameters
# Get current values
current_values = {}
for name, param in params.items():
if name == "self":
continue
value = getattr(obj, name, param.default)
# Only include if different from default, considering empty values
if not (is_empty_value(value) and is_empty_value(param.default)):
if value != param.default and not ignore_default_value:
current_values[name] = to_serializable_dict(value)
# Don't serialize private __slots__ - they're internal implementation details
# not constructor parameters. This was causing URLPatternFilter to fail
# because _simple_suffixes was being serialized as 'simple_suffixes'
# if hasattr(obj, '__slots__'):
# for slot in obj.__slots__:
# if slot.startswith('_'): # Handle private slots
# attr_name = slot[1:] # Remove leading '_'
# value = getattr(obj, slot, None)
# if value is not None:
# current_values[attr_name] = to_serializable_dict(value)
return {
"type": obj.__class__.__name__,
"params": current_values
}
return str(obj)
def from_serializable_dict(data: Any, provenance: "Provenance" = None) -> Any:
"""
Recursively convert a serializable dictionary back to an object instance.
provenance defaults to TRUSTED (SDK/in-process). When UNTRUSTED, every typed
object must be in UNTRUSTED_ALLOWED_TYPES and its params are filtered (drop
unknown, raise on forbidden) and clamped before construction.
"""
if provenance is None:
provenance = Provenance.TRUSTED
if data is None:
return None
# Handle basic types
if isinstance(data, (str, int, float, bool)):
return data
# Handle typed data.
# Only enter the typed-object path for dicts that match the shapes produced
# by to_serializable_dict(): {"type": "<ClassName>", "params": {...}} or
# {"type": "dict", "value": {...}}. Plain business dicts that happen to
# carry a "type" key (e.g. JSON-Schema fragments, JsonCss field specs like
# {"type": "text", "name": "..."}) have neither "params" nor "value" and
# must fall through to the raw-dict path below so they are passed as data.
if (
isinstance(data, dict)
and "type" in data
and ("params" in data or (data["type"] == "dict" and "value" in data))
):
# Handle plain dictionaries
if data["type"] == "dict" and "value" in data:
return {k: from_serializable_dict(v, provenance) for k, v in data["value"].items()}
# Security: only allow known-safe types to be deserialized.
# Unknown types (e.g. logging.Logger serialized by older clients) are
# silently dropped (returned as None) instead of crashing the request.
type_name = data["type"]
if type_name not in ALLOWED_DESERIALIZE_TYPES:
return None
# Untrusted bodies may only construct the strict subset of types and
# may not set forbidden power-fields.
if provenance == Provenance.UNTRUSTED and type_name not in UNTRUSTED_ALLOWED_TYPES:
raise UntrustedConfigError(
f"type '{type_name}' may not be constructed from an untrusted request"
)
cls = None
module_paths = ["crawl4ai"]
for module_path in module_paths:
try:
mod = importlib.import_module(module_path)
if hasattr(mod, type_name):
cls = getattr(mod, type_name)
break
except (ImportError, AttributeError):
continue
if cls is not None:
# Handle Enum
if issubclass(cls, Enum):
return cls(data["params"])
if "params" in data:
params = data["params"]
if provenance == Provenance.UNTRUSTED:
params = _enforce_untrusted(type_name, dict(params))
# Handle class instances
constructor_args = {
k: from_serializable_dict(v, provenance) for k, v in params.items()
}
return cls(**constructor_args)
# Handle lists
if isinstance(data, list):
return [from_serializable_dict(item, provenance) for item in data]
# Handle raw dictionaries (legacy support)
if isinstance(data, dict):
return {k: from_serializable_dict(v, provenance) for k, v in data.items()}
return data
def is_empty_value(value: Any) -> bool:
"""Check if a value is effectively empty/null."""
if value is None:
return True
if isinstance(value, (list, tuple, set, dict, str)) and len(value) == 0:
return True
return False
class GeolocationConfig:
def __init__(
self,
latitude: float,
longitude: float,
accuracy: Optional[float] = 0.0
):
"""Configuration class for geolocation settings.
Args:
latitude: Latitude coordinate (e.g., 37.7749)
longitude: Longitude coordinate (e.g., -122.4194)
accuracy: Accuracy in meters. Default: 0.0
"""
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
@staticmethod
def from_dict(geo_dict: Dict) -> "GeolocationConfig":
"""Create a GeolocationConfig from a dictionary."""
return GeolocationConfig(
latitude=geo_dict.get("latitude"),
longitude=geo_dict.get("longitude"),
accuracy=geo_dict.get("accuracy", 0.0)
)
def to_dict(self) -> Dict:
"""Convert to dictionary representation."""
return {
"latitude": self.latitude,
"longitude": self.longitude,
"accuracy": self.accuracy
}
def clone(self, **kwargs) -> "GeolocationConfig":
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
GeolocationConfig: A new instance with the specified updates
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return GeolocationConfig.from_dict(config_dict)
class ProxyConfig:
DIRECT = "direct" # Sentinel: use in proxy_config list to mean "no proxy"
def __init__(
self,
server: str,
username: Optional[str] = None,
password: Optional[str] = None,
ip: Optional[str] = None,
):
"""Configuration class for a single proxy.
Args:
server: Proxy server URL (e.g., "http://127.0.0.1:8080")
username: Optional username for proxy authentication
password: Optional password for proxy authentication
ip: Optional IP address for verification purposes
"""
self.server = server
self.username = username
self.password = password
# Extract IP from server if not explicitly provided
self.ip = ip or self._extract_ip_from_server()
def _extract_ip_from_server(self) -> Optional[str]:
"""Extract IP address from server URL."""
try:
# Simple extraction assuming http://ip:port format
if "://" in self.server:
parts = self.server.split("://")[1].split(":")
return parts[0]
else:
parts = self.server.split(":")
return parts[0]
except Exception:
return None
@staticmethod
def from_string(proxy_str: str) -> "ProxyConfig":
"""Create a ProxyConfig from a string.
Supported formats:
- 'http://username:password@ip:port'
- 'http://ip:port'
- 'socks5://ip:port'
- 'ip:port:username:password'
- 'ip:port'
"""
s = (proxy_str or "").strip()
# URL with credentials
if "@" in s and "://" in s:
auth_part, server_part = s.split("@", 1)
protocol, credentials = auth_part.split("://", 1)
if ":" in credentials:
username, password = credentials.split(":", 1)
return ProxyConfig(
server=f"{protocol}://{server_part}",
username=username,
password=password,
)
# URL without credentials (keep scheme)
if "://" in s and "@" not in s:
return ProxyConfig(server=s)
# Colon separated forms
parts = s.split(":")
if len(parts) == 4:
ip, port, username, password = parts
return ProxyConfig(server=f"http://{ip}:{port}", username=username, password=password)
if len(parts) == 2:
ip, port = parts
return ProxyConfig(server=f"http://{ip}:{port}")
raise ValueError(f"Invalid proxy string format: {proxy_str}")
@staticmethod
def from_dict(proxy_dict: Dict) -> "ProxyConfig":
"""Create a ProxyConfig from a dictionary."""
return ProxyConfig(
server=proxy_dict.get("server"),
username=proxy_dict.get("username"),
password=proxy_dict.get("password"),
ip=proxy_dict.get("ip"),
)
@staticmethod
def from_env(env_var: str = "PROXIES") -> List["ProxyConfig"]:
"""Load proxies from environment variable.
Args:
env_var: Name of environment variable containing comma-separated proxy strings
Returns:
List of ProxyConfig objects
"""
proxies = []
try:
proxy_list = os.getenv(env_var, "").split(",")
for proxy in proxy_list:
if not proxy:
continue
proxies.append(ProxyConfig.from_string(proxy))
except Exception as e:
print(f"Error loading proxies from environment: {e}")
return proxies
def to_dict(self) -> Dict:
"""Convert to dictionary representation."""
return {
"server": self.server,
"username": self.username,
"password": self.password,
"ip": self.ip,
}
def clone(self, **kwargs) -> "ProxyConfig":
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
ProxyConfig: A new instance with the specified updates
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return ProxyConfig.from_dict(config_dict)
@_with_defaults
class BrowserConfig:
"""
Configuration class for setting up a browser instance and its context in AsyncPlaywrightCrawlerStrategy.
This class centralizes all parameters that affect browser and context creation. Instead of passing
scattered keyword arguments, users can instantiate and modify this configuration object. The crawler
code will then reference these settings to initialize the browser in a consistent, documented manner.
Attributes:
browser_type (str): The type of browser to launch. Supported values: "chromium", "firefox", "webkit".
Default: "chromium".
headless (bool): Whether to run the browser in headless mode (no visible GUI).
Default: True.
browser_mode (str): Determines how the browser should be initialized:
"builtin" - use the builtin CDP browser running in background
"dedicated" - create a new dedicated browser instance each time
"cdp" - use explicit CDP settings provided in cdp_url
"docker" - run browser in Docker container with isolation
Default: "dedicated"
use_managed_browser (bool): Launch the browser using a managed approach (e.g., via CDP), allowing
advanced manipulation. Default: False.
cdp_url (str): URL for the Chrome DevTools Protocol (CDP) endpoint. Default: "ws://localhost:9222/devtools/browser/".
browser_context_id (str or None): Pre-existing CDP browser context ID to use. When provided along with
cdp_url, the crawler will reuse this context instead of creating a new one.
Useful for cloud browser services that pre-create isolated contexts.
Default: None.
target_id (str or None): Pre-existing CDP target ID (page) to use. When provided along with
browser_context_id, the crawler will reuse this target instead of creating
a new page. Default: None.
cdp_cleanup_on_close (bool): When True and using cdp_url, the close() method will still clean up
the local Playwright client resources. Useful for cloud/server scenarios
where you don't own the remote browser but need to prevent memory leaks
from accumulated Playwright instances. Default: False.
cdp_close_delay (float): Seconds to wait after disconnecting a CDP WebSocket before stopping the
Playwright subprocess. Gives the connection time to fully release. Set to
0 to skip the delay entirely. Only applies when cdp_cleanup_on_close=True.
Default: 1.0.
cache_cdp_connection (bool): When True and using cdp_url, the Playwright subprocess and CDP WebSocket
are cached at the class level and shared across multiple BrowserManager
instances connecting to the same cdp_url. Reference-counted; the connection
is only closed when the last user releases it. Eliminates the overhead of
repeated Playwright/CDP setup and teardown. Default: False.
create_isolated_context (bool): When True and using cdp_url, forces creation of a new browser context
instead of reusing the default context. Essential for concurrent crawls
on the same browser to prevent navigation conflicts. Default: False.
debugging_port (int): Port for the browser debugging protocol. Default: 9222.
use_persistent_context (bool): Use a persistent browser context (like a persistent profile).
Automatically sets use_managed_browser=True. Default: False.
user_data_dir (str or None): Path to a user data directory for persistent sessions. If None, a
temporary directory may be used. Default: None.
chrome_channel (str): The Chrome channel to launch (e.g., "chrome", "msedge"). Only applies if browser_type
is "chromium". Default: "chromium".
channel (str): The channel to launch (e.g., "chromium", "chrome", "msedge"). Only applies if browser_type
is "chromium". Default: "chromium".
proxy (Optional[str]): Proxy server URL (e.g., "http://username:password@proxy:port"). If None, no proxy is used.
Default: None.
proxy_config (ProxyConfig or dict or None): Detailed proxy configuration, e.g. {"server": "...", "username": "..."}.
If None, no additional proxy config. Default: None.
viewport_width (int): Default viewport width for pages. Default: 1080.
viewport_height (int): Default viewport height for pages. Default: 600.
viewport (dict): Default viewport dimensions for pages. If set, overrides viewport_width and viewport_height.
Default: None.
device_scale_factor (float): The device pixel ratio used for rendering pages. Controls how many
physical pixels map to one CSS pixel, allowing simulation of HiDPI
or Retina displays. For example, a viewport of 1920x1080 with a
device_scale_factor of 2.0 produces screenshots at 3840x2160 resolution.
Increasing this value improves screenshot quality but may increase
memory usage and rendering time.
Default: 1.0.
verbose (bool): Enable verbose logging.
Default: True.
accept_downloads (bool): Whether to allow file downloads. If True, requires a downloads_path.
Default: False.
downloads_path (str or None): Directory to store downloaded files. If None and accept_downloads is True,
a default path will be created. Default: None.
storage_state (str or dict or None): An in-memory storage state (cookies, localStorage).
Default: None.
ignore_https_errors (bool): Ignore HTTPS certificate errors. Default: True.
java_script_enabled (bool): Enable JavaScript execution in pages. Default: True.
cookies (list): List of cookies to add to the browser context. Each cookie is a dict with fields like
{"name": "...", "value": "...", "url": "..."}.
Default: [].
headers (dict): Extra HTTP headers to apply to all requests in this context.
Default: {}.
user_agent (str): Custom User-Agent string to use. Default: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36".
user_agent_mode (str or None): Mode for generating the user agent (e.g., "random"). If None, use the provided
user_agent as-is. Default: None.
user_agent_generator_config (dict or None): Configuration for user agent generation if user_agent_mode is set.
Default: None.
text_mode (bool): If True, disables images and other rich content for potentially faster load times.
Default: False.
light_mode (bool): Disables certain background features for performance gains. Default: False.
extra_args (list): Additional command-line arguments passed to the browser.
Default: [].
enable_stealth (bool): If True, applies playwright-stealth to bypass basic bot detection.
Cannot be used with use_undetected browser mode. Default: False.
memory_saving_mode (bool): If True, adds aggressive cache discard and V8 heap cap flags
to reduce Chromium memory growth. Recommended for high-volume
crawling (1000+ pages). May slightly reduce performance due to
cache eviction. Default: False.
max_pages_before_recycle (int): Number of pages to crawl before recycling the browser
process to reclaim leaked memory. 0 = disabled.
Recommended: 500-1000 for long-running crawlers.
Default: 0.
avoid_ads (bool): If True, blocks ad-related and tracker network requests at the
browser context level using a curated blocklist of top ad/tracker
domains. Default: False.
avoid_css (bool): If True, blocks loading of CSS files (css, less, scss, sass) to
reduce resource usage and speed up crawling. Default: False.
"""
def __init__(
self,
browser_type: str = "chromium",
headless: bool = True,
browser_mode: str = "dedicated",
use_managed_browser: bool = False,
cdp_url: str = None,
browser_context_id: str = None,
target_id: str = None,
cdp_cleanup_on_close: bool = False,
cdp_close_delay: float = 1.0,
cache_cdp_connection: bool = False,
create_isolated_context: bool = False,
use_persistent_context: bool = False,
user_data_dir: str = None,
chrome_channel: str = "chromium",
channel: str = "chromium",
proxy: str = None,
proxy_config: Union[ProxyConfig, dict, None] = None,
viewport_width: int = 1080,
viewport_height: int = 600,
viewport: dict = None,
device_scale_factor: float = 1.0,
accept_downloads: bool = False,
downloads_path: str = None,
storage_state: Union[str, dict, None] = None,
ignore_https_errors: bool = True,
java_script_enabled: bool = True,
sleep_on_close: bool = False,
verbose: bool = True,
cookies: list = None,
headers: dict = None,
user_agent: str = (
# "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: str = "",
user_agent_generator_config: dict = {},
text_mode: bool = False,
light_mode: bool = False,
extra_args: list = None,
debugging_port: int = 9222,
host: str = "localhost",
enable_stealth: bool = False,
avoid_ads: bool = False,
avoid_css: bool = False,
init_scripts: List[str] = None,
memory_saving_mode: bool = False,
max_pages_before_recycle: int = 0,
):
self.browser_type = browser_type
self.headless = headless
self.browser_mode = browser_mode
self.use_managed_browser = use_managed_browser
self.cdp_url = cdp_url
self.browser_context_id = browser_context_id
self.target_id = target_id
self.cdp_cleanup_on_close = cdp_cleanup_on_close
self.cdp_close_delay = cdp_close_delay
self.cache_cdp_connection = cache_cdp_connection
self.create_isolated_context = create_isolated_context
self.use_persistent_context = use_persistent_context
self.user_data_dir = user_data_dir
self.chrome_channel = chrome_channel or self.browser_type or "chromium"
self.channel = channel or self.browser_type or "chromium"
if self.browser_type in ["firefox", "webkit"]:
self.channel = ""
self.chrome_channel = ""
if proxy:
warnings.warn("The 'proxy' parameter is deprecated and will be removed in a future release. Use 'proxy_config' instead.", UserWarning)
self.proxy = proxy
self.proxy_config = proxy_config
if isinstance(self.proxy_config, dict):
self.proxy_config = ProxyConfig.from_dict(self.proxy_config)
if isinstance(self.proxy_config, str):
self.proxy_config = ProxyConfig.from_string(self.proxy_config)
if self.proxy and self.proxy_config:
warnings.warn("Both 'proxy' and 'proxy_config' are provided. 'proxy_config' will take precedence.", UserWarning)
self.proxy = None
elif self.proxy:
# Convert proxy string to ProxyConfig if proxy_config is not provided
self.proxy_config = ProxyConfig.from_string(self.proxy)
self.proxy = None
self.viewport_width = viewport_width
self.viewport_height = viewport_height
self.viewport = viewport
if self.viewport is not None:
self.viewport_width = self.viewport.get("width", 1080)
self.viewport_height = self.viewport.get("height", 600)
self.device_scale_factor = device_scale_factor
self.accept_downloads = accept_downloads
self.downloads_path = downloads_path
self.storage_state = storage_state
self.ignore_https_errors = ignore_https_errors
self.java_script_enabled = java_script_enabled
self.cookies = cookies if cookies is not None else []
self.headers = headers if headers is not None else {}
self.user_agent = user_agent
self.user_agent_mode = user_agent_mode
self.user_agent_generator_config = user_agent_generator_config
self.text_mode = text_mode
self.light_mode = light_mode
self.extra_args = extra_args if extra_args is not None else []
self.sleep_on_close = sleep_on_close
self.verbose = verbose
self.debugging_port = debugging_port
self.host = host
self.enable_stealth = enable_stealth
self.avoid_ads = avoid_ads
self.avoid_css = avoid_css
self.init_scripts = init_scripts if init_scripts is not None else []
self.memory_saving_mode = memory_saving_mode
self.max_pages_before_recycle = max_pages_before_recycle
fa_user_agenr_generator = ValidUAGenerator()
if self.user_agent_mode == "random":
self.user_agent = fa_user_agenr_generator.generate(
**(self.user_agent_generator_config or {})
)
else:
pass
self.browser_hint = UAGen.generate_client_hints(self.user_agent)
self.headers.setdefault("sec-ch-ua", self.browser_hint)
# Set appropriate browser management flags based on browser_mode
if self.browser_mode == "builtin":
# Builtin mode uses managed browser connecting to builtin CDP endpoint
self.use_managed_browser = True
# cdp_url will be set later by browser_manager
elif self.browser_mode == "docker":
# Docker mode uses managed browser with CDP to connect to browser in container
self.use_managed_browser = True
# cdp_url will be set later by docker browser strategy
elif self.browser_mode == "custom" and self.cdp_url:
# Custom mode with explicit CDP URL
self.use_managed_browser = True
elif self.browser_mode == "dedicated":
# Dedicated mode uses a new browser instance each time
pass
# If persistent context is requested, ensure managed browser is enabled
if self.use_persistent_context:
self.use_managed_browser = True
# Validate stealth configuration
if self.enable_stealth and self.use_managed_browser and self.browser_mode == "builtin":
raise ValueError(
"enable_stealth cannot be used with browser_mode='builtin'. "
"Stealth mode requires a dedicated browser instance."
)
@staticmethod
def from_kwargs(kwargs: dict) -> "BrowserConfig":
# Auto-deserialize any dict values that use the {"type": ..., "params": ...}
# serialization format (e.g. from JSON API requests or dump()/load() roundtrips).
kwargs = {
k: from_serializable_dict(v) if isinstance(v, dict) and "type" in v else v
for k, v in kwargs.items()
}
# Only pass keys present in kwargs so that __init__ defaults (and
# set_defaults() overrides) are respected for missing keys.
valid = inspect.signature(BrowserConfig.__init__).parameters.keys() - {"self"}
return BrowserConfig(**{k: v for k, v in kwargs.items() if k in valid})
def to_dict(self):
result = {
"browser_type": self.browser_type,
"headless": self.headless,
"browser_mode": self.browser_mode,
"use_managed_browser": self.use_managed_browser,
"cdp_url": self.cdp_url,
"browser_context_id": self.browser_context_id,
"target_id": self.target_id,
"cdp_cleanup_on_close": self.cdp_cleanup_on_close,
"create_isolated_context": self.create_isolated_context,
"use_persistent_context": self.use_persistent_context,
"user_data_dir": self.user_data_dir,
"chrome_channel": self.chrome_channel,
"channel": self.channel,
"proxy": self.proxy,
"proxy_config": self.proxy_config.to_dict() if hasattr(self.proxy_config, 'to_dict') else self.proxy_config,
"viewport_width": self.viewport_width,
"viewport_height": self.viewport_height,
"device_scale_factor": self.device_scale_factor,
"accept_downloads": self.accept_downloads,
"downloads_path": self.downloads_path,
"storage_state": self.storage_state,
"ignore_https_errors": self.ignore_https_errors,
"java_script_enabled": self.java_script_enabled,
"cookies": self.cookies,
"headers": self.headers,
"user_agent": self.user_agent,
"user_agent_mode": self.user_agent_mode,
"user_agent_generator_config": self.user_agent_generator_config,
"text_mode": self.text_mode,
"light_mode": self.light_mode,
"extra_args": self.extra_args,
"sleep_on_close": self.sleep_on_close,
"verbose": self.verbose,
"debugging_port": self.debugging_port,
"host": self.host,
"enable_stealth": self.enable_stealth,
"avoid_ads": self.avoid_ads,
"avoid_css": self.avoid_css,
"init_scripts": self.init_scripts,
"memory_saving_mode": self.memory_saving_mode,
"max_pages_before_recycle": self.max_pages_before_recycle,
}
return result
def clone(self, **kwargs):
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
BrowserConfig: A new instance with the specified updates
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return BrowserConfig.from_kwargs(config_dict)
# Create a funciton returns dict of the object
def dump(self) -> dict:
# Serialize the object to a dictionary
return to_serializable_dict(self)
@staticmethod
def load(data: dict, provenance: "Provenance" = None) -> "BrowserConfig":
# Deserialize the object from a dictionary
if provenance is None:
provenance = Provenance.TRUSTED
config = from_serializable_dict(data, provenance)
if isinstance(config, BrowserConfig):
return config
# Plain-dict path: enforce the untrusted gate on top-level fields too,
# so a body that sends bare kwargs (no {type,params}) cannot bypass it.
if provenance == Provenance.UNTRUSTED and isinstance(config, dict):
config = _enforce_untrusted("BrowserConfig", config)
return BrowserConfig.from_kwargs(config)
def set_nstproxy(
self,
token: str,
channel_id: str,
country: str = "ANY",
state: str = "",
city: str = "",
protocol: str = "http",
session_duration: int = 10,
):
"""
Fetch a proxy from NSTProxy API and automatically assign it to proxy_config.
Get your NSTProxy token from: https://app.nstproxy.com/profile
Args:
token (str): NSTProxy API token.
channel_id (str): NSTProxy channel ID.
country (str, optional): Country code (default: "ANY").
state (str, optional): State code (default: "").
city (str, optional): City name (default: "").
protocol (str, optional): Proxy protocol ("http" or "socks5"). Defaults to "http".
session_duration (int, optional): Session duration in minutes (0 = rotate each request). Defaults to 10.
Raises:
ValueError: If the API response format is invalid.
PermissionError: If the API returns an error message.
"""
# --- Validate input early ---
if not token or not channel_id:
raise ValueError("[NSTProxy] token and channel_id are required")
if protocol not in ("http", "socks5"):
raise ValueError(f"[NSTProxy] Invalid protocol: {protocol}")
# --- Build NSTProxy API URL ---
params = {
"fType": 2,
"count": 1,
"channelId": channel_id,
"country": country,
"protocol": protocol,
"sessionDuration": session_duration,
"token": token,
}
if state:
params["state"] = state
if city:
params["city"] = city
url = "https://api.nstproxy.com/api/v1/generate/apiproxies"
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# --- Handle API error response ---
if isinstance(data, dict) and data.get("err"):
raise PermissionError(f"[NSTProxy] API Error: {data.get('msg', 'Unknown error')}")
if not isinstance(data, list) or not data:
raise ValueError("[NSTProxy] Invalid API response — expected a non-empty list")
proxy_info = data[0]
# --- Apply proxy config ---
self.proxy_config = ProxyConfig(
server=f"{protocol}://{proxy_info['ip']}:{proxy_info['port']}",
username=proxy_info["username"],
password=proxy_info["password"],
)
except Exception as e:
print(f"[NSTProxy] ❌ Failed to set proxy: {e}")
raise
class VirtualScrollConfig:
"""Configuration for virtual scroll handling.
This config enables capturing content from pages with virtualized scrolling
(like Twitter, Instagram feeds) where DOM elements are recycled as user scrolls.
"""
def __init__(
self,
container_selector: str,
scroll_count: int = 10,
scroll_by: Union[str, int] = "container_height",
wait_after_scroll: float = 0.5,
):
"""
Initialize virtual scroll configuration.
Args:
container_selector: CSS selector for the scrollable container
scroll_count: Maximum number of scrolls to perform
scroll_by: Amount to scroll - can be:
- "container_height": scroll by container's height
- "page_height": scroll by viewport height
- int: fixed pixel amount
wait_after_scroll: Seconds to wait after each scroll for content to load
"""
self.container_selector = container_selector
self.scroll_count = scroll_count
self.scroll_by = scroll_by
self.wait_after_scroll = wait_after_scroll
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {
"container_selector": self.container_selector,
"scroll_count": self.scroll_count,
"scroll_by": self.scroll_by,
"wait_after_scroll": self.wait_after_scroll,
}
@classmethod
def from_dict(cls, data: dict) -> "VirtualScrollConfig":
"""Create instance from dictionary."""
return cls(**data)
class LinkPreviewConfig:
"""Configuration for link head extraction and scoring."""
def __init__(
self,
include_internal: bool = True,
include_external: bool = False,
include_patterns: Optional[List[str]] = None,
exclude_patterns: Optional[List[str]] = None,
concurrency: int = 10,
timeout: int = 5,
max_links: int = 100,
query: Optional[str] = None,
score_threshold: Optional[float] = None,
verbose: bool = False
):
"""
Initialize link extraction configuration.
Args:
include_internal: Whether to include same-domain links
include_external: Whether to include different-domain links
include_patterns: List of glob patterns to include (e.g., ["*/docs/*", "*/api/*"])
exclude_patterns: List of glob patterns to exclude (e.g., ["*/login*", "*/admin*"])
concurrency: Number of links to process simultaneously
timeout: Timeout in seconds for each link's head extraction
max_links: Maximum number of links to process (prevents overload)
query: Query string for BM25 contextual scoring (optional)
score_threshold: Minimum relevance score to include links (0.0-1.0, optional)
verbose: Show detailed progress during extraction
"""
self.include_internal = include_internal
self.include_external = include_external
self.include_patterns = include_patterns
self.exclude_patterns = exclude_patterns
self.concurrency = concurrency
self.timeout = timeout
self.max_links = max_links
self.query = query
self.score_threshold = score_threshold
self.verbose = verbose
# Validation
if concurrency <= 0:
raise ValueError("concurrency must be positive")
if timeout <= 0:
raise ValueError("timeout must be positive")
if max_links <= 0:
raise ValueError("max_links must be positive")
if score_threshold is not None and not (0.0 <= score_threshold <= 1.0):
raise ValueError("score_threshold must be between 0.0 and 1.0")
if not include_internal and not include_external:
raise ValueError("At least one of include_internal or include_external must be True")
@staticmethod
def from_dict(config_dict: Dict[str, Any]) -> "LinkPreviewConfig":
"""Create LinkPreviewConfig from dictionary (for backward compatibility)."""
if not config_dict:
return None
return LinkPreviewConfig(
include_internal=config_dict.get("include_internal", True),
include_external=config_dict.get("include_external", False),
include_patterns=config_dict.get("include_patterns"),
exclude_patterns=config_dict.get("exclude_patterns"),
concurrency=config_dict.get("concurrency", 10),
timeout=config_dict.get("timeout", 5),
max_links=config_dict.get("max_links", 100),
query=config_dict.get("query"),
score_threshold=config_dict.get("score_threshold"),
verbose=config_dict.get("verbose", False)
)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary format."""
return {
"include_internal": self.include_internal,
"include_external": self.include_external,
"include_patterns": self.include_patterns,
"exclude_patterns": self.exclude_patterns,
"concurrency": self.concurrency,
"timeout": self.timeout,
"max_links": self.max_links,
"query": self.query,
"score_threshold": self.score_threshold,
"verbose": self.verbose
}
def clone(self, **kwargs) -> "LinkPreviewConfig":
"""Create a copy with updated values."""
config_dict = self.to_dict()
config_dict.update(kwargs)
return LinkPreviewConfig.from_dict(config_dict)
class HTTPCrawlerConfig:
"""HTTP-specific crawler configuration"""
method: str = "GET"
headers: Optional[Dict[str, str]] = None
data: Optional[Dict[str, Any]] = None
json: Optional[Dict[str, Any]] = None
follow_redirects: bool = True
verify_ssl: bool = True
downloads_path: Optional[str] = None
def __init__(
self,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
data: Optional[Dict[str, Any]] = None,
json: Optional[Dict[str, Any]] = None,
follow_redirects: bool = True,
verify_ssl: bool = True,
downloads_path: Optional[str] = None,
):
self.method = method
self.headers = headers
self.data = data
self.json = json
self.follow_redirects = follow_redirects
self.verify_ssl = verify_ssl
self.downloads_path = downloads_path
@staticmethod
def from_kwargs(kwargs: dict) -> "HTTPCrawlerConfig":
return HTTPCrawlerConfig(
method=kwargs.get("method", "GET"),
headers=kwargs.get("headers"),
data=kwargs.get("data"),
json=kwargs.get("json"),
follow_redirects=kwargs.get("follow_redirects", True),
verify_ssl=kwargs.get("verify_ssl", True),
downloads_path=kwargs.get("downloads_path"),
)
def to_dict(self):
return {
"method": self.method,
"headers": self.headers,
"data": self.data,
"json": self.json,
"follow_redirects": self.follow_redirects,
"verify_ssl": self.verify_ssl,
"downloads_path": self.downloads_path,
}
def clone(self, **kwargs):
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
HTTPCrawlerConfig: A new instance with the specified updates
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return HTTPCrawlerConfig.from_kwargs(config_dict)
def dump(self) -> dict:
return to_serializable_dict(self)
@staticmethod
def load(data: dict, provenance: "Provenance" = None) -> "HTTPCrawlerConfig":
if provenance is None:
provenance = Provenance.TRUSTED
config = from_serializable_dict(data, provenance)
if isinstance(config, HTTPCrawlerConfig):
return config
if provenance == Provenance.UNTRUSTED and isinstance(config, dict):
config = _enforce_untrusted("HTTPCrawlerConfig", config)
return HTTPCrawlerConfig.from_kwargs(config)
@_with_defaults
class CrawlerRunConfig():
"""
Configuration class for controlling how the crawler runs each crawl operation.
This includes parameters for content extraction, page manipulation, waiting conditions,
caching, and other runtime behaviors.
This centralizes parameters that were previously scattered as kwargs to `arun()` and related methods.
By using this class, you have a single place to understand and adjust the crawling options.
Attributes:
# Deep Crawl Parameters
deep_crawl_strategy (DeepCrawlStrategy or None): Strategy to use for deep crawling.
# Content Processing Parameters
word_count_threshold (int): Minimum word count threshold before processing content.
Default: MIN_WORD_THRESHOLD (typically 200).
extraction_strategy (ExtractionStrategy or None): Strategy to extract structured data from crawled pages.
Default: None (NoExtractionStrategy is used if None).
chunking_strategy (ChunkingStrategy): Strategy to chunk content before extraction.
Default: RegexChunking().
markdown_generator (MarkdownGenerationStrategy): Strategy for generating markdown.
Default: None.
only_text (bool): If True, attempt to extract text-only content where applicable.
Default: False.
css_selector (str or None): CSS selector to extract a specific portion of the page.
Default: None.
target_elements (list of str or None): List of CSS selectors for specific elements for Markdown generation
and structured data extraction. When you set this, only the contents
of these elements are processed for extraction and Markdown generation.
If you do not set any value, the entire page is processed.
The difference between this and css_selector is that this will shrink
the initial raw HTML to the selected element, while this will only affect
the extraction and Markdown generation.
Default: None
excluded_tags (list of str or None): List of HTML tags to exclude from processing.
Default: None.
excluded_selector (str or None): CSS selector to exclude from processing.
Default: None.
keep_data_attributes (bool): If True, retain `data-*` attributes while removing unwanted attributes.
Default: False.
keep_attrs (list of str): List of HTML attributes to keep during processing.
Default: [].
remove_forms (bool): If True, remove all `<form>` elements from the HTML.
Default: False.
prettiify (bool): If True, apply `fast_format_html` to produce prettified HTML output.
Default: False.
parser_type (str): Type of parser to use for HTML parsing.
Default: "lxml".
scraping_strategy (ContentScrapingStrategy): Scraping strategy to use.
Default: LXMLWebScrapingStrategy.
proxy_config (ProxyConfig or dict or None): Detailed proxy configuration, e.g. {"server": "...", "username": "..."}.
If None, no additional proxy config. Default: None.
# Sticky Proxy Session Parameters
proxy_session_id (str or None): When set, maintains the same proxy for all requests sharing this session ID.
The proxy is acquired on first request and reused for subsequent requests.
Session expires when explicitly released or crawler context is closed.
Default: None.
proxy_session_ttl (int or None): Time-to-live for sticky session in seconds.
After TTL expires, a new proxy is acquired on next request.
Default: None (session lasts until explicitly released or crawler closes).
proxy_session_auto_release (bool): If True, automatically release the proxy session after a batch operation.
Useful for arun_many() to clean up sessions automatically.
Default: False.
# Browser Location and Identity Parameters
locale (str or None): Locale to use for the browser context (e.g., "en-US").
Default: None.
timezone_id (str or None): Timezone identifier to use for the browser context (e.g., "America/New_York").
Default: None.
geolocation (GeolocationConfig or None): Geolocation configuration for the browser.
Default: None.
# SSL Parameters
fetch_ssl_certificate: bool = False,
# Caching Parameters
cache_mode (CacheMode or None): Defines how caching is handled.
If None, defaults to CacheMode.ENABLED internally.
Default: CacheMode.BYPASS.
session_id (str or None): Optional session ID to persist the browser context and the created
page instance. If the ID already exists, the crawler does not
create a new page and uses the current page to preserve the state.
bypass_cache (bool): Legacy parameter, if True acts like CacheMode.BYPASS.
Default: False.
disable_cache (bool): Legacy parameter, if True acts like CacheMode.DISABLED.
Default: False.
no_cache_read (bool): Legacy parameter, if True acts like CacheMode.WRITE_ONLY.
Default: False.
no_cache_write (bool): Legacy parameter, if True acts like CacheMode.READ_ONLY.
Default: False.
shared_data (dict or None): Shared data to be passed between hooks.
Default: None.
# Cache Validation Parameters (Smart Cache)
check_cache_freshness (bool): If True, validates cached content freshness using HTTP
conditional requests (ETag/Last-Modified) and head fingerprinting
before returning cached results. Avoids full browser crawls when
content hasn't changed. Only applies when cache_mode allows reads.
Default: False.
cache_validation_timeout (float): Timeout in seconds for cache validation HTTP requests.
Default: 10.0.
# Page Navigation and Timing Parameters
wait_until (str): The condition to wait for when navigating, e.g. "domcontentloaded".
Default: "domcontentloaded".
page_timeout (int): Timeout in ms for page operations like navigation.
Default: 60000 (60 seconds).
wait_for (str or None): A CSS selector or JS condition to wait for before extracting content.
Default: None.
wait_for_timeout (int or None): Specific timeout in ms for the wait_for condition.
If None, uses page_timeout instead.
Default: None.
wait_for_images (bool): If True, wait for images to load before extracting content.
Default: False.
delay_before_return_html (float): Delay in seconds before retrieving final HTML.
Default: 0.1.
mean_delay (float): Mean base delay between requests when calling arun_many.
Default: 0.1.
max_range (float): Max random additional delay range for requests in arun_many.
Default: 0.3.
semaphore_count (int): Number of concurrent operations allowed.
Default: 5.
# Page Interaction Parameters
js_code (str or list of str or None): JavaScript code/snippets to run on the page
after wait_for and delay_before_return_html.
Default: None.
js_code_before_wait (str or list of str or None): JavaScript to run BEFORE wait_for.
Use for triggering loading that wait_for then checks.
Default: None.
js_only (bool): If True, indicates subsequent calls are JS-driven updates, not full page loads.
Default: False.
ignore_body_visibility (bool): If True, ignore whether the body is visible before proceeding.
Default: True.
scan_full_page (bool): If True, scroll through the entire page to load all content.
Default: False.
scroll_delay (float): Delay in seconds between scroll steps if scan_full_page is True.
Default: 0.2.
max_scroll_steps (Optional[int]): Maximum number of scroll steps to perform during full page scan.
If None, scrolls until the entire page is loaded. Default: None.
process_iframes (bool): If True, attempts to process and inline iframe content.
Default: False.
flatten_shadow_dom (bool): If True, flatten shadow DOM content into the light DOM
before HTML capture so page.content() includes it.
Also injects an init script to force-open closed shadow roots.
Default: False.
remove_overlay_elements (bool): If True, remove overlays/popups before extracting HTML.
Default: False.
remove_consent_popups (bool): If True, remove GDPR/cookie consent popups (IAB TCF/CMP)
before extracting HTML. Targets known CMP providers like
OneTrust, Cookiebot, TrustArc, Quantcast, Didomi, etc.
Default: False.
simulate_user (bool): If True, simulate user interactions (mouse moves, clicks) for anti-bot measures.
Default: False.
override_navigator (bool): If True, overrides navigator properties for more human-like behavior.
Default: False.
magic (bool): If True, attempts automatic handling of overlays/popups.
Default: False.
adjust_viewport_to_content (bool): If True, adjust viewport according to the page content dimensions.
Default: False.
# Media Handling Parameters
screenshot (bool): Whether to take a screenshot after crawling.
Default: False.
screenshot_wait_for (float or None): Additional wait time before taking a screenshot.
Default: None.
screenshot_height_threshold (int): Threshold for page height to decide screenshot strategy.
Default: SCREENSHOT_HEIGHT_TRESHOLD (from config, e.g. 20000).
force_viewport_screenshot (bool): If True, always take viewport-only screenshots regardless of page height.
When False, uses automatic decision (viewport for short pages, full-page for long pages).
Default: False.
pdf (bool): Whether to generate a PDF of the page.
Default: False.
image_description_min_word_threshold (int): Minimum words for image description extraction.
Default: IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD (e.g., 50).
image_score_threshold (int): Minimum score threshold for processing an image.
Default: IMAGE_SCORE_THRESHOLD (e.g., 3).
exclude_external_images (bool): If True, exclude all external images from processing.
Default: False.
table_score_threshold (int): Minimum score threshold for processing a table.
Default: 7.
table_extraction (TableExtractionStrategy): Strategy to use for table extraction.
Default: DefaultTableExtraction with table_score_threshold.
# Virtual Scroll Parameters
virtual_scroll_config (VirtualScrollConfig or dict or None): Configuration for handling virtual scroll containers.
Used for capturing content from pages with virtualized
scrolling (e.g., Twitter, Instagram feeds).
Default: None.
# Link and Domain Handling Parameters
exclude_social_media_domains (list of str): List of domains to exclude for social media links.
Default: SOCIAL_MEDIA_DOMAINS (from config).
exclude_external_links (bool): If True, exclude all external links from the results.
Default: False.
exclude_internal_links (bool): If True, exclude internal links from the results.
Default: False.
exclude_social_media_links (bool): If True, exclude links pointing to social media domains.
Default: False.
exclude_domains (list of str): List of specific domains to exclude from results.
Default: [].
exclude_internal_links (bool): If True, exclude internal links from the results.
Default: False.
score_links (bool): If True, calculate intrinsic quality scores for all links using URL structure,
text quality, and contextual relevance metrics. Separate from link_preview_config.
Default: False.
# Debugging and Logging Parameters
verbose (bool): Enable verbose logging.
Default: True.
log_console (bool): If True, log console messages from the page.
Default: False.
# HTTP Crwler Strategy Parameters
method (str): HTTP method to use for the request, when using AsyncHTTPCrwalerStrategy.
Default: "GET".
data (dict): Data to send in the request body, when using AsyncHTTPCrwalerStrategy.
Default: None.
json (dict): JSON data to send in the request body, when using AsyncHTTPCrwalerStrategy.
# Connection Parameters
stream (bool): If True, enables streaming of crawled URLs as they are processed when used with arun_many.
Default: False.
process_in_browser (bool): If True, forces raw:/file:// URLs to be processed through the browser
pipeline (enabling js_code, wait_for, scrolling, etc.). When False (default),
raw:/file:// URLs use a fast path that returns HTML directly without browser
interaction. This is automatically enabled when browser-requiring parameters
are detected (js_code, wait_for, screenshot, pdf, etc.).
Default: False.
check_robots_txt (bool): Whether to check robots.txt rules before crawling. Default: False
Default: False.
user_agent (str): Custom User-Agent string to use.
Default: None.
user_agent_mode (str or None): Mode for generating the user agent (e.g., "random"). If None, use the provided user_agent as-is.
Default: None.
user_agent_generator_config (dict or None): Configuration for user agent generation if user_agent_mode is set.
Default: None.
# Experimental Parameters
experimental (dict): Dictionary containing experimental parameters that are in beta phase.
This allows passing temporary features that are not yet fully integrated
into the main parameter set.
Default: None.
url: str = None # This is not a compulsory parameter
"""
_UNWANTED_PROPS = {
'disable_cache' : 'Instead, use cache_mode=CacheMode.DISABLED',
'bypass_cache' : 'Instead, use cache_mode=CacheMode.BYPASS',
'no_cache_read' : 'Instead, use cache_mode=CacheMode.WRITE_ONLY',
'no_cache_write' : 'Instead, use cache_mode=CacheMode.READ_ONLY',
}
def __init__(
self,
# Content Processing Parameters
word_count_threshold: int = MIN_WORD_THRESHOLD,
extraction_strategy: ExtractionStrategy = None,
chunking_strategy: ChunkingStrategy = RegexChunking(),
markdown_generator: MarkdownGenerationStrategy = DefaultMarkdownGenerator(),
only_text: bool = False,
css_selector: str = None,
target_elements: List[str] = None,
excluded_tags: list = None,
excluded_selector: str = None,
keep_data_attributes: bool = False,
keep_attrs: list = None,
remove_forms: bool = False,
prettiify: bool = False,
parser_type: str = "lxml",
scraping_strategy: ContentScrapingStrategy = None,
proxy_config: Union["ProxyConfig", List["ProxyConfig"], dict, str, None] = None,
proxy_rotation_strategy: Optional[ProxyRotationStrategy] = None,
# Sticky Proxy Session Parameters
proxy_session_id: Optional[str] = None,
proxy_session_ttl: Optional[int] = None,
proxy_session_auto_release: bool = False,
# Browser Location and Identity Parameters
locale: Optional[str] = None,
timezone_id: Optional[str] = None,
geolocation: Optional[GeolocationConfig] = None,
# SSL Parameters
fetch_ssl_certificate: bool = False,
# Caching Parameters
cache_mode: CacheMode = CacheMode.BYPASS,
session_id: str = None,
bypass_cache: bool = False,
disable_cache: bool = False,
no_cache_read: bool = False,
no_cache_write: bool = False,
shared_data: dict = None,
# Cache Validation Parameters (Smart Cache)
check_cache_freshness: bool = False,
cache_validation_timeout: float = 10.0,
# Page Navigation and Timing Parameters
wait_until: str = "domcontentloaded",
page_timeout: int = PAGE_TIMEOUT,
wait_for: str = None,
wait_for_timeout: int = None,
wait_for_images: bool = False,
delay_before_return_html: float = 0.1,
mean_delay: float = 0.1,
max_range: float = 0.3,
semaphore_count: int = 5,
# Page Interaction Parameters
js_code: Union[str, List[str]] = None,
js_code_before_wait: Union[str, List[str]] = None,
c4a_script: Union[str, List[str]] = None,
js_only: bool = False,
ignore_body_visibility: bool = True,
scan_full_page: bool = False,
scroll_delay: float = 0.2,
max_scroll_steps: Optional[int] = None,
process_iframes: bool = False,
flatten_shadow_dom: bool = False,
remove_overlay_elements: bool = False,
remove_consent_popups: bool = False,
simulate_user: bool = False,
override_navigator: bool = False,
magic: bool = False,
adjust_viewport_to_content: bool = False,
# Media Handling Parameters
screenshot: bool = False,
screenshot_wait_for: float = None,
screenshot_height_threshold: int = SCREENSHOT_HEIGHT_TRESHOLD,
force_viewport_screenshot: bool = False,
pdf: bool = False,
capture_mhtml: bool = False,
image_description_min_word_threshold: int = IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD,
image_score_threshold: int = IMAGE_SCORE_THRESHOLD,
table_score_threshold: int = 7,
table_extraction: TableExtractionStrategy = None,
exclude_external_images: bool = False,
exclude_all_images: bool = False,
# Link and Domain Handling Parameters
exclude_social_media_domains: list = None,
exclude_external_links: bool = False,
exclude_social_media_links: bool = False,
exclude_domains: list = None,
exclude_internal_links: bool = False,
score_links: bool = False,
preserve_https_for_internal_links: bool = False,
# Debugging and Logging Parameters
verbose: bool = True,
log_console: bool = False,
# Network and Console Capturing Parameters
capture_network_requests: bool = False,
capture_console_messages: bool = False,
# Connection Parameters
method: str = "GET",
stream: bool = False,
prefetch: bool = False, # When True, return only HTML + links (skip heavy processing)
process_in_browser: bool = False, # Force browser processing for raw:/file:// URLs
url: str = None,
base_url: str = None, # Base URL for markdown link resolution (used with raw: HTML)
check_robots_txt: bool = False,
user_agent: str = None,
user_agent_mode: str = None,
user_agent_generator_config: dict = {},
# Deep Crawl Parameters
deep_crawl_strategy: Optional[DeepCrawlStrategy] = None,
# Link Extraction Parameters
link_preview_config: Union[LinkPreviewConfig, Dict[str, Any]] = None,
# Virtual Scroll Parameters
virtual_scroll_config: Union[VirtualScrollConfig, Dict[str, Any]] = None,
# URL Matching Parameters
url_matcher: Optional[UrlMatcher] = None,
match_mode: MatchMode = MatchMode.OR,
# Experimental Parameters
experimental: Dict[str, Any] = None,
# Anti-Bot Retry Parameters
max_retries: int = 0,
fallback_fetch_function: Optional[Callable[[str], Awaitable[str]]] = None,
):
# TODO: Planning to set properties dynamically based on the __init__ signature
self.url = url
self.base_url = base_url # Base URL for markdown link resolution
# Content Processing Parameters
self.word_count_threshold = word_count_threshold
self.extraction_strategy = extraction_strategy
self.chunking_strategy = chunking_strategy
self.markdown_generator = markdown_generator
self.only_text = only_text
self.css_selector = css_selector
self.target_elements = target_elements or []
self.excluded_tags = excluded_tags or []
self.excluded_selector = excluded_selector or ""
self.keep_data_attributes = keep_data_attributes
self.keep_attrs = keep_attrs or []
self.remove_forms = remove_forms
self.prettiify = prettiify
self.parser_type = parser_type
self.scraping_strategy = scraping_strategy or LXMLWebScrapingStrategy()
self.proxy_config = proxy_config # runs through property setter
self.proxy_rotation_strategy = proxy_rotation_strategy
# Sticky Proxy Session Parameters
self.proxy_session_id = proxy_session_id
self.proxy_session_ttl = proxy_session_ttl
self.proxy_session_auto_release = proxy_session_auto_release
# Browser Location and Identity Parameters
self.locale = locale
self.timezone_id = timezone_id
self.geolocation = geolocation
# SSL Parameters
self.fetch_ssl_certificate = fetch_ssl_certificate
# Caching Parameters
self.cache_mode = cache_mode
self.session_id = session_id
self.bypass_cache = bypass_cache
self.disable_cache = disable_cache
self.no_cache_read = no_cache_read
self.no_cache_write = no_cache_write
self.shared_data = shared_data
# Cache Validation (Smart Cache)
self.check_cache_freshness = check_cache_freshness
self.cache_validation_timeout = cache_validation_timeout
# Page Navigation and Timing Parameters
self.wait_until = wait_until
self.page_timeout = page_timeout
self.wait_for = wait_for
self.wait_for_timeout = wait_for_timeout
self.wait_for_images = wait_for_images
self.delay_before_return_html = delay_before_return_html
self.mean_delay = mean_delay
self.max_range = max_range
self.semaphore_count = semaphore_count
# Page Interaction Parameters
self.js_code = js_code
self.js_code_before_wait = js_code_before_wait
self.c4a_script = c4a_script
self.js_only = js_only
self.ignore_body_visibility = ignore_body_visibility
self.scan_full_page = scan_full_page
self.scroll_delay = scroll_delay
self.max_scroll_steps = max_scroll_steps
self.process_iframes = process_iframes
self.flatten_shadow_dom = flatten_shadow_dom
self.remove_overlay_elements = remove_overlay_elements
self.remove_consent_popups = remove_consent_popups
self.simulate_user = simulate_user
self.override_navigator = override_navigator
self.magic = magic
self.adjust_viewport_to_content = adjust_viewport_to_content
# Media Handling Parameters
self.screenshot = screenshot
self.screenshot_wait_for = screenshot_wait_for
self.screenshot_height_threshold = screenshot_height_threshold
self.force_viewport_screenshot = force_viewport_screenshot
self.pdf = pdf
self.capture_mhtml = capture_mhtml
self.image_description_min_word_threshold = image_description_min_word_threshold
self.image_score_threshold = image_score_threshold
self.exclude_external_images = exclude_external_images
self.exclude_all_images = exclude_all_images
self.table_score_threshold = table_score_threshold
# Table extraction strategy (default to DefaultTableExtraction if not specified)
if table_extraction is None:
self.table_extraction = DefaultTableExtraction(table_score_threshold=table_score_threshold)
else:
self.table_extraction = table_extraction
# Link and Domain Handling Parameters
self.exclude_social_media_domains = (
exclude_social_media_domains or SOCIAL_MEDIA_DOMAINS
)
self.exclude_external_links = exclude_external_links
self.exclude_social_media_links = exclude_social_media_links
self.exclude_domains = exclude_domains or []
self.exclude_internal_links = exclude_internal_links
self.score_links = score_links
self.preserve_https_for_internal_links = preserve_https_for_internal_links
# Debugging and Logging Parameters
self.verbose = verbose
self.log_console = log_console
# Network and Console Capturing Parameters
self.capture_network_requests = capture_network_requests
self.capture_console_messages = capture_console_messages
# Connection Parameters
self.stream = stream
self.prefetch = prefetch # Prefetch mode: return only HTML + links
self.process_in_browser = process_in_browser # Force browser processing for raw:/file:// URLs
self.method = method
# Robots.txt Handling Parameters
self.check_robots_txt = check_robots_txt
# User Agent Parameters
self.user_agent = user_agent
self.user_agent_mode = user_agent_mode
self.user_agent_generator_config = user_agent_generator_config
# Validate type of extraction strategy and chunking strategy if they are provided
if self.extraction_strategy is not None and not isinstance(
self.extraction_strategy, ExtractionStrategy
):
raise ValueError(
"extraction_strategy must be an instance of ExtractionStrategy"
)
if self.chunking_strategy is not None and not isinstance(
self.chunking_strategy, ChunkingStrategy
):
raise ValueError(
"chunking_strategy must be an instance of ChunkingStrategy"
)
if self.markdown_generator is not None and not isinstance(
self.markdown_generator, MarkdownGenerationStrategy
):
hint = ""
if isinstance(self.markdown_generator, dict):
hint = (
' The JSON format must be {"type": "<ClassName>", "params": {...}}.'
' Note: "params" is required — "options" or other keys are not recognized.'
)
raise ValueError(
"markdown_generator must be an instance of MarkdownGenerationStrategy, "
f"got {type(self.markdown_generator).__name__}.{hint}"
)
# Set default chunking strategy if None
if self.chunking_strategy is None:
self.chunking_strategy = RegexChunking()
# Deep Crawl Parameters
self.deep_crawl_strategy = deep_crawl_strategy
# Link Extraction Parameters
if link_preview_config is None:
self.link_preview_config = None
elif isinstance(link_preview_config, LinkPreviewConfig):
self.link_preview_config = link_preview_config
elif isinstance(link_preview_config, dict):
# Convert dict to config object for backward compatibility
self.link_preview_config = LinkPreviewConfig.from_dict(link_preview_config)
else:
raise ValueError("link_preview_config must be LinkPreviewConfig object or dict")
# Virtual Scroll Parameters
if virtual_scroll_config is None:
self.virtual_scroll_config = None
elif isinstance(virtual_scroll_config, VirtualScrollConfig):
self.virtual_scroll_config = virtual_scroll_config
elif isinstance(virtual_scroll_config, dict):
# Convert dict to config object for backward compatibility
self.virtual_scroll_config = VirtualScrollConfig.from_dict(virtual_scroll_config)
else:
raise ValueError("virtual_scroll_config must be VirtualScrollConfig object or dict")
# URL Matching Parameters
self.url_matcher = url_matcher
self.match_mode = match_mode
# Experimental Parameters
self.experimental = experimental or {}
# Anti-Bot Retry Parameters
self.max_retries = max_retries
self.fallback_fetch_function = fallback_fetch_function
# Compile C4A scripts if provided
if self.c4a_script and not self.js_code:
self._compile_c4a_script()
@staticmethod
def _normalize_proxy_config(value):
"""Normalize proxy_config to ProxyConfig, list of ProxyConfig/None, or None."""
if isinstance(value, list):
normalized = []
for p in value:
if p is None or p == "direct":
normalized.append(None)
elif isinstance(p, dict):
normalized.append(ProxyConfig.from_dict(p))
elif isinstance(p, str):
normalized.append(ProxyConfig.from_string(p))
else:
normalized.append(p)
return normalized
elif isinstance(value, dict):
return ProxyConfig.from_dict(value)
elif isinstance(value, str):
if value == "direct":
return None
return ProxyConfig.from_string(value)
return value # ProxyConfig or None
@property
def proxy_config(self):
return self._proxy_config
@proxy_config.setter
def proxy_config(self, value):
self._proxy_config = CrawlerRunConfig._normalize_proxy_config(value)
def _get_proxy_list(self) -> list:
"""Normalize proxy_config to a list for the retry loop."""
if self.proxy_config is None:
return [None]
if isinstance(self.proxy_config, list):
return self.proxy_config if self.proxy_config else [None]
return [self.proxy_config]
def _compile_c4a_script(self):
"""Compile C4A script to JavaScript"""
try:
# Try importing the compiler
try:
from .script import compile
except ImportError:
from crawl4ai.script import compile
# Handle both string and list inputs
if isinstance(self.c4a_script, str):
scripts = [self.c4a_script]
else:
scripts = self.c4a_script
# Compile each script
compiled_js = []
for i, script in enumerate(scripts):
result = compile(script)
if result.success:
compiled_js.extend(result.js_code)
else:
# Format error message following existing patterns
error = result.first_error
error_msg = (
f"C4A Script compilation error (script {i+1}):\n"
f" Line {error.line}, Column {error.column}: {error.message}\n"
f" Code: {error.source_line}"
)
if error.suggestions:
error_msg += f"\n Suggestion: {error.suggestions[0].message}"
raise ValueError(error_msg)
self.js_code = compiled_js
except ImportError:
raise ValueError(
"C4A script compiler not available. "
"Please ensure crawl4ai.script module is properly installed."
)
except Exception as e:
# Re-raise with context
if "compilation error" not in str(e).lower():
raise ValueError(f"Failed to compile C4A script: {str(e)}")
raise
def is_match(self, url: str) -> bool:
"""Check if this config matches the given URL.
Args:
url: The URL to check against this config's matcher
Returns:
bool: True if this config should be used for the URL or if no matcher is set.
"""
if self.url_matcher is None:
return True
if callable(self.url_matcher):
# Single function matcher
return self.url_matcher(url)
elif isinstance(self.url_matcher, str):
# Single pattern string
from fnmatch import fnmatch
return fnmatch(url, self.url_matcher)
elif isinstance(self.url_matcher, list):
# List of mixed matchers
if not self.url_matcher: # Empty list
return False
results = []
for matcher in self.url_matcher:
if callable(matcher):
results.append(matcher(url))
elif isinstance(matcher, str):
from fnmatch import fnmatch
results.append(fnmatch(url, matcher))
else:
# Skip invalid matchers
continue
# Apply match mode logic
if self.match_mode == MatchMode.OR:
return any(results) if results else False
else: # AND mode
return all(results) if results else False
return False
def __getattr__(self, name):
"""Handle attribute access."""
if name in self._UNWANTED_PROPS:
raise AttributeError(f"Getting '{name}' is deprecated. {self._UNWANTED_PROPS[name]}")
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
def __setattr__(self, name, value):
"""Handle attribute setting."""
# TODO: Planning to set properties dynamically based on the __init__ signature
sig = inspect.signature(self.__init__)
all_params = sig.parameters # Dictionary of parameter names and their details
if name in self._UNWANTED_PROPS and value is not all_params[name].default:
raise AttributeError(f"Setting '{name}' is deprecated. {self._UNWANTED_PROPS[name]}")
super().__setattr__(name, value)
@staticmethod
def from_kwargs(kwargs: dict) -> "CrawlerRunConfig":
# Auto-deserialize any dict values that use the {"type": ..., "params": ...}
# serialization format (e.g. from JSON API requests or dump()/load() roundtrips).
# This covers markdown_generator, extraction_strategy, content_filter, etc.
kwargs = {
k: from_serializable_dict(v) if isinstance(v, dict) and "type" in v else v
for k, v in kwargs.items()
}
# Only pass keys present in kwargs so that __init__ defaults (and
# set_defaults() overrides) are respected for missing keys.
valid = inspect.signature(CrawlerRunConfig.__init__).parameters.keys() - {"self"}
return CrawlerRunConfig(**{k: v for k, v in kwargs.items() if k in valid})
# Create a funciton returns dict of the object
def dump(self) -> dict:
# Serialize the object to a dictionary
return to_serializable_dict(self)
@staticmethod
def load(data: dict, provenance: "Provenance" = None) -> "CrawlerRunConfig":
# Deserialize the object from a dictionary
if provenance is None:
provenance = Provenance.TRUSTED
config = from_serializable_dict(data, provenance)
if isinstance(config, CrawlerRunConfig):
return config
if provenance == Provenance.UNTRUSTED and isinstance(config, dict):
config = _enforce_untrusted("CrawlerRunConfig", config)
return CrawlerRunConfig.from_kwargs(config)
def to_dict(self):
return {
"word_count_threshold": self.word_count_threshold,
"extraction_strategy": self.extraction_strategy,
"chunking_strategy": self.chunking_strategy,
"markdown_generator": self.markdown_generator,
"only_text": self.only_text,
"css_selector": self.css_selector,
"target_elements": self.target_elements,
"excluded_tags": self.excluded_tags,
"excluded_selector": self.excluded_selector,
"keep_data_attributes": self.keep_data_attributes,
"keep_attrs": self.keep_attrs,
"remove_forms": self.remove_forms,
"prettiify": self.prettiify,
"parser_type": self.parser_type,
"scraping_strategy": self.scraping_strategy,
"proxy_config": (
[p.to_dict() if hasattr(p, 'to_dict') else p for p in self.proxy_config]
if isinstance(self.proxy_config, list)
else (self.proxy_config.to_dict() if hasattr(self.proxy_config, 'to_dict') else self.proxy_config)
),
"proxy_rotation_strategy": self.proxy_rotation_strategy,
"proxy_session_id": self.proxy_session_id,
"proxy_session_ttl": self.proxy_session_ttl,
"proxy_session_auto_release": self.proxy_session_auto_release,
"locale": self.locale,
"timezone_id": self.timezone_id,
"geolocation": self.geolocation,
"fetch_ssl_certificate": self.fetch_ssl_certificate,
"cache_mode": self.cache_mode,
"session_id": self.session_id,
"bypass_cache": self.bypass_cache,
"disable_cache": self.disable_cache,
"no_cache_read": self.no_cache_read,
"no_cache_write": self.no_cache_write,
"shared_data": self.shared_data,
"wait_until": self.wait_until,
"page_timeout": self.page_timeout,
"wait_for": self.wait_for,
"wait_for_timeout": self.wait_for_timeout,
"wait_for_images": self.wait_for_images,
"delay_before_return_html": self.delay_before_return_html,
"mean_delay": self.mean_delay,
"max_range": self.max_range,
"semaphore_count": self.semaphore_count,
"js_code": self.js_code,
"js_code_before_wait": self.js_code_before_wait,
"js_only": self.js_only,
"ignore_body_visibility": self.ignore_body_visibility,
"scan_full_page": self.scan_full_page,
"scroll_delay": self.scroll_delay,
"max_scroll_steps": self.max_scroll_steps,
"process_iframes": self.process_iframes,
"flatten_shadow_dom": self.flatten_shadow_dom,
"remove_overlay_elements": self.remove_overlay_elements,
"remove_consent_popups": self.remove_consent_popups,
"simulate_user": self.simulate_user,
"override_navigator": self.override_navigator,
"magic": self.magic,
"adjust_viewport_to_content": self.adjust_viewport_to_content,
"screenshot": self.screenshot,
"screenshot_wait_for": self.screenshot_wait_for,
"screenshot_height_threshold": self.screenshot_height_threshold,
"pdf": self.pdf,
"capture_mhtml": self.capture_mhtml,
"image_description_min_word_threshold": self.image_description_min_word_threshold,
"image_score_threshold": self.image_score_threshold,
"table_score_threshold": self.table_score_threshold,
"table_extraction": self.table_extraction,
"exclude_all_images": self.exclude_all_images,
"exclude_external_images": self.exclude_external_images,
"exclude_social_media_domains": self.exclude_social_media_domains,
"exclude_external_links": self.exclude_external_links,
"exclude_social_media_links": self.exclude_social_media_links,
"exclude_domains": self.exclude_domains,
"exclude_internal_links": self.exclude_internal_links,
"score_links": self.score_links,
"preserve_https_for_internal_links": self.preserve_https_for_internal_links,
"verbose": self.verbose,
"log_console": self.log_console,
"capture_network_requests": self.capture_network_requests,
"capture_console_messages": self.capture_console_messages,
"method": self.method,
"stream": self.stream,
"prefetch": self.prefetch,
"process_in_browser": self.process_in_browser,
"check_robots_txt": self.check_robots_txt,
"user_agent": self.user_agent,
"user_agent_mode": self.user_agent_mode,
"user_agent_generator_config": self.user_agent_generator_config,
"deep_crawl_strategy": self.deep_crawl_strategy,
"link_preview_config": self.link_preview_config.to_dict() if self.link_preview_config else None,
"url": self.url,
"url_matcher": self.url_matcher,
"match_mode": self.match_mode,
"experimental": self.experimental,
"max_retries": self.max_retries,
}
def clone(self, **kwargs):
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
CrawlerRunConfig: A new instance with the specified updates
Example:
```python
# Create a new config with streaming enabled
stream_config = config.clone(stream=True)
# Create a new config with multiple updates
new_config = config.clone(
stream=True,
cache_mode=CacheMode.BYPASS,
verbose=True
)
```
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return CrawlerRunConfig.from_kwargs(config_dict)
class LLMConfig:
def __init__(
self,
provider: str = DEFAULT_PROVIDER,
api_token: Optional[str] = None,
base_url: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
top_p: Optional[float] = None,
frequency_penalty: Optional[float] = None,
presence_penalty: Optional[float] = None,
stop: Optional[List[str]] = None,
n: Optional[int] = None,
backoff_base_delay: Optional[int] = None,
backoff_max_attempts: Optional[int] = None,
backoff_exponential_factor: Optional[int] = None,
provenance: "Provenance" = None,
):
"""Configuaration class for LLM provider and API token."""
if provenance is None:
provenance = Provenance.TRUSTED
# Defense in depth: untrusted callers can already not reach here (the
# type gate forbids constructing LLMConfig from a request body), but if
# they ever do, never resolve env vars or read provider keys from the
# environment - that is the credential-exfil gadget.
if provenance == Provenance.UNTRUSTED:
if api_token and api_token.startswith("env:"):
raise UntrustedConfigError(
"LLMConfig.api_token may not reference an environment variable "
"from an untrusted request"
)
self.provider = provider
self.api_token = api_token # never os.getenv
self.base_url = base_url
self.temperature = temperature
self.max_tokens = max_tokens
self.top_p = top_p
self.frequency_penalty = frequency_penalty
self.presence_penalty = presence_penalty
self.stop = stop
self.n = n
self.backoff_base_delay = backoff_base_delay if backoff_base_delay is not None else 2
self.backoff_max_attempts = backoff_max_attempts if backoff_max_attempts is not None else 3
self.backoff_exponential_factor = backoff_exponential_factor if backoff_exponential_factor is not None else 2
return
self.provider = provider
if api_token and not api_token.startswith("env:"):
self.api_token = api_token
elif api_token and api_token.startswith("env:"):
self.api_token = os.getenv(api_token[4:])
else:
# Check if given provider starts with any of key in PROVIDER_MODELS_PREFIXES
# If not, check if it is in PROVIDER_MODELS
prefixes = PROVIDER_MODELS_PREFIXES.keys()
if any(provider.startswith(prefix) for prefix in prefixes):
selected_prefix = next(
(prefix for prefix in prefixes if provider.startswith(prefix)),
None,
)
self.api_token = PROVIDER_MODELS_PREFIXES.get(selected_prefix)
else:
self.provider = DEFAULT_PROVIDER
self.api_token = os.getenv(DEFAULT_PROVIDER_API_KEY)
self.base_url = base_url
self.temperature = temperature
self.max_tokens = max_tokens
self.top_p = top_p
self.frequency_penalty = frequency_penalty
self.presence_penalty = presence_penalty
self.stop = stop
self.n = n
self.backoff_base_delay = backoff_base_delay if backoff_base_delay is not None else 2
self.backoff_max_attempts = backoff_max_attempts if backoff_max_attempts is not None else 3
self.backoff_exponential_factor = backoff_exponential_factor if backoff_exponential_factor is not None else 2
@staticmethod
def from_kwargs(kwargs: dict) -> "LLMConfig":
return LLMConfig(
provider=kwargs.get("provider", DEFAULT_PROVIDER),
api_token=kwargs.get("api_token"),
base_url=kwargs.get("base_url"),
temperature=kwargs.get("temperature"),
max_tokens=kwargs.get("max_tokens"),
top_p=kwargs.get("top_p"),
frequency_penalty=kwargs.get("frequency_penalty"),
presence_penalty=kwargs.get("presence_penalty"),
stop=kwargs.get("stop"),
n=kwargs.get("n"),
backoff_base_delay=kwargs.get("backoff_base_delay"),
backoff_max_attempts=kwargs.get("backoff_max_attempts"),
backoff_exponential_factor=kwargs.get("backoff_exponential_factor")
)
def to_dict(self):
return {
"provider": self.provider,
"api_token": self.api_token,
"base_url": self.base_url,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"frequency_penalty": self.frequency_penalty,
"presence_penalty": self.presence_penalty,
"stop": self.stop,
"n": self.n,
"backoff_base_delay": self.backoff_base_delay,
"backoff_max_attempts": self.backoff_max_attempts,
"backoff_exponential_factor": self.backoff_exponential_factor
}
def clone(self, **kwargs):
"""Create a copy of this configuration with updated values.
Args:
**kwargs: Key-value pairs of configuration options to update
Returns:
llm_config: A new instance with the specified updates
"""
config_dict = self.to_dict()
config_dict.update(kwargs)
return LLMConfig.from_kwargs(config_dict)
class SeedingConfig:
"""
Configuration class for URL discovery and pre-validation via AsyncUrlSeeder.
"""
def __init__(
self,
source: str = "sitemap+cc",
pattern: Optional[str] = "*",
live_check: bool = False,
extract_head: bool = False,
max_urls: int = -1,
concurrency: int = 1000,
hits_per_sec: int = 5,
force: bool = False,
base_directory: Optional[str] = None,
llm_config: Optional[LLMConfig] = None,
verbose: Optional[bool] = None,
query: Optional[str] = None,
score_threshold: Optional[float] = None,
scoring_method: str = "bm25",
filter_nonsense_urls: bool = True,
cache_ttl_hours: int = 24,
validate_sitemap_lastmod: bool = True,
):
"""
Initialize URL seeding configuration.
Args:
source: Discovery source(s) to use. Options: "sitemap", "cc" (Common Crawl),
or "sitemap+cc" (both). Default: "sitemap+cc"
pattern: URL pattern to filter discovered URLs (e.g., "*example.com/blog/*").
Supports glob-style wildcards. Default: "*" (all URLs)
live_check: Whether to perform HEAD requests to verify URL liveness.
Default: False
extract_head: Whether to fetch and parse <head> section for metadata extraction.
Required for BM25 relevance scoring. Default: False
max_urls: Maximum number of URLs to discover. Use -1 for no limit.
Default: -1
concurrency: Maximum concurrent requests for live checks/head extraction.
Default: 1000
hits_per_sec: Rate limit in requests per second to avoid overwhelming servers.
Default: 5
force: If True, bypasses the AsyncUrlSeeder's internal .jsonl cache and
re-fetches URLs. Default: False
base_directory: Base directory for UrlSeeder's cache files (.jsonl).
If None, uses default ~/.crawl4ai/. Default: None
llm_config: LLM configuration for future features (e.g., semantic scoring).
Currently unused. Default: None
verbose: Override crawler's general verbose setting for seeding operations.
Default: None (inherits from crawler)
query: Search query for BM25 relevance scoring (e.g., "python tutorials").
Requires extract_head=True. Default: None
score_threshold: Minimum relevance score (0.0-1.0) to include URL.
Only applies when query is provided. Default: None
scoring_method: Scoring algorithm to use. Currently only "bm25" is supported.
Future: "semantic". Default: "bm25"
filter_nonsense_urls: Filter out utility URLs like robots.txt, sitemap.xml,
ads.txt, favicon.ico, etc. Default: True
cache_ttl_hours: Hours before sitemap cache expires. Set to 0 to disable TTL
(only lastmod validation). Default: 24
validate_sitemap_lastmod: If True, compares sitemap's <lastmod> with cache
timestamp and refetches if sitemap is newer. Default: True
"""
self.source = source
self.pattern = pattern
self.live_check = live_check
self.extract_head = extract_head
self.max_urls = max_urls
self.concurrency = concurrency
self.hits_per_sec = hits_per_sec
self.force = force
self.base_directory = base_directory
self.llm_config = llm_config
self.verbose = verbose
self.query = query
self.score_threshold = score_threshold
self.scoring_method = scoring_method
self.filter_nonsense_urls = filter_nonsense_urls
self.cache_ttl_hours = cache_ttl_hours
self.validate_sitemap_lastmod = validate_sitemap_lastmod
# Add to_dict, from_kwargs, and clone methods for consistency
def to_dict(self) -> Dict[str, Any]:
return {k: v for k, v in self.__dict__.items() if k != 'llm_config' or v is not None}
@staticmethod
def from_kwargs(kwargs: Dict[str, Any]) -> 'SeedingConfig':
return SeedingConfig(**kwargs)
def clone(self, **kwargs: Any) -> 'SeedingConfig':
config_dict = self.to_dict()
config_dict.update(kwargs)
return SeedingConfig.from_kwargs(config_dict)
class DomainMapperConfig:
"""
Configuration for comprehensive domain URL discovery via DomainMapper.
Discovers all URLs under a domain using multiple sources (sitemap, Common Crawl,
Wayback Machine, Certificate Transparency, path probing, robots.txt mining,
RSS/Atom feeds, homepage link extraction) without deep crawling.
"""
def __init__(
self,
source: str = "sitemap+cc+crt+probe",
max_urls: int = -1,
concurrency: int = 50,
hits_per_sec: int = 10,
force: bool = False,
extract_head: bool = True,
filter_nonsense_urls: bool = True,
soft_404_detection: bool = True,
query: Optional[str] = None,
score_threshold: Optional[float] = None,
scoring_method: str = "bm25",
probe_paths: Optional[List[str]] = None,
common_subdomains: Optional[List[str]] = None,
include_subdomains: bool = True,
source_timeout: float = 30.0,
use_browser_for_homepage: bool = False,
verbose: Optional[bool] = None,
cache_ttl_hours: int = 24,
base_directory: Optional[str] = None,
dns_timeout: float = 3.0,
http_timeout: float = 10.0,
):
"""
Args:
source: Discovery sources joined by '+'. Valid: sitemap, cc, wayback,
crt, probe, robots, feed, homepage. Default: "sitemap+cc+crt+probe"
max_urls: Maximum URLs to return. -1 for unlimited.
concurrency: Max concurrent requests across all hosts.
hits_per_sec: Rate limit in requests/second.
force: Bypass all caches.
extract_head: Fetch and parse <head> metadata for each URL.
filter_nonsense_urls: Filter utility URLs (robots.txt, favicon, etc.).
soft_404_detection: Fingerprint and filter soft-404 pages (SPAs).
query: BM25 relevance query. Requires extract_head=True.
score_threshold: Minimum relevance score (0.0-1.0) to include URL.
scoring_method: Scoring algorithm. Currently only "bm25".
probe_paths: Extra paths to probe on each host (added to defaults).
common_subdomains: Extra subdomain prefixes to guess (added to defaults).
include_subdomains: Discover and scan subdomains. When False, only scans
the exact domain provided (skips crt.sh, DNS guessing,
Wayback/CC host discovery). Default True.
source_timeout: Max seconds per discovery source. Sources that exceed
this are killed and skipped. Prevents slow sources from
blocking fast results. Default 30.0.
use_browser_for_homepage: Use Playwright for JS-rendered homepages.
verbose: Override logger verbose setting.
cache_ttl_hours: Hours before cached results expire.
base_directory: Base directory for cache files.
dns_timeout: Timeout in seconds for DNS resolution.
http_timeout: Timeout in seconds for HTTP requests.
"""
self.source = source
self.max_urls = max_urls
self.concurrency = concurrency
self.hits_per_sec = hits_per_sec
self.force = force
self.extract_head = extract_head
self.filter_nonsense_urls = filter_nonsense_urls
self.soft_404_detection = soft_404_detection
self.query = query
self.score_threshold = score_threshold
self.scoring_method = scoring_method
self.probe_paths = probe_paths
self.common_subdomains = common_subdomains
self.include_subdomains = include_subdomains
self.source_timeout = source_timeout
self.use_browser_for_homepage = use_browser_for_homepage
self.verbose = verbose
self.cache_ttl_hours = cache_ttl_hours
self.base_directory = base_directory
self.dns_timeout = dns_timeout
self.http_timeout = http_timeout
def to_dict(self) -> Dict[str, Any]:
return {k: v for k, v in self.__dict__.items()}
@staticmethod
def from_kwargs(kwargs: Dict[str, Any]) -> 'DomainMapperConfig':
return DomainMapperConfig(**kwargs)
def clone(self, **kwargs: Any) -> 'DomainMapperConfig':
config_dict = self.to_dict()
config_dict.update(kwargs)
return DomainMapperConfig.from_kwargs(config_dict)