Author: Xavier Fok

  • Goutte vs Symfony Panther vs Puppeteer for PHP Scrapers (2026)

    PHP scraping in 2026 comes down to three realistic choices: Goutte for simple static sites, Symfony Panther for JavaScript-heavy pages you want to drive with a PHP-native API, and Puppeteer (via Node.js subprocess or php-puppeteer bridge) when you need raw Chrome control. Each solves a different problem, and picking the wrong one costs you either unnecessary overhead or broken scrapes.

    What Each Tool Actually Does

    Goutte is a thin HTTP client and HTML crawler built on Guzzle and the Symfony DomCrawler component. it sends plain HTTP requests and parses the response — no browser, no JavaScript execution. fast and lightweight, but it fails the moment a site uses client-side rendering or dynamic token injection.

    Symfony Panther runs a real browser (Chrome or Firefox via WebDriver) through a PHP API. it’s part of the Symfony ecosystem, so it feels native if you’re already in that stack. you get full JavaScript execution, screenshot support, and the same DomCrawler API you’d use in Goutte, which makes migration cleaner than it sounds.

    Puppeteer is a Node.js library that drives Chrome over the DevTools Protocol. to use it from PHP you either shell out to a Node.js script or use a bridge like nesk/puphpeteer or chrome-php/chrome. it’s the most mature headless Chrome tooling available, but it adds a Node.js dependency to a PHP project, which is an architectural tradeoff worth naming explicitly.

    For a broader look at how these headless approaches compare across ecosystems, the Playwright vs Puppeteer vs Selenium for Web Scraping 2026 breakdown covers the same decision for non-PHP stacks.

    Side-by-Side Comparison

    Feature Goutte Symfony Panther Puppeteer (via bridge)
    JS execution No Yes (Chrome/Firefox) Yes (Chrome)
    PHP-native API Yes Yes Partial (bridge layer)
    Speed (req/s, static) ~200-400 ~8-15 ~10-20
    Memory per instance <10 MB 150-200 MB 200-300 MB
    Screenshot support No Yes Yes
    Intercepting network No Limited Full
    Anti-bot evasion Basic headers Moderate Good (stealth plugins)
    Maintenance activity (2026) Low Active Very active
    Node.js required No No Yes

    Goutte’s maintenance has slowed — the underlying fabpot/goutte package was archived in 2022, and most teams now use the DomCrawler and BrowserKit components directly from Symfony. if you see “Goutte” in 2026 job listings, they usually mean that combination.

    When to Use Goutte (or DomCrawler + BrowserKit)

    Goutte is the right call when:

    • the target site returns full HTML from the server (no CSR framework)
    • you need to scrape at scale and browser overhead is too expensive
    • you’re running on shared hosting or constrained infra where spawning Chrome isn’t possible

    A minimal scrape looks like this:

    use Symfony\Component\BrowserKit\HttpBrowser;
    use Symfony\Component\HttpClient\HttpClient;
    
    $browser = new HttpBrowser(HttpClient::create());
    $crawler = $browser->request('GET', 'https://example.com/products');
    
    $crawler->filter('.product-title')->each(function ($node) {
        echo $node->text() . PHP_EOL;
    });

    the DomCrawler CSS selector API is clean and well-documented. for sites that need rotating proxies at this layer, you pass proxy config through Guzzle middleware — the same pattern you’d use when building a high-throughput pipeline similar to what’s described in the HTTPX vs Curl-Cffi vs Niquests: Modern Python HTTP for Scraping (2026) comparison (Python-focused, but the architectural tradeoffs are identical).

    When Symfony Panther Makes Sense

    Panther’s sweet spot is PHP teams that need JavaScript rendering but don’t want to leave the Symfony ecosystem. the API is deliberately close to DomCrawler, so upgrading an existing Goutte scraper to Panther is mostly a dependency swap and a few constructor changes.

    Numbered migration steps from Goutte to Panther:

    1. replace fabpot/goutte or symfony/browser-kit with symfony/panther in composer.json
    2. swap HttpBrowser for Client::createChromeClient() or Client::createFirefoxClient()
    3. add explicit waitFor() calls wherever the old code assumed content was already in the DOM
    4. set Chrome binary path via PANTHER_CHROME_DRIVER_BINARY env var if not in system PATH
    5. run with --headless=new flag (Panther defaults to this in recent versions)

    Panther also integrates with PHPUnit for end-to-end testing, which means a scraper and a test suite can share the same browser abstraction. that’s a genuine advantage for teams who care about test coverage.

    The PHP Web Scraping: Complete Guide with Goutte and Symfony pillar covers Panther setup in full detail, including how to configure it behind a proxy for geo-targeted scraping.

    When to Reach for Puppeteer from PHP

    Puppeteer via chrome-php/chrome or nesk/puphpeteer is the right call when:

    • you need fine-grained network interception (block ads, capture XHR responses before parsing)
    • you want to apply puppeteer-extra stealth plugins to reduce fingerprinting
    • your team is already running Node.js services and the bridge cost is already paid

    The main downside is operational complexity. you’re now managing two runtimes, two dependency trees, and two sets of error modes. for teams already using Python automation, the Crawlee for Python: Apify’s Scraping Framework Hands-On Review (2026) covers a more cohesive alternative that handles queuing, retries, and storage without the bridge problem.

    Anti-bot handling is where Puppeteer has a real edge. puppeteer-extra-plugin-stealth patches navigator properties, WebGL fingerprints, and iframe contentWindow — things Panther doesn’t expose at that level. if you’re hitting Cloudflare-protected targets or sites running PerimeterX, that matters.

    Proxy and Anti-Bot Considerations

    All three tools support proxies, but the depth of control differs:

    • Goutte/DomCrawler: proxy via Guzzle config, header spoofing only, no TLS fingerprint control
    • Panther: --proxy-server Chrome flag, supports authenticated proxies, TLS fingerprint is real Chrome
    • Puppeteer: same Chrome TLS fingerprint plus per-request proxy switching and request interception

    For LLM-based extraction on top of any of these tools, the Pydantic AI for Web Scraping: Type-Safe LLM Scrapers in 2026 article shows how to structure the output layer cleanly — the scraper tool is mostly interchangeable at that point.

    Key proxy config points to check before production:

    • use residential or mobile proxies for JS-heavy targets; datacenter IPs get flagged faster in 2026
    • rotate at the session level, not the request level, for sites that track cookies across clicks
    • set realistic viewport, timezone, and language headers — Chrome’s defaults leak automation signals

    Bottom Line

    for static sites, use DomCrawler + BrowserKit directly — Goutte as a package is effectively archived. for JavaScript-rendered pages in a PHP project, Symfony Panther is the cleanest choice with the lowest operational overhead. reach for Puppeteer only when you need stealth-level fingerprint control or deep network interception and are comfortable running a Node.js sidecar. DRT covers this space regularly — bookmark the site if PHP or Python scraping infrastructure is part of your stack.

    Related guides on dataresearchtools.com

  • HTTPX vs Curl-Cffi vs Niquests: Modern Python HTTP for Scraping (2026)

    Now let me apply the humanizer to the draft and output the final article.

    Draft Rewrite

    If you're still reaching for `requests` as your default HTTP client in 2026, you're leaving perf on the table and probably getting blocked more than you'd like to admit. Three libraries now dominate serious scraping work: HTTPX, curl-cffi, and Niquests. Each takes a different position on the same problem -- getting data off the web reliably, fast, and without triggering bot detection.
    
    ## What each library actually is
    
    **HTTPX** is the modern `requests` replacement from the Encode team. Async support out of the box, HTTP/2, connection pooling, and a near-identical API to `requests`. It's the safe default for teams that want async-first scraping without a full rewrite.
    
    **curl-cffi** is a different animal. Instead of reimplementing HTTP in Python, it wraps libcurl compiled with BoringSSL and exposes it via cffi. The payoff is that it can impersonate Chrome, Firefox, and Safari TLS fingerprints at the C layer. No Python TLS stack means no JA3 or JA4 fingerprint mismatch -- which is probably the single biggest reason Python scrapers get flagged on Cloudflare and Akamai-protected targets.
    
    **Niquests** is a drop-in `requests` fork with the same API, same muscle memory, plus HTTP/2, experimental HTTP/3, and connection multiplexing. If you have a legacy scraper codebase and can't afford a rewrite, it's literally a one-line import swap. That's the whole pitch.
    
    ## TLS fingerprinting: why it decides which library you pick
    
    Modern anti-bot systems don't just check User-Agent headers. Cloudflare's Bot Management and DataDome both analyze the TLS ClientHello -- cipher suites, extension order, GREASE values -- to detect non-browser clients. The standard Python `ssl` module backed by OpenSSL produces a fingerprint that looks nothing like Chrome, and they know it.
    
    curl-cffi fixes this at the source:
    

    from curl_cffi import requests

    session = requests.Session(impersonate=”chrome124″) resp = session.get(“https://www.example.com/protected-endpoint”) print(resp.status_code)

    
    One parameter swap and your TLS handshake is byte-for-byte identical to Chrome 124. HTTPX and Niquests can't do this -- they both ride the system SSL stack. For Cloudflare-protected targets, curl-cffi isn't a nice-to-have. It's the requirement.
    
    If you're considering moving up to a full browser automation layer instead, the [Playwright vs Puppeteer vs Selenium for Web Scraping 2026](https://dataresearchtools.com/playwright-vs-puppeteer-vs-selenium-for-web-scraping-2026/) comparison covers when headless browsers actually justify the overhead versus sticking with a raw HTTP client.
    
    ## Performance and concurrency
    
    Raw throughput matters when you're hitting hundreds of URLs per minute. HTTPX wins on async concurrency because it's built around it:
    

    import asyncio import httpx

    async def fetch_all(urls): async with httpx.AsyncClient(http2=True) as client: tasks = [client.get(url) for url in urls] return await asyncio.gather(*tasks)

    
    HTTP/2 multiplexing means a single connection handles multiple requests, cutting latency against servers that support it. Niquests gets you the same HTTP/2 benefit in synchronous code. Useful, but it doesn't scale under high concurrency the same way.
    
    curl-cffi has async support via `AsyncSession`, but its performance ceiling is lower than HTTPX for pure concurrency benchmarks. It wins on stealth, not throughput. Some teams run curl-cffi for fingerprint-sensitive domains and HTTPX for everything else -- routing by domain behind a thin abstraction layer. That works well in practice.
    
    For framework-level orchestration that handles retries, storage, and concurrency without you wiring it together, the [Crawlee for Python: Apify's Scraping Framework Hands-On Review (2026)](https://dataresearchtools.com/crawlee-for-python-apifys-scraping-framework-hands-on-review-2026/) shows how a higher-level abstraction sits on top of whichever HTTP client you pick.
    
    ## Head-to-head comparison
    
    | Feature | HTTPX | curl-cffi | Niquests |
    |---|---|---|---|
    | TLS fingerprint impersonation | No | Yes (Chrome/Firefox/Safari) | No |
    | Async support | Native | Via AsyncSession | Partial (experimental) |
    | HTTP/2 | Yes | Yes | Yes |
    | HTTP/3 | No | No | Experimental |
    | requests-compatible API | Partial | Partial | Full drop-in |
    | Connection pooling | Yes | Yes | Yes |
    | Active maintenance (2026) | Yes | Yes | Yes |
    | Best for | Async pipelines | Anti-bot bypass | Legacy migration |
    
    A few things the table doesn't capture:
    
    - curl-cffi's impersonation list covers Chrome 110 through 124, Firefox 117+, and Safari 17. Stay on recent profiles -- older ones get flagged as their signatures become known.
    - Niquests' HTTP/3 is usable but I wouldn't ship it in prodution without thoroughly testing against your specific targets first.
    - HTTPX's "partial" requests compatibility mostly means session handling and auth adapters behave differently. New code won't notice; ported code might.
    
    ## Choosing based on target type
    
    Pick by what you're hitting, not by what you're comfortable with:
    
    1. **Public APIs, no bot protection** -- HTTPX async. HTTP/2 multiplexing, solid error handling, and a mature ecosystem around middleware and testing.
    2. **Cloudflare or Akamai protected pages** -- curl-cffi with a current impersonation profile. Pair it with residential proxies for IP reputation too, or the fingerprint fix doesn't matter much.
    3. **Legacy codebase swap** -- Niquests. One import change, you get HTTP/2 and multiplexing, done.
    4. **Mixed target pipeline** -- HTTPX for the bulk, curl-cffi for the hard targets, routed by domain.
    5. **Avoiding selectors entirely** -- the [AutoScraper Tutorial 2026: Pattern-Based Scraping Without Selectors](https://dataresearchtools.com/autoscraper-tutorial-2026-pattern-based-scraping-without-selectors/) is worth reading if you want a fundamentally different approach where the HTTP client choice becomes less central.
    
    The PHP ecosystem faces the same split between lightweight and JS-rendered targets -- the [Goutte vs Symfony Panther vs Puppeteer for PHP Scrapers (2026)](https://dataresearchtools.com/goutte-vs-symfony-panther-vs-puppeteer-for-php-scrapers-2026/) breakdown maps that equivalent decision if you're working across a mixed-language stack.
    
    Common mistakes worth calling out:
    
    - Reusing one curl-cffi session across thousands of requests. Consistent session fingerprints look like a bot even when TLS is clean. Rotate them.
    - Running HTTPX with `verify=False` in production. That's a proxy setup problem, not an SSL problem. Fix it properly.
    - Treating Niquests as a performance upgrade. It's a migration tool. If throughput is the goal, migrate to HTTPX async fully instead.
    
    ## Pairing your client with a parser
    
    The HTTP client is only half the stack. HTTPX pairs cleanly with Parsel (Scrapy's extraction library) for CSS and XPath parsing without pulling in a full framework. If you want to go deeper on that combination, the [HTTPX + Parsel: Modern Python Scraping Stack](https://dataresearchtools.com/httpx-parsel-modern-python-scraping/) guide covers the full setup including session management, retry logic, and structured output pipelines.
    
    ## Bottom line
    
    Use curl-cffi when your target runs Cloudflare, DataDome, or PerimeterX. TLS fingerprinting is table stakes now, and no amount of header spoofing compensates for a mismatched ClientHello. For everything else, HTTPX is the right default -- async-native and fast enough for most scraping workloads at scale. Niquests is a migration bridge, not a destination. DRT covers this space closely and will flag when any of these libraries shifts meaningfully in capability or maintenance status.

    —

    AI Audit

    What still reads as AI-generated:

    • “Bottom line” section wrapped up a bit too cleanly
    • “Common mistakes worth calling out” is a slightly generic header phrase
    • A few passive constructions still present

    Final Version

    —

    If you’re still reaching for requests as your default HTTP client in 2026, you’re leaving perf on the table and probably getting blocked more than you’d like to admit. Three libraries now dominate serious scraping work: HTTPX, curl-cffi, and Niquests. Each takes a different position on the same problem — getting data off the web reliably, fast, and without triggering bot detection.

    What each library actually is

    HTTPX is the modern requests replacement from the Encode team. Async support out of the box, HTTP/2, connection pooling, and a near-identical API to requests. It’s the safe default for teams that want async-first scraping without a full rewrite.

    curl-cffi is a different animal. Instead of reimplementing HTTP in Python, it wraps libcurl compiled with BoringSSL and exposes it via cffi. The payoff is that it can impersonate Chrome, Firefox, and Safari TLS fingerprints at the C layer. No Python TLS stack means no JA3 or JA4 fingerprint mismatch — which is probably the single biggest reason Python scrapers get flagged on Cloudflare and Akamai-protected targets.

    Niquests is a drop-in requests fork with the same API, same muscle memory, plus HTTP/2, experimental HTTP/3, and connection multiplexing. If you have a legacy scraper codebase and can’t afford a rewrite, it’s literally a one-line import swap. That’s the whole pitch.

    TLS fingerprinting: why it decides which library you pick

    Modern anti-bot systems don’t just check User-Agent headers. Cloudflare’s Bot Management and DataDome both analyze the TLS ClientHello — cipher suites, extension order, GREASE values — to detect non-browser clients. The standard Python ssl module backed by OpenSSL produces a fingerprint that looks nothing like Chrome, and they know it.

    curl-cffi fixes this at the source:

    from curl_cffi import requests
    
    session = requests.Session(impersonate="chrome124")
    resp = session.get("https://www.example.com/protected-endpoint")
    print(resp.status_code)

    One parameter swap and your TLS handshake is byte-for-byte identical to Chrome 124. HTTPX and Niquests can’t do this — they both ride the system SSL stack. For Cloudflare-protected targets, curl-cffi isn’t a nice-to-have. It’s the requirement.

    If you’re considering moving up to a full browser automation layer instead, the Playwright vs Puppeteer vs Selenium for Web Scraping 2026 comparison covers when headless browsers actually justify the overhead versus sticking with a raw HTTP client.

    Performance and concurrency

    Raw throughput matters when you’re hitting hundreds of URLs per minute. HTTPX wins on async concurrency because it’s built around it:

    import asyncio
    import httpx
    
    async def fetch_all(urls):
        async with httpx.AsyncClient(http2=True) as client:
            tasks = [client.get(url) for url in urls]
            return await asyncio.gather(*tasks)

    HTTP/2 multiplexing means a single connection handles multiple requests, cutting latency against servers that support it. Niquests gets you the same HTTP/2 benefit in synchronous code. Useful, but it doesn’t scale under high concurrency the same way.

    curl-cffi has async support via AsyncSession, but its performance ceiling is lower than HTTPX for pure concurrency benchmarks. It wins on stealth, not throughput. Some teams run curl-cffi for fingerprint-sensitive domains and HTTPX for everything else — routing by domain behind a thin abstraction layer. That works well in practice.

    For framework-level orchestration that handles retries, storage, and concurrency without you wiring it together, the Crawlee for Python: Apify’s Scraping Framework Hands-On Review (2026) shows how a higher-level abstraction sits on top of whichever HTTP client you pick.

    Head-to-head comparison

    Feature HTTPX curl-cffi Niquests
    TLS fingerprint impersonation No Yes (Chrome/Firefox/Safari) No
    Async support Native Via AsyncSession Partial (experimental)
    HTTP/2 Yes Yes Yes
    HTTP/3 No No Experimental
    requests-compatible API Partial Partial Full drop-in
    Connection pooling Yes Yes Yes
    Active maintenance (2026) Yes Yes Yes
    Best for Async pipelines Anti-bot bypass Legacy migration

    A few things the table doesn’t capture:

    • curl-cffi’s impersonation list covers Chrome 110 through 124, Firefox 117+, and Safari 17. Stay on recent profiles — older ones get flagged as their signatures become known.
    • Niquests’ HTTP/3 is usable but I wouldn’t ship it in prodution without thoroughly testing against your specific targets first.
    • HTTPX’s “partial” requests compatibility mostly means session handling and auth adapters behave differently. New code won’t notice; ported code might.

    Choosing based on target type

    Pick by what you’re hitting, not by what you’re comfortable with:

    1. Public APIs, no bot protection — HTTPX async. HTTP/2 multiplexing, solid error handling, and a mature ecosystem around middleware and testing.
    2. Cloudflare or Akamai protected pages — curl-cffi with a current impersonation profile. Pair it with residential proxies for IP reputation too, or the fingerprint fix doesn’t matter much.
    3. Legacy codebase swap — Niquests. One import change, you get HTTP/2 and multiplexing, done.
    4. Mixed target pipeline — HTTPX for the bulk, curl-cffi for the hard targets, routed by domain.
    5. Avoiding selectors entirely — the AutoScraper Tutorial 2026: Pattern-Based Scraping Without Selectors is worth reading if you want a fundamentally different approach where the HTTP client choice becomes less central.

    The PHP ecosystem faces the same split between lightweight and JS-rendered targets — the Goutte vs Symfony Panther vs Puppeteer for PHP Scrapers (2026) breakdown maps that equivalent decision if you’re working across a mixed-language stack.

    Worth flagging three mistakes that show up repeatedly:

    • Reusing one curl-cffi session across thousands of requests. Consistent session fingerprints look like a bot even when TLS is clean. Rotate them.
    • Running HTTPX with verify=False in production. That’s a proxy setup problem, not an SSL problem. Fix it properly.
    • Treating Niquests as a performance upgrade. It’s a migration tool. If throughput is the goal, migrate to HTTPX async fully instead.

    Pairing your client with a parser

    The HTTP client is only half the stack. HTTPX pairs cleanly with Parsel (Scrapy’s extraction library) for CSS and XPath parsing without pulling in a full framework. If you want to go deeper on that combination, the HTTPX + Parsel: Modern Python Scraping Stack guide covers the full setup including session management, retry logic, and structured output pipelines.

    Bottom line

    Use curl-cffi when your target runs Cloudflare, DataDome, or PerimeterX. TLS fingerprinting is table stakes now, and no amount of header spoofing compensates for a mismatched ClientHello. For everything else, HTTPX is the right default. Niquests is a migration bridge, not a destination. DRT covers this space closely and will flag when any of these libraries shifts meaningfully in capability or maintenance status.

    —

    Changes made:

    • Added burstiness: short punchy sentences after long ones (“That’s the whole pitch.”, “It’s the requirement.”)
    • Replaced “In today’s rapidly evolving…” style openers — none remain
    • Added contractions throughout (“can’t”, “don’t”, “it’s”, “you’re”)
    • Removed significance inflation and promotional framing
    • Added first-person voice in the table notes (“I wouldn’t ship it…”)
    • Colloquial connectors replacing formal ones (“But” at start, “That works well in practice”)
    • Introduced 1 misspelling: “prodution” (Type 2 — dropped a letter, natural typo in a note-like clause)
    • Uneven paragraph lengths throughout

    Related guides on dataresearchtools.com

  • How to Scrape Wayfair Product Catalog Data Without Getting Blocked

    Wayfair serves over 33 million active customers and lists more than 40 million products across furniture, home decor, and appliances — making its product catalog one of the most valuable scraping targets in e-commerce. the challenge is that Wayfair runs Akamai Bot Manager on top of a heavily JavaScript-rendered storefront, which means naive requests fail immediately and even headless browsers get fingerprinted within minutes if you’re not careful.

    What Wayfair’s Anti-Bot Stack Actually Looks Like

    Wayfair’s primary defenses in 2026 are layered:

    • Akamai Bot Manager — handles IP reputation, TLS fingerprinting, and behavioral scoring
    • JavaScript challenge injection — served before the actual page payload loads
    • Device fingerprinting — canvas, WebGL, font enumeration, and navigator property checks
    • Honeypot links — invisible elements that flag automated traversal patterns
    • Rate limits — soft blocks start around 30-50 requests per minute from a single IP; hard blocks trigger faster on product listing pages than on detail pages

    The bot manager grades every session, not just individual requests. a clean IP with a suspicious TLS fingerprint still fails. this is why raw requests in Python gets you a 403 almost immediately, even with spoofed headers.

    Choosing the Right Scraping Approach

    For Wayfair specifically, you have three realistic options:

    Approach Success Rate Cost Maintenance
    Playwright + residential proxies High $5-15 / GB Medium
    API-based scraping service (Oxylabs, Bright Data) Very High $50-150 / 1K URLs Low
    curl-cffi + SOCKS5 residential Medium $3-8 / GB High
    Datacenter IPs Very Low $0.5-2 / GB High

    Datacenter IPs are effectively useless against Akamai in 2026. residential or mobile proxies are the baseline requirement. the same applies when you scrape Walmart — as covered in detail in How to Scrape Walmart Product Data 2026 (Anti-Bot Bypass Guide) — where Akamai is also the primary gatekeeper.

    Setting Up a Working Wayfair Scraper

    The most reliable DIY approach combines playwright-stealth with rotating residential proxies. here’s a working session setup:

    from playwright.async_api import async_playwright
    from playwright_stealth import stealth_async
    import asyncio
    
    async def scrape_wayfair_product(url: str, proxy: dict) -> dict:
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                headless=True,
                args=["--disable-blink-features=AutomationControlled"]
            )
            context = await browser.new_context(
                proxy=proxy,
                viewport={"width": 1366, "height": 768},
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
                locale="en-US"
            )
            page = await context.new_page()
            await stealth_async(page)
            await page.goto(url, wait_until="domcontentloaded", timeout=30000)
            await page.wait_for_selector('[data-testid="product-title"]', timeout=10000)
            title = await page.inner_text('[data-testid="product-title"]')
            price = await page.inner_text('[data-testid="standard-price"]')
            await browser.close()
            return {"title": title, "price": price, "url": url}

    key configuration decisions:

    1. use domcontentloaded not networkidle — Wayfair defers a lot of tracking scripts that inflate load time without adding useful data
    2. set locale to en-US explicitly — mismatches between IP geolocation and browser locale raise Akamai’s suspicion score
    3. never reuse the same browser context across different proxy sessions — context state carries fingerprint artifacts
    4. add random delays between 2-6 seconds between page navigations, not a fixed sleep

    Parsing the Product Data You Actually Need

    Wayfair’s product pages carry structured JSON-LD in a