mcpField
S
scrapfly-scraper

scrapfly-scraper

github.com/scrapflyData & databases5257 installsUpdated 2w ago
Repository

About scrapfly-scraper

scrapfly-scraper is a Python skill for collecting web and API data through the Scrapfly Scraper API. It serves scraping tasks involving dynamic pages, proxies, and anti-bot protections, with support for multiple output formats, browser scenarios, and reusable sessions.

Key features

  1. 01Proxy rotation and geo-targeting
  2. 02Anti-bot bypass and JavaScript rendering
  3. 03Run browser interaction scenarios
  4. 04Return HTML, JSON, Markdown, or text
  5. 05Reuse sessions and cache responses

Use cases

  1. 01Collect content from sites with anti-bot protections
  2. 02Scrape pages that require JavaScript rendering
  3. 03Scrape after login, clicks, or form filling
  4. 04Collect data through a proxy in a chosen country

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/scrapfly/skills --skill scrapfly-scraper

After installing, type/scrapfly-scraperin Claude Code to confirm it loaded.

Details

From the project README

Scrapfly Scraper

Use the Scrapfly Scraper API to collect web page data with proxy rotation, anti-bot bypass, JavaScript rendering, and JavaScript scenarios for browser control.

When to use

  • Scraping web pages (HTML, JSON, text, markdown)
  • Bypassing anti-bot protections (Cloudflare, DataDome, PerimeterX, Kasada, and more)
  • Collecting data through rotating proxies with geo-targeting
  • Rendering JavaScript-heavy pages with headless browsers
  • Control the browser using JavaScript scenario for common actions (waiting for selectors, clicking, filling elements, etc.)
  • Session-reuse for session-presited scraping
  • Capturing browser XHR call data

Setup

pip install scrapfly-sdk

The API key must be provided via environment variable SCRAPFLY_API_KEY or passed directly to the client.

API Reference

Endpoint: https://api.scrapfly.io/scrape The HTTP method is forwarded to the upstream URL. To retrieve data from a web page or an API, use the GET method, which is the default method. If the API resource like an API requries other methods, like POST. You can use it via the method parameter of the ScrapeConfig

ScrapflyClient
from scrapfly import ScrapflyClient, ScrapeConfig, ScrapeApiResponse
import os

