Skip to main content

Complete Guide: How to Scrape Milanuncios Classifieds Using Python

Run the Milanuncios Scraper Apify Actor from Python, extract clean listing and price-drop data, and export to JSON without browser overhead.

Updated 13 min readMarkdown
Complete Guide: How to Scrape Milanuncios Classifieds Using Python

This complete guide to Milanuncios Scraper demonstrates how to run a Milanuncios web scraper from Python using the Apify API and retrieve structured, typed marketplace records: product titles, prices in euros, previous prices, price drop percentages, structured condition tags, parsed storage capacities, parsed battery health figures, and seller classifications. The extraction behavior and technical specifications described below are based on empirical runs and dataset inspections.

Milanuncios is an online classifieds marketplace serving Spain. However, developing a custom scraper for the platform presents technical hurdles: requests from Apify's datacenter range were refused (HTTP 405) when measured in August 2026, and the rendered HTML pages contain only skeleton markup.

In this tutorial, you will learn how to automate data collection from Milanuncios using the Milanuncios Scraper Apify Actor (reventadata/milanuncios-scraper). Prerequisites are minimal: Python 3.11+, the official apify-client library, and an Apify API token. Estimated time to complete: about 30 minutes. The live benchmark run supporting this guide (configured with searchQueries: ["iphone"], category: "telefonia", onlyPriceDropped: True, and maxItems: 200) collected 200 structured listings in 31.5 seconds.


TL;DR

In this tutorial, you will learn to configure and trigger the Milanuncios Scraper Apify Actor from Python, inspect run execution states, extract clean dataset items, and handle structured condition tags and price drops. Key steps: install apify-client, export your APIFY_API_TOKEN, define target search parameters in run_input, call the Actor, and iterate over items in run.default_dataset_id.

Expected outcome: a JSON dataset of Milanuncios listings with current price, previous price, price drop percentage, structured condition, storage, battery health, and location where the ad states them, ready for immediate ingestion into databases or spreadsheets.

The Actor is available at apify.com/reventadata/milanuncios-scraper.


Prerequisites and setup

Before running our extraction script, verify the following prerequisites:

To follow this tutorial, you will need:

  1. Python 3.11+ installed locally (apify-client 3.x requires it).
  2. The Apify API client for Python (pip install "apify-client~=3.0").
  3. An Apify account and API token.

An Apify account. Your personal API token is shown on the API & Integrations page in Apify Console. Platform documentation is indexed at docs.apify.com.

Install the Apify client library via pip:

pip install "apify-client~=3.0"

Set your API token in your environment:

export APIFY_API_TOKEN="your_apify_token_here"

Understanding Milanuncios Scraper

The Milanuncios Scraper extracts marketplace data by targeting Milanuncios's internal data structures:

  • Embedded data per page: Rather than parsing rendered HTML markup with a browser, each results page embeds up to 100 listings inside an inline JavaScript state blob (window.__INITIAL_PROPS__). The Actor extracts this JSON payload and fetches page by page.
  • Proxy unblocking: Requests from Apify's datacenter range were refused (HTTP 405) when measured in August 2026. Live runs fetch through Bright Data's Web Unlocker to retrieve pages.

Limitations to be aware of

When configuring the Actor, note three specific platform constraints:

  1. Four-value condition filter: The conditions filter accepts only four values: sin estrenar, prácticamente nuevo, en buen estado, and aceptable. For other values the platform refuses the input before the run starts.
  2. Unvalidated category slugs: A category slug is not validated; a wrong slug can return the unfiltered search.
  3. Exact province slugs: Province identifiers must be typed exactly as listed, underscores included (such as la_coruna); for a typo the platform refuses the input before the run starts.

Deep Dive: Structured Condition Tags and Parsed Hardware Attributes

