DEV Community
I Turned Spain's BOE Public Auctions Portal into a Searchable Site
Spain's BOE runs a public auction portal. Every day it lists court-ordered repossessions, tax-agency seizures and notarial sales: flats, garages, warehouses, plots, vehicles. Each listing states an auction value, a minimum bid, a deposit, whether the property is occupied and what charges survive the sale. All of it is public, and none of it is designed to be read by someone deciding whether to bid. I built Subastas BOE around it: a Spanish-language site that turns those listings into a searchable catalogue. The hard part was not the frontend, it was making a government HTML portal behave like a dataset. See it in Spanish: subastas.devetty.es has a filterable search, one page per province, per municipality and per asset type, and a guide to bidding. Two pages per auction The portal exposes a search page and a detail page. The search page returns result blocks; each block has a title, an authority, a file number and a link to the auction. The record itself is split across tabs, and each tab is a separate request with a different ver parameter. So the scrape is one list request with pagination, then two detail requests per auction — one for the auction terms, one for the asset. The list config is short: "list": {
"selector": "li.resultado-busqueda",
"dedupeBy": "id",
"required": ["id", "url_ficha"],
"fields": {
"id": {
"selector": "h3",
"transform": ["collapse", { "regex": "(SUB-[A-Z0-9-]+)" }]
}
}
}
The identifier is the only thing I trust from the list. It looks like SUB-JA-2026-..., and it is what the detail URL needs: https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=1
https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=3
Pagination follows a link labelled siguiente, capped at 30 pages and paced 1.5–2.5 seconds apart. Detail requests run one at a time with a 2–4 second delay, and the whole thing is a single daily run, not a tight loop. This is a public service. The cost of being polite is a slower crawl; the cost of not being polite is being blocked, deservedly. Parsing with configuration instead of a parser I did not write selectors in code. The scraper takes a JSON config where each field declares a CSS selector and a chain of transforms. Extracting the minimum bid becomes a line in a file, not a function to debug: "puja_minima": {
"selector": "th:contains('Puja mínima') + td",
"transform": ["collapse", "number"]
}
The portal renders label/value tables, so th:contains(...) + td does most of the work. Dates are the exception: the conclusion date is human text with an ISO timestamp inside parentheses, so I regex the ISO: part for fecha_fin and keep the display text separately. The province problem is a slug problem Asset details carry a province in the source spelling, and Spain's provinces are officially bilingual in several regions. The portal is inconsistent: Araba/Álava, Bizkaia, Gipuzkoa, Illes Balears, Valencia/València, Palmas (Las). Slug those directly and you get three URLs for one place, plus a hub page that splits its own inventory. I resolve it at ingestion, as a chain: "provincia_es": { "op": "clean_regex", "from": "provincia", "pattern": "/^Araba\s\/\s/u" },
"provincia_slug_base": { "op": "clean_regex", "from": "provincia_es", "pattern": "/\s\/.$/u" },
"provincia_alias": { "op": "map", "from": "provincia_slug_base",
"map": { "Bizkaia": "Vizcaya", "Gipuzkoa": "Guipúzcoa", "Illes Balears": "Baleares" } },
"provincia_canon": { "op": "coalesce", "fields": ["provincia_alias", "provincia_slug_base"] },
"provincia_slug": { "op": "slug", "from": "provincia_canon" }
Two regexes and one alias map reduce every variant to one canonical name and one slug. Araba/Álava becomes alava, Valencia/València becomes valencia. The display name stays in provincia; only the URL is canonicalised. That single decision is why the site can cross-link. The same slug works on my sister sites, so an auction in madrid can point at publi...
Powered by Readybot.io
External Link
Platform
You are about to visit:
https://example.comThis will open in a new tab. Make sure you trust this link.