Skip to content

Commit 0f968e8

Browse files
vdusekjanbuchar
andauthored
docs: add request storage guide (#521)
### Description - Add request storage guide. - It is highly inspired by the TS version. ### Issues - Closes: #355 - Closes: #478 ### Testing - It was rendered locally. ### Checklist - [x] CI passed --------- Co-authored-by: Jan Buchar <[email protected]>
1 parent eb20cfe commit 0f968e8

10 files changed

+361
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import asyncio
2+
3+
from crawlee.configuration import Configuration
4+
from crawlee.http_crawler import HttpCrawler, HttpCrawlingContext
5+
6+
7+
async def main() -> None:
8+
# highlight-next-line
9+
config = Configuration(purge_on_start=False)
10+
crawler = HttpCrawler(configuration=config)
11+
12+
@crawler.router.default_handler
13+
async def request_handler(context: HttpCrawlingContext) -> None:
14+
context.log.info(f'Processing {context.request.url} ...')
15+
16+
await crawler.run(['https://crawlee.dev/'])
17+
18+
19+
if __name__ == '__main__':
20+
asyncio.run(main())
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from crawlee.beautifulsoup_crawler import BeautifulSoupCrawler, BeautifulSoupCrawlingContext
4+
5+
6+
async def main() -> None:
7+
crawler = BeautifulSoupCrawler()
8+
9+
@crawler.router.default_handler
10+
async def request_handler(context: BeautifulSoupCrawlingContext) -> None:
11+
context.log.info(f'Processing {context.request.url} ...')
12+
# highlight-next-line
13+
await context.add_requests(['https://apify.com/'])
14+
15+
await crawler.run(['https://crawlee.dev/'])
16+
17+
18+
if __name__ == '__main__':
19+
asyncio.run(main())
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from crawlee.beautifulsoup_crawler import BeautifulSoupCrawler, BeautifulSoupCrawlingContext
4+
5+
6+
async def main() -> None:
7+
crawler = BeautifulSoupCrawler()
8+
9+
@crawler.router.default_handler
10+
async def request_handler(context: BeautifulSoupCrawlingContext) -> None:
11+
context.log.info(f'Processing {context.request.url} ...')
12+
# highlight-next-line
13+
await context.enqueue_links()
14+
15+
await crawler.run(['https://crawlee.dev/'])
16+
17+
18+
if __name__ == '__main__':
19+
asyncio.run(main())
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import asyncio
2+
3+
from crawlee.memory_storage_client import MemoryStorageClient
4+
5+
6+
async def main() -> None:
7+
storage_client = MemoryStorageClient()
8+
# highlight-next-line
9+
await storage_client.purge_on_start()
10+
11+
12+
if __name__ == '__main__':
13+
asyncio.run(main())
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import asyncio
2+
3+
from crawlee.storages import RequestList
4+
5+
6+
async def main() -> None:
7+
# Open the request list, if it does not exist, it will be created.
8+
# Leave name empty to use the default request list.
9+
request_list = RequestList(
10+
name='my-request-list',
11+
requests=['https://apify.com/', 'https://crawlee.dev/', 'https://crawlee.dev/python/'],
12+
)
13+
14+
# You can interact with the request list in the same way as with the request queue.
15+
await request_list.add_requests_batched(
16+
[
17+
'https://crawlee.dev/python/docs/quick-start',
18+
'https://crawlee.dev/python/api',
19+
]
20+
)
21+
22+
# Fetch and process requests from the queue.
23+
while request := await request_list.fetch_next_request():
24+
# Do something with it..
25+
26+
# And mark it as handled.
27+
await request_list.mark_request_as_handled(request)
28+
29+
# Remove the request queue.
30+
await request_list.drop()
31+
32+
33+
if __name__ == '__main__':
34+
asyncio.run(main())
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import asyncio
2+
3+
from crawlee.http_crawler import HttpCrawler, HttpCrawlingContext
4+
from crawlee.storages import RequestList
5+
6+
7+
async def main() -> None:
8+
# Open the request list, if it does not exist, it will be created.
9+
# Leave name empty to use the default request list.
10+
request_list = RequestList(
11+
name='my-request-list',
12+
requests=['https://apify.com/', 'https://crawlee.dev/'],
13+
)
14+
15+
# Create a new crawler (it can be any subclass of BasicCrawler) and pass the request
16+
# list as request provider to it. It will be managed by the crawler.
17+
crawler = HttpCrawler(request_provider=request_list)
18+
19+
# Define the default request handler, which will be called for every request.
20+
@crawler.router.default_handler
21+
async def request_handler(context: HttpCrawlingContext) -> None:
22+
context.log.info(f'Processing {context.request.url} ...')
23+
24+
# Use context's add_requests method helper to add new requests from the handler.
25+
await context.add_requests(['https://crawlee.dev/python/docs/quick-start'])
26+
27+
# Use crawler's add_requests method helper to add new requests.
28+
await crawler.add_requests(['https://crawlee.dev/python/api'])
29+
30+
# Run the crawler. You can optionally pass the list of initial requests.
31+
await crawler.run(['https://crawlee.dev/python/'])
32+
33+
34+
if __name__ == '__main__':
35+
asyncio.run(main())
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
3+
from crawlee.storages import RequestQueue
4+
5+
6+
async def main() -> None:
7+
# Open the request queue, if it does not exist, it will be created.
8+
# Leave name empty to use the default request queue.
9+
request_queue = await RequestQueue.open(name='my-request-queue')
10+
11+
# Add a single request.
12+
await request_queue.add_request('https://apify.com/')
13+
14+
# Add multiple requests as a batch.
15+
await request_queue.add_requests_batched(['https://crawlee.dev/', 'https://crawlee.dev/python/'])
16+
17+
# Fetch and process requests from the queue.
18+
while request := await request_queue.fetch_next_request():
19+
# Do something with it..
20+
21+
# And mark it as handled.
22+
await request_queue.mark_request_as_handled(request)
23+
24+
# Remove the request queue.
25+
await request_queue.drop()
26+
27+
28+
if __name__ == '__main__':
29+
asyncio.run(main())
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import asyncio
2+
3+
from crawlee.http_crawler import HttpCrawler, HttpCrawlingContext
4+
5+
6+
async def main() -> None:
7+
# Create a new crawler (it can be any subclass of BasicCrawler). Request queue is a default
8+
# request provider, it will be opened, and fully managed if not specified.
9+
crawler = HttpCrawler()
10+
11+
# Define the default request handler, which will be called for every request.
12+
@crawler.router.default_handler
13+
async def request_handler(context: HttpCrawlingContext) -> None:
14+
context.log.info(f'Processing {context.request.url} ...')
15+
16+
# Use context's add_requests method helper to add new requests from the handler.
17+
await context.add_requests(['https://crawlee.dev/python/'])
18+
19+
# Use crawler's add_requests method helper to add new requests.
20+
await crawler.add_requests(['https://apify.com/'])
21+
22+
# Run the crawler. You can optionally pass the list of initial requests.
23+
await crawler.run(['https://crawlee.dev/'])
24+
25+
26+
if __name__ == '__main__':
27+
asyncio.run(main())
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
3+
from crawlee.http_crawler import HttpCrawler, HttpCrawlingContext
4+
from crawlee.storages import RequestQueue
5+
6+
7+
async def main() -> None:
8+
# Open the request queue, if it does not exist, it will be created.
9+
# Leave name empty to use the default request queue.
10+
request_queue = await RequestQueue.open(name='my-request-queue')
11+
12+
# Interact with the request queue directly, e.g. add a batch of requests.
13+
await request_queue.add_requests_batched(['https://apify.com/', 'https://crawlee.dev/'])
14+
15+
# Create a new crawler (it can be any subclass of BasicCrawler) and pass the request
16+
# list as request provider to it. It will be managed by the crawler.
17+
crawler = HttpCrawler(request_provider=request_queue)
18+
19+
# Define the default request handler, which will be called for every request.
20+
@crawler.router.default_handler
21+
async def request_handler(context: HttpCrawlingContext) -> None:
22+
context.log.info(f'Processing {context.request.url} ...')
23+
24+
# And execute the crawler.
25+
await crawler.run()
26+
27+
28+
if __name__ == '__main__':
29+
asyncio.run(main())

docs/guides/request_storage.mdx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
id: request-storage
3+
title: Request storage
4+
description: How to store the requests your crawler will go through
5+
---
6+
7+
import ApiLink from '@site/src/components/ApiLink';
8+
9+
import Tabs from '@theme/Tabs';
10+
import TabItem from '@theme/TabItem';
11+
import CodeBlock from '@theme/CodeBlock';
12+
13+
import RqBasicExample from '!!raw-loader!./code/request_storage_rq_basic.py';
14+
import RqWithCrawlerExample from '!!raw-loader!./code/request_storage_rq_with_crawler.py';
15+
import RqWithCrawlerExplicitExample from '!!raw-loader!./code/request_storage_rq_with_crawler_explicit.py';
16+
17+
import RlBasicExample from '!!raw-loader!./code/request_storage_rl_basic.py';
18+
import RlWithCrawlerExample from '!!raw-loader!./code/request_storage_rl_with_crawler.py';
19+
20+
import RsHelperAddRequestsExample from '!!raw-loader!./code/request_storage_helper_add_requests.py';
21+
import RsHelperEnqueueLinksExample from '!!raw-loader!./code/request_storage_helper_enqueue_links.py';
22+
23+
import RsDoNotPurgeExample from '!!raw-loader!./code/request_storage_do_not_purge.py';
24+
import RsPurgeExplicitlyExample from '!!raw-loader!./code/request_storage_purge_explicitly.py';
25+
26+
This guide explains the different types of request storage available in Crawlee, how to store the requests that your crawler will process, and which storage type to choose based on your needs.
27+
28+
## Request providers overview
29+
30+
All request storage types in Crawlee implement the same interface - <ApiLink to="class/RequestProvider">`RequestProvider`</ApiLink>. This unified interface allows them to be used in a consistent manner, regardless of the storage backend. The request providers are managed by storage clients - subclasses of <ApiLink to="class/BaseStorageClient">`BaseStorageClient`</ApiLink>. For instance, <ApiLink to="class/MemoryStorageClient">`MemoryStorageClient`</ApiLink> stores data in memory while it can also offload them to the local directory. Data are stored in the following directory structure:
31+
32+
```text
33+
{CRAWLEE_STORAGE_DIR}/{request_provider}/{QUEUE_ID}/
34+
```
35+
:::note
36+
37+
Local directory is specified by the `CRAWLEE_STORAGE_DIR` environment variable with default value `./storage`. `{QUEUE_ID}` is the name or ID of the specific request storage. The default value is `default`, unless we override it by setting the `CRAWLEE_DEFAULT_REQUEST_QUEUE_ID` environment variable.
38+
39+
:::
40+
41+
## Request queue
42+
43+
The <ApiLink to="class/RequestQueue">`RequestQueue`</ApiLink> is the primary storage for URLs in Crawlee, especially useful for deep crawling. It supports dynamic addition and removal of URLs, making it ideal for recursive tasks where URLs are discovered and added during the crawling process (e.g., following links across multiple pages). Each Crawlee project has a **default request queue**, which can be used to store URLs during a specific run. The <ApiLink to="class/RequestQueue">`RequestQueue`</ApiLink> is highly useful for large-scale and complex crawls.
44+
45+
The following code demonstrates the usage of the <ApiLink to="class/RequestQueue">`RequestQueue`</ApiLink>:
46+
47+
<Tabs groupId="request_queue">
48+
<TabItem value="request_queue_basic_example" label="Basic usage" default>
49+
<CodeBlock className="language-python">
50+
{RqBasicExample}
51+
</CodeBlock>
52+
</TabItem>
53+
<TabItem value="request_queue_with_crawler" label="Usage with Crawler">
54+
<CodeBlock className="language-python">
55+
{RqWithCrawlerExample}
56+
</CodeBlock>
57+
</TabItem>
58+
<TabItem value="request_queue_with_crawler_explicit" label="Explicit usage with Crawler" default>
59+
<CodeBlock className="language-python">
60+
{RqWithCrawlerExplicitExample}
61+
</CodeBlock>
62+
</TabItem>
63+
</Tabs>
64+
65+
## Request list
66+
67+
The <ApiLink to="class/RequestList">`RequestList`</ApiLink> is a simpler, lightweight storage option, used when all URLs to be crawled are known upfront. It represents the list of URLs to crawl that is stored in a crawler run memory (or optionally in default <ApiLink to="class/KeyValueStore">`KeyValueStore`</ApiLink> associated with the run, if specified). The list is used for the crawling of a large number of URLs, when we know all the URLs which should be visited by the crawler and no URLs would be added during the run. The URLs can be provided either in code or parsed from a text file hosted on the web. The <ApiLink to="class/RequestList">`RequestList`</ApiLink> is typically created exclusively for a single crawler run, and its usage must be explicitly specified.
68+
69+
:::warning
70+
71+
The <ApiLink to="class/RequestList">`RequestList`</ApiLink> class is in its early version and is not fully
72+
implemented. It is currently intended mainly for testing purposes and small-scale projects. The current
73+
implementation is only in-memory storage and is very limited. It will be (re)implemented in the future.
74+
For more details, see the GitHub issue [crawlee-python#99](https://github.com/apify/crawlee-python/issues/99).
75+
For production usage we recommend to use the <ApiLink to="class/RequestQueue">`RequestQueue`</ApiLink>.
76+
77+
:::
78+
79+
The following code demonstrates the usage of the <ApiLink to="class/RequestList">`RequestList`</ApiLink>:
80+
81+
<Tabs groupId="request_list">
82+
<TabItem value="request_list_basic_example" label="Basic usage" default>
83+
<CodeBlock className="language-python">
84+
{RlBasicExample}
85+
</CodeBlock>
86+
</TabItem>
87+
<TabItem value="request_list_with_crawler" label="Usage with Crawler">
88+
<CodeBlock className="language-python">
89+
{RlWithCrawlerExample}
90+
</CodeBlock>
91+
</TabItem>
92+
</Tabs>
93+
94+
{/*
95+
96+
## Which one to choose?
97+
98+
TODO: write this section, once https://github.com/apify/crawlee-python/issues/99 is resolved
99+
100+
*/}
101+
102+
## Request-related helpers
103+
104+
We offer several helper functions to simplify interactions with request storages:
105+
106+
- The <ApiLink to="class/AddRequestsFunction">`add_requests`</ApiLink> function allows you to manually add specific URLs to the configured request storage. In this case, you must explicitly provide the URLs you want to be added to the request storage. If you need to specify further details of the request, such as a `label` or `user_data`, you have to pass instances of the <ApiLink to="class/Request">`Request`</ApiLink> class to the helper.
107+
- The <ApiLink to="class/EnqueueLinksFunction">`enqueue_links`</ApiLink> function is designed to discover new URLs in the current page and add them to the request storage. It can be used with default settings, requiring no arguments, or you can customize its behavior by specifying link element selectors, choosing different enqueue strategies, or applying include/exclude filters to control which URLs are added. See [Crawl website with relative links](../examples/crawl-website-with-relative-links) example for more details.
108+
109+
<Tabs groupId="request_helpers">
110+
<TabItem value="request_helper_add_requests" label="Add requests" default>
111+
<CodeBlock className="language-python">
112+
{RsHelperAddRequestsExample}
113+
</CodeBlock>
114+
</TabItem>
115+
<TabItem value="request_helper_enqueue_links" label="Enqueue links">
116+
<CodeBlock className="language-python">
117+
{RsHelperEnqueueLinksExample}
118+
</CodeBlock>
119+
</TabItem>
120+
</Tabs>
121+
122+
## Cleaning up the storages
123+
124+
Default storages are purged before the crawler starts, unless explicitly configured otherwise. For that case, see <ApiLink to="class/Configuration#purge_on_start">`Configuration.purge_on_start`</ApiLink>. This cleanup happens as soon as a storage is accessed, either when you open a storage (e.g. using <ApiLink to="class/RequestQueue#open">`RequestQueue.open`</ApiLink>) or when interacting with a storage through one of the helper functions (e.g. <ApiLink to="class/AddRequestsFunction">`add_requests`</ApiLink> or <ApiLink to="class/EnqueueLinksFunction">`enqueue_links`</ApiLink>, which implicitly opens the request storage).
125+
126+
<CodeBlock className="language-python">
127+
{RsDoNotPurgeExample}
128+
</CodeBlock>
129+
130+
If you do not explicitly interact with storages in your code, the purging will occur automatically when the <ApiLink to="class/BasicCrawler#run">`BasicCrawler.run`</ApiLink> method is invoked.
131+
132+
If you need to purge storages earlier, you can call <ApiLink to="class/MemoryStorageClient#purge_on_start">`MemoryStorageClient.purge_on_start`</ApiLink> directly. This method triggers the purging process for the underlying storage implementation you are currently using.
133+
134+
<CodeBlock className="language-python">
135+
{RsPurgeExplicitlyExample}
136+
</CodeBlock>

0 commit comments

Comments
 (0)