Skip to content

Commit aef1813

Browse files
committed
docs: renumber Crawl4AI guide to 08 and switch to a single-file example
1 parent a62a06b commit aef1813

7 files changed

Lines changed: 137 additions & 190 deletions

File tree

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
---
22
id: crawl4ai
3-
title: Use Crawl4AI
3+
title: LLM-ready scraping with Crawl4AI
44
description: Build an Apify Actor that scrapes web pages into LLM-ready markdown using the Crawl4AI library.
55
---
66

7-
import CodeBlock from '@theme/CodeBlock';
8-
import Tabs from '@theme/Tabs';
9-
import TabItem from '@theme/TabItem';
7+
import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock';
108

11-
import Crawl4aiMain from '!!raw-loader!./code/crawl4ai_project/my_actor/main.py';
12-
import Crawl4aiScraper from '!!raw-loader!./code/crawl4ai_project/my_actor/scraper.py';
13-
import Crawl4aiEntrypoint from '!!raw-loader!./code/crawl4ai_project/my_actor/__main__.py';
14-
import Crawl4aiDockerfile from '!!raw-loader!./code/crawl4ai_project/Dockerfile';
9+
import Crawl4aiExample from '!!raw-loader!roa-loader!./code/08_crawl4ai.py';
1510

16-
In this guide, you'll learn how to use the [Crawl4AI](https://crawl4ai.com/) library in your Apify Actors.
11+
In this guide, you'll learn how to use the [Crawl4AI](https://crawl4ai.com/) library for LLM-ready web scraping in your Apify Actors.
1712

1813
## Introduction
1914

@@ -39,65 +34,39 @@ crawl4ai-setup
3934

4035
The following Actor recursively crawls pages, starting from the URLs in the Actor input and following links up to a user-defined maximum depth. It uses Crawl4AI's `AsyncWebCrawler` to render each page through [Apify Proxy](https://docs.apify.com/platform/proxy), stores the page's markdown in the dataset, and follows the internal links that Crawl4AI discovers.
4136