Milanuncios features distinct data structures that require careful handling:

  1. Condition Tags (estado del producto):
    • The conditions input filter accepts four enum values, but the output condition field can carry other values when the ad carries a condition tag (this run: one mejorable).
    • In our 200-row benchmark run, the distribution covered all six buckets: en buen estado 92, prácticamente nuevo 62, sin estrenar 12, aceptable 3, mejorable 1, and no condition 30.
    • The Actor extracts this attribute into the condition field.
  2. Hardware Attribute Parsing (storageGb and batteryHealth):
    • Milanuncios does not provide a structured storage field; storage and battery health are parsed from the title and description.
    • The Actor parses free-text titles and descriptions using regular expressions. Storage values are normalized to integer gigabytes (converting TB values to 1024 GB, though storageGb holds three odd values as the sellers wrote them: 126, 250, and 265 on individual rows); the battery health parser accepts integers from 40 to 100, and in this run observed values spanned 70 to 100.
    • In our benchmark run of 200 items, 148 records yielded a parsed storageGb value, and 105 records contained a parsed batteryHealth percentage.
  3. Price Drop Detection (previousPrice and priceDropPercent):
    • When sellers reduce their asking price, Milanuncios records both the original price and a markdown percentage.
    • In our benchmark run, all 200 rows have previousPrice, and 197 have priceDropPercent (the other 3 carry no percentage). priceDropPercent reached 100 on five €0 replica listings.

Handling Spanish Regional and Category Navigation

