scrapeunblocker-haystack 0.4.1__tar.gz → 0.5.0__tar.gz
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.
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/CHANGELOG.md +18 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/PKG-INFO +21 -2
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/README.md +20 -1
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/src/scrapeunblocker_haystack/search.py +14 -1
- scrapeunblocker_haystack-0.5.0/src/scrapeunblocker_haystack/version.py +1 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/tests/test_components.py +40 -2
- scrapeunblocker_haystack-0.4.1/src/scrapeunblocker_haystack/version.py +0 -1
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/.github/workflows/publish.yml +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/.github/workflows/test.yml +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/.gitignore +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/LICENSE +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/pyproject.toml +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/src/scrapeunblocker_haystack/__init__.py +0 -0
- {scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/src/scrapeunblocker_haystack/fetcher.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- `ScrapeUnblockerWebSearch` takes `full_urls` (default `False`): wait until
|
|
6
|
+
Google gives out every result's real URL. Google releases a results page's
|
|
7
|
+
links a few at a time, the last ones up to about 40 seconds after the search.
|
|
8
|
+
Without the flag the call returns sooner, and a result whose link has not
|
|
9
|
+
come yet has the site's home page as its `link` (or a Google redirect link
|
|
10
|
+
when Google shows no site). With it, the call takes up to about 40 seconds
|
|
11
|
+
longer. Billed the same. Sent to `/serpApi` only when `True`, and saved with
|
|
12
|
+
the component in `to_dict()`.
|
|
13
|
+
|
|
14
|
+
## 0.4.2
|
|
15
|
+
|
|
16
|
+
- `ScrapeUnblockerWebSearch` adds `sitelinks` (the extra links under a result:
|
|
17
|
+
`title`, `url`, `description`), `rating` (`value`, `max`, `reviews`, `url`)
|
|
18
|
+
and `rich_snippet` (the facts line under the snippet) to each Document's
|
|
19
|
+
metadata, from the new `/serpApi` fields.
|
|
20
|
+
|
|
3
21
|
## 0.4.1
|
|
4
22
|
|
|
5
23
|
- `ScrapeUnblockerWebSearch` adds `displayed_url`, `domain`, `source` (the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scrapeunblocker-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Haystack integration for ScrapeUnblocker - scrape pages behind anti-bot protections
|
|
5
5
|
Project-URL: Homepage, https://www.scrapeunblocker.com
|
|
6
6
|
Project-URL: Documentation, https://docs.scrapeunblocker.com
|
|
@@ -186,7 +186,8 @@ print(doc.content) # {"url": ..., "count": ..., "elements": [...]
|
|
|
186
186
|
|
|
187
187
|
Searches Google and returns the organic results as `Document` objects, with the
|
|
188
188
|
snippet as content and `title` / `link` / `position` / `displayed_url` / `domain` /
|
|
189
|
-
`source` (the site's name) / `date`
|
|
189
|
+
`source` (the site's name) / `date` / `sitelinks` / `rating` / `rich_snippet` (the
|
|
190
|
+
facts line under the snippet) in the metadata.
|
|
190
191
|
|
|
191
192
|
```python
|
|
192
193
|
from scrapeunblocker_haystack import ScrapeUnblockerWebSearch
|
|
@@ -204,6 +205,24 @@ for doc in result["documents"]:
|
|
|
204
205
|
| `pages_to_check` | `1` | How many result pages to scrape |
|
|
205
206
|
| `proxy_country` | `None` | Two-letter country code for localised results |
|
|
206
207
|
| `top_k` | `None` | Keep at most this many results |
|
|
208
|
+
| `full_urls` | `False` | Wait until Google gives out every result's real URL (see below) |
|
|
209
|
+
| `base_url` | `https://api.scrapeunblocker.com` | API base URL |
|
|
210
|
+
| `timeout` | `180` | HTTP timeout in seconds |
|
|
211
|
+
|
|
212
|
+
#### Every result's real URL
|
|
213
|
+
|
|
214
|
+
Google releases a results page's links a few at a time, the last ones up to
|
|
215
|
+
about 40 seconds after the search. By default the search returns sooner, so
|
|
216
|
+
most results carry their real URL in `meta["link"]`, and a result whose link
|
|
217
|
+
has not come yet has the site's home page there (or a Google redirect link when
|
|
218
|
+
Google shows no site). Set `full_urls=True` when you need the exact page of
|
|
219
|
+
every result, for example to fetch or cite them:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
search = ScrapeUnblockerWebSearch(full_urls=True)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The call then takes up to about 40 seconds longer. It is billed the same.
|
|
207
226
|
|
|
208
227
|
## In a pipeline
|
|
209
228
|
|
|
@@ -161,7 +161,8 @@ print(doc.content) # {"url": ..., "count": ..., "elements": [...]
|
|
|
161
161
|
|
|
162
162
|
Searches Google and returns the organic results as `Document` objects, with the
|
|
163
163
|
snippet as content and `title` / `link` / `position` / `displayed_url` / `domain` /
|
|
164
|
-
`source` (the site's name) / `date`
|
|
164
|
+
`source` (the site's name) / `date` / `sitelinks` / `rating` / `rich_snippet` (the
|
|
165
|
+
facts line under the snippet) in the metadata.
|
|
165
166
|
|
|
166
167
|
```python
|
|
167
168
|
from scrapeunblocker_haystack import ScrapeUnblockerWebSearch
|
|
@@ -179,6 +180,24 @@ for doc in result["documents"]:
|
|
|
179
180
|
| `pages_to_check` | `1` | How many result pages to scrape |
|
|
180
181
|
| `proxy_country` | `None` | Two-letter country code for localised results |
|
|
181
182
|
| `top_k` | `None` | Keep at most this many results |
|
|
183
|
+
| `full_urls` | `False` | Wait until Google gives out every result's real URL (see below) |
|
|
184
|
+
| `base_url` | `https://api.scrapeunblocker.com` | API base URL |
|
|
185
|
+
| `timeout` | `180` | HTTP timeout in seconds |
|
|
186
|
+
|
|
187
|
+
#### Every result's real URL
|
|
188
|
+
|
|
189
|
+
Google releases a results page's links a few at a time, the last ones up to
|
|
190
|
+
about 40 seconds after the search. By default the search returns sooner, so
|
|
191
|
+
most results carry their real URL in `meta["link"]`, and a result whose link
|
|
192
|
+
has not come yet has the site's home page there (or a Google redirect link when
|
|
193
|
+
Google shows no site). Set `full_urls=True` when you need the exact page of
|
|
194
|
+
every result, for example to fetch or cite them:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
search = ScrapeUnblockerWebSearch(full_urls=True)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The call then takes up to about 40 seconds longer. It is billed the same.
|
|
182
201
|
|
|
183
202
|
## In a pipeline
|
|
184
203
|
|
|
@@ -19,7 +19,7 @@ class ScrapeUnblockerWebSearch:
|
|
|
19
19
|
|
|
20
20
|
Each result becomes a Document whose content is the snippet (the title when
|
|
21
21
|
Google shows none), with the title, link, position, displayed URL, domain,
|
|
22
|
-
site name and
|
|
22
|
+
site name, date, sitelinks, star rating and facts line in the metadata.
|
|
23
23
|
|
|
24
24
|
### Usage example
|
|
25
25
|
|
|
@@ -41,6 +41,7 @@ class ScrapeUnblockerWebSearch:
|
|
|
41
41
|
top_k: Optional[int] = None,
|
|
42
42
|
base_url: str = DEFAULT_BASE_URL,
|
|
43
43
|
timeout: int = DEFAULT_TIMEOUT,
|
|
44
|
+
full_urls: bool = False,
|
|
44
45
|
) -> None:
|
|
45
46
|
"""
|
|
46
47
|
:param api_key: ScrapeUnblocker API key. Read from `SCRAPEUNBLOCKER_API_KEY` by default.
|
|
@@ -49,6 +50,11 @@ class ScrapeUnblockerWebSearch:
|
|
|
49
50
|
:param top_k: Keep at most this many results. `None` keeps all of them.
|
|
50
51
|
:param base_url: API base URL. Override to target a different environment.
|
|
51
52
|
:param timeout: HTTP timeout in seconds.
|
|
53
|
+
:param full_urls: Wait until Google gives out every result's real URL. Google releases a
|
|
54
|
+
results page's links a few at a time, the last ones up to about 40 seconds after the
|
|
55
|
+
search. Without this flag the call returns sooner, and a result whose link has not come
|
|
56
|
+
yet has the site's home page as its `link` (or a Google redirect link when Google shows
|
|
57
|
+
no site). With it, the call takes up to about 40 seconds longer. Billed the same.
|
|
52
58
|
"""
|
|
53
59
|
self.api_key = api_key
|
|
54
60
|
self.pages_to_check = pages_to_check
|
|
@@ -56,6 +62,7 @@ class ScrapeUnblockerWebSearch:
|
|
|
56
62
|
self.top_k = top_k
|
|
57
63
|
self.base_url = base_url.rstrip("/")
|
|
58
64
|
self.timeout = timeout
|
|
65
|
+
self.full_urls = full_urls
|
|
59
66
|
|
|
60
67
|
def to_dict(self) -> Dict[str, Any]:
|
|
61
68
|
"""Serialize this component to a dictionary."""
|
|
@@ -67,6 +74,7 @@ class ScrapeUnblockerWebSearch:
|
|
|
67
74
|
top_k=self.top_k,
|
|
68
75
|
base_url=self.base_url,
|
|
69
76
|
timeout=self.timeout,
|
|
77
|
+
full_urls=self.full_urls,
|
|
70
78
|
)
|
|
71
79
|
|
|
72
80
|
@classmethod
|
|
@@ -86,6 +94,8 @@ class ScrapeUnblockerWebSearch:
|
|
|
86
94
|
params: Dict[str, Any] = {"keyword": query, "pages_to_check": self.pages_to_check}
|
|
87
95
|
if self.proxy_country:
|
|
88
96
|
params["proxy_country"] = self.proxy_country
|
|
97
|
+
if self.full_urls:
|
|
98
|
+
params["full_urls"] = "true"
|
|
89
99
|
|
|
90
100
|
response = requests.post(
|
|
91
101
|
f"{self.base_url}/serpApi",
|
|
@@ -112,6 +122,9 @@ class ScrapeUnblockerWebSearch:
|
|
|
112
122
|
"domain": result.get("domain"),
|
|
113
123
|
"source": result.get("source"),
|
|
114
124
|
"date": result.get("date"),
|
|
125
|
+
"sitelinks": result.get("sitelinks") or [],
|
|
126
|
+
"rating": result.get("rating"),
|
|
127
|
+
"rich_snippet": result.get("richSnippet"),
|
|
115
128
|
"query": query,
|
|
116
129
|
},
|
|
117
130
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -213,7 +213,10 @@ class TestWebSearch:
|
|
|
213
213
|
"organic": [
|
|
214
214
|
{"title": "First", "url": "https://a.com", "description": "snippet a", "position": "1",
|
|
215
215
|
"displayedUrl": "https://a.com › docs", "domain": "a.com", "source": "A",
|
|
216
|
-
"date": "Oct 2, 2026"
|
|
216
|
+
"date": "Oct 2, 2026",
|
|
217
|
+
"sitelinks": [{"title": "Docs", "url": "https://a.com/docs", "description": None}],
|
|
218
|
+
"rating": {"value": 4.5, "max": 5.0, "reviews": 120, "url": None},
|
|
219
|
+
"richSnippet": "4.5 (120) · Free"},
|
|
217
220
|
{"title": "Second", "url": "https://b.com", "description": "snippet b", "position": "2"},
|
|
218
221
|
]
|
|
219
222
|
}
|
|
@@ -231,6 +234,11 @@ class TestWebSearch:
|
|
|
231
234
|
assert result["documents"][0].meta["source"] == "A"
|
|
232
235
|
assert result["documents"][0].meta["date"] == "Oct 2, 2026"
|
|
233
236
|
assert result["documents"][1].meta["date"] is None
|
|
237
|
+
assert result["documents"][0].meta["sitelinks"][0]["url"] == "https://a.com/docs"
|
|
238
|
+
assert result["documents"][0].meta["rating"]["value"] == 4.5
|
|
239
|
+
assert result["documents"][0].meta["rich_snippet"] == "4.5 (120) · Free"
|
|
240
|
+
assert result["documents"][1].meta["sitelinks"] == []
|
|
241
|
+
assert result["documents"][1].meta["rating"] is None
|
|
234
242
|
|
|
235
243
|
_, kwargs = mock_post.call_args
|
|
236
244
|
assert kwargs["params"]["keyword"] == "test query"
|
|
@@ -251,12 +259,42 @@ class TestWebSearch:
|
|
|
251
259
|
|
|
252
260
|
assert search.run(query="q")["documents"] == []
|
|
253
261
|
|
|
262
|
+
@patch("scrapeunblocker_haystack.search.requests.post")
|
|
263
|
+
def test_full_urls_off_by_default(self, mock_post):
|
|
264
|
+
mock_post.return_value = _response(json_data={"organic": []})
|
|
265
|
+
search = ScrapeUnblockerWebSearch(api_key=API_KEY)
|
|
266
|
+
assert search.full_urls is False
|
|
267
|
+
|
|
268
|
+
search.run(query="q")
|
|
269
|
+
|
|
270
|
+
_, kwargs = mock_post.call_args
|
|
271
|
+
assert "full_urls" not in kwargs["params"]
|
|
272
|
+
|
|
273
|
+
@patch("scrapeunblocker_haystack.search.requests.post")
|
|
274
|
+
def test_full_urls_sent_when_on(self, mock_post):
|
|
275
|
+
mock_post.return_value = _response(json_data={"organic": []})
|
|
276
|
+
search = ScrapeUnblockerWebSearch(api_key=API_KEY, full_urls=True)
|
|
277
|
+
|
|
278
|
+
search.run(query="q")
|
|
279
|
+
|
|
280
|
+
_, kwargs = mock_post.call_args
|
|
281
|
+
assert kwargs["params"]["full_urls"] == "true"
|
|
282
|
+
|
|
254
283
|
def test_to_dict_and_back(self):
|
|
255
|
-
search = ScrapeUnblockerWebSearch(api_key=ENV_KEY, top_k=5, proxy_country="us")
|
|
284
|
+
search = ScrapeUnblockerWebSearch(api_key=ENV_KEY, top_k=5, proxy_country="us", full_urls=True)
|
|
256
285
|
data = search.to_dict()
|
|
286
|
+
assert data["init_parameters"]["full_urls"] is True
|
|
257
287
|
restored = ScrapeUnblockerWebSearch.from_dict(data)
|
|
258
288
|
assert restored.top_k == 5
|
|
259
289
|
assert restored.proxy_country == "us"
|
|
290
|
+
assert restored.full_urls is True
|
|
291
|
+
|
|
292
|
+
def test_from_dict_of_older_version(self):
|
|
293
|
+
data = ScrapeUnblockerWebSearch(api_key=ENV_KEY).to_dict()
|
|
294
|
+
del data["init_parameters"]["full_urls"]
|
|
295
|
+
|
|
296
|
+
restored = ScrapeUnblockerWebSearch.from_dict(data)
|
|
297
|
+
assert restored.full_urls is False
|
|
260
298
|
|
|
261
299
|
|
|
262
300
|
class TestTargetNotFound:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.1"
|
{scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
{scrapeunblocker_haystack-0.4.1 → scrapeunblocker_haystack-0.5.0}/.github/workflows/test.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|