> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Extract data with CSS selectors

> Pull exactly the elements you want from any page as JSON with CSS selectors in Zenrows, or parse the HTML yourself with BeautifulSoup or Cheerio.

When you know exactly which elements you want, send a map of names to CSS selectors with `css_extractor` and Zenrows returns those values as JSON. For common data such as tables, links, emails or headings, `outputs` does it with no selectors at all. And when you'd rather keep your own parser, fetch the HTML and run BeautifulSoup or Cheerio on it.

**Uses:** [Fetch](/fetch/api-reference) with [`css_extractor`](/fetch/features/css-extractor), [`outputs`](/fetch/features/output-filters) and [Adaptive Stealth Mode](/fetch/features/adaptive-stealth-mode) (`mode=auto`)

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use Zenrows to read https://www.scrapingcourse.com/ecommerce/ with these CSS selectors:
names from .product-name, prices from .product .price, and links from the href of
a.woocommerce-LoopProduct-link. Return a table of name, price and URL.
```

## Extract elements with css\_extractor

Send `css_extractor` as a JSON object: each key is the name you want in the output, each value is a CSS selector. Fetch applies the selectors to the rendered page and returns JSON instead of HTML.

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import json
  import requests

  css_extractor = {
      "title": "h1",
      "names": ".product-name",
      "prices": ".product .price",
      "links": "a.woocommerce-LoopProduct-link @href",
      "next_page": "a.next @href",
  }

  params = {
      "apikey": "YOUR_ZENROWS_API_KEY",
      "url": "https://www.scrapingcourse.com/ecommerce/",
      "mode": "auto",
      "css_extractor": json.dumps(css_extractor),
  }

  response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
  response.raise_for_status()
  print(json.dumps(response.json(), indent=2))
  ```

  ```javascript Node.js theme={"dark"}
  const cssExtractor = {
    title: "h1",
    names: ".product-name",
    prices: ".product .price",
    links: "a.woocommerce-LoopProduct-link @href",
    next_page: "a.next @href",
  };

  const params = new URLSearchParams({
    apikey: "YOUR_ZENROWS_API_KEY",
    url: "https://www.scrapingcourse.com/ecommerce/",
    mode: "auto",
    css_extractor: JSON.stringify(cssExtractor),
  });

  const response = await fetch(`https://api.zenrows.com/v1/?${params}`, {
    signal: AbortSignal.timeout(180_000),
  });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

  console.log(JSON.stringify(await response.json(), null, 2));
  ```

  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.scrapingcourse.com%2Fecommerce%2F&mode=auto&css_extractor=%7B%22title%22%3A%22h1%22%2C%22names%22%3A%22.product-name%22%2C%22prices%22%3A%22.product%20.price%22%2C%22links%22%3A%22a.woocommerce-LoopProduct-link%20%40href%22%2C%22next_page%22%3A%22a.next%20%40href%22%7D"
  ```

  ```bash CLI theme={"dark"}
  zenrows extract https://www.scrapingcourse.com/ecommerce/ --css '{"names":".product-name","prices":".product .price","next_page":"a.next @href"}'
  ```
</CodeGroup>

In cURL, URL-encode the JSON. The Python and Node.js clients encode it for you. In the CLI, `zenrows extract --css` sends `css_extractor` and `--outputs` sends `outputs`.

## Example output

Trimmed to three products:

```json theme={"dark"}
{
  "title": "Shop",
  "names": ["Abominable Hoodie", "Adrienne Trek Jacket", "Aeon Capri"],
  "prices": ["$69.00", "$57.00", "$48.00"],
  "links": [
    "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
    "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
    "https://www.scrapingcourse.com/ecommerce/product/aeon-capri/"
  ],
  "next_page": [
    "https://www.scrapingcourse.com/ecommerce/page/2/",
    "https://www.scrapingcourse.com/ecommerce/page/2/"
  ]
}
```

## How selectors map to JSON

| You write | You get |
| - | - |
| `"title": "h1"` | The element's text. One match returns a string. |
| `"names": ".product-name"` | Every match, as an array of strings. |
| `"links": "a.woocommerce-LoopProduct-link @href"` | The `href` attribute instead of the text. Use `@src`, `@content`, `@data-id` or any attribute name the same way. |
| `"next_page": "a.next @href"` | An array when the page repeats the element, as this pagination does at the top and bottom. Take the first item. |

Any CSS selector works: attribute filters such as `a[href*='/product/']`, combinators such as `.product .price`, and pseudo-classes such as `li:first-child`. Test a selector in your browser's console with `document.querySelectorAll(...)` before you send it.

## Get common data with outputs

For data every site shares, skip selectors. `outputs` takes a comma-separated list of filters: `emails`, `phone_numbers`, `headings`, `images`, `audios`, `videos`, `links`, `menus`, `hashtags`, `metadata`, `tables` and `favicon`, or `*` for all of them. Tables come back as rows keyed by their column headings.

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import json
  import requests

  params = {
      "apikey": "YOUR_ZENROWS_API_KEY",
      "url": "https://www.scrapingcourse.com/table-parsing",
      "mode": "auto",
      "outputs": "headings,tables",
  }

  response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
  response.raise_for_status()

  data = response.json()
  print(data["headings"])
  print(json.dumps(data["tables"][0]["Content"][:2], indent=2))
  ```

  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.scrapingcourse.com%2Ftable-parsing&mode=auto&outputs=headings,tables"
  ```

  ```bash CLI theme={"dark"}
  zenrows extract https://www.scrapingcourse.com/table-parsing --outputs headings,tables
  ```
