     [Blog](https://scrapfly.io/blog)   /  [beautifulsoup](https://scrapfly.io/blog/tag/beautifulsoup)   /  [How to Scrape Zoro.com Without Getting Blocked in 2026](https://scrapfly.io/blog/posts/how-to-scrape-zoro-dot-com)   # How to Scrape Zoro.com Without Getting Blocked in 2026

 by [Hisham Medhat](https://scrapfly.io/blog/author/hisham) May 29, 2026 19 min read [\#beautifulsoup](https://scrapfly.io/blog/tag/beautifulsoup) [\#python](https://scrapfly.io/blog/tag/python) [\#requests](https://scrapfly.io/blog/tag/requests) [\#scrapeguide](https://scrapfly.io/blog/tag/scrapeguide) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com "Share on LinkedIn")    

 

 

         

[Zoro.com](https://www.zoro.com/) is a major industrial supply marketplace offering millions of products across categories like tools, safety equipment, and industrial supplies. With comprehensive product data including prices, specifications, reviews, and inventory levels, Zoro.com is a valuable target for web scraping projects focused on market research, price monitoring, and competitive analysis.

In this guide we'll build a full Zoro.com scraper in Python using the Scrapfly SDK. We'll cover product pages, reviews, and search listings, then wire everything together in a runnable script.

## Key Takeaways

Master Zoro.com scraping with the Scrapfly SDK, structured product extraction, and search listing pagination for full market analysis.

- Use Scrapfly's web scraping API to bypass DataDome and Akamai Bot Manager (`_abck`, `bm_sz`) and render JavaScript content
- Parse structured product data from embedded JSON-LD payloads on every product page
- Capture customer reviews from PowerReviews XHR calls using browser data capture
- Crawl Zoro search results with concurrent pagination and auto-scroll rendering

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## Why Scrape Zoro.com?

Zoro.com lists millions of industrial SKUs with prices, stock status, full specs, and verified reviews. That makes it a rich source for a few practical jobs.

- **Price monitoring** track competitor prices on tools, safety gear, and MRO supplies in near real time.
- **Market research** spot pricing trends and demand signals across product categories.
- **Procurement intelligence** check availability and lead times before placing bulk orders.
- **Catalog enrichment** pull clean product specs, brand info, and images for your own storefront or PIM.
- **Review mining** analyze customer feedback to surface common defects or feature gaps.

Each of those workflows needs structured data at scale, which is exactly what a Scrapfly-backed scraper delivers.



The platform's catalog includes detailed product specifications, manufacturer information, and real-time pricing data, which makes it useful for data-driven decision making in the industrial supply sector.



## Project Setup

We'll use the official Scrapfly SDK along with a few standard helpers. Install the dependencies:

bash```bash
$ pip install "scrapfly-sdk[all]" loguru
```



Then export your Scrapfly API key. You can grab one from your [Scrapfly dashboard](https://scrapfly.io/dashboard).

bash```bash
$ export SCRAPFLY_KEY="your scrapfly api key"
```



The scraper is split across two files. `zoro.py` holds the scraping logic and parsers, and `run.py` drives the scraper and writes JSON output to a `results/` folder.

### Base Configuration

Open `zoro.py` and start with the imports, client setup, and base scrape configuration:

python```python
import os
import json
import re
from datetime import datetime
import urllib.parse
from pathlib import Path
from loguru import logger as log
from typing import List, Dict, TypedDict, Optional, Any
from scrapfly import (
    ScrapeConfig,
    ScrapflyClient,
    ScrapeApiResponse,
    ScrapflyScrapeError,
)


SCRAPFLY = ScrapflyClient(key=os.environ["SCRAPFLY_KEY"])
BASE_CONFIG = {
    # requires Anti Scraping Protection bypass feature.
    "asp": True,
    "render_js": True,
    "proxy_pool": "public_residential_pool"
}


output = Path(__file__).parent / "results"
output.mkdir(exist_ok=True)
```



`asp=True` activates Scrapfly's anti-scraping protection bypass, `render_js=True` runs the page through a real browser, and `proxy_pool="public_residential_pool"` routes requests through residential IPs.

### Typed Result Schemas

Next we define `TypedDict` classes so our extracted data has a clear, documented shape:

python```python
class ZoroReview(TypedDict):
    review_id: Optional[int]
    rating: Optional[int]
    headline: str
    comments: str
    nickname: str
    location: str
    created_date: str
    is_verified_buyer: bool
    helpful_votes: int
    media_count: int


class ZoroProduct(TypedDict):
    sku: str
    mpn: str
    name: str
    brand: str
    description: str
    price: str
    currency: str
    availability: str
    url: str
    specifications: Dict[str, str]
    images: List[str]
    rating: Optional[float]
    review_count: int
    reviews: List[ZoroReview]


class ZoroSearchListing(TypedDict):
    total_pages: int
    total_results: int
    products: List[Dict[str, Any]]
```





## Scraping Zoro.com Product Pages

Every Zoro product page embeds a JSON-LD block tagged with `data-za="product-microdata"`. That block contains the SKU, MPN, brand, description, price, availability, images, and aggregate rating. Specifications live in a sibling `div.product-specifications` block, and reviews come from PowerReviews XHR calls.

### Parsing Reviews

Reviews load from `display.powerreviews.com`. We capture those XHR calls via Scrapfly's browser data, then walk the response bodies and deduplicate by `review_id`.

python```python
def _timestamp_to_iso(timestamp_ms: int) -> str:
    """Convert millisecond timestamp to ISO format string."""
    if not timestamp_ms:
        return ""
    return datetime.fromtimestamp(timestamp_ms / 1000).isoformat()


def parse_reviews(xhr_calls: List[Dict]) -> List[ZoroReview]:
    reviews = []
    seen_review_ids = set()  # Track review IDs to avoid duplicates

    for xhr in xhr_calls:
        url = xhr.get("url", "")
        if url.startswith("https://display.powerreviews.com"):
            try:
                response = xhr.get("response", {})
                body = response.get("body", "")
                if not body:
                    log.warning(f"Empty response body for URL: {url}")
                    continue
                data = json.loads(body)
                # PowerReviews API structure: results[0].reviews
                results = data.get("results", [])
                if not results:
                    log.debug(f"No results in data for URL: {url}")
                    continue

                review_list = results[0].get("reviews", [])
                if review_list:
                    for review in review_list:
                        review_id = review.get("review_id") or review.get("ugc_id")
                        # Skip duplicates
                        if review_id and review_id in seen_review_ids:
                            continue
                        if review_id:
                            seen_review_ids.add(review_id)

                        # Extract only useful information
                        details = review.get("details", {})
                        metrics = review.get("metrics", {})
                        badges = review.get("badges", {})
                        media = review.get("media", [])

                        parsed_review = {
                            "review_id": review_id,
                            "rating": metrics.get("rating"),
                            "headline": details.get("headline", ""),
                            "comments": details.get("comments", ""),
                            "nickname": details.get("nickname", ""),
                            "location": details.get("location", ""),
                            "created_date": _timestamp_to_iso(
                                details.get("created_date")
                            ),
                            "is_verified_buyer": badges.get("is_verified_buyer", False),
                            "helpful_votes": metrics.get("helpful_votes", 0),
                            "media_count": len(media),
                        }
                        reviews.append(parsed_review)
                else:
                    log.debug(f"No reviews in results for URL: {url}")
            except Exception as e:
                log.error(f"Error processing XHR call {url}: {e}")

    log.info(f"Total reviews parsed: {len(reviews)}")
    return reviews
```



### Parsing Product Data

The main parser pulls the JSON-LD block, flattens the specifications grid, and merges in any reviews captured from XHR.

python```python
def parse_product(response: ScrapeApiResponse) -> ZoroProduct:
    sel = response.selector

    json_ld = sel.xpath('//script[@type="application/ld+json"][@data-za="product-microdata"]/text()').get()
    if not json_ld:
        log.error(f"Could not find JSON-LD data on {response.context['url']}")
        raise ValueError("No product data found")

    data = json.loads(json_ld)

    # Extract basic product info from JSON-LD data
    sku = data.get("sku", "")
    mpn = data.get("mpn", "")
    name = data.get("name", "")
    brand = data.get("brand", {}).get("name", "") if data.get("brand") else ""
    description_html = data.get("description", "")
    description = re.sub(r"<[^>]+>", "", description_html).strip()

    # Extract pricing and availability from JSON-LD data
    offers = data.get("offers", {})
    price = offers.get("price", "")
    currency = offers.get("priceCurrency", "USD")
    availability = offers.get("availability", "").replace("http://schema.org/", "")
    url = offers.get("url", response.context.get("url", ""))

    # Extract image urls from JSON-LD data
    images = []
    for img in data.get("image", []):
        if isinstance(img, dict):
            images.append(img.get("contentUrl", ""))
        else:
            images.append(img)

    # Extract rating and review count from JSON-LD data
    rating_data = data.get("aggregateRating", {})
    rating = rating_data.get("ratingValue") if rating_data else None
    review_count = rating_data.get("reviewCount", 0) if rating_data else 0

    # Extract specifications from product-specifications div (attribute-group elements)
    specifications = {}
    for group in sel.css("div.product-specifications div.attribute-group"):
        label_nodes = group.xpath('./div[contains(@class, "attribute-name")]')
        value_nodes = group.xpath('./div[not(contains(@class, "attribute-name"))]')
        if not label_nodes or not value_nodes:
            continue
        label = " ".join(label_nodes[0].xpath(".//text()").getall()).strip()
        value = " ".join(t.strip() for t in value_nodes[0].xpath(".//text()").getall() if t.strip())
        if label and value and value != "-":
            specifications[label] = value

    # Extract reviews from XHR calls
    reviews = []
    if "browser_data" in response.scrape_result:
        browser_data = response.scrape_result["browser_data"]
        xhr_calls = browser_data.get("xhr_call", [])
        reviews = parse_reviews(xhr_calls)

    return {
        "sku": sku,
        "mpn": mpn,
        "name": name,
        "brand": brand,
        "description": description,
        "price": price,
        "currency": currency,
        "availability": availability,
        "url": url,
        "specifications": specifications,
        "images": images,
        "rating": rating,
        "review_count": review_count,
        "reviews": reviews,
    }
```



### Scraping Multiple Products Concurrently

`scrape_product` accepts a list of URLs and fans them out using Scrapfly's `concurrent_scrape`. A small JavaScript scenario clicks the "Show more" reviews button up to fifty times so we can collect older reviews too.

python```python
async def scrape_product(urls: List[str]) -> List[ZoroProduct]:
    """
    Scrape Zoro product pages for product data

    Args:
        urls: List of product URLs to scrape

    Returns:
        List of ZoroProduct dictionaries
    """
    log.info(f"Scraping {len(urls)} product pages")
    # JavaScript code to click the "Show more" button  to get all the product data
    JS = [
        {
            "execute": {
                "script": "async function clickUntilDisabled(){const maxClicks=50;const waitBetweenClicks=1500;let clicks=0;while(clicks<maxClicks){const btn=document.querySelector('button.pr-rd-show-more:not([disabled])');if(!btn||!btn.offsetParent){break;}try{btn.click();clicks++;await new Promise(r=>setTimeout(r,waitBetweenClicks));}catch(e){break;}}return clicks;}return await clickUntilDisabled();",
                "timeout": 5000,  # you can increase this if needed to get more reviews
            }
        }
    ]
    to_scrape = [ScrapeConfig(url, **BASE_CONFIG, js_scenario=JS) for url in urls]
    products = []
    async for response in SCRAPFLY.concurrent_scrape(to_scrape):
        try:
            products.append(parse_product(response))
        except Exception as e:
            log.error(f"Error scraping product page {response.context['url']}: {e}")
    log.info(f"Scraped {len(products)} product pages")

    return products
```



### Example Product Output

Running the product scraper against a single URL produces a result like this:



Example Product Outputjson```json
{
  "sku": "G0050565",
  "mpn": "48-101",
  "name": "Hyflex Coated Gloves, Polyurethane, Dipped, Palm Coated, ANSI Abrasion Level 3, Black, Large, 1 Pair",
  "brand": "Ansell",
  "description": "Coated Gloves, ANSI/ISEA Cut Level A1, Hem Style Finished, Glove Style Knit, Knit Material Nylon, ANSI/ISEA Abrasion Level 3, Hand Protection Style Glove, Heat-Resistant Glove Type Seamless Knit, Additional Hazard Protection None, EN 388 Rating 4131A, Standards ANSI/ASTM/EN388 3111, Insulated No, Glove Coating Cover Palm, Glove Hand Style Left and Right, Double-Dipped No, Coating Type Dipped, Glove Performance High Dexterity, 1 Pair",
  "price": "26.28",
  "currency": "USD",
  "availability": "InStock",
  "url": "https://www.zoro.com/ansell-hyflex-coated-gloves-polyurethane-dipped-palm-coated-ansi-abrasion-level-3-black-large-1-pair-48-101/i/G0050565/",
  "specifications": {
    "Glove Size": "L (9)",
    "Thickness": "13 ga",
    "Glove Length": "8.66-10.63\"",
    "Size": "L",
    "Item": "Coated Gloves",
    "Color": "Black",
    "Glove Coating Material": "Polyurethane",
    "Glove Coating Coverage": "Palm",
    "ANSI/ISEA Abrasion Level": "3",
    "Glove Coating Finish": "Textured",
    "Standards": "ANSI/ASTM/EN388 3111",
    "Glove Construction": "Seamless Knit",
    "ANSI/ISEA Cut Level": "1",
    "Application": "Assembly of Small Pieces",
    "Brand and Series": "Ansell HyFlex 48-101",
    "Glove Materials": "Nylon",
    "Cuff Style": "Knit Wrist",
    "EN 388 Rating": "4131A",
    "Series": "Hyflex 48-101"
  },
  "images": [
    "https://www.zoro.com/static/cms/product/large/Z-D-xxfo5o1.JPG",
    "https://www.zoro.com/static/cms/product/large/Grainger_HyFlex4801propswhitexx6r7f7f6rv.jpg",
    "https://www.zoro.com/static/cms/product/large/Grainger_HyFlex48101backwhitexx0e6e06.jpg"
  ],
  "rating": 4.87,
  "review_count": 36,
  "reviews": [
    {
      "review_id": 566403072,
      "rating": 5,
      "headline": "yes",
      "comments": "really good",
      "nickname": "walanst",
      "location": "graham,nc",
      "created_date": "2025-10-21T15:08:54.836000",
      "is_verified_buyer": true,
      "helpful_votes": 0,
      "media_count": 0
    },
    {
      "review_id": 481371224,
      "rating": 5,
      "headline": "Good comfort and dexterity.",
      "comments": "These are comfortable gloves and do the job to protect your hands. I wear an additional layer of nitrile protection under these since they are fairly thin. Good dexterity!",
      "nickname": "Guest",
      "location": "Undisclosed",
      "created_date": "2023-11-12T19:14:31.525000",
      "is_verified_buyer": true,
      "helpful_votes": 0,
      "media_count": 0
    }
  ]
}
```







## Scraping Zoro.com Search Listings

Search pages use the URL pattern `https://www.zoro.com/search?q=<query>&page=<n>`. Each card carries a `data-za="search-product-card"` attribute, with consistent inner selectors for title, brand, price, image, rating, and stock status.

### Parsing the Listing

`parse_search_listing` reads the page-level metadata first, then walks each product card. Pagination text is pulled from the label span, the total result count from `span.result-count`, and every card is converted into a dictionary with title, brand, price, slug, image, rating, and stock status.

python```python
def parse_search_listing(response: ScrapeApiResponse) -> ZoroSearchListing:
    sel = response.selector
    # Extract total pages from pagination info from html
    total_pages = 0
    page_text = (
        sel.css('span[data-za="pagination-label"]::text').get()
        or sel.css("#pagination-label-info::text").get()
    )
    if page_text:
        pages_match = re.search(r"of\s+(\d+)\s+pages", page_text.strip())
        if pages_match:
            total_pages = int(pages_match.group(1))

    total_results = 0
    results_text = sel.css("span.result-count::text").get()
    if results_text:
        results_match = re.search(r"\(([\d,]+)\+?\s+items?\)", results_text)
        if results_match:
            # Remove commas and convert to int
            total_results = int(results_match.group(1).replace(",", ""))

    products = []
    for card in sel.css('[data-za="search-product-card"]'):
        href = card.css('a[data-za="product-title"]::attr(href)').get() or ""
        slug_match = re.search(r"^/([^/]+)/i/(G\d+)/?$", href)
        slug = slug_match.group(1) if slug_match else ""
        zoro_no = slug_match.group(2) if slug_match else ""
        if not zoro_no:
            zoro_no = (card.css("span.zoro-no::text").get() or "").strip()

        title = (
            card.css('a[data-za="product-title"] h2::text').get()
            or card.css('a[data-za="product-title"]::attr(title)').get()
            or ""
        ).strip()

        brand = " ".join(
            t.strip()
            for t in card.css('[data-za="product-brand"] ::text').getall()
            if t.strip()
        )

        price = None
        price_text = " ".join(
            t.strip()
            for t in card.css('[data-za="product-price"] ::text').getall()
            if t.strip()
        )
        price_match = re.search(r"\$([\d,]+(?:\.\d{1,2})?)", price_text)
        if price_match:
            try:
                price = float(price_match.group(1).replace(",", ""))
            except ValueError:
                price = None

        mfr_no = (card.css("span.mfr-no::text").get() or "").strip() or None

        rating = None
        rating_text = card.css(
            '[data-za="product-review-snippet"] .sr-only span::text'
        ).get() or ""
        rating_match = re.search(r"Rated\s+([\d.]+)\s+stars", rating_text)
        if rating_match:
            try:
                rating = float(rating_match.group(1))
            except ValueError:
                rating = None

        review_count = 0
        rc_text = (card.css('[data-za="review-count"] ::text').get() or "").strip()
        rc_match = re.search(r"([\d,]+)", rc_text)
        if rc_match:
            try:
                review_count = int(rc_match.group(1).replace(",", ""))
            except ValueError:
                review_count = 0

        img_src = card.css('img[data-za="product-image"]::attr(src)').get() or ""
        img_alt = card.css('img[data-za="product-image"]::attr(alt)').get() or ""

        sales_status = (
            card.css('[data-za="stock-status-badge"]::text').get() or ""
        ).strip() or None

        url = f"https://www.zoro.com{href}" if href.startswith("/") else href

        products.append({
            "title": title,
            "brand": brand or None,
            "price": price,
            "zoroNo": zoro_no,
            "mfrNo": mfr_no,
            "slug": slug,
            "url": url,
            "image": img_src,
            "imageAlt": img_alt,
            "rating": rating,
            "review_count": review_count,
            "salesStatus": sales_status,
        })

    log.info(f"Scraped {len(products)} products from search listing")
    return {
        "total_pages": total_pages,
        "total_results": total_results,
        "products": products,
    }
```



### Crawling All Pages

`scrape_search_listing` fetches the first page to discover pagination metadata, then concurrently scrapes the remaining pages. Auto-scroll is enabled so lazy-loaded cards are populated before the HTML is captured.

python```python
async def scrape_search_listing(query: str, max_pages: int = 3, scrape_all_pages: bool = False) -> ZoroSearchListing:
    """
    Scrape Zoro search pages for product listings

    Args:
        query: Search query string
        max_pages: Maximum number of pages to scrape (default: 3)
        scrape_all_pages: Whether to scrape all pages (default: False)
    Returns:
        ZoroSearchListing dictionary with products and metadata
    """
    log.info(f"Scraping Zoro search page for query: {query}")
    encoded_query = urllib.parse.quote(query)
    base_url = f"https://www.zoro.com/search?q={encoded_query}"
    log.info(f"Scraping first page of search listing: {base_url}")
    first_page = await SCRAPFLY.async_scrape(
        ScrapeConfig(base_url, auto_scroll=True, **BASE_CONFIG, rendering_wait=5000)
    )

    # first page data
    first_page_data = parse_search_listing(first_page)
    total_pages = first_page_data["total_pages"]
    total_results = first_page_data["total_results"]
    products = first_page_data["products"]

    if scrape_all_pages:
        pages_to_scrape = total_pages
    else:
        pages_to_scrape = min(max_pages, total_pages)

    log.info(
        f"Scraping {pages_to_scrape - 1} additional pages (total: {pages_to_scrape})"
    )
    scraped_pages = 1
    if pages_to_scrape > 1:
        other_pages = [
            ScrapeConfig(f"{base_url}&page={page}", auto_scroll=True, **BASE_CONFIG, rendering_wait=5000)
            for page in range(2, pages_to_scrape + 1)
        ]
        async for response in SCRAPFLY.concurrent_scrape(other_pages):
            scraped_pages += 1
            if isinstance(response, ScrapflyScrapeError):
                log.error(f"Error scraping page {scraped_pages}: {response.error}")
                continue
            log.info(f"Scraped page {scraped_pages} of {pages_to_scrape}")
            page_data = parse_search_listing(response)
            products.extend(page_data["products"])
    log.info(f"Scraped {len(products)} products from {pages_to_scrape} pages")

    data = {
        "total_pages": total_pages,
        "total_results": total_results,
        "products": products,
    }
    return data
```



### Example Search Output

A search for "Gloves" returns pagination metadata plus a list of product cards:

 Example Search Outputjson```json
{
  "total_pages": 277,
  "total_results": 44500,
  "products": [
    {
      "title": "GWGN, Disposable Gloves, 8 mil Palm, Nitrile, Powder-Free, L, 100 PK, Green",
      "brand": "Gloveworks By Ammex",
      "price": 26.39,
      "zoroNo": "G3278864",
      "mfrNo": "GWGN46100",
      "slug": "gloveworks-by-ammex-gwgn-disposable-gloves-8-mil-palm-nitrile-powder-free-l-100-pk-green-gwgn46100",
      "url": "https://www.zoro.com/gloveworks-by-ammex-gwgn-disposable-gloves-8-mil-palm-nitrile-powder-free-l-100-pk-green-gwgn46100/i/G3278864/",
      "image": "https://www.zoro.com/static/cms/product/full/Paint Sundries Solutions Inc_A697383968906xx008c8a.jpeg",
      "imageAlt": "Gloveworks By Ammex GWGN, Disposable Gloves, 8 mil Palm, Nitrile, Powder-Free, L, 100 PK, Green GWGN46100",
      "rating": 4.2,
      "review_count": 5,
      "salesStatus": "In Stock"
    }
  ]
}
```





## Running the Scraper

The `run.py` script glues everything together. It calls both scrapers and saves results to `./results/` as JSON.

python```python
import asyncio
import json
from pathlib import Path
import zoro
from loguru import logger as log

output = Path(__file__).parent / "results"
output.mkdir(exist_ok=True)


async def run():
    # enable scrapfly cache for basic use
    zoro.BASE_CONFIG["cache"] = False

    log.info("running Zoro scrape and saving results to ./results directory")

    # scrape product
    log.info("scraping product")
    urls = [
        "https://www.zoro.com/proto-general-purpose-double-latch-tool-box-with-tray-steel-red-20-w-x-85-d-x-95-h-j9975r/i/G0067825/",
        "https://www.zoro.com/stanley-series-2000-tool-box-plastic-blackyellow-19-in-w-x-10-14-in-d-x-10-in-h-019151m/i/G6197466/",
        "https://www.zoro.com/ansell-hyflex-coated-gloves-polyurethane-dipped-palm-coated-ansi-abrasion-level-3-black-large-1-pair-48-101/i/G0050565/"
    ]
    product = await zoro.scrape_product(urls)
    with open(output.joinpath("product.json"), "w", encoding="utf-8") as file:
        json.dump(product, file, indent=2, ensure_ascii=False)

    log.info("scraping search listing")
    search_listing = await zoro.scrape_search_listing("Gloves")
    with open(output.joinpath("search_listing.json"), "w", encoding="utf-8") as file:
        json.dump(search_listing, file, indent=2, ensure_ascii=False)

if __name__ == "__main__":
    asyncio.run(run())
```



Run it from the project directory:

bash```bash
$ python run.py
```



The script writes `product.json` and `search_listing.json` to the `results/` folder.



Scrapfly

#### Extract structured data automatically?

Scrapfly's Extraction API uses AI to turn any webpage into structured data — no selectors needed.

[Try Free →](https://scrapfly.io/register)## Anti-Blocking Strategies

Zoro runs DataDome plus Akamai Bot Manager (the `_abck` and `bm_sz` cookies on the redirect path). DataDome's first check is your TLS fingerprint, not your User-Agent, which is why UA rotation, header spoofing, session reuse, and rate throttling don't get past it. The Scrapfly client clears both with the same `BASE_CONFIG` used throughout this guide, in three ways.

1. **ASP bypass** activates Scrapfly's anti scraping protection feature, which automatically handles fingerprinting, JS challenges, and CAPTCHA solving.
2. **Residential proxy pool** rotates IPs across real residential networks instead of datacenter ranges.
3. **Browser rendering** runs the page through a headless browser so DataDome's runtime checks pass.

For more anti-blocking background, see our guide on

[How to Bypass Anti-Bot Protection When Web ScrapingLearn how anti-bot systems detect scrapers and 5 universal bypass techniques including proxy rotation, fingerprinting, and fortified headless browsers.](https://scrapfly.io/blog/posts/how-to-bypass-anti-bot-protection-when-web-scraping)

which covers TLS fingerprinting, IP rotation, and other detection methods.



## Scraping with Scrapfly

Check out [Scrapfly's web scraping API](https://scrapfly.io/web-scraping-api) for all the details.

ScrapFly's [Web Scraping API](https://scrapfly.io/web-scraping-api) is a single HTTP endpoint for collecting web data at scale, with a **99.99% success rate** across **130M+ proxies in 120+ countries**.

- [Anti-Scraping Protection bypass](https://scrapfly.io/docs/scrape-api/anti-scraping-protection) - automatically defeats Cloudflare, DataDome, PerimeterX, Akamai, and 90+ other bot systems.
- [Smart proxy rotation](https://scrapfly.io/docs/scrape-api/proxy) - residential and datacenter pools with country and ASN level geo-targeting.
- [JavaScript rendering](https://scrapfly.io/docs/scrape-api/javascript-rendering) - render SPAs and dynamic pages through real cloud browsers.
- [Browser automation scenarios](https://scrapfly.io/docs/scrape-api/javascript-scenario) - scroll, click, fill forms, and wait for elements without managing a browser fleet.
- [Format conversion](https://scrapfly.io/docs/scrape-api/getting-started#api_param_format) - return pages as HTML, JSON, clean text, or LLM ready Markdown.
- [Session management](https://scrapfly.io/docs/scrape-api/session) - keep cookies, headers, and IPs consistent across multi step flows.
- [Smart caching](https://scrapfly.io/docs/scrape-api/getting-started#api_param_cache) - cache successful responses to cut cost on repeat scraping jobs.
- [Python](https://scrapfly.io/docs/sdk/python), [TypeScript](https://scrapfly.io/docs/sdk/typescript), [Scrapy](https://scrapfly.io/docs/sdk/scrapy), and [no-code integrations](https://scrapfly.io/docs/integration/getting-started) including Make, n8n, Zapier, LangChain, and LlamaIndex.

For a minimal one-off request without the full scraper structure, the SDK call looks like this:

python```python
from scrapfly import ScrapflyClient, ScrapeConfig, ScrapeApiResponse

scrapfly = ScrapflyClient(key="YOUR-SCRAPFLY-KEY")

result: ScrapeApiResponse = scrapfly.scrape(ScrapeConfig(
    tags=["player", "project:default"],
    asp=True,
    render_js=True,
    url="https://www.zoro.com/ansell-hyflex-coated-gloves-polyurethane-dipped-palm-coated-ansi-abrasion-level-3-black-large-1-pair-48-101/i/G0050565/"
))

print(result.scrape_result["content"])
```



For more comprehensive web scraping background, see our

[Everything to Know to Start Web Scraping in Python TodayComplete introduction to web scraping using Python: http, parsing, AI, scaling and deployment.](https://scrapfly.io/blog/posts/everything-to-know-about-web-scraping-python)



## FAQ

Is it legal to scrape Zoro.com?Scraping publicly available product data such as prices, specifications, and reviews is generally legal. You should still review Zoro's terms of service, avoid collecting personal data, and respect rate limits so you don't disrupt their service. When in doubt, consult legal counsel for your specific use case.







Why does my Zoro scraper get 403 even with rotating User-Agents?Rotating User-Agents will not help, because DataDome checks your TLS fingerprint before it ever looks at the UA string, and Python's `requests` produces a handshake that DataDome flags on the first request. Getting past it requires a real browser context (a real TLS handshake plus execution of the JavaScript challenge) running over residential IPs. Scrapfly's ASP (`asp=True`) handles all of that upstream.







Can I scrape Zoro reviews without bypassing DataDome separately?Yes. Reviews are served by PowerReviews, but the review widget only loads once the product page renders past DataDome. With `asp=True` and `render_js=True` on a single request, Scrapfly clears DataDome and captures the PowerReviews XHR calls in `browser_data["xhr_call"]`, so there's no separate bypass step.







Does Zoro have an official API I should use instead?Zoro does not offer a public product API for general use. Scraping the rendered pages and the underlying JSON-LD and PowerReviews calls is the practical way to collect catalog and review data programmatically.









## Conclusion

This guide walked through a full Zoro.com scraper using the Scrapfly SDK. We extracted structured product data from JSON-LD, captured reviews from PowerReviews XHR calls, and crawled search listings with concurrent pagination. Everything lives in two files, `zoro.py` for the logic and `run.py` for execution.

For low-volume work, a residential proxy plus a clean cookie state can get you through. For anything resembling production volume, Scrapfly's Web Scraping API or a similar managed bypass is the practical path. The DIY scraper in this guide is useful for understanding Zoro's page structure and prototyping against pre-fetched HTML.



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   Table of Contents















 

  Table of Contents- [Key Takeaways](#key-takeaways)
- [Why Scrape Zoro.com?](#why-scrape-zoro-com)
- [Project Setup](#project-setup)
- [Base Configuration](#base-configuration)
- [Typed Result Schemas](#typed-result-schemas)
- [Scraping Zoro.com Product Pages](#scraping-zoro-com-product-pages)
- [Parsing Reviews](#parsing-reviews)
- [Parsing Product Data](#parsing-product-data)
- [Scraping Multiple Products Concurrently](#scraping-multiple-products-concurrently)
- [Example Product Output](#example-product-output)
- [Scraping Zoro.com Search Listings](#scraping-zoro-com-search-listings)
- [Parsing the Listing](#parsing-the-listing)
- [Crawling All Pages](#crawling-all-pages)
- [Example Search Output](#example-search-output)
- [Running the Scraper](#running-the-scraper)
- [Anti-Blocking Strategies](#anti-blocking-strategies)
- [Scraping with Scrapfly](#scraping-with-scrapfly)
- [FAQ](#faq)
- [Conclusion](#conclusion)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

## Explore this Article with AI

 [ ChatGPT ](https://chat.openai.com/?q=Summarize%20this%20page%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com) [ Gemini ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20page%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com) [ Grok ](https://x.com/i/grok?text=Summarize%20this%20page%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com) [ Perplexity ](https://www.perplexity.ai/search/new?q=Summarize%20this%20page%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com) [ Claude ](https://claude.ai/new?q=Summarize%20this%20page%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-zoro-dot-com) 



 ## Related Articles

 [  

 python data-parsing 

### How to Parse Web Data with Python and Beautifulsoup

Beautifulsoup is one the most popular libraries in web scraping. In this tutorial, we'll take a hand-on overview of how ...

 

 ](https://scrapfly.io/blog/posts/web-scraping-with-python-beautifulsoup) [  

 blocking 

### How to Bypass Datadome Anti Scraping in 2026

Learn how Datadome detects web scrapers using TLS, IP, and ML analysis, and discover practical bypass techniques and too...

 

 ](https://scrapfly.io/blog/posts/how-to-bypass-datadome-anti-scraping) [     

 python beautifulsoup 

### How to Scrape Allegro.pl in 2026

Scrape Allegro.pl product listings and detail pages with Scrapfly. Bypass DataDome anti-bot protection. Route through Po...

 

 ](https://scrapfly.io/blog/posts/how-to-scrape-allegro) 

  ## Related Questions

- [ Q What are some BeautifulSoup alternatives in Python? ](https://scrapfly.io/blog/answers/what-are-some-beautifulsoup-alternatives)
- [ Q How to find elements without a specific attribute in BeautifulSoup? ](https://scrapfly.io/blog/answers/how-to-find-elements-without-attribute-in-beautifulsoup)
 
  



   



 Extract structured data with AI, **1,000 free credits** [Start Free](https://scrapfly.io/register)