scrapedo-haystack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Haystack component that fetches web pages through the Scrape.do API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from haystack import Document, component, default_from_dict, default_to_dict
|
|
11
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
SCRAPEDO_API_URL = "https://api.scrape.do/"
|
|
16
|
+
|
|
17
|
+
OutputFormat = Literal["markdown", "raw"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@component
|
|
21
|
+
class ScrapeDoFetcher:
|
|
22
|
+
"""
|
|
23
|
+
Fetches web pages through the Scrape.do API and returns them as Haystack `Document`s.
|
|
24
|
+
|
|
25
|
+
Scrape.do handles proxies, retries and anti-bot protection. Set `render=True` for pages
|
|
26
|
+
that need JavaScript, `super_proxy=True` for residential and mobile IPs, and `geo_code`
|
|
27
|
+
to route the request through a specific country.
|
|
28
|
+
|
|
29
|
+
Usage example:
|
|
30
|
+
```python
|
|
31
|
+
from scrapedo_haystack import ScrapeDoFetcher
|
|
32
|
+
|
|
33
|
+
fetcher = ScrapeDoFetcher() # reads the SCRAPEDO_API_KEY environment variable
|
|
34
|
+
result = fetcher.run(urls=["https://example.com"])
|
|
35
|
+
print(result["documents"][0].content)
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: Secret = Secret.from_env_var("SCRAPEDO_API_KEY"),
|
|
42
|
+
output: OutputFormat = "markdown",
|
|
43
|
+
render: bool = False,
|
|
44
|
+
super_proxy: bool = False,
|
|
45
|
+
geo_code: str | None = None,
|
|
46
|
+
extra_params: dict[str, Any] | None = None,
|
|
47
|
+
timeout: int = 60,
|
|
48
|
+
max_workers: int = 4,
|
|
49
|
+
raise_on_failure: bool = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Create a ScrapeDoFetcher.
|
|
53
|
+
|
|
54
|
+
:param api_key: Scrape.do API token. Defaults to the `SCRAPEDO_API_KEY` environment variable.
|
|
55
|
+
:param output: `"markdown"` returns the page as Markdown, `"raw"` returns the original HTML.
|
|
56
|
+
:param render: Render the page in a headless browser so JavaScript runs.
|
|
57
|
+
:param super_proxy: Use residential and mobile proxies.
|
|
58
|
+
:param geo_code: Two-letter country code to route the request through, for example `"us"`.
|
|
59
|
+
:param extra_params: Any other Scrape.do API parameters, passed as-is (for example `{"waitUntil": "networkidle0"}`).
|
|
60
|
+
:param timeout: Request timeout in seconds.
|
|
61
|
+
:param max_workers: Number of URLs fetched in parallel.
|
|
62
|
+
:param raise_on_failure: Raise an error when a URL fails. If `False`, failed URLs are logged and skipped.
|
|
63
|
+
"""
|
|
64
|
+
if output not in ("markdown", "raw"):
|
|
65
|
+
raise ValueError(f"output must be 'markdown' or 'raw', got {output!r}")
|
|
66
|
+
self.api_key = api_key
|
|
67
|
+
self.output = output
|
|
68
|
+
self.render = render
|
|
69
|
+
self.super_proxy = super_proxy
|
|
70
|
+
self.geo_code = geo_code
|
|
71
|
+
self.extra_params = extra_params or {}
|
|
72
|
+
self.timeout = timeout
|
|
73
|
+
self.max_workers = max_workers
|
|
74
|
+
self.raise_on_failure = raise_on_failure
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, Any]:
|
|
77
|
+
"""Serialize the component to a dictionary."""
|
|
78
|
+
return default_to_dict(
|
|
79
|
+
self,
|
|
80
|
+
api_key=self.api_key.to_dict(),
|
|
81
|
+
output=self.output,
|
|
82
|
+
render=self.render,
|
|
83
|
+
super_proxy=self.super_proxy,
|
|
84
|
+
geo_code=self.geo_code,
|
|
85
|
+
extra_params=self.extra_params,
|
|
86
|
+
timeout=self.timeout,
|
|
87
|
+
max_workers=self.max_workers,
|
|
88
|
+
raise_on_failure=self.raise_on_failure,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, data: dict[str, Any]) -> "ScrapeDoFetcher":
|
|
93
|
+
"""Deserialize the component from a dictionary."""
|
|
94
|
+
deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
|
|
95
|
+
return default_from_dict(cls, data)
|
|
96
|
+
|
|
97
|
+
def _build_params(self, url: str) -> dict[str, Any]:
|
|
98
|
+
token = self.api_key.resolve_value()
|
|
99
|
+
if not token:
|
|
100
|
+
raise ValueError("Scrape.do API key is missing. Set the SCRAPEDO_API_KEY environment variable.")
|
|
101
|
+
params: dict[str, Any] = {"token": token, "url": url}
|
|
102
|
+
if self.output == "markdown":
|
|
103
|
+
params["output"] = "markdown"
|
|
104
|
+
if self.render:
|
|
105
|
+
params["render"] = "true"
|
|
106
|
+
if self.super_proxy:
|
|
107
|
+
params["super"] = "true"
|
|
108
|
+
if self.geo_code:
|
|
109
|
+
params["geoCode"] = self.geo_code
|
|
110
|
+
params.update(self.extra_params)
|
|
111
|
+
return params
|
|
112
|
+
|
|
113
|
+
def _fetch(self, url: str) -> Document | None:
|
|
114
|
+
try:
|
|
115
|
+
response = requests.get(SCRAPEDO_API_URL, params=self._build_params(url), timeout=self.timeout)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
except Exception as error:
|
|
118
|
+
if self.raise_on_failure:
|
|
119
|
+
raise
|
|
120
|
+
# never log the request URL: it contains the API token
|
|
121
|
+
logger.warning("Scrape.do could not fetch %s: %s", url, type(error).__name__)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
meta = {
|
|
125
|
+
"url": url,
|
|
126
|
+
"status_code": response.status_code,
|
|
127
|
+
"content_type": "text/markdown" if self.output == "markdown" else response.headers.get("Content-Type"),
|
|
128
|
+
}
|
|
129
|
+
return Document(content=response.text, meta=meta)
|
|
130
|
+
|
|
131
|
+
@component.output_types(documents=list[Document])
|
|
132
|
+
def run(self, urls: list[str]) -> dict[str, list[Document]]:
|
|
133
|
+
"""
|
|
134
|
+
Fetch the given URLs through Scrape.do.
|
|
135
|
+
|
|
136
|
+
:param urls: The web pages to fetch.
|
|
137
|
+
:returns: A dictionary with `documents`: one Document per successfully fetched URL,
|
|
138
|
+
with the page content and `url`, `status_code` and `content_type` in `meta`.
|
|
139
|
+
"""
|
|
140
|
+
if not urls:
|
|
141
|
+
return {"documents": []}
|
|
142
|
+
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(urls))) as pool:
|
|
143
|
+
results = list(pool.map(self._fetch, urls))
|
|
144
|
+
return {"documents": [doc for doc in results if doc is not None]}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scrapedo-haystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fetch web pages through the Scrape.do API in Haystack pipelines and agents.
|
|
5
|
+
Project-URL: Homepage, https://scrape.do
|
|
6
|
+
Project-URL: Documentation, https://scrape.do/documentation/
|
|
7
|
+
Project-URL: Source, https://github.com/bugrahansss/scrapedo-haystack
|
|
8
|
+
Project-URL: Issues, https://github.com/bugrahansss/scrapedo-haystack/issues
|
|
9
|
+
Author: Scrape.do
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: haystack,llm,rag,scrape.do,web scraping
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: haystack-ai>=2.0
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# scrapedo-haystack
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/scrapedo-haystack/)
|
|
26
|
+
|
|
27
|
+
A [Haystack](https://haystack.deepset.ai/) component that fetches web pages through the [Scrape.do](https://scrape.do) API and returns them as Haystack `Document`s.
|
|
28
|
+
|
|
29
|
+
Scrape.do handles proxies, retries, CAPTCHAs and anti-bot protection, so the component works on pages that block normal HTTP requests. Pages can be returned as Markdown (ready for an LLM) or raw HTML.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install scrapedo-haystack
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Get an API token from [Scrape.do](https://scrape.do) and set it as an environment variable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export SCRAPEDO_API_KEY="your-token"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Fetch pages
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from scrapedo_haystack import ScrapeDoFetcher
|
|
49
|
+
|
|
50
|
+
fetcher = ScrapeDoFetcher()
|
|
51
|
+
result = fetcher.run(urls=["https://example.com"])
|
|
52
|
+
|
|
53
|
+
doc = result["documents"][0]
|
|
54
|
+
print(doc.meta["url"])
|
|
55
|
+
print(doc.content) # page content as Markdown
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Options
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
fetcher = ScrapeDoFetcher(
|
|
62
|
+
output="markdown", # "markdown" or "raw" (original HTML)
|
|
63
|
+
render=True, # run JavaScript in a headless browser
|
|
64
|
+
super_proxy=True, # use residential and mobile IPs
|
|
65
|
+
geo_code="us", # route the request through a country
|
|
66
|
+
extra_params={"waitUntil": "networkidle0"}, # any other Scrape.do parameter
|
|
67
|
+
timeout=60,
|
|
68
|
+
max_workers=4, # URLs fetched in parallel
|
|
69
|
+
raise_on_failure=False, # skip failed URLs instead of raising
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All Scrape.do API parameters are listed in the [Scrape.do documentation](https://scrape.do/documentation/).
|
|
74
|
+
|
|
75
|
+
### Use in a RAG pipeline
|
|
76
|
+
|
|
77
|
+
Fetch pages, split them, and answer a question from their content:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from haystack import Pipeline
|
|
81
|
+
from haystack.components.builders import ChatPromptBuilder
|
|
82
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
83
|
+
from haystack.components.preprocessors import DocumentSplitter
|
|
84
|
+
from haystack.dataclasses import ChatMessage
|
|
85
|
+
|
|
86
|
+
from scrapedo_haystack import ScrapeDoFetcher
|
|
87
|
+
|
|
88
|
+
template = [
|
|
89
|
+
ChatMessage.from_user(
|
|
90
|
+
"""Answer the question using only these pages.
|
|
91
|
+
|
|
92
|
+
{% for doc in documents %}
|
|
93
|
+
Source: {{ doc.meta.url }}
|
|
94
|
+
{{ doc.content }}
|
|
95
|
+
{% endfor %}
|
|
96
|
+
|
|
97
|
+
Question: {{ question }}"""
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
pipe = Pipeline()
|
|
102
|
+
pipe.add_component("fetcher", ScrapeDoFetcher(render=True))
|
|
103
|
+
pipe.add_component("splitter", DocumentSplitter(split_by="word", split_length=300))
|
|
104
|
+
pipe.add_component("prompt", ChatPromptBuilder(template=template, required_variables=["question"]))
|
|
105
|
+
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
106
|
+
|
|
107
|
+
pipe.connect("fetcher.documents", "splitter.documents")
|
|
108
|
+
pipe.connect("splitter.documents", "prompt.documents")
|
|
109
|
+
pipe.connect("prompt.prompt", "llm.messages")
|
|
110
|
+
|
|
111
|
+
result = pipe.run({
|
|
112
|
+
"fetcher": {"urls": ["https://haystack.deepset.ai/overview/intro"]},
|
|
113
|
+
"prompt": {"question": "What is Haystack?"},
|
|
114
|
+
})
|
|
115
|
+
print(result["llm"]["replies"][0].text)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This example also needs `OPENAI_API_KEY` set.
|
|
119
|
+
|
|
120
|
+
### Load a saved pipeline
|
|
121
|
+
|
|
122
|
+
Haystack 3 only loads components from allowed modules when reading a pipeline from YAML. Add this package to the allowlist:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
pipe = Pipeline.loads(yaml_text, allowed_modules=["scrapedo_haystack.fetcher"])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The API token is never written to the YAML; it is read from `SCRAPEDO_API_KEY` when the pipeline loads.
|
|
129
|
+
|
|
130
|
+
## Output
|
|
131
|
+
|
|
132
|
+
Each fetched URL becomes one `Document`:
|
|
133
|
+
|
|
134
|
+
| Field | Value |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `content` | Page as Markdown, or raw HTML when `output="raw"` |
|
|
137
|
+
| `meta["url"]` | The requested URL |
|
|
138
|
+
| `meta["status_code"]` | HTTP status returned by Scrape.do |
|
|
139
|
+
| `meta["content_type"]` | `text/markdown`, or the page's content type for raw output |
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install -e ".[test]"
|
|
145
|
+
pytest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
Apache-2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
scrapedo_haystack/__init__.py,sha256=i5LrKHQBM5d6fuJfio0DXCv2icDL63IyiamxLI2EXEU,107
|
|
2
|
+
scrapedo_haystack/fetcher.py,sha256=dYx540GIkch1OaNlxXE5Ie07-wJn2ixEp70oThh-_m4,5751
|
|
3
|
+
scrapedo_haystack-0.1.0.dist-info/METADATA,sha256=f_pzB1hV8VOVDr-5zgO0x8sAYcdzlPs3uPPU-j0k4S0,4595
|
|
4
|
+
scrapedo_haystack-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
5
|
+
scrapedo_haystack-0.1.0.dist-info/licenses/LICENSE,sha256=S8lp3ijo6jkSfxEyZIQIh6Wnu1t7IGcfoLZd59rNPTs,738
|
|
6
|
+
scrapedo_haystack-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Scrape.do
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|