</CodeGroup>

```json Output (trimmed) theme={"dark"}
{
  "headings": ["Table Parsing Challenge", "Product Catalog", "Challenge"],
  "tables": [
    {
      "Dimensions": { "Rows": 15, "Cols": 5, "Headings": true },
      "Headings": ["Product ID", "Name", "Category", "Price", "In Stock"],
      "Content": [
        { "Product ID": "001", "Name": "Laptop", "Category": "Electronics", "Price": "$999.99", "In Stock": "Yes" },
        { "Product ID": "002", "Name": "Smartphone", "Category": "Electronics", "Price": "$599.99", "In Stock": "Yes" }
      ]
    }
  ]
}
```

## Using external libraries

When you need parsing logic of your own, such as reading one product card at a time so each name stays paired with its price, request the HTML and parse it with your favorite library. Zenrows still handles rendering and anti-bot protection.

### Python with BeautifulSoup

```python Python (Requests) theme={"dark"}
# pip install requests beautifulsoup4
import requests
from bs4 import BeautifulSoup

params = {
    "apikey": "YOUR_ZENROWS_API_KEY",
    "url": "https://www.scrapingcourse.com/ecommerce/",
    "mode": "auto",
}

response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
response.raise_for_status()

soup = BeautifulSoup(response.text, "html.parser")
products = [
    {
        "name": item.select_one(".product-name").get_text(strip=True),
        "price": item.select_one(".price").get_text(strip=True),
        "url": item.select_one("a.woocommerce-LoopProduct-link")["href"],
    }
    for item in soup.select("li.product")
]

print(len(products), products[:2])
```

```text Output theme={"dark"}
16 [{'name': 'Abominable Hoodie', 'price': '$69.00', 'url': 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/'}, {'name': 'Adrienne Trek Jacket', 'price': '$57.00', 'url': 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/'}]
```

### JavaScript with Cheerio

```javascript Node.js theme={"dark"}
// npm install cheerio
import * as cheerio from "cheerio";

const params = new URLSearchParams({
  apikey: "YOUR_ZENROWS_API_KEY",
  url: "https://www.scrapingcourse.com/ecommerce/",
  mode: "auto",
});

const response = await fetch(`https://api.zenrows.com/v1/?${params}`, {
  signal: AbortSignal.timeout(180_000),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

const $ = cheerio.load(await response.text());
const products = $("li.product")
  .map((_, item) => ({
    name: $(item).find(".product-name").text().trim(),
    price: $(item).find(".price").text().trim(),
    url: $(item).find("a.woocommerce-LoopProduct-link").attr("href"),
  }))
  .get();

console.log(products.length, products.slice(0, 2));
```

## Which method to choose

| Method | Reach for it when |
| - | - |
| [Extract](/academy/extract/auto-extraction) (`extract=auto`) | You want a page's data as JSON with no selectors to maintain. |
| `css_extractor` | You know the exact elements, on one site, and want JSON back. |
| `outputs` | You need tables, links, emails, images or other common data from any page. |
| BeautifulSoup or Cheerio | You need custom logic, such as grouping fields per item or cleaning values as you parse. |

## Customize it

| Change | How |
| - | - |
| Wait for an element before extracting | Use `js_render=true` with `wait_for=.product` instead of `mode=auto`, see [Wait for selector](/fetch/features/wait-for) |
| Content from a specific country | Add `proxy_country=us`, see [Proxy country](/fetch/features/proxy-country) |
| Every page of a paginated list | Follow `next_page`, or collect the page URLs with [Crawl](/academy/scrape-and-crawl-from-a-seed-url) |
| The same selectors on thousands of URLs | Put `css_extractor` in a Batch job's `zenrows_params`, see [Scrape a list of URLs](/academy/scrape-from-a-list-of-urls) |
| Test selectors visually | Try them in the [Request Builder](https://app.zenrows.com/fetch), which also generates code |

## Troubleshooting

* **A key comes back as an empty string**: the selector matched nothing in the final HTML. Check it against the page with your browser's console, and if the element loads late, switch to `js_render=true` with `wait_for` set to that selector.
* **You get HTML instead of JSON**: the `css_extractor` value isn't valid JSON or isn't encoded. Build it with `json.dumps` or `JSON.stringify`, and URL-encode it in cURL.
* **Values from different products don't line up**: separate arrays lose the grouping when one card lacks a field. Parse one card at a time with [BeautifulSoup or Cheerio](#using-external-libraries).
* **`413` response**: the page is very large. `css_extractor` or `outputs` returns just the data you ask for and keeps responses small.

## Related recipes

<CardGroup cols={2}>
  <Card title="Get structured JSON from any page" icon="wand" href="/academy/extract/auto-extraction">
    Let Extract find the fields for you, no selectors needed.
  </Card>

  <Card title="Crawl a website from a start URL" icon="sitemap" href="/academy/scrape-and-crawl-from-a-seed-url">
    Collect every product URL, then extract each page.
  </Card>

  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    Apply the same selectors to thousands of pages in one job.
  </Card>

  <Card title="Turn any web page into LLM-ready markdown" icon="markdown" href="/academy/fetch/markdown-for-llms">
    Hand a whole page to an LLM as clean markdown.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.