DEV Community

Cover image for How to Scrape eMAG.ro Prices, Stock and Discounts with Python
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape eMAG.ro Prices, Stock and Discounts with Python

By the end of this tutorial, you will have a short Python script that turns an eMAG.ro search or category into structured rows: product name, price in lei as a number, recommended price and discount, rating, review count and stock status.

eMAG.ro is a Romanian online store. It sits behind an AWS WAF captcha: in testing on 2026-10-21, plain, datacenter and Apify Unblocker requests got a captcha page, while Apify residential proxies in Romania reached real pages.

In this guide, we use Python and the Apify Actor eMAG.ro Scraper (reventadata/emag-scraper). It is unofficial and not affiliated with eMAG. You will configure a search, run the Actor and filter rows by price, rating, discount and stock. The row shown below is a sample from a capture dated 2026-10-21, parsed with the Actor's own parser.

⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.


Prerequisites

To follow this tutorial, you will need:

  1. Python 3.11+ (apify-client 3.3.0 requires it).
  2. The Apify API client for Python.
  3. An Apify account and its API token, which sits on the API & Integrations page in Apify Console.

Install the client:

pip install "apify-client~=3.0"
Enter fullscreen mode Exit fullscreen mode

The code 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

You will turn an eMAG.ro listing into one row per product. Each row carries the offer id, productId, sku, name and url; price in lei as a number with currency: "RON"; listPrice (the recommended retail price, PRP, printed on the card), previousPrice (a struck-through price, when shown) and discountPercent; rating and reviewCount; availability, inStock and the card's stockLabel; category, categoryTrail, badges, image and position.

Note: The Actor reads listing cards only. Per its README, seller names and full specifications sit on the product page and are not read.

Example use case: Suppose you want iPhones you can buy today at 20% or more under their recommended price. You would set searchQueries: ["iphone"], exactMatchesOnly: true, onlyInStock: true and minDiscountPercent: 20, and every saved row would meet all three conditions.


Step 1: Access eMAG.ro Scraper

Go to the eMAG.ro Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.


Step 2: Configure the search

There are two ways to say what to read:

  • searchQueries: one eMAG.ro search per term, for example televizor, aspirator robot or iphone.
  • startUrls: listing URLs copied from the site. Only https://www.emag.ro/search/<term> and category paths ending in /c (for example https://www.emag.ro/laptopuri/c) are accepted; the query string is dropped.

Each result page holds up to 60 products. maxPagesPerListing (1 to 100, default 5) bounds how many pages each term or URL walks, and maxItems (default 100) caps the rows saved across all of them. The site itself serves at most 100 pages, 6,000 products, per listing.

Here is the setup. It searches iphone, reads one result page and saves at most 20 rows:

import os
from apify_client import ApifyClient

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

# 2. Define the search: one term, one result page, at most 20 rows
run_input = {
    "searchQueries": ["iphone"],
    "maxItems": 20,
    "maxPagesPerListing": 1,
}
Enter fullscreen mode Exit fullscreen mode

💡 Tip: eMAG's search is fuzzy. In the capture used for this post, iphone was answered with the site's "Telefoane Mobile" category, and 3 of the 60 products on page 1 were other phones (a mini XS18 Pro, a Honor 600 Pro and a ZTE nubia). exactMatchesOnly: true keeps only names that contain every word of the term.

No proxy setting is needed in run_input: when none is given, the Actor uses Apify residential proxies in Romania, which reached real pages in testing on 2026-10-21.


Step 3: Add optional filters

Parameter Type What it keeps
sort string RELEVANCE, PRICE_ASC, PRICE_DESC, REVIEWS, DISCOUNT or NEWEST, applied through the page's own sort link.
minPrice / maxPrice number Products inside the band, in lei. The band is also sent to the site through its own price-interval link, so it narrows what the site returns.
minRating number Average rating at or above the value (0 to 5). Unrated products are dropped when set.
minDiscountPercent integer At least N% under the struck-through price, or under the PRP when none is shown. Products showing neither are dropped when set.
onlyInStock boolean Products you can buy now: IN_STOCK, LIMITED_STOCK or IN_STOCK_SUPPLIER.
exactMatchesOnly boolean For search terms, names containing every word of the term, ignoring case and accents.
includeKeywords / excludeKeywords array Words the name or category must / must not contain, ignoring case and accents.

Three combinations that work:

  • Cheapest TVs in a band: searchQueries: ["televizor"], sort: "PRICE_ASC", minPrice: 1000, maxPrice: 2000.
  • Laptops on sale: startUrls: ["https://www.emag.ro/laptopuri/c"], sort: "DISCOUNT", onlyInStock: true.
  • Well-reviewed iPhones only: searchQueries: ["iphone"], exactMatchesOnly: true, minRating: 4.5.

⚠️ A sort outside the six values is refused before the run starts. A startUrls entry that is not a search or category listing (a product page, say, or emag.bg) stops the run with an error at its start.


Step 4: Run the Actor