Milanuncios handles categories and provinces through URL path structures rather than standard query parameters:

  • Combined Path Structure: Category and province combine into a single URL segment: /<category>-en-<province>/ (for instance, /telefonia-en-madrid/). Supplying the category as a URL query parameter (?categoria=telefonia) is silently ignored by the server.
  • Category Slugs: The category is a free-text slug (the input schema's description lists 17 top-level ones). A category slug is not validated; a wrong slug can return the unfiltered search, so copy the slug from a Milanuncios URL.
  • 52 Provincial Jurisdictions: Spain comprises 50 provinces plus the autonomous cities of Ceuta and Melilla. The slug must be typed exactly as listed, underscores included (four use underscores: la_coruna, la_rioja, las_palmas, and ciudad_real). The province list is fixed, so for a hyphen spelling the platform refuses the input before the run starts.

Step-by-step walkthrough

Step 1: Navigating to the Actor

Visit the Milanuncios Scraper Store page. The page lists the input parameters. Clicking Try for free opens Apify Console.

Step 2: Defining inputs and target categories

In your Python script, configure the target search keywords, category slug, and filter constraints:

import os
from apify_client import ApifyClient

# 1. Initialize ApifyClient with token from environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])

# 2. Define the extraction parameters
run_input = {
    "searchQueries": ["iphone"],
    "category": "telefonia",
    "onlyPriceDropped": True,
    "maxItems": 20,
}

(Note: While this introductory snippet specifies maxItems: 20 for a quick demonstration, our live benchmark run ran with searchQueries: ["iphone"], category: "telefonia", onlyPriceDropped: True, and maxItems: 200.)

Step 3: Executing via Apify Python Client

Trigger cloud execution using client.actor("reventadata/milanuncios-scraper").call(run_input=run_input):

# 3. Start the Actor run and wait for it to complete
print("Starting Milanuncios extraction...")
run = client.actor("reventadata/milanuncios-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
    status = run.status if run else "unknown"
    raise SystemExit(f"Run did not succeed (status: {status}).")

# 4. Fetch the resulting dataset items
dataset = client.dataset(run.default_dataset_id).list_items().items

print(f"Extraction complete! Retrieved {len(dataset)} listings.\n")

Execution characteristics:

  • The Actor launches in an isolated container with Bright Data's Web Unlocker.
  • It fetches search-result pages one after another up to maxItems.
  • In our test run, the Actor extracted 200 listings.

Step 4: Iterating and processing dataset items

After completion, iterate across the dataset records in Python:

for item in dataset:
    title = item.get("title")
    price = item.get("price")
    previous_price = item.get("previousPrice")
    drop = item.get("priceDropPercent")
    condition = item.get("condition")
    storage = item.get("storageGb")
    battery = item.get("batteryHealth")
    city = item.get("city")
    seller_type = item.get("sellerType")

    print(f"Listing: {title}")
    if previous_price and drop:
        print(f"  Price: €{price} (was €{previous_price}, -{drop}%)")
    else:
        print(f"  Price: €{price}")
    print(f"  Condition: {condition or 'not specified'}")
    if storage:
        print(f"  Storage: {storage} GB")
    if battery:
        print(f"  Battery Health: {battery}%")
    print(f"  Location: {city} ({seller_type})")
    print("-" * 50)

Verbatim dataset sample

Below is a complete, unedited record extracted during the benchmark run:

{
    "id": "575075470",
    "title": "Iphone X",
    "description": "Se vende Iphone X en muy buen estado, con su caja y accesorios y dos fundas nuevas sin usar. El estado de la bateria es 75%. Memoria 64gb.",
    "url": "https://www.milanuncios.com/iphone/iphone-x-575075470.htm",
    "imageUrls": [
        "https://images.milanuncios.com/api/v1/ma-ad-media-pro/images/86f22102-6da3-44df-9375-68f332e3150d",
        "https://images.milanuncios.com/api/v1/ma-ad-media-pro/images/9195d341-33b1-49d9-afb5-31f598262fab"
    ],
    "price": 125,
    "currency": "EUR",
    "previousPrice": 150,
    "priceDropPercent": 17,
    "condition": "en buen estado",
    "color": "blanco",
    "sellerType": "private",
    "adType": "offer",
    "sellerId": null,
    "sellerName": null,
    "city": "Santa Cruz de Tenerife",
    "province": "Tenerife",
    "region": "Canarias",
    "category": "iPhone",
    "categoryPath": "telefonia/moviles/telefonos-moviles/iphone",
    "publishDate": "2026-01-08T11:33:28Z",
    "updateDate": "2026-10-23T12:18:18Z",
    "isHighlighted": false,
    "isReserved": false,
    "storageGb": 64,
    "batteryHealth": 75,
    "matchedKeyword": "iphone"
}

Detailed schema breakdown

Each dataset field:

  • id: Milanuncios ad identifier string (e.g. "575075470").
  • title: Listing title.
  • description: Free-text description, newlines included.
  • url: Absolute link to the ad.
  • imageUrls: List of absolute photo URLs.
  • price: Asking price in EUR (numeric, or null if unstated).
  • currency: Always "EUR".
  • previousPrice: Stated pre-drop price in EUR, or null.
  • priceDropPercent: Integer from the site's own drop label; null if none.
  • condition: Condition tag (e.g. "en buen estado"), or null.
  • sellerType: "private" or "professional".
  • adType: "offer" (supply) or "wanted" (demand).
  • sellerId: Identifier for professional sellers, or null for private sellers.
  • sellerName: Name for professional sellers, or null for private sellers.
  • city: Locality or town name.
  • province: Spanish province name.
  • category: Leaf category name.
  • categoryPath: Hierarchical category path.
  • publishDate: ISO 8601 time the ad was first published.
  • updateDate: ISO 8601 update timestamp.
  • isHighlighted: Whether the ad is a paid, promoted listing.
  • isReserved: Normalized boolean for reservation status.
  • storageGb: Storage in GB parsed from text, or null.
  • batteryHealth: Battery percentage parsed from text, or null.
  • matchedKeyword: Search keyword that returned this listing.

Working with the API

You can automate Milanuncios data extraction programmatically by integrating the Apify API into backend services, ETL pipelines, or scheduled workflows.

When initiating extractions from code, the Apify Python client provides two primary execution methods: .call() and .start(). Calling client.actor("reventadata/milanuncios-scraper").call(run_input=run_input) triggers the container and blocks until execution finishes, returning the settled run record. For asynchronous or distributed workflows where holding open an HTTP connection is impractical, invoke client.actor("reventadata/milanuncios-scraper").start(run_input=run_input) instead. This initiates the container on Apify infrastructure and immediately returns a run object containing the run ID and execution status.

To read the extracted data once execution finishes, query the default dataset through client.dataset(run.default_dataset_id).list_items().items or stream records using iterate_items(). The dataset retains clean, structured JSON items matching the schema detailed above.

For event-driven architectures, configure an Apify webhook in Apify Console or pass webhook definitions in the API call. You can configure the webhook to fire on the ACTOR.RUN.SUCCEEDED event. The webhook payload's resource is the response of the Get Actor run API endpoint, which carries defaultDatasetId (see docs.apify.com/platform/integrations/webhooks/actions). This allows ingestion workers to fetch records directly without polling.

For scheduled executions, configure recurring schedules (for example, every morning at 08:00; a schedule has a timezone field, see docs.apify.com/api/v2/schedules-post) to rerun the same search, then compare the datasets yourself.


Advanced filtering and price drops

  1. Filtering by Markdown Status (onlyPriceDropped): Setting onlyPriceDropped: true instructs the scraper to send isPriceDropped=true in the Milanuncios search URL. This isolates ads Milanuncios flags as price-dropped.
  2. Differentiating Private Individuals from Commercial Dealers: Setting sellerType: "private" restricts results to private individual sellers, while sellerType: "professional" collects listings from commercial dealers. Filtering by seller classification allows market analysts to separate consumer peer-to-peer pricing from commercial dealer inventory.
  3. Condition Filtering (conditions): Passing a list of strings such as conditions: ["prácticamente nuevo", "en buen estado"] restricts output to ads with matching condition tags. The conditions filter accepts exactly four values: sin estrenar, prácticamente nuevo, en buen estado, and aceptable. For any other value the platform refuses the input before the run starts.
  4. Damage and Brick Filtering (excludeDamaged): Setting excludeDamaged: true evaluates listing titles and descriptions against the Actor's list of damage wording. This discards units advertising broken parts (such as "pantalla rota", "agrietada", or "cristal roto"), non-functional hardware ("no enciende", "no funciona", "para piezas", "despiece", "averiado", "defectuoso"), or locked status ("icloud bloqueado", "lista negra"). Note that the list does not cover water damage and misses the masculine form like "pantalla roto".
  5. Sorting Modes (sortBy): You can order results by relevance ("relevance"), lowest price ("baratos"), or newest listings ("date"). Using "baratos" lists the cheapest ads first.

Troubleshooting

When operating the Milanuncios Scraper in production, keep these operational considerations in mind:

  • Empty dataset returned: The Actor does not fail when zero listings match your search criteria. Instead, it completes normally and returns an empty dataset. If you receive zero results, verify that your keywords are not overly restrictive, or check whether filters like minPrice, maxPrice, and conditions eliminated all matching inventory.
  • Category typos: A category slug is not validated; a wrong slug can return the unfiltered search. Always copy the category slug from a live Milanuncios search URL.
  • Province validation: Province slugs must match exact identifiers, underscores included. For any other spelling, a hyphen spelling such as la-coruna included, the platform refuses the input before the run starts.
  • IP-scoped block stubs (HTTP 200): When Milanuncios rate-limits requests, it responds with HTTP 200 and an HTML stub missing the embedded __INITIAL_PROPS__ state payload. A stub gets a retry.
  • Where to get help: For support, bug reports, and questions, visit the Actor's Store page at apify.com/reventadata/milanuncios-scraper.

Best practices

Follow these operational guidelines to optimize extraction reliability and expense:

  1. Use maxItems as a budget cap: Set maxItems to impose a strict ceiling on total records collected per execution, preventing unintended usage on broad keywords.
  2. Filter before storage to avoid per-result fees: Apply minPrice, maxPrice, conditions, and onlyPriceDropped in your input parameters. Records filtered out before dataset storage incur no per-result event fees.
  3. Sort strategically: Use sortBy: "baratos" when targeting entry-level price floors, or sortBy: "date" for newly published listings.
  4. Handle nullable attributes defensively: Fields such as sellerId, sellerName, batteryHealth, and storageGb can be null (private sellers always have null seller identifiers). Ensure downstream data ingestion pipelines account for optional attributes.

Pricing model and cost transparency

For current pricing and details, see the Store page.


Conclusion and resources

With the Milanuncios Scraper and a few lines of Python, you can collect structured marketplace data, price-drop data, and hardware specifications across Spain.

Try Milanuncios Scraper yourself and let me know what you build!

I build and maintain marketplace scrapers under the reventadata name on the Apify Store.

About the author

  • Hi, I’m the engineer behind ReventaData. I build automated web scrapers, reverse-engineer undocumented APIs, and design data pipelines on Apify that turn complex, unstructured websites into clean, reliable feeds. Here on Hashnode, I share practical tutorials on modern web scraping with Python, bypassing aggressive anti-bot protections (Cloudflare, Akamai, PerimeterX), and data pipeline automation, reverse-engineering hidden endpoints, and automating data extraction. Explore our turnkey scrapers: https://apify.com/reventadata

No comments yet. Be the first to comment.

More from this blog