fastrag.ingestion.steps.fetching.web#
Link-following crawling: start from one or more URLs, follow what they link to.
Unlike SitemapCrawler
(which trusts a sitemap to enumerate every page up front), WebCrawler
discovers pages by fetching a start URL, extracting its links, and following
them within the configured bounds — the right tool for a site with no
sitemap, or where only a subtree reachable from one URL matters.
Caching is bucketed per start URL, not per page: targets() returns
the (normalized) start URLs, and evaluate_target() crawls the whole
subtree reachable from one of them, exactly
docs/specification/API.md’s “Example 1”
(source_cache_id = "webcrawler:1a2b3c4" -> [Source(a.com/1), Source(a.com/2), ...]).
This means a start URL whose linked pages changed at all is revalidated (and,
if stale, recrawled) as one unit — the granularity API.md’s own example
describes, not a per-page bucket the way SitemapCrawler uses. See
.agent/results/013-web-crawler.md for why this differs from
SitemapCrawler’s per-page bucketing despite both opting into the same
targets/evaluate_target hooks.
Classes
|
Follow links from one or more start URLs, within configured bounds. |
- class fastrag.ingestion.steps.fetching.web.WebCrawler(*, url=None, start_urls=None, max_urls=None, max_depth=None, include=None, exclude=None, same_origin=True, renderer=None, headers=None, timeout=30.0, follow_redirects=True, user_agent='fastrag/0.1 (+https://github.com/AgrospAI/fastrag)', cache=None, policy=None, cache_validation_interval=None)[source]#
Bases:
FetcherFollow links from one or more start URLs, within configured bounds.
Accepts either a single
url=(the formexamples/08_sources.py/09_filters.pyuse) or severalstart_urls=(docs/specification/API.md’s multi-start form) — exactly one of the two must be given. Both are stored the same way internally (self.start_urls), so the rest of this class does not distinguish them.from fastrag.ingestion.steps.fetching import WebCrawler crawler = WebCrawler(url="https://docs.pontus-x.eu/", max_urls=200) crawler = WebCrawler(start_urls=["a.com", "https://b.com/"], max_depth=3)
- Parameters:
- streams_targets = True#
A start URL’s crawl bounds, retries and times out each page itself, so the cache wrap streams it rather than running it as one timed item.
- config()[source]#
This crawler’s cache fingerprint: only what changes a page’s bytes.
start_urls,max_urls,max_depth,include,exclude, andsame_origindecide which pages are discovered, not what a given page returns, so they are deliberately excluded here — the same principleconfig()follows, for the same reason. Per-start-URL cache ids already encode the start URL itself as theirtarget.
- async targets(context)[source]#
Return the normalized start URLs — one independent cache bucket each.
- Parameters:
context (ExecutionContext)
- Return type:
- async evaluate_target(target, context)[source]#
Crawl the subtree reachable from one start URL, within bounds.
Fetches breadth-first, one level of link depth at a time, each level’s pages fetched concurrently (bounded by
policy.concurrency) viarun_concurrently()— so a page that fails to fetch after retries emits an attributedItemFailedand is skipped (policy.on_error == "skip") rather than aborting the whole crawl. Avisitedset, seeded withtargetitself, guarantees every URL is fetched at most once per call even when several pages link to it, and pages are deduplicated again by theSource.idthey actually resolve to, so two URLs that redirect to one page are yielded (and cached) once.- Parameters:
target (str)
context (ExecutionContext)
- Return type:
- async evaluate(context)[source]#
Crawl every start URL, uncached, bounded by
policy.concurrency.Used directly when no cache is in effect — the per-target cache wrap (
targets()/evaluate_target()) is what applies when one is. Start URLs are themselves fetched concurrently, mirroringevaluate_target()’s own per-level concurrency.- Parameters:
context (ExecutionContext)
- Return type:
- async is_source_modified(source_id, cached_etag, cached_last_modified, cached_content_hash)[source]#
Conditional freshness check: ETag, then Last-Modified, then content hash.
Identical ladder to
is_source_modified(), applied here to one page within a start URL’s cached bucket — the base class’s_any_modifiedcalls this once per cached page, and treats the whole bucket as stale (recrawling the entire start URL viaevaluate_target()) if any one page reports modified.
- async aclose()[source]#
Release the shared HTTP client and renderer resources. Idempotent.
- Return type:
None
- property cache: CacheStore | Literal[False] | None#
Return the cache explicitly configured for this step.
- canonical_uri(uri)#
Identity under which
uriis fetched, to deduplicate equivalent spellings.
- property config_hash: str#
A stable hash of this step’s class name and
config().Two identically configured steps of the same class hash equal; changing any declared config value changes the hash; the hash is stable across processes. Used as part of every cache id this step’s evaluation reads or writes.
- effective_policy(context)#
Resolve this step’s policy over the one inherited from
context.The step’s own explicitly-set fields take priority; everything else falls through to the pipeline (or enclosing router/aggregator) policy.
- Parameters:
context (ExecutionContext)
- Return type:
- event_name: str = 'Fetching'#
The stage this step’s work belongs to, e.g.
"Chunking".Declared as a plain class attribute by a family base class, and inherited by every concrete step in that family:
class Chunker(DocumentStep, ABC): event_name = "Chunking" class ChunkerRouter(RoutedStep[Document], Chunker): ... # -> "Chunking", inherited from `Chunker`
A concrete step overrides it to opt out of its family’s label:
class ChunkerRouter(RoutedStep[Document], Chunker): event_name = "ChunkerRouter" # -> "ChunkerRouter"
Resolution walks the MRO for the first class that declares it in its own class body (so a mixin listed first, e.g.
RoutedStep, contributes nothing unless it declares one itself), and falls back to the class’s own name when nothing in the MRO does — a step family that never declares anevent_namestill labels each of its steps distinctly rather than collapsing them under one blank heading. That fallback is computed per instance rather than assigned onto the class: assigning it would make it look “declared” to every subclass, which would then inherit their base’s name instead of getting their own.
- async fetch(uri)#
Fetch one URL in-process, uncached. Needed to be a
route()target.- Raises:
fastrag.exceptions.ConfigurationError – This fetcher cannot fetch an arbitrary URL.
- Parameters:
uri (str)
- Return type:
- route(filter_rule, fetcher)#
Delegate the URLs matching
filter_ruletofetcher.The filter sees a content-less
Sourcecarrying only the URL, so URL-based filters such asURLFilterapply. Routes are tried in registration order, and the first match wins.
- route_for(uri)#
The fetcher routed to
uri, orNoneto fetch it normally.
- routes_config()#
Routed fetchers’ fingerprints, for inclusion in a crawler’s
config().
- async run()#
Execute this step independently and collect every item.
Builds a default
ExecutionContextwhen none is installed (this step’s ownpolicy, or a freshPolicy). Intended primarily for testing and standalone use — pipeline-wide event observation isPipeline’s job.- Return type:
list[T]
- property upstream: PipelineStep[Any] | None#
Return the upstream step bound via
__call__, orNone.Read-only counterpart to
__call__’sself._upstreamassignment — lets external code (e.g.Pipeline, walking a graph from its root to classify each step by position) introspect composition without reaching into a private attribute.