Skip to main content
If you already have a Scrapy project, the scrapeunblocker-scrapy-middleware package lets you keep your spiders unchanged - every routed Request is rewritten to go through /getPageSource, and your callbacks receive the HTML as if the spider had fetched the URL directly.
The middleware is open source at github.com/ScrapeUnblocker/scrapeunblocker-scrapy-middleware and published on PyPI. The README there has the full reference.

Install

Requires Python 3.9+ and Scrapy 2.8+.

Enable in settings.py

Per-request overrides

Pass options via Request.meta["scrapeunblocker"] to override defaults for a single request. A value of None removes a default.
The options are the /getPageSource parameters: proxy_country, parsed_data, get_cookies, steps (a list of browser actions) and list_elements. Booleans are sent as true / false and steps as a JSON array. Browser rendering always happens, so there is no option for it. A key the API does not know has no effect, so the middleware logs a warning for it. To send a request directly, without ScrapeUnblocker, set meta={"scrapeunblocker": False} (or meta={"dont_scrapeunblocker": True}).

What the middleware does

For every routed request, the middleware:
  1. Rewrites the URL to https://api.scrapeunblocker.com/getPageSource?url=<original>.
  2. Changes the method to POST.
  3. Adds the x-scrapeunblocker-key header. The target site’s own headers and cookies are not sent to the API.
  4. Merges SCRAPEUNBLOCKER_DEFAULTS and meta["scrapeunblocker"] into the query string.
  5. On response, restores the original URL and request on the Scrapy Response, so selectors, response.follow() and relative links work against the URL you requested, not the API URL.
The API call keeps the target site as its download slot, so CONCURRENT_REQUESTS_PER_DOMAIN and DOWNLOAD_DELAY still apply per target site. Requests already addressed to the API host are never wrapped twice, so retries are safe. These requests are sent directly, because /getPageSource cannot serve them:
  • requests that are not GET (for example a FormRequest post) - to submit a form through ScrapeUnblocker, use browser steps;
  • requests Scrapy components make for themselves: robots.txt and FilesPipeline / ImagesPipeline downloads;
  • URLs that are not http / https.
The API returns the HTML re-indented, so text nodes carry surrounding whitespace and newlines. Strip them with .get(default="").strip() or normalize-space() in XPath.

Handling parsed data

When parsed_data is set, the response body is JSON, not HTML, and response.json() works as usual. response.meta["scrapeunblocker_params"] holds the options the response was fetched with, defaults included:
The body is {"data": {"page_type": ..., "data": {...}}} - see parsed data for the fields, and for the 200 with data_extracted: false and the page’s html when nothing could be extracted.

Status codes

When the target site itself answers with an error, the API adds an X-Origin-Status header and the middleware makes it the response status, so Scrapy handles it as it would the site’s own answer:
  • a dead URL (404 / 410 with X-Origin-Status) reaches your spider as 404 / 410;
  • an origin error page or the target’s own 401 / 451 / 500 (a 200 with X-Origin-Status) becomes that status.
These calls are billed and a retry returns the same answer, so the middleware marks them dont_retry. Scrapy’s HttpErrorMiddleware drops them unless you allow the status, for example meta={"handle_httpstatus_list": [404, 410]}. See dead URLs. API errors (401, 402, 403, 429, 5xx, 523, …) keep their status and are logged with the API’s message and a hint. See errors.

Retry behavior

The middleware does not override Scrapy’s RetryMiddleware. Scrapy’s default RETRY_HTTP_CODES already retry 429 and the 5xx failures; add 523 (target unreachable for the moment) and, if you like, 403:
None of these statuses is billed, so retrying them costs nothing. A 403 retry hits ScrapeUnblocker again, which independently rotates through bypass routes - so a second 403 usually means the target is truly hard-blocked on that day. A request with steps runs its actions again on every retry. See handling failures.
Requests over your plan’s limit answer 429 - see concurrency limits.