DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Fotocasa Scraper: the null field that hid every discounted listing

Quick answer

We assumed reducedPrice was a raw integer, because in the single listing we'd probed it, the field was null — and null has no type to infer from. A mandated live run against real listings surfaced the truth: when a Fotocasa listing actually carries a discount, reducedPrice is a formatted string, "75.000 €", the exact same shape as the regular price field. Ship the integer assumption and the parser silently skips every discounted listing — no error, no warning, just a quietly incomplete dataset on precisely the rows a buyer cares about most. The Fotocasa Spain Property Scraper parses it as a formatted string, same as price, before converting to reduced_price_eur.

Can you infer a field's type from a null sample?

No — and that's the whole lesson here. A field that reads null in your one probed sample has no observable shape; you cannot know from an absence whether the real value, when it exists, is an integer, a formatted string, or something else entirely. The only fix is running against enough live data that the field actually appears populated at least once. We did — and reducedPrice turned out to share the exact same "75.000 €"-style formatting as the primary price field, not the raw integer a schema guess would produce. reduced_price_eur in our output is legitimately null on listings with no discount; it's only populated, as an integer in euros, on the listings that actually have one.

Does this scraper need a browser to run?

No. Fotocasa.es server-renders each listing's full structured data into a <script id="__initial_props__"> block on the search-results page itself — price, address, coordinates, features, photo URLs, and the description all arrive in that one JSON payload. No separate API calls, no client-side rendering to wait on. We read it with curl-cffi under browser TLS impersonation, no headless browser required, which keeps the run cheap and the dataset arriving fast.

A field that's null in your sample carries no type information at all — the only way to learn its real shape is a live run against data where it's actually populated.

What the Actor gives you

One validated row per listing: price in euros (price_eur, plus the human-formatted price_display) and reduced_price_eur when discounted, building_type/building_subtype, rooms, bathrooms, surface_m2, full address (address_province, address_city, address_district, address_neighborhood, address_zip_code), latitude/longitude, dynamic_features, up to ~18 image_urls, the full Spanish-language description, and agency_alias/agency_type/phone when the seller publishes them.

Honest limitations 🚧

Search-results-page fields only in v1 — no per-listing detail-page fetch, since the results page already embeds everything above. Rent (alquiler) URLs are supported best-effort; sale (comprar) is the wire-confirmed path. Price filters apply after fetching, so a narrow band still costs the request for out-of-range listings on that page. propertyTypeId passes through as Fotocasa's raw internal ID — there's no confirmed ID-to-label table yet.

FAQ

Why did the original spec get reducedPrice's type wrong?
The only sample probed at spec time had it as null, and null doesn't reveal a type. A live run against listings that actually carry a discount showed it's a formatted string like price, not a raw integer.

Do I need a Fotocasa account or API key?
No — Fotocasa publishes no public API for this data; this Actor reads the same public search-results pages a browser loads and parses the JSON already embedded in them.

Why is the proxy pinned to residential Spain (ES) by default?
Because a Spain-only portal can render the wrong currency, or block outright, from a foreign exit — we force the ES residential exit regardless of other settings.

$0.20 per run plus $0.004 per validated listing — $4.20 per 1,000 results. No data, no charge beyond the start fee.

→ Fotocasa Spain Property Scraper on Apify


Built by Devil Scrapes. We rotate browser fingerprints, rotate residential proxies, retry with backoff, and ship clean typed rows — and we don't guess a field's shape from a sample where it's empty.

Top comments (0)