Skip to content

Commit 7e3518e

Browse files
authored
Merge pull request #2289 from talelboussetta/bugfix/crawlresult-failed-shape
fix(models): default CrawlResult links and media to the scraped shape
2 parents 1f68e5b + e88c441 commit 7e3518e

6 files changed

Lines changed: 83 additions & 10 deletions

File tree

‎crawl4ai/models.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,15 @@ class CrawlResult(BaseModel):
133133
fit_html: Optional[str] = None
134134
success: bool
135135
cleaned_html: Optional[str] = None
136-
media: Dict[str, List[Dict]] = {}
137-
links: Dict[str, List[Dict]] = {}
136+
# The keys a scraped page gets (arun moves media's "tables" to .tables), so
137+
# a result built without them -- robots.txt refusal, a failed fetch --
138+
# keeps the shape.
139+
media: Dict[str, List[Dict]] = Field(
140+
default_factory=lambda: {"images": [], "videos": [], "audios": []}
141+
)
142+
links: Dict[str, List[Dict]] = Field(
143+
default_factory=lambda: {"internal": [], "external": []}
144+
)
138145
downloaded_files: Optional[List[str]] = None
139146
js_execution_result: Optional[Dict[str, Any]] = None
140147
screenshot: Optional[str] = None

‎deploy/docker/server.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,8 @@ class CrawlResult(BaseModel):
842842
html: str
843843
success: bool
844844
cleaned_html: Optional[str] = None
845-
media: Dict[str, List[Dict]] = {}
846-
links: Dict[str, List[Dict]] = {}
845+
media: Dict[str, List[Dict]] = {"images": [], "videos": [], "audios": []}
846+
links: Dict[str, List[Dict]] = {"internal": [], "external": []}
847847
downloaded_files: Optional[List[str]] = None
848848
js_execution_result: Optional[Dict[str, Any]] = None
849849
screenshot: Optional[str] = None

‎docs/md_v2/api/crawl-result.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class CrawlResult(BaseModel):
1111
success: bool
1212
cleaned_html: Optional[str] = None
1313
fit_html: Optional[str] = None # Preprocessed HTML optimized for extraction
14-
media: Dict[str, List[Dict]] = {}
15-
links: Dict[str, List[Dict]] = {}
14+
media: Dict[str, List[Dict]] = {"images": [], "videos": [], "audios": []}
15+
links: Dict[str, List[Dict]] = {"internal": [], "external": []}
1616
downloaded_files: Optional[List[str]] = None
1717
screenshot: Optional[str] = None
1818
pdf : Optional[bytes] = None

‎docs/md_v2/complete-sdk-reference.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,8 +1077,8 @@ class CrawlResult(BaseModel):
10771077
success: bool
10781078
cleaned_html: Optional[str] = None
10791079
fit_html: Optional[str] = None # Preprocessed HTML optimized for extraction
1080-
media: Dict[str, List[Dict]] = {}
1081-
links: Dict[str, List[Dict]] = {}
1080+
media: Dict[str, List[Dict]] = {"images": [], "videos": [], "audios": []}
1081+
links: Dict[str, List[Dict]] = {"internal": [], "external": []}
10821082
downloaded_files: Optional[List[str]] = None
10831083
screenshot: Optional[str] = None
10841084
pdf : Optional[bytes] = None

‎docs/md_v2/core/crawler-result.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class CrawlResult(BaseModel):
2222
fit_html: Optional[str] = None
2323
success: bool
2424
cleaned_html: Optional[str] = None
25-
media: Dict[str, List[Dict]] = {}
26-
links: Dict[str, List[Dict]] = {}
25+
media: Dict[str, List[Dict]] = {"images": [], "videos": [], "audios": []}
26+
links: Dict[str, List[Dict]] = {"internal": [], "external": []}
2727
downloaded_files: Optional[List[str]] = None
2828
js_execution_result: Optional[Dict[str, Any]] = None
2929
screenshot: Optional[str] = None
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""A CrawlResult built without links/media must still have their keys.
2+
3+
Successful crawls fill `links` and `media` from the scraper's result (arun moves
4+
media's "tables" to CrawlResult.tables). Results built without them -- the
5+
robots.txt refusal, the "all proxies failed" and exception paths in
6+
AsyncWebCrawler.arun -- used to default to `{}`, so `result.links["internal"]`
7+
raised KeyError on exactly the results a batch caller has to handle, and the
8+
Docker API returned `"links": {}` / `"media": {}` for them.
9+
"""
10+
11+
from crawl4ai.models import CrawlResult
12+
13+
14+
def links_shape():
15+
return {"internal": [], "external": []}
16+
17+
18+
def media_shape():
19+
return {"images": [], "videos": [], "audios": []}
20+
21+
22+
def _refused(url="https://example.com/private"):
23+
# The shape AsyncWebCrawler.arun returns when robots.txt disallows a URL.
24+
return CrawlResult(
25+
url=url,
26+
html="",
27+
success=False,
28+
status_code=403,
29+
error_message="Access denied by robots.txt",
30+
)
31+
32+
33+
def test_result_without_links_or_media_has_the_scraped_shape():
34+
result = _refused()
35+
36+
assert result.links == links_shape()
37+
assert result.media == media_shape()
38+
39+
40+
def test_default_is_not_shared_between_results():
41+
first, second = _refused(), _refused("https://example.com/other")
42+
43+
assert first.links["internal"] is not second.links["internal"]
44+
assert first.media["images"] is not second.media["images"]
45+
46+
first.links["internal"].append({"href": "https://example.com/a"})
47+
first.media["images"].append({"src": "https://example.com/a.png"})
48+
49+
assert second.links == links_shape()
50+
assert second.media == media_shape()
51+
52+
53+
def test_serialized_result_keeps_the_keys():
54+
dumped = _refused().model_dump()
55+
56+
assert dumped["links"] == links_shape()
57+
assert dumped["media"] == media_shape()
58+
59+
60+
def test_explicit_values_are_kept():
61+
links = {"internal": [{"href": "https://example.com/a"}], "external": []}
62+
result = CrawlResult(
63+
url="https://example.com/", html="<p></p>", success=True, links=links
64+
)
65+
66+
assert result.links == links

0 commit comments

Comments
 (0)