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.
@@ -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.4.1
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` in the metadata.
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` in the metadata.
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 date in the metadata.
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"