scrapeunblocker 0.8.0 → 0.10.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +51 -37
- data/lib/scrapeunblocker/client.rb +8 -2
- data/lib/scrapeunblocker/errors.rb +22 -9
- data/lib/scrapeunblocker/plugins.rb +3815 -0
- data/lib/scrapeunblocker/version.rb +1 -1
- data/lib/scrapeunblocker.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ffe8b5253c990d2b8f3754b32da58cd5a74150513c5592606dcc70c24d98e6c
|
|
4
|
+
data.tar.gz: a086e3553aeb1fd48bd57884a2315adfbe92b55763277298db932365b42c44bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c0efc8818178442559f1d4ee6ab1e1fe16938e5f5f3c4371eae0506a6ec5d498df37345e726d26293807be62783051bf5824510cb23b3099fa6eb44cdb54b44
|
|
7
|
+
data.tar.gz: 15d718a1db74279f32f17bfbfda2034987f411e61514c15c122567115edcabb3849dcbeae4ed1fbe9e94e1c913ac6997eea1c638ba2c645d93ef618ea234562e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- 104 new plugin methods on `ScrapeUnblocker::Client`, one for every public plugin endpoint: e-commerce (Walmart, Target, Best Buy, Home Depot, Etsy, Temu, SHEIN, Zalando, AliExpress, Alibaba, 1688, TikTok Shop, Amazon Best Sellers and reviews), groceries (Woolworths, Coles, ALDI Australia), travel (Booking.com reviews, Airbnb listings and reviews, Tripadvisor), Google (Trends, Shopping, Jobs, Maps place details), Bing / DuckDuckGo / Yahoo / Brave search and images, real estate (Zillow, Redfin, Rightmove, Idealista, ImmobilienScout24), vehicles (mobile.de, AutoScout24), jobs (Indeed, Glassdoor, Naukri, Wellfound, company career sites), reviews and companies (Trustpilot, G2, Capterra, Product Hunt, Crunchbase), Yelp, Naver Place, Google Play, the App Store, YouTube, Spotify, Telegram, Quora, Similarweb, Ahrefs and Semrush. The README lists them by category; every parameter is on the docs plugin pages.
|
|
6
|
+
- Each method takes the plugin's main input positionally and the rest as keyword options; options left `nil` are not sent, so the API's defaults apply. Methods for one mode of a multi-mode endpoint set the mode themselves (`woolworths_product("888137")`, `spotify_artist(...)`).
|
|
7
|
+
- New `plugin(path, **params)` calls any plugin endpoint by path. Array values are sent as a repeated query parameter.
|
|
8
|
+
|
|
9
|
+
No breaking changes.
|
|
10
|
+
|
|
11
|
+
## 0.9.0 (2026-10-07)
|
|
12
|
+
|
|
13
|
+
- New `ScrapeUnblocker::BudgetExceededError` (a subclass of `PaymentRequiredError`): raised when the API answers 402 with `User set budget exceeded`, meaning this billing period's spend reached the monthly budget limit (EUR, excluding VAT) set in the [profile](https://app.scrapeunblocker.com/dashboard/profile). It clears at the start of the next billing period, or within about a minute after the limit is raised or removed. Like the other 402s it is never retried and not billed. Before, this body surfaced as a plain `PaymentRequiredError`.
|
|
14
|
+
|
|
15
|
+
No breaking changes: `rescue ScrapeUnblocker::PaymentRequiredError` still catches it.
|
|
16
|
+
|
|
3
17
|
## 0.8.0 (2026-10-06)
|
|
4
18
|
|
|
5
19
|
- `get_parsed` on a page that rendered but held no structured data now returns a normal `ParsedPage` instead of raising `NoDataExtractedError`. The API answers this case with a billed 200: `data` is empty, the new `data_extracted?` is `false`, the new `detail` explains it and the new `html` carries the rendered page, so no second call is needed. On a successful parse `data_extracted?` is `true` and `html` / `detail` are `nil`.
|
data/README.md
CHANGED
|
@@ -108,13 +108,54 @@ result["elements"].each { |el| p el }
|
|
|
108
108
|
```ruby
|
|
109
109
|
result = su.get_parsed("https://www.walmart.com/ip/12345")
|
|
110
110
|
puts result.page_type # e.g. "product"
|
|
111
|
-
puts result.source # how it was extracted
|
|
112
111
|
p result.data # the fields
|
|
113
112
|
|
|
114
113
|
# If a parse ever comes back wrong, force a fresh set of rules:
|
|
115
114
|
fresh = su.get_parsed(url, refresh_rules: true, rules_hint: "price is missing")
|
|
116
115
|
```
|
|
117
116
|
|
|
117
|
+
## Plugins
|
|
118
|
+
|
|
119
|
+
Ready-made scrapers for specific sites that return structured JSON. Each
|
|
120
|
+
method takes the plugin's main input positionally and everything else as
|
|
121
|
+
keyword options; options you leave out are not sent, so the API's own
|
|
122
|
+
defaults apply. Every parameter is documented on the
|
|
123
|
+
[plugin pages](https://docs.scrapeunblocker.com/plugins/overview?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk).
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
products = su.target_search("coffee maker", sort: "price_low", max_results: 48)
|
|
127
|
+
milk = su.woolworths_product("888137") # one product, with nutrition
|
|
128
|
+
trends = su.google_trends(%w[coffee tea], geo: "US") # a list is sent comma-separated
|
|
129
|
+
reviews = su.trustpilot_reviews("www.nike.com", stars: [4, 5])
|
|
130
|
+
jobs = su.career_site_jobs("https://jobs.ashbyhq.com/ramp", keyword: "engineer")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
<!-- plugin-methods:start -->
|
|
134
|
+
| Category | Methods |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| Travel | `skyscanner.flights`, `skyscanner.hotels`, `skyscanner.carhire`, `booking_hotels`, `booking_reviews`, `airbnb_search`, `airbnb_room`, `airbnb_reviews`, `tripadvisor_list`, `tripadvisor_reviews` |
|
|
137
|
+
| Google | `google_local`, `google_images`, `maps_place_details`, `google_trends`, `google_shopping`, `google_jobs` |
|
|
138
|
+
| Search engines | `bing_search`, `bing_images`, `duckduckgo_search`, `yahoo_search`, `yahoo_images`, `brave_search`, `brave_images` |
|
|
139
|
+
| E-commerce | `amazon_product`, `amazon_search`, `amazon_bestsellers`, `amazon_reviews`, `walmart_search`, `target_search`, `bestbuy_search`, `homedepot_search`, `etsy_search`, `temu_search`, `temu_product`, `shein_search`, `zalando_search`, `aliexpress_search`, `alibaba_search`, `alibaba_product`, `alibaba1688_search`, `alibaba1688_product`, `tiktok_shop_search`, `tiktok_shop_product`, `tiktok_shop_store`, `oopbuy_search` |
|
|
140
|
+
| Classifieds & Resale | `ebay_search`, `vinted_search`, `leboncoin_search` |
|
|
141
|
+
| Groceries | `woolworths_search`, `woolworths_category`, `woolworths_specials`, `woolworths_product`, `coles_search`, `coles_category`, `coles_specials`, `coles_product`, `aldi_au_search`, `aldi_au_category`, `aldi_au_specials`, `aldi_au_product` |
|
|
142
|
+
| Real estate | `zillow_search`, `zillow_detail`, `redfin_search`, `redfin_detail`, `rightmove_search`, `idealista_search`, `idealista_detail`, `immobilienscout24_search` |
|
|
143
|
+
| Vehicles | `mobile_de`, `autoscout24_search` |
|
|
144
|
+
| Jobs | `indeed_jobs`, `glassdoor_jobs`, `glassdoor_reviews`, `glassdoor_salaries`, `naukri_jobs`, `wellfound_jobs`, `wellfound_job`, `wellfound_company`, `wellfound_company_search`, `career_site_jobs` |
|
|
145
|
+
| Reviews & Companies | `trustpilot_reviews`, `g2_reviews`, `g2_search`, `capterra_reviews`, `capterra_search`, `product_hunt`, `crunchbase_company`, `crunchbase_search` |
|
|
146
|
+
| Local & Maps | `yelp_search`, `yelp_business`, `yelp_reviews`, `naver_place_search`, `naver_place_details` |
|
|
147
|
+
| Apps | `google_play_app`, `google_play_search`, `app_store_app`, `app_store_search`, `app_store_reviews` |
|
|
148
|
+
| Video, Music & Communities | `youtube_channel`, `youtube_search`, `youtube_comments`, `youtube_transcript`, `spotify_search`, `spotify_artist`, `spotify_album`, `spotify_track`, `spotify_playlist`, `telegram_channel`, `quora_question`, `quora_profile` |
|
|
149
|
+
| SEO & Ads | `similarweb_overview`, `ahrefs`, `semrush_overview`, `meta_ad_library` |
|
|
150
|
+
<!-- plugin-methods:end -->
|
|
151
|
+
|
|
152
|
+
Any plugin endpoint can also be called by path, with parameters named as the
|
|
153
|
+
API names them - handy for an endpoint added after your gem version:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
data = su.plugin("/marketplace/target-search", query: "tv", max_results: 24)
|
|
157
|
+
```
|
|
158
|
+
|
|
118
159
|
## Google search (SERP)
|
|
119
160
|
|
|
120
161
|
```ruby
|
|
@@ -185,19 +226,6 @@ end
|
|
|
185
226
|
|
|
186
227
|
`marketplace` is any of the 19 regional eBay hosts (`ebay.com` default). `condition` is one of `"new"`, `"open_box"`, `"refurbished"`, `"used"` or `"for_parts"`; `sort` is one of `"best_match"` (default), `"newly_listed"`, `"ending_soon"`, `"price_asc"` or `"price_desc"`; `page_size` is 60, 120 or 240. `exactMatches` is `false` when eBay found nothing for the keyword and answered with its own loosely-related suggestions instead, so check it before using the listings.
|
|
187
228
|
|
|
188
|
-
### TikTok
|
|
189
|
-
|
|
190
|
-
```ruby
|
|
191
|
-
profile = su.tiktok_profile("nasa", max_videos: 5) # exact stats + newest videos
|
|
192
|
-
video = su.tiktok_video("https://www.tiktok.com/@nasa/video/7665075736742530317", include_transcript: true)
|
|
193
|
-
tag = su.tiktok_hashtag("nasa", max_videos: 10)
|
|
194
|
-
results = su.tiktok_search("space telescope", max_results: 25) # TikTok's own ranking
|
|
195
|
-
comments = su.tiktok_comments("https://www.tiktok.com/@nasa/video/7665075736742530317", max_comments: 40)
|
|
196
|
-
puts profile["stats"]["followers"], video["stats"]["plays"], tag["stats"]["views"], comments["totalComments"]
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Profiles and hashtags list up to 10 videos in a couple of seconds from TikTok's server-rendered widget; ask for more (up to 200) and the real grid is scrolled in a browser session.
|
|
200
|
-
|
|
201
229
|
## Cookies and the serving proxy
|
|
202
230
|
|
|
203
231
|
```ruby
|
|
@@ -230,25 +258,6 @@ hotels = su.skyscanner.hotels(destination: "Madrid", checkin: "2026-09-01", chec
|
|
|
230
258
|
cars = su.skyscanner.carhire(pickup: "Madrid", pickup_datetime: "2026-09-01T10:00", dropoff_datetime: "2026-09-03T10:00")
|
|
231
259
|
```
|
|
232
260
|
|
|
233
|
-
## Southwest plugin
|
|
234
|
-
|
|
235
|
-
Southwest Airlines flight quotes as JSON. `origin` and `dest` are IATA airport codes; omit `return_date` for a one-way search.
|
|
236
|
-
|
|
237
|
-
```ruby
|
|
238
|
-
flights = su.southwest.flights(
|
|
239
|
-
origin: "DAL", dest: "HOU",
|
|
240
|
-
depart_date: "2026-10-20", return_date: "2026-10-27"
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
# One-way, priced in Rapid Rewards points
|
|
244
|
-
oneway = su.southwest.flights(
|
|
245
|
-
origin: "DAL", dest: "HOU",
|
|
246
|
-
depart_date: "2026-10-20", adults: 2, fare_type: "points"
|
|
247
|
-
)
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
`adults` is 1-8 (default 1), `fare_type` is `"dollars"` (default) or `"points"`, `proxy_country` defaults to `"US"` and `max_attempts` is 1-5 (default 3). The call returns the raw booking / shopping JSON.
|
|
251
|
-
|
|
252
261
|
## Error handling
|
|
253
262
|
|
|
254
263
|
Non-2xx responses raise typed errors, all subclasses of `ScrapeUnblocker::Error`.
|
|
@@ -259,7 +268,7 @@ begin
|
|
|
259
268
|
rescue ScrapeUnblocker::BlockedError
|
|
260
269
|
# 403: the target blocked every bypass path (not billed)
|
|
261
270
|
rescue ScrapeUnblocker::PaymentRequiredError
|
|
262
|
-
# 402: quota, credit limit, or a failed payment - fix billing
|
|
271
|
+
# 402: quota, credit limit, your budget limit, or a failed payment - fix billing
|
|
263
272
|
rescue ScrapeUnblocker::RateLimitError
|
|
264
273
|
# 429: slow down
|
|
265
274
|
rescue ScrapeUnblocker::UpstreamOutageError
|
|
@@ -272,9 +281,10 @@ end
|
|
|
272
281
|
| `InvalidRequestError` | 400 | Bad URL, unsupported scheme, or the API key header was not sent |
|
|
273
282
|
| `AuthenticationError` | 401 | Key not recognised - typo, stray whitespace, or a rotated key |
|
|
274
283
|
| `NoSubscriptionError` | 401 | Key is fine, but the account has no active plan |
|
|
275
|
-
| `PaymentRequiredError` | 402 | Billing block - base class for the
|
|
284
|
+
| `PaymentRequiredError` | 402 | Billing block - base class for the four below |
|
|
276
285
|
| `QuotaExceededError` | 402 | The plan's requests for this period are used up |
|
|
277
286
|
| `CreditLimitExceededError` | 402 | Unpaid balance is past the account's credit limit |
|
|
287
|
+
| `BudgetExceededError` | 402 | This billing period's spend reached the monthly budget limit you set |
|
|
278
288
|
| `PaymentFailedError` | 402 | A card payment was declined three times |
|
|
279
289
|
| `BlockedError` | 403 | Blocked by bot protection on every path |
|
|
280
290
|
| `NotFoundError` | 404 | What you asked for does not exist - no image on the page (`get_image`), or a plugin lookup found nothing |
|
|
@@ -323,7 +333,7 @@ Transient failures (429, 502, 503, 504 and network errors) are retried automatic
|
|
|
323
333
|
|
|
324
334
|
### Billing errors (402)
|
|
325
335
|
|
|
326
|
-
The
|
|
336
|
+
The four billing blocks share a status code and differ only in their message, so the client raises a dedicated error for each:
|
|
327
337
|
|
|
328
338
|
```ruby
|
|
329
339
|
begin
|
|
@@ -332,12 +342,16 @@ rescue ScrapeUnblocker::QuotaExceededError
|
|
|
332
342
|
# plan quota (plus any overage allowance) is used up for this period
|
|
333
343
|
rescue ScrapeUnblocker::CreditLimitExceededError
|
|
334
344
|
# unpaid balance passed the account credit limit
|
|
345
|
+
rescue ScrapeUnblocker::BudgetExceededError
|
|
346
|
+
# this period's spend reached the monthly budget limit set in your profile
|
|
335
347
|
rescue ScrapeUnblocker::PaymentFailedError
|
|
336
348
|
# card declined three times - update the payment method
|
|
337
349
|
end
|
|
338
350
|
```
|
|
339
351
|
|
|
340
|
-
When more than one applies, the most serious wins: failed payment outranks credit limit, which outranks quota. All
|
|
352
|
+
When more than one applies, the most serious wins: failed payment outranks credit limit, which outranks quota, which outranks your own budget limit. All four lift by themselves once the billing state changes - access returns within about a minute, and the API key stays the same. One catch worth knowing: subscribing to a new plan does **not** clear `PaymentFailedError`, because the old unpaid invoice stays open until it is paid.
|
|
353
|
+
|
|
354
|
+
`BudgetExceededError` (body `User set budget exceeded`) means this billing period's spend reached the monthly budget limit you set in your [profile](https://app.scrapeunblocker.com/dashboard/profile?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk) (EUR, excluding VAT). The key works again at the start of the next billing period, or within about a minute after you raise or remove the limit.
|
|
341
355
|
|
|
342
356
|
Full details for every status code: [docs.scrapeunblocker.com/errors](https://docs.scrapeunblocker.com/errors).
|
|
343
357
|
|
|
@@ -6,6 +6,7 @@ require "json"
|
|
|
6
6
|
|
|
7
7
|
require_relative "errors"
|
|
8
8
|
require_relative "parsed_page"
|
|
9
|
+
require_relative "plugins"
|
|
9
10
|
require_relative "skyscanner"
|
|
10
11
|
require_relative "southwest"
|
|
11
12
|
require_relative "version"
|
|
@@ -16,6 +17,8 @@ module ScrapeUnblocker
|
|
|
16
17
|
# su = ScrapeUnblocker::Client.new(api_key: "YOUR_API_KEY")
|
|
17
18
|
# html = su.get_page_source("https://example.com")
|
|
18
19
|
class Client
|
|
20
|
+
include Plugins
|
|
21
|
+
|
|
19
22
|
DEFAULT_BASE_URL = "https://api.scrapeunblocker.com"
|
|
20
23
|
API_KEY_HEADER = "x-scrapeunblocker-key"
|
|
21
24
|
RETRYABLE = [429, 502, 503, 504].freeze
|
|
@@ -302,8 +305,11 @@ module ScrapeUnblocker
|
|
|
302
305
|
params.each_with_object([]) do |(key, value), acc|
|
|
303
306
|
next if value.nil?
|
|
304
307
|
|
|
305
|
-
|
|
306
|
-
|
|
308
|
+
# A list is a repeated parameter (stars=4&stars=5), as the API reads it.
|
|
309
|
+
Array(value).each do |item|
|
|
310
|
+
item = item ? "true" : "false" if item == true || item == false
|
|
311
|
+
acc << "#{key}=#{URI.encode_www_form_component(item.to_s)}"
|
|
312
|
+
end
|
|
307
313
|
end.join("&")
|
|
308
314
|
end
|
|
309
315
|
|
|
@@ -36,15 +36,16 @@ module ScrapeUnblocker
|
|
|
36
36
|
# The account has a billing problem (HTTP 402).
|
|
37
37
|
#
|
|
38
38
|
# Credentials are fine - the request was stopped for a billing reason. There
|
|
39
|
-
# are
|
|
40
|
-
# CreditLimitExceededError and PaymentFailedError.
|
|
41
|
-
# handle all
|
|
39
|
+
# are four, each raised as a dedicated subclass: QuotaExceededError,
|
|
40
|
+
# CreditLimitExceededError, BudgetExceededError and PaymentFailedError.
|
|
41
|
+
# Rescue this base class to handle all four.
|
|
42
42
|
#
|
|
43
43
|
# When more than one applies, the most serious wins: failed payment outranks
|
|
44
|
-
# credit limit, which outranks quota
|
|
45
|
-
#
|
|
46
|
-
# change needed. Like a 401, a 402 is
|
|
47
|
-
# it is never billed. Retrying is
|
|
44
|
+
# credit limit, which outranks quota, which outranks your own budget limit.
|
|
45
|
+
# All four lift by themselves once the billing state changes - access returns
|
|
46
|
+
# within roughly a minute, with no key change needed. Like a 401, a 402 is
|
|
47
|
+
# refused before anything is scraped, so it is never billed. Retrying is
|
|
48
|
+
# pointless; fix the billing state first.
|
|
48
49
|
class PaymentRequiredError < APIError; end
|
|
49
50
|
|
|
50
51
|
# Every request the plan allows this period has been used (HTTP 402).
|
|
@@ -64,6 +65,17 @@ module ScrapeUnblocker
|
|
|
64
65
|
# usually clears itself within about a minute.
|
|
65
66
|
class CreditLimitExceededError < PaymentRequiredError; end
|
|
66
67
|
|
|
68
|
+
# This billing period's spend reached the monthly budget limit you set
|
|
69
|
+
# (HTTP 402).
|
|
70
|
+
#
|
|
71
|
+
# The limit is set in your profile (EUR, excluding VAT), and spend is counted
|
|
72
|
+
# the way the invoice is: the plan's fixed monthly fee, if any, plus the
|
|
73
|
+
# requests billed on top of it. Requests paid from coupon credit do not count.
|
|
74
|
+
# The key works again at the start of the next billing period, or within
|
|
75
|
+
# about a minute after you raise or remove the limit at
|
|
76
|
+
# https://app.scrapeunblocker.com/dashboard/profile.
|
|
77
|
+
class BudgetExceededError < PaymentRequiredError; end
|
|
78
|
+
|
|
67
79
|
# A card payment has been declined three times (HTTP 402).
|
|
68
80
|
#
|
|
69
81
|
# Those attempts are the payment provider's automatic retries spread over
|
|
@@ -167,7 +179,7 @@ module ScrapeUnblocker
|
|
|
167
179
|
BASE_MESSAGES = {
|
|
168
180
|
400 => "Invalid request (bad URL, unsupported scheme, or missing API key header)",
|
|
169
181
|
401 => "Authentication failed - key not recognised, or account has no active plan",
|
|
170
|
-
402 => "Billing block - quota exceeded, credit limit exceeded, or a failed payment",
|
|
182
|
+
402 => "Billing block - quota exceeded, credit limit exceeded, budget limit reached, or a failed payment",
|
|
171
183
|
403 => "Target blocked by bot protection on every bypass path",
|
|
172
184
|
404 => "Requested element not found on the page",
|
|
173
185
|
408 => "Browser run timed out before the page was ready",
|
|
@@ -187,12 +199,13 @@ module ScrapeUnblocker
|
|
|
187
199
|
end
|
|
188
200
|
private_class_method :auth_error_class
|
|
189
201
|
|
|
190
|
-
# The
|
|
202
|
+
# The four billing blocks share a status code and differ only in their
|
|
191
203
|
# plain-text body. An unrecognised body falls back to PaymentRequiredError.
|
|
192
204
|
def self.billing_error_class(body)
|
|
193
205
|
text = (body || "").downcase
|
|
194
206
|
return QuotaExceededError if text.include?("quota exceeded")
|
|
195
207
|
return CreditLimitExceededError if text.include?("credit limit exceeded")
|
|
208
|
+
return BudgetExceededError if text.include?("user set budget exceeded")
|
|
196
209
|
return PaymentFailedError if text.include?("payment failed")
|
|
197
210
|
|
|
198
211
|
PaymentRequiredError
|