Now start the run with client.actor("reventadata/emag-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:

# 3. Start the Actor run and wait for it to finish
print("Starting eMAG.ro extraction...")
run = client.actor("reventadata/emag-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 saved products
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} products.\n")
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

  • The Actor builds the search URL (/search/iphone here, /search/iphone/p2 for page 2) and fetches each page through an Apify residential proxy in Romania.
  • It reads the site's own result count on page 1 and walks no further than that count divided by 60, the 100-page ceiling or maxPagesPerListing.
  • It parses each product card, skips a product it already saved (the site repeats some products across pages), applies your filters and saves the rows. It stops at maxItems.

Step 5: View and download results

Loop over the rows to print the parts you need:

for item in items:
    print(item["name"])
    print(f"  Price: {item['price']} {item['currency']}")
    if item.get("discountPercent") is not None:
        reference = item.get("previousPrice") or item.get("listPrice")
        print(f"  Discount: {item['discountPercent']}% under {reference}")
    print(f"  Stock: {item['availability']}")
    print(f"  Rating: {item.get('rating')} ({item.get('reviewCount')} reviews)")
    print("-" * 40)
Enter fullscreen mode Exit fullscreen mode

Here is one row, a sample from a capture dated 2026-10-21 (page 1 of the iphone search) parsed with the Actor's own parser. A saved row also carries scrapedAt, added at save time:

{
    "id": "83822499",
    "productId": "40684424",
    "sku": "DQFCMXMBM",
    "name": "Telefon mobil Apple iPhone 13, 128GB, 5G, Midnight",
    "url": "https://www.emag.ro/telefon-mobil-apple-iphone-13-128gb-5g-midnight-mlpf3rm-a/pd/DQFCMXMBM/",
    "price": 3100.0,
    "currency": "RON",
    "listPrice": 3863.87,
    "previousPrice": null,
    "discountPercent": 20,
    "rating": 4.81,
    "reviewCount": 1101,
    "availability": "IN_STOCK",
    "inStock": true,
    "stockLabel": "în stoc",
    "category": "Telefoane Mobile",
    "categoryTrail": [
        "Laptop, Tablete & Telefoane",
        "Telefoane mobile & accesorii",
        "Telefoane Mobile"
    ],
    "badges": [
        "Top Favorite"
    ],
    "hasGeniusBadge": true,
    "image": "https://s13emagst.akamaized.net/products/40685/40684414/images/res_974dc0e4b6a1180b612c24afef8eb494.jpg?width=720&height=720&hash=27A6CA20492D41746C0C8A0226456760",
    "position": 26,
    "matchedKeyword": "iphone",
    "page": 1
}
Enter fullscreen mode Exit fullscreen mode

Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.


Understanding your results

The fields that matter most:

  • price, currency: the current price in lei as a number (RON).
  • listPrice, previousPrice, discountPercent: the PRP printed on the card, the struck-through price when the card shows one, and the drop from the struck-through price, or from the PRP when none is shown. In the 60-product capture, 10 rows had a PRP, none a struck-through price, and their discounts ran from 9% to 33%.
  • availability, inStock: one of IN_STOCK, LIMITED_STOCK, IN_STOCK_SUPPLIER, PREORDER, OUT_OF_STOCK, SHOWROOM_ONLY, or UNKNOWN for a code not yet understood. The capture had 36 IN_STOCK, 6 LIMITED_STOCK, 18 PREORDER and none in the other states, so inStock was true on 42.
  • rating, reviewCount: null when a product has no reviews; 8 rows in the capture, all pre-orders.
  • stockLabel: the card's own text, such as în stoc or ultimul produs in stoc (last one in stock); most cards carry none.
  • matchedKeyword, page, position: which term or URL, which page and which slot produced the row.

Pro tips

  1. Use a price band to go past 6,000 products. A listing stops at 100 pages. Because minPrice and maxPrice are also sent to the site, splitting a big category into bands reaches products the unbanded listing never shows.
  2. Pair price sorting with exact matches. In a capture of iphone sorted by PRICE_ASC, the first three rows were a Nokia 105, a MaxCom MM142 and a replacement LCD display for an iPhone. exactMatchesOnly drops the first two; excludeKeywords: ["display"] drops the third.
  3. Batch terms in one run. Put several terms in searchQueries. A product kept under one term is not saved again under the next.
  4. Read stock as data. onlyInStock keeps buyable products; for a pre-order tracker, filter availability == "PREORDER" in your own code instead.
  5. Start small. Keep maxPagesPerListing: 1 and maxItems: 20 until your downstream code handles every field, then raise them step by step.

Troubleshooting

  • Zero rows: a run that is blocked or cannot read a page ends FAILED; a search that loads normally but matches nothing ends SUCCEEDED with an empty dataset and a status message saying so. Loosen minRating, minDiscountPercent or the keywords.
  • Run stops at once: neither searchQueries nor startUrls was given, or a URL is not a /search/ or /c listing on www.emag.ro.
  • Unrelated products: eMAG's search is fuzzy. Add exactMatchesOnly or includeKeywords.
  • Fewer rows than expected: read the summary line near the end of the log, Run finished: N rows pushed. Rejections: ...; it counts each reason a product was dropped (price-band, rating, not-in-stock, discount, not-exact-match, keywords, duplicate, scope-changed). From Python: client.log(run.id).get().

For help, use the Actor's Store page.


Pricing and next steps

For current pricing and details, see the Store page.

To repeat a search, save the input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.

Which products or categories are you planning to track on eMAG.ro? Drop a comment! 💬

Top comments (0)