Declarative Extraction¶
silkworm.declarative lets you describe the data on a page instead of writing
selector code for every field. Use it when the extraction is regular, and keep
ordinary callbacks for pagination or site-specific behavior. See the
declarative API reference for the full API.
Defining Items¶
Subclass Item, set __selector__ to the CSS selector of each record’s root
element, and assign Text or Attr fields:
from silkworm import HTMLResponse, Response, Spider
from silkworm.declarative import Attr, Item, Text
def parse_price(value: str) -> float:
return float(value.removeprefix("$").strip())
class Product(Item):
__selector__ = ".product"
title: str = Text("h2", strip=True)
price: float = Text(".price", transform=parse_price)
url: str = Attr("a", "href", absolute=True)
image: str | None = Attr("img", "src", absolute=True)
tags: list[str] = Text(".tag")
class ProductsSpider(Spider):
start_urls = ("https://shop.example.com/products/",)
async def parse(self, response: Response) -> None:
if not isinstance(response, HTMLResponse):
return
async for product in Product.extract(response):
# Existing pipelines consume JSON-compatible values.
await self.emit(product.to_dict())
Text(selector)reads the text of the matching element.Attr(selector, attribute)reads an HTML attribute of the matching element.Item.extract(response)is an async iterator producing one item per element matching__selector__.item.to_dict()returns a JSON-compatible dict for pipelines.
Cardinality From Annotations¶
The field annotation controls how the selector is evaluated:
Annotation |
Selector operation |
Missing result |
|---|---|---|
|
|
raises |
|
|
|
|
|
|
Field Options¶
default=...supplies the final scalar value when an element or attribute is missing.transform=...applies a synchronous conversion to each extracted string. Annotations are not coerced implicitly.Text(..., strip=True)strips surrounding whitespace before the transform.Attr(..., absolute=True)resolves the attribute throughresponse.url_join().
Post-processing¶
Plans are compiled from annotations once per Item class and then cached.
Override after_extract() for the irregular part of an otherwise declarative
extraction:
class Article(Item):
title: str = Text("h1")
async def after_extract(self, response: HTMLResponse) -> None:
self.title = self.title.strip()
Inspecting Plans¶
Item.extraction_plan() returns the compiled, cached ExtractionPlan for a class:
its item_type, root_selector, and a tuple of FieldPlan objects (name, field,
cardinality, value_type, annotation) in declaration order. Invalid declarations
raise DeclarativeConfigurationError when the plan is first compiled.
plan = Product.extraction_plan()
for field_plan in plan.fields:
print(field_plan.name, field_plan.cardinality, field_plan.field.selector)
Field is the shared base of Text and Attr (arguments: selector, transform, default) and is the extension point for custom field kinds.
Errors¶
All declarative errors derive from DeclarativeError (a SilkwormError):
DeclarativeConfigurationError: theItemclass declaration is invalid.FieldExtractionError: base class for per-field extraction failures:MissingFieldError: a required field matched nothing.FieldTransformError: atransformraised.
DeclarativeSerializationError: a value cannot be converted byto_dict().
Extraction errors include the item, field, selector, response URL, and root index.
Item.extract() intentionally does not replace Spider, callbacks, requests,
middlewares, or pipelines. See
examples/declarative_quotes_spider.py
for a complete spider with pagination.