client = ScrapflyClient(key=os.environ["SCRAPFLY_API_KEY"])
ScrapeConfig Parameters
ParameterTypeDefaultDescription
urlstrrequiredTarget URL to scrape
methodstr"GET"HTTP method: GET, POST, PUT, PATCH, HEAD, OPTIONS
headersdictNoneCustom HTTP headers
cookiesdictNoneCustom cookies (merged into headers["cookie"])
bodystrNoneRaw request body for POST/PUT/PATCH; used when you pre-encode payload
datadictNoneForm/JSON data for POST/PUT/PATCH; encoded according to Content-Type when body is not provided
timeoutintNoneRequest timeout in ms (default ~150,000ms)
retryboolTrueAuto-retry on network failures
countrystrNoneProxy country (ISO 3166-1 alpha-2, e.g. "us", "de")
proxy_poolstrNoneProxy pool: "basic" (datacenter proxies, Unblocker off, 1 credit), "unblocker" (residential proxies and the Unblocker anti-bot bypass, 25 credits), "unblocker_ultra" (the same, and the only pool a target or an anti-bot protection with a price of its own accepts; that price is added) or "auto" (the API picks the pool before the request, from the target's profile: the Unblocker on datacenter proxies, or the pool the target needs; accepted on every target, the ones that require unblocker_ultra included, where their price is added; priced as the pool it runs on). Each pool sets the Unblocker itself. Unset, the SDK sends no proxy_pool. The migration guide covers the requests written before the proxy pools, the deprecated pool names "public_datacenter_pool" and "public_residential_pool" included
sessionstrNoneSession ID to persist cookies/fingerprint across requests
session_sticky_proxyboolTrueKeep the same proxy IP for a given session; False is refused on "unblocker", "unblocker_ultra" and "auto"
unblockerboolFalseDeprecated: use proxy_pool ("unblocker_ultra" for the Unblocker). The migration guide covers requests written before the proxy pools. asp is its alias; when both are supplied, asp wins
render_jsboolFalseEnable headless browser JavaScript rendering (+5 credits)
rendering_waitintNoneWait time in ms after page load (requires render_js=True)
rendering_stagestr"complete"Browser readiness stage: "complete" or "domcontentloaded" (requires render_js=True)
wait_for_selectorstrNoneCSS/XPath selector to wait for (requires render_js=True)
jsstrNoneJavaScript code to execute in the browser (auto-encoded)
js_scenariolistNoneList of browser actions (click, fill, scroll, wait, etc.)
auto_scrollboolNoneAutomatically scroll page during rendering for lazy-loaded content (requires render_js=True)
screenshotsdictNoneCapture screenshots: {"fullpage": "png"} or {"selector": ".element"} (requires render_js=True)
screenshot_flagslist[str]NoneScreenshot options: "load_images", "dark_mode", "block_banners", "high_quality", "print_media_format"
formatstr"raw"Output format: "raw", "clean_html", "json", "markdown", "text"
format_optionslist[str]NoneFormat modifiers (markdown only): "no_images", "no_links", "only_content"
extractdictNoneRaw extraction spec to apply on the response (encoded and sent as extract)
extraction_templatestrNoneName of a saved server-side extraction template
extraction_ephemeral_templatedictNoneInline JSON extraction template used once (ephemeral:)
extraction_promptstrNoneNatural language instructions for Extraction API
extraction_modelstrNoneLLM model name to use for Extraction API
cacheboolFalseEnable response caching
cache_ttlintNoneCache time-to-live in seconds
cache_clearboolFalseClear any existing cached response when cache=True
dnsboolFalseCollect DNS records and timings in result.dns (slower)
sslboolFalseCollect SSL certificate details in result.ssl (slower)
debugboolFalseEnable debug recording and extra metadata
raise_on_upstream_errorboolTrueRaise exceptions for upstream 4xx/5xx HTTP responses
correlation_idstrNoneCustom ID for request tracking across systems
tagslist[str]NoneCustom tags for request organization and analytics
langlist[str]NoneAccept-Language values, e.g. ["en-US", "en"]
osstrNoneOverride browser OS fingerprint, e.g. "windows", "macos"
webhookstrNoneNamed webhook for async scrape completion callbacks
cost_budgetintNoneMax credits the Unblocker may spend on a request on the unblocker, unblocker_ultra and auto pools
ScrapeApiResponse
response = client.scrape(ScrapeConfig(url="https://httpbin.dev"))

response.content                      # Page content (HTML/JSON/text)
response.scrape_result                # Full result dict
response.status_code                  # Scrapfly API HTTP status code
response.scrape_result["status_code"] # Upsteam HTTP status code
response.headers                      # Response headers
response.context                      # Metadata about features used

Examples

Basic scrape
from scrapfly import ScrapflyClient, ScrapeConfig
import os

client = ScrapflyClient(key=os.environ["SCRAPFLY_API_KEY"])

result = client.scrape(ScrapeConfig(
    url="https://httpbin.dev",
))
print(result.content)
Scrape with anti-bot bypass and geo-targeting
result = client.scrape(ScrapeConfig(
    url="https://httpbin.dev",
    proxy_pool="unblocker", # residential proxies and the Unblocker to bypass antibots
    country="us", # match the proxy with the domain country
))
Scrape with JavaScript rendering
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/products",
    render_js=True, # enable JavaScript rendering using a cloud browser
    rendering_wait=5000, # rendering wait for wait for
    wait_for_selector="div.product", # wait a for a selector to be present on the page
))
Scrape as markdown
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/products",
    format="markdown", # other supported formats are: json, text, clean_html 
))
print(result.content)
POST request with body
result = client.scrape(ScrapeConfig(
    url="https://httpbin.dev/anything",
    method="POST",
    headers={"Content-Type": "application/json"},
    body='{"query": "search term"}',
))
print(result.content)
JavaScript scenario (browser actions)
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/login",
    render_js=True,
    # browser control
    js_scenario=[
        {
            "fill":{
                "selector":"input[name='username']","clear":True,"value":"user123"
                }
            },{
            "fill":{
                "selector":"input[name='password']","clear":True,"value":"password"
                }
            },{
            "click":{
                "selector":"form > button[type='submit']"
                }
            },{
            "wait_for_navigation":{
                "timeout":5000
                }
            }
    ],
    # request headers
    headers={
        "cookie":"cookiesAccepted=true"
    }
))

