elements
use extract.elements when you know which parts of a page you want. you give each value a name and
a css selector. we send back just those values, under the same names, in the top-level elements
field of the result. no need to parse the whole page yourself.
elements cost no extra credits, also when the page comes from cache. see cost.
a first example
this request reads the page heading, every book on the page (with its title, price and link), and the link to the next page:
curl
curl -X POST https://api.crawlbrulee.com/api/scrape \
-H "Authorization: Bearer $CRAWLBRULEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://books.toscrape.com/",
"extract": {
"elements": {
"heading": "h1",
"books": {
"selector": "article.product_pod",
"all": true,
"fields": {
"title": { "selector": "h3 a", "output": "attribute", "attribute": "title" },
"price": ".price_color",
"url": { "selector": "h3 a", "output": "attribute", "attribute": "href" }
}
},
"next_page": { "selector": "li.next a", "output": "attribute", "attribute": "href" }
}
}
}'response (condensed: books has 20 items, the first 3 are shown)
{
"url": "https://books.toscrape.com/",
"requested_url": "https://books.toscrape.com/",
"page_status_code": 200,
"content_type": "text/html",
"elements": {
"heading": "All products",
"books": [
{
"title": "A Light in the Attic",
"price": "£51.77",
"url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
},
{
"title": "Tipping the Velvet",
"price": "£53.74",
"url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"
},
{
"title": "Soumission",
"price": "£50.10",
"url": "https://books.toscrape.com/catalogue/soumission_998/index.html"
}
],
"next_page": "https://books.toscrape.com/catalogue/page-2.html"
}
}the page links to each book with a short relative path. url and next_page come back as full
urls, because href and src values always do.
elements comes on top of the other outputs. metadata and cleaned_html stay on by default, so
the full response carries them too. if you only want the values, turn them off:
{
"url": "https://books.toscrape.com/",
"extract": {
"metadata": false,
"cleaned_html": false,
"elements": { "heading": "h1" }
}
}how to ask for a value
extract.elements is an object. each key is a name you pick, and it comes back as a key of
elements. each value is a selector string or an object.
a selector string
the short form. you get the text of the first match.
{ "heading": "h1", "price": ".price_color" }an object
the long form, when you want more than the text of the first match:
| field | type | required | default | what it does |
|---|---|---|---|---|
selector | string | yes | — | the css selector, at most 500 characters |
output | string | no | "text" | "text" for the text, "html" for the outer html of the match, "attribute" for one attribute |
attribute | string | no | — | the attribute to read, 1 to 100 characters. required when output is "attribute", and only allowed then |
all | boolean | no | false | true returns every match as a list, instead of the first match |
fields | object | no | — | used instead of output: turns each match into an object (see fields) |
{
"next_page": { "selector": "li.next a", "output": "attribute", "attribute": "href" },
"all_prices": { "selector": ".price_color", "all": true },
"first_book_html": { "selector": "article.product_pod", "output": "html" }
}fields: one object per match
when a value has parts, like a product card with a title, a price and a link, use fields. each
field is a name and a selector (string or object), read inside that match only. so every card
gives you one object, and its title, price and link always belong together.
a field can’t read the match itself. if you match <article> and a field asks for an attribute of
that <article>, it comes back null. read it with output: "attribute" on the outer spec
instead, or match one level higher.
with all: true you get a list of objects, one per match. without it, you get one object from the
first match.
fields can have fields of their own, up to 3 levels deep. the innermost level has no fields.
here a category page lists sections, each with products, each with tags:
{
"sections": {
"selector": "section.category",
"all": true,
"fields": {
"name": "h2",
"products": {
"selector": ".product",
"all": true,
"fields": {
"title": "h3",
"tags": { "selector": ".tag", "all": true }
}
}
}
}
}a spec with fields can’t also set output or attribute, and fields needs at least one field.
either mistake is a 400.
what comes back
the values come back in the top-level elements field, under the names you sent:
| you asked for | a match was found | nothing matched |
|---|---|---|
| a selector string, or an object | a string | null |
all: true | a list of strings | [] |
fields | an object | null |
fields with all: true | a list of objects | [] |
every name you asked for is always there, at every level of fields. a name with nothing to read
is null (or [] with all: true), never missing. so you can read book["price"] without checking
first that the key exists.
with output: "attribute", a match that doesn’t have the attribute gives null, and is left out of
an all: true list.
a page the site answered with an error status, such as a 404, is still a page, and we read your
elements from it. if values come back null that you didn’t expect, check page_status_code
first.
selector rules
- standard css selectors. tags, classes, ids, attribute selectors, combinators, and
pseudo-classes such as
:nth-child(),:not()and:has()all work. - pseudo-elements and invalid selectors are refused. a selector like
p::before, or one with a typo, is a400before we fetch the page, so it costs nothing (see when a request is refused). - values are read after cleanup.
cleanup.ads_and_popups(on by default) and your owncleanup.exclude_selectorsremove parts of the page first, so a selector that points into a removed part matches nothing. see excluding content. elements are read from the same page aslinksandimages. <script>and<style>can’t be selected. they are not part of the page we read, so JSON-LD (script[type="application/ld+json"]) and data inside inline scripts come backnull. useextract.raw_htmlwhen you need those.- attribute names match in any case.
data-skuandDATA-SKUread the same attribute. hrefandsrccome back as full urls, resolved against the page, likelinksandimages. a link to a#fragmenton the same page stays as written, such as#reviews.- unsafe links are dropped. with
output: "attribute", a link such asjavascript:…givesnull, and is left out of anall: truelist. so twoall: truelists from the same page can have different lengths, and pairing them by position can match the wrong values. read values that belong together asfieldsof one match instead. - tables are read the way a browser reads them. a browser adds
<tbody>to a table when the page’s html leaves it out. sotable > tbody > trfinds the rows either way, whether or not the page was rendered with JavaScript. - fields look only inside the match. a field selector that points outside the match finds
nothing. that includes one that starts with
+or~, because a sibling is outside the match, and the match itself: an attribute of the matched element comes backnullfrom a field (see fields).
limits
| what | limit |
|---|---|
selectors per request, counting every level of fields | 50 |
| characters per selector | 500 |
levels of fields | 3 |
matches per list (all: true) | 1,000 |
| matches per request, across every list and level | 10,000 |
| size of all values in one response | about 1 MB |
more than 50 selectors, or a selector over 500 characters, is a 400.
the other limits never fail the request. when a value hits one, the response’s warnings array
carries elements_truncated:
{
"warnings": ["elements_truncated"]
}it means at least one of these happened:
- a list stopped at 1,000 matches.
- the request reached 10,000 matches, so later matches were left out.
- a value didn’t fit in the 1 MB or so of values a response can carry. that value is
null: a value is never cut short.
output: "html" on a large match is the usual way to reach the size limit. ask for the parts you
need with fields instead.
pages that aren’t html
elements need an html page. for json, plain text, markdown or xml, the response has no elements
field and unsupported_fields includes "elements". the rest of the scrape comes back as usual.
{
"content_type": "application/json",
"unsupported_fields": ["elements"]
}cost
elements cost no extra credits. a scrape with elements costs what the same scrape costs without them (see credits & pricing).
that holds for a cache hit too. extract is not part of the cache key, so a
cached page can answer a request with any elements. we read your values from the cached page, and
the hit still costs 0. to read the live page instead, send cache.max_age: 0.
async jobs
POST /api/scrape/async takes the same extract.elements. the result from
GET /api/scrape/result/{job_id} carries elements, like the rest of the result, for the same 24
hours (see scrape (async)).
the scrape.complete webhook doesn’t carry results, elements included. fetch
the result when the webhook arrives.
when a request is refused
a request that breaks the rules above is refused with a 400 validation_error before we fetch the
page, so it costs nothing. that covers things like a selector that isn’t valid css, more than 50
selectors, or fields together with output. the message says what’s wrong and where. for example,
a typo in a pseudo-class:
{
"name": "validation_error",
"message": "Request validation failed: extract.elements.books.fields.price: invalid CSS selector: Unknown pseudo-class :frist-child"
}switch on name, not on the message text. see errors for the error format.
in the sdks, cli and mcp server
- js/ts sdk:
extract: { elements: { … } }, and the values onpage.elements. see js/ts sdk. - python sdk:
ScrapeExtract(elements={…}). each value is a selector string, aScrapeElementSpec, or the same spec as a plain dict (ScrapeElementSpecDict). the values come back onpage.elements. see python sdk. - cli:
--element name=selector(repeatable), or--elementswith the full object as json. on their own, they return only the elements (plus metadata unless--no-metadata); add-mor another content flag to get the page too. see cli. - mcp server: the
scrapeandscrape_asynctools takeextract.elements. see mcp server.