DEV Community

Cover image for How to Scrape Subito.it Listings into JSON with Python
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Subito.it Listings into JSON with Python

By the end of this tutorial you will have a short Python script that turns a Subito.it search into structured rows: title, asking price in euros, condition, region, province and town, seller fields, posting time, photos and the extra attributes each category carries.

Subito.it is an Italian classifieds site, and its result pages are built for people, not scripts. The Apify Actor Subito.it Scraper (reventadata/subito-scraper) reads those public result pages, no login needed, and returns one dataset row per listing. We call it from Python with the official Apify client, then print and inspect the rows.

The sample row below comes from a run of the Actor on 2026-11-15: keyword iphone, all of Italy, ten rows. The run took about four seconds.


Prerequisites

  1. Python 3.11 or newer (the Apify client for Python 3.x requires it).
  2. An Apify account and API token, from API & Integrations in Apify Console.
  3. The Apify client for Python:
pip install -U "apify-client~=3.0"
Enter fullscreen mode Exit fullscreen mode

The script reads the token from an environment variable:

export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

What you'll achieve

One row per listing, with the price as a number, the place split into region, province and town, the seller's id, the listing's photos, and the category's own attributes: rooms, area and energy class for real estate, year, mileage and fuel for vehicles. Every other attribute a listing carries lands in a features object.

The Actor reads search result pages only. It does not open each listing's detail page and does not return phone numbers.

Example use case: you want every apartment listing in the province of Milan under 300000 EUR, with rooms, area and energy class in their own columns. The input in Step 3 asks for exactly that, and the rows can go straight into a spreadsheet or a database. The run I made for this post searched phones, so the real estate columns are null in my sample.


Step 1: Open the Actor

Go to the Subito.it Scraper page on Apify Store. It lists the input fields we use below. The rest of this tutorial drives the Actor from Python.


Step 2: Choose what to search

The Actor takes these inputs:

  • searchQueries: keywords, one search per keyword. Leave the list empty to browse a category and place without a keyword.
  • category: usato (all categories, the default), immobili, appartamenti, moto-e-scooter, telefonia, auto.
  • region: a region slug such as lombardia, or italia (the default) for the whole country.
  • province: optional, for example milano.
  • maxPages and maxItems: caps on pages per keyword and on rows in total.

Copy the slugs from a real Subito.it search URL. This is the input behind my sample row (keyword iphone, all of Italy, ten rows, one page):

import os
from apify_client import ApifyClient

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