# access the element under login
print(result.selector.css("div#secret-message::text").get())

Built-in Parsel selector

result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/products",
    render_js=True
))

# access the built-in Parsel selector
selector = result.selector
for product in selector.css("div.product"):
    print("product name:", product.xpath(".//a/text()").get()) # using xpath
    print("product price:", product.css("div.price::text").get()) # using css
Concurrent scraping
import asyncio
from scrapfly import ScrapflyClient, ScrapeConfig

client = ScrapflyClient(key=os.environ["SCRAPFLY_API_KEY"])

urls = [
    "https://web-scraping.dev/products",
    "https://web-scraping.dev/products?page=2",
    "https://web-scraping.dev/products?page=3",
]

configs = [ScrapeConfig(url=url) for url in urls]

async def concurrent_scraping():
    results = []
    async for result in client.concurrent_scrape(configs):
        results.append(result)

    for result in results:
        print(result.content[:100])
        print("===========")

asyncio.run(concurrent_scraping())
Using sessions for stateful scraping
# First request: login
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/login",
    render_js=True,
    # browser control
    js_scenario=[
        {
            "fill":{
                "selector":"input[name='username']","clear":True,"value":"user123"
                }
            },{
            "fill":{
                "selector":"input[name='password']","clear":True,"value":"password"
                }
            },{
            "click":{
                "selector":"form > button[type='submit']"
                }
            },{
            "wait_for_navigation":{
                "timeout":5000
                }
            }
    ],
    # request headers
    headers={
        "cookie":"cookiesAccepted=true"
    },
    session="logged-in-session"
))

# Second request: access protected page with same session
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/login",
    session="logged-in-session"
))

# access the built-in selector
print(result.selector.css("div#secret-message::text").get())
Caching responses
result = client.scrape(ScrapeConfig(
    url="https://example.com",
    cache=True,
    cache_ttl=3600,  # Cache for 1 hour
))

Error Handling

from scrapfly import ScrapflyClient, ScrapeConfig
from scrapfly.errors import (
    ScrapflyError,
    UpstreamHttpClientError,
    UpstreamHttpServerError,
    ScrapflyProxyError,
    ScrapflyThrottleError,
)

try:
    result = client.scrape(ScrapeConfig(url="https://httpbin.dev", proxy_pool="unblocker"))
except ScrapflyThrottleError as e:
    print(f"Rate limited, retry after {e.retry_delay}s")
except UpstreamHttpClientError as e:
    print(f"Target returned 4xx: {e.message}")
except UpstreamHttpServerError as e:
    print(f"Target returned 5xx: {e.message}")
except ScrapflyProxyError as e:
    print(f"Proxy error: {e.message}")
except ScrapflyError as e:
    print(f"Scrapfly error: {e.message}")

A target or an anti-bot protection with a price of its own accepts proxy_pool="unblocker_ultra" and "auto", which add that price: basic and unblocker get ERR::PROXY::UNBLOCKER_ULTRA_REQUIRED (HTTP 422, not billed). Resend on unblocker_ultra:

try:
    result = client.scrape(ScrapeConfig(url=url, proxy_pool="unblocker"))
except ScrapflyError as e:
    if e.code != "ERR::PROXY::UNBLOCKER_ULTRA_REQUIRED":
        raise
    result = client.scrape(ScrapeConfig(url=url, proxy_pool="unblocker_ultra"))

Important Notes

  • render_js=True is required for rendering_wait, wait_for_selector, js, js_scenario, and screenshots
  • Pick the proxy pool a site needs: "basic" where datacenter proxies get through, "unblocker" where it needs residential proxies and the Unblocker, "unblocker_ultra" when the API answers ERR::PROXY::UNBLOCKER_ULTRA_REQUIRED, or "auto" to let the API pick it from the target's profile
  • unblocker=True and its alias asp=True are deprecated: use proxy_pool ("unblocker_ultra" for the Unblocker). The migration guide covers requests written before the proxy pools
  • Use format="markdown" for clean content accessible for LLMs
  • Use session parameter to maintain state across multiple requests
  • The concurrent_scrape method handles rate limiting automatically

Comments

Markdown supported