42-
The code is split into three small modules, following the structure of the Apify Python Actor templates:
43-
44-
- `my_actor/main.py` - The Actor's main coroutine. It handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, sets up [Apify Proxy](https://docs.apify.com/platform/proxy) and the [request queue](https://docs.apify.com/platform/storage/request-queue), opens a single browser-backed crawler, and drives the crawl.
45-
- `my_actor/scraper.py` - The Crawl4AI-specific logic. A single `scrape_page` function crawls a page and returns the extracted data together with the links found on it.
46-
- `my_actor/__main__.py` - The entry point that runs the `main` coroutine with `asyncio`.
47-
48-
<Tabs>
49-
<TabItem value="main.py" label="my_actor/main.py">
50-
<CodeBlock className="language-python">
51-
{Crawl4aiMain}
52-
</CodeBlock>
53-
</TabItem>
54-
<TabItem value="scraper.py" label="my_actor/scraper.py">
55-
<CodeBlock className="language-python">
56-
{Crawl4aiScraper}
57-
</CodeBlock>
58-
</TabItem>
59-
<TabItem value="__main__.py" label="my_actor/__main__.py">
60-
<CodeBlock className="language-python">
61-
{Crawl4aiEntrypoint}
62-
</CodeBlock>
63-
</TabItem>
64-
</Tabs>
37+
The whole Actor fits in a single file. A `scrape_page` helper holds the Crawl4AI-specific crawling and parsing, while the `main` coroutine handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, sets up [Apify Proxy](https://docs.apify.com/platform/proxy) and the [request queue](https://docs.apify.com/platform/storage/request-queue), opens a single browser-backed crawler, and drives the crawl:
38+
39+
<RunnableCodeBlock className="language-python" language="python">
40+
{Crawl4aiExample}
41+
</RunnableCodeBlock>
6542

6643
A few things worth pointing out:
6744

6845
- A single `AsyncWebCrawler` is opened once and reused for every request. The crawler manages one browser instance, so reusing it across the whole crawl is far cheaper than launching a new browser per page.
69-
- Keeping the crawling and parsing in `scrape_page` separates the Crawl4AI-specific code from the Actor's orchestration logic. The function returns the extracted data together with the discovered links, so `my_actor/main.py` decides what to store and what to enqueue.
46+
- Keeping the crawling and parsing in `scrape_page` separates the Crawl4AI-specific code from the Actor's orchestration logic. The function returns the extracted data together with the discovered links, so `main` decides what to store and what to enqueue.
7047
- `result.markdown` is the rendered page as clean markdown, and `result.metadata` carries page-level fields such as the title - exactly the kind of output you want when preparing data for an LLM.
7148
- `result.links` already separates `internal` (same-site) links from `external` ones, so the example follows only the internal links to keep the crawl on the same website.
7249
- `CacheMode.BYPASS` tells Crawl4AI to always fetch a fresh copy of the page instead of serving it from its local cache.
7350

7451
## Using Apify Proxy
7552

76-
Running on the Apify platform gives your scraper access to [Apify Proxy](https://docs.apify.com/platform/proxy), which rotates IP addresses to avoid rate limiting and blocking. In the example above, `my_actor/main.py` creates a proxy configuration with `Actor.create_proxy_configuration` and passes a fresh proxy URL to `scrape_page` for every request, which forwards it to Crawl4AI's per-request `CrawlerRunConfig`.
53+
Running on the Apify platform gives your scraper access to [Apify Proxy](https://docs.apify.com/platform/proxy), which rotates IP addresses to avoid rate limiting and blocking. In the example above, `main` creates a proxy configuration with `Actor.create_proxy_configuration` and passes a fresh proxy URL to `scrape_page` for every request, which forwards it to Crawl4AI's per-request `CrawlerRunConfig`.
7754

7855
`ProxyConfig.from_string` parses the proxy URL returned by `ProxyConfiguration.new_url` (for example `http://groups-RESIDENTIAL:<password>@proxy.apify.com:8000`) into the server, username, and password that the browser needs - the browser cannot take the credentials embedded directly in the URL. To select specific proxy groups or a country, pass the relevant arguments to `Actor.create_proxy_configuration`. For more details, see the [Proxy management](../concepts/proxy-management) guide.
7956

8057
## Running on the Apify platform
8158

8259
Because Crawl4AI renders pages in a real browser, the Actor image needs a browser and its system-level dependencies. Build on top of the [Apify Playwright base image](https://hub.docker.com/r/apify/actor-python-playwright), which already ships a browser - Crawl4AI reuses those binaries, so no separate browser-install step is required in the Dockerfile.
8360

61+
Pin the Python 3.13 variant of that image (for example `apify/actor-python-playwright:3.13-1.60.0`), because some of Crawl4AI's dependencies do not yet publish wheels for the newest Python versions, which would otherwise force a slow source build during the image build.
62+
8463
Add `apify` and `crawl4ai` to your `requirements.txt`:
8564

8665
```text
8766
apify
8867
crawl4ai
8968
```
9069

91-
<Tabs>
92-
<TabItem value="Dockerfile" label="Dockerfile">
93-
<CodeBlock className="language-docker">
94-
{Crawl4aiDockerfile}
95-
</CodeBlock>
96-
</TabItem>
97-
</Tabs>
98-
99-
The example pins the Python 3.13 base image because some of Crawl4AI's dependencies do not yet publish wheels for the newest Python versions, which would otherwise force a slow source build during the image build.
100-
10170
## Conclusion
10271

10372
In this guide, you learned how to use Crawl4AI in your Apify Actors. You can now render pages in a real browser, turn them into LLM-ready markdown, follow the links Crawl4AI discovers, route requests through Apify Proxy, and run the whole thing on the Apify platform. See the [Actor templates](https://apify.com/templates/categories/python) to get started with your own scraping tasks. If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy scraping!

docs/03_guides/code/08_crawl4ai.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import asyncio
2+
from typing import Any
3+
4+
from crawl4ai import (
5+
AsyncWebCrawler,
6+
BrowserConfig,
7+
CacheMode,
8+
CrawlerRunConfig,
9+
ProxyConfig,
10+
)
11+
12+
from apify import Actor, Request
13+
from apify.storages import RequestQueue
14+
15+
16+
async def scrape_page(
17+
crawler: AsyncWebCrawler,
18+
url: str,
19+
*,
20+
proxy_url: str | None = None,
21+
) -> tuple[dict[str, Any], list[str]]:
22+
"""Crawl a page with Crawl4AI and return its markdown and same-site links."""
23+
run_config = CrawlerRunConfig(
24+
cache_mode=CacheMode.BYPASS,
25+
proxy_config=ProxyConfig.from_string(proxy_url) if proxy_url else None,
26+
)
27+
28+
result = await crawler.arun(url, config=run_config)
29+
if not result.success:
30+
raise RuntimeError(result.error_message or f'Failed to crawl {url}')
31+
32+
data = {
33+
'url': result.url,
34+
'title': (result.metadata or {}).get('title'),
35+
'markdown': str(result.markdown),
36+
}
37+
38+
# Crawl4AI already classifies links; follow only the internal ones.
39+
internal_links = result.links.get('internal', [])
40+
links = [link['href'] for link in internal_links if link.get('href')]
41+
42+
return data, links
43+
44+
45+
async def enqueue_links(
46+
request_queue: RequestQueue,
47+
links: list[str],
48+
*,
49+
depth: int,
50+
max_depth: int,
51+
) -> None:
52+
"""Enqueue the links one level deeper, unless max_depth was reached."""
53+
if depth >= max_depth:
54+
return
55+
56+
for link_url in links:
57+
Actor.log.info(f'Enqueuing {link_url} ...')
58+
request = Request.from_url(link_url)
59+
request.crawl_depth = depth + 1
60+
await request_queue.add_request(request)
61+
62+
63+
async def main() -> None:
64+
async with Actor:
65+
# Read the Actor input.
66+
actor_input = await Actor.get_input() or {}
67+
start_urls = actor_input.get('startUrls', [{'url': 'https://crawlee.dev'}])
68+
max_depth = actor_input.get('maxDepth', 1)
69+
70+
if not start_urls:
71+
Actor.log.info('No start URLs specified in Actor input, exiting...')
72+
await Actor.exit()
73+
74+
# Set up Apify Proxy and the request queue.
75+
proxy_configuration = await Actor.create_proxy_configuration()
76+
request_queue = await Actor.open_request_queue()
77+
78+
# Enqueue the start URLs (crawl depth defaults to 0).
79+
for start_url in start_urls:
80+
url = start_url.get('url')
81+
Actor.log.info(f'Enqueuing start URL: {url}')
82+
await request_queue.add_request(Request.from_url(url))
83+
84+
# Cap the crawl; raise or remove to follow more pages.
85+
max_requests = 50
86+
handled_requests = 0
87+
88+
# Reuse one headless browser-backed crawler for every request.
89+
browser_config = BrowserConfig(headless=True)
90+
91+
async with AsyncWebCrawler(config=browser_config) as crawler:
92+
while handled_requests < max_requests and (
93+
request := await request_queue.fetch_next_request()
94+
):
95+
handled_requests += 1
96+
url = request.url
97+
depth = request.crawl_depth
98+
Actor.log.info(f'Scraping {url} (depth={depth}) ...')
99+
100+
try:
101+
# Fresh proxy URL per request (None if no proxy).
102+
proxy_url = None
103+
if proxy_configuration:
104+
proxy_url = await proxy_configuration.new_url()
105+
106+
data, links = await scrape_page(crawler, url, proxy_url=proxy_url)
107+
await Actor.push_data(data)
108+
Actor.log.info(
109+
f'Stored data from {url} '
110+
f'(title={data["title"]!r}, {len(links)} links found).'
111+
)
112+
await enqueue_links(
113+
request_queue, links, depth=depth, max_depth=max_depth
114+
)
115+
116+
except Exception:
117+
Actor.log.exception(f'Cannot extract data from {url}.')
118+
119+
finally:
120+
await request_queue.mark_request_as_handled(request)
121+
122+
123+
if __name__ == '__main__':
124+
asyncio.run(main())

docs/03_guides/code/crawl4ai_project/Dockerfile

Lines changed: 0 additions & 19 deletions
This file was deleted.

docs/03_guides/code/crawl4ai_project/my_actor/__init__.py

Whitespace-only changes.

docs/03_guides/code/crawl4ai_project/my_actor/__main__.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/03_guides/code/crawl4ai_project/my_actor/main.py

Lines changed: 0 additions & 73 deletions
This file was deleted.

docs/03_guides/code/crawl4ai_project/my_actor/scraper.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)