# 2. One keyword, all of Italy, all categories, first result page only
run_input = {
    "searchQueries": ["iphone"],
    "category": "usato",
    "region": "italia",
    "maxPages": 1,
    "maxItems": 10,
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Add filters and browse without a keyword

minPrice and maxPrice are in euros. The Actor reads full result pages and drops rows outside your limits afterwards; while a limit is set, listings without a price are dropped too. includeSponsored is off by default: each result page also carries 12 gallery listings that look sponsored, and rows kept with it on have isSponsored set to true.

To browse apartments in the province of Milan under 300000 EUR with no keyword, replace the input with:

# Browse a category and place without a keyword:
# apartments in the province of Milan, up to 300000 EUR
run_input = {
    "searchQueries": [],
    "category": "appartamenti",
    "region": "lombardia",
    "province": "milano",
    "maxPrice": 300000,
    "maxItems": 20,
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Run the Actor

Call the Actor with the client. It starts the run in Apify's cloud and waits for it to finish:

# 3. Start the Actor and wait for the run to finish
run = client.actor("reventadata/subito-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. Read the rows from the run's default dataset
dataset = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(dataset)} listings.\n")
Enter fullscreen mode Exit fullscreen mode

A result page holds up to 30 listings and the site serves at most 300 pages per search, so one keyword yields at most 9,000 rows. The Actor removes duplicate listing ids across pages and keywords.


Step 5: Print and download the results

Loop over the rows. Use .get() for category-specific columns, which are null where a category has no such attribute:

for item in dataset:
    title = item.get("title")
    price = item.get("price")
    town = item.get("town")
    province = item.get("province")
    condition = item.get("condition")
    posted_at = item.get("postedAt")

    print(f"Listing: {title}")
    if price is None:
        print("  Price: not listed")
    else:
        print(f"  Price: {price:.0f} EUR")
    print(f"  Place: {town} ({province})")
    if condition:
        print(f"  Condition: {condition}")
    if item.get("rooms"):
        print(f"  Rooms: {item['rooms']}, area: {item.get('areaM2')} m2")
    print(f"  Posted: {posted_at}")
    print(f"  URL: {item.get('url')}")
    print("-" * 50)
Enter fullscreen mode Exit fullscreen mode

Here is a row from the 2026-11-15 run (imageUrls cut to its first entry):

{
  "id": "668513970",
  "url": "https://www.subito.it/telefonia/iphone-13-pro-256-gb-trapani-668513970.htm",
  "title": "IPhone 13 Pro 256 GB",
  "description": "Telefono perfetto tutto originale \n",
  "price": 400,
  "currency": "EUR",
  "category": "Telefonia",
  "categoryId": "12",
  "parentCategoryId": "9",
  "categorySlug": "telefonia",
  "listingType": "In vendita",
  "condition": "Come nuovo - perfetto o ricondizionato",
  "region": "Sicilia",
  "province": "Trapani",
  "provinceCode": "TP",
  "town": "Petrosino",
  "sellerName": null,
  "sellerId": "4565054",
  "isCompany": false,
  "postedAt": "2026-11-15T20:17:54",
  "shippable": false,
  "shippingCost": null,
  "imageUrls": [
    "https://images.sbito.it/api/v1/sbt-ads-images-pro/images/b2/b2d78f8f-b810-4d1c-8d37-bc7c239a37f7?rule=gallery-desktop-2x-auto"
  ],
  "imageCount": 4,
  "rooms": null,
  "bathrooms": null,
  "areaM2": null,
  "floor": null,
  "energyClass": null,
  "buildingCondition": null,
  "vehicleBrand": null,
  "vehicleYear": null,
  "mileageKm": null,
  "fuel": null,
  "gearbox": null,
  "features": {
    "item_shippable": "No",
    "item_condition": "Come nuovo - perfetto o ricondizionato",
    "price": "400 €",
    "phone_type": "Cellulari e Smartphone"
  },
  "isSponsored": false,
  "searchQuery": "iphone",
  "searchUrl": "https://www.subito.it/annunci-italia/vendita/usato/?q=iphone"
}
Enter fullscreen mode Exit fullscreen mode

The dataset can also be exported as JSON, CSV or Excel.


Understanding your results

  • id, url, title, description: the numeric listing id, the listing page, the ad title and the ad text.
  • price, currency: asking price as a number; currency is EUR when a price is present, and both are null when the listing shows none. All 10 rows of my run had a price.
  • category, categorySlug, listingType, condition: category label and slug, the ad type (such as "In vendita") and the item condition where the category has one. In my run the conditions were "Buono - usato ma ben conservato" (4 rows), "Ottimo - poco usato e ben conservato" (3) and "Come nuovo - perfetto o ricondizionato" (3).
  • region, province, provinceCode, town: where the item is.
  • sellerName, sellerId, isCompany: sellerName was null on all 10 rows of my run, and isCompany was false on all 10; sellerId was filled on each.
  • postedAt: date and time as the site gives them, without a time zone. My ten rows range from 2026-11-15T20:11:25 to 2026-11-15T20:24:48.
  • shippable, shippingCost: whether the item can be shipped and the shipping cost, where the listing shows it. 4 of my 10 rows were shippable, each with a cost of 0.99; on the other 6, shippingCost was null.
  • imageUrls, imageCount: photo links and how many there are. The count was between 3 and 6 on my rows.
  • rooms, areaM2, vehicleYear, mileageKm, fuel: category columns, null elsewhere. They were null on all 10 phone rows.
  • features: every attribute, keyed by the site's own attribute name.
  • searchQuery, searchUrl, isSponsored: which search found the row. isSponsored was false on all 10.

Pro tips

  1. Start small. maxPages: 1 and maxItems: 10 check your mapping before a large sweep, and a small run shows you which features keys your category uses.
  2. The search is Subito's own. The Actor has no text filter. In my run, nine of the ten titles contain "iPhone" and one is "Oppo find n5 Oppo X2 e Oppo enco 4pro", so filter on title in your script if you need only one brand.
  3. Split big searches. Narrow by region, province or category to go past 9,000 rows for one keyword, then merge the datasets on id.
  4. Sponsored rows. Leave includeSponsored off unless you need the gallery block; with it on, filter on isSponsored to tell the two kinds of row apart.
  5. Compare only like with like. postedAt has no time zone, so compare it only with other postedAt values.
  6. Webhooks. A webhook's resource is the run object, so it carries defaultDatasetId (Apify docs).

Troubleshooting

  • Run FAILED with a slug message. A region, category or province with characters other than letters, digits and single hyphens, or one Subito does not know, ends the run FAILED with a message naming it. Copy the slugs from a real search URL.
  • Zero rows. A search that loads normally but matches nothing ends SUCCEEDED with a status message saying so. A run whose pages could not be read ends FAILED, with a status message saying the pages could not be read.
  • Prices missing. Listings without a price have price null. None of my ten rows lacked one, but the column allows for it.
  • Use the data responsibly. The rows are public listing data; check Subito's terms and the law that applies to you before you use it.

Next steps

Save the input as a task and add an Apify schedule to rerun the search on a cron expression. Pull the features object into your own columns for the category you track.

Links:

For current pricing and details, see the Store page.

Which Subito categories are you planning to track? Drop a comment!

Top comments (0)