Fragrantica.com Scraper With Dupe & Similarity Scoring
Pricing
from $4.99 / 1,000 results
Fragrantica.com Scraper With Dupe & Similarity Scoring
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
5
Total users
2
Monthly active users
4 days ago
Last modified
Categories
Share
Fragrantica Scraper — Notes, Accords and Dupe Similarity Scores
Fragrantica Scraper extracts public perfume pages into structured rows: title, brandName, the full pyramid of top, middle and base notes, mainAccords with their strength values, perfumeRating, ratingCount and community reviews. It then fetches each perfume's recommended fragrances, reads their real notes and accords, and scores every pair into a ranked similarityGraph with a dupeScore from 0 to 100. Typed JSON, no HTML parsing. Run it on Apify with a single perfume URL.
What is Fragrantica.com Scraper With Dupe & Similarity Scoring?
Fragrantica.com Scraper With Dupe & Similarity Scoring is an Apify Actor that turns Fragrantica perfume pages into a dataset and adds a computed similarity layer on top. For each perfume it scrapes, it also fetches the fragrances Fragrantica recommends alongside it, extracts their own note pyramids and accords, and ranks them by note and accord overlap.
No Fragrantica account, login or cookie is required. Everything it returns is visible to an anonymous visitor on a public page.
It is built for fragrance retailers and marketplace teams building "similar to" shelves, market researchers mapping a category by scent profile, and developers feeding perfume data into a recommender or a RAG index.
What Fragrantica perfume data is publicly available to scrape?
Fragrantica renders the perfume name, brand, note pyramid, main accords, community rating, vote breakdowns, the recommendation lists and user reviews to any visitor who is not signed in. What sits behind a member account is participation — voting, collections and messaging — not the perfume data itself.
| Data category | Public without login | Requires a Fragrantica member account |
|---|---|---|
| Perfume name, brand, description, images | Yes | — |
| Note pyramid: top, middle and base notes | Yes | — |
| Main accords and their strength bars | Yes | — |
| Community rating, vote count, longevity / sillage / price-value breakdowns | Yes | — |
| "People who like this also like" recommendations | Yes, in the static page HTML | — |
| "This perfume reminds me of" recommendations | Yes, but rendered in the browser | — |
| User reviews, reviewer display names and their vote tags | Yes | — |
| Casting a rating, longevity or sillage vote | No | Member account |
| Member wardrobes, collections and private messages | No | Member account |
| Retail prices and where-to-buy links | Not returned by this Actor | — |
Fragrantica.com Scraper With Dupe & Similarity Scoring only returns publicly visible data — what any visitor sees. Nothing behind a login wall.
What data can I extract with Fragrantica.com Scraper With Dupe & Similarity Scoring?
The Actor writes two kinds of row. A parent row is one perfume, carrying 52 top-level keys: identity, notes and accords, community metrics, recommendation lists, reviews and the computed similarity block. A child row is one scored similar perfume, carrying 23 keys, and is written only when includeSimilarChildren is on.
Parent row — perfume fields
| Field name | Description |
|---|---|
type | Always "perfume" on a parent row. The key that separates parents from similar-perfume rows. |
isChild | Always false on a parent row. |
parentId | Always null on a parent row. |
id | Fragrantica's numeric perfume ID, parsed out of the URL and returned as a string (e.g. "9828"). Stable across runs, so it is the join key. |
url | The perfume URL that was requested, with any __cf_chl_tk challenge parameter stripped. |
title | The raw contents of the page <title> tag — Fragrantica writes this as a sentence, e.g. "Aventus Creed cologne - a fragrance for men 2010". It is not a bare perfume name. |
description | The perfume description paragraph, tags stripped and whitespace normalised. |
primaryImageUrl | Main bottle image URL. |
images | Array of gallery image URLs from the photo block, de-duplicated. Often empty. |
brandName | Brand / designer name. |
brandUrl | Brand page URL on Fragrantica. |
brandLogo | Brand logo image URL. |
mainAccords | Array of {accord, color, rgb, hex, value}. value is the width percentage of the accord bar as Fragrantica renders it — the dominant accord is normally 100. color is a human-readable name mapped from the hex, and is null for a hex the Actor has no name for. |
pros | Community "Pros" chips as {label, like, dislike, score, userVote}. score is computed by the Actor as like * 2 - dislike; userVote is always 0. Browser-rendered section. |
cons | Same shape as pros, for the "Cons" chips. |
pyramid | Note pyramid object. {"type": "full", "topNotes": [...], "middleNotes": [...], "baseNotes": [...]} normally; {"type": "single", "allNotes": [...]} for perfumes listed as a flat note set. Each note is {name, img, url}. |
longevityBreakout | Vote counts per longevity bucket as an ordered array of single-key objects: very weak, weak, moderate, long lasting, eternal. |
longevityAverage | Actor-computed mean position on that 1–5 bucket scale, rounded to 4 decimals. Not a figure Fragrantica publishes. |
longevitySum | Total longevity votes. |
longevityMax | Vote count of the largest longevity bucket. |
sillageBreakout | Vote counts per sillage bucket: intimate, moderate, strong, enormous. |
sillageAverage | Actor-computed mean position on that 1–4 scale. |
sillageSum | Total sillage votes. |
sillageMax | Vote count of the largest sillage bucket. |
priceValueBreakout | Vote counts per value bucket: way overpriced, overpriced, ok, good value, great value. |
priceValueAverage | Actor-computed mean position on that 1–5 scale. |
priceValueSum | Total price-value votes. |
priceValueMax | Vote count of the largest price-value bucket. |
perfumeRating | Fragrantica's published aggregate rating, read from the page's ratingValue markup. |
ratingBreakout | Vote counts per rating bucket, ordered hate, dislike, ok, like, love. |
ratingAverage | Actor-computed mean of that 1–5 distribution. Close to perfumeRating but derived independently. |
ratingSum | Total rating votes. Falls back to ratingCount when the breakdown block could not be parsed. |
ratingMax | Vote count of the largest rating bucket. |
bestRating | The top of Fragrantica's rating scale, from the page markup. |
ratingCount | Fragrantica's published number of ratings. |
reviewCount | Number of reviews this run actually collected, not Fragrantica's total review count. null when no reviews were collected. |
gender | "female", "male", "unisex" or null, derived by the Actor from the phrase in title ("for women", "for men", "for women and men"). |
genderBreakout | Object of community gender votes: female, femaleUnisex, unisex, maleUnisex, male. |
genderSum | Total gender votes. |
genderMax | Largest single gender vote count. |
thisPerfumeRemindsMeOf | "This perfume reminds me of" cards as {id, title, brand, gender, url, image}. Browser-rendered, so it is empty when the browser layer did not run. id here is an integer. |
seasonBreakout | Object of when-to-wear votes keyed winter, spring, summer, autumn, day, night. Only the keys Fragrantica rendered are present. |
relationBreakout | Object of ownership votes keyed have, had, want. |
relationSum | Total ownership votes. |
relationMax | Largest single ownership vote count. |
perfumers | Array of perfumer (nose) names linked on the page. |
peopleWhoLikeThisAlsoLike | "People who like this also like" cards, parsed from the static HTML as {url, brand, title, image}. When similarity scoring runs against this list, each card is enriched in place with resolved, accords, notes, neighborRating and the full metric set. |
reviews | Array of {name, published, comment, votes}. See the review fields below. |
similarityGraph | Ranked array of scored neighbours. The core output of this Actor — documented in full below. |
similarResolvedCount | How many neighbours were successfully fetched and scored. 0 when none resolved, null when resolveSimilar is off. |
topDupe | The highest-scoring neighbour, or null. Object of {id, title, brand, url, dupeScore, overlapScore, sharedAccords, sharedNotes}. |
scrapedAt | UTC timestamp of the run, YYYY-MM-DDTHH:MM:SSZ. |
Review fields
Each entry in reviews is {name, published, comment, votes}. name is the reviewer's Fragrantica display name, published is normalised to YYYY-MM-DD where the Actor can parse it, and comment is the review text with tags stripped. votes is a nested object holding the tags attached to that review: rating on a 1–5 scale (hate → love), longevity on 1–5 (very weak → eternal), sillage on 1–4 (intimate → enormous), the six season flags winter, spring, summer, autumn, day, night as 1/0, plus gender and relation as strings or null. A review with neither a name nor a comment is discarded.
Similarity and child-row fields
Each entry in similarityGraph carries rank, id, title, brand, url, image, neighborRating, accords, notes, sharedNotes, sharedAccords, sharedNoteCount, sharedAccordCount, noteJaccard, accordCosine, overlapScore and dupeScore.
Note the naming drift between levels, because it will bite a downstream mapper: a parent row uses brandName, primaryImageUrl and mainAccords, while a similarityGraph entry uses brand, image and accords for the same values. Child rows switch back to brandName, primaryImageUrl and mainAccords. neighborRating uses the American spelling. And accords inside the graph is the reduced {accord, value} shape — the color, rgb and hex keys are dropped there.
A child row (type: "similarPerfume") carries 23 keys: type, isChild, parentId, parentTitle, parentBrand, rank, id, title, brandName, url, primaryImageUrl, perfumeRating, mainAccords, notes, sharedAccords, sharedNotes, sharedAccordCount, sharedNoteCount, noteJaccard, accordCosine, overlapScore, dupeScore and scrapedAt. A child row has no pyramid — its notes arrive flattened into the notes array of lowercased strings, which is exactly the set the score was computed over.
🤖 Add-on: Need additional review and rating data?
This is the Scraper Engine Actor for Fragrantica. If you are comparing community sentiment across catalogues, Amazon Top-Rated Products Scraper applies rating and review-count floors to Amazon search results and ranks survivors with a Bayesian score, and Goodreads Review Scraper With Reviewer Lead Enrichment returns long-form review text with reviewer detail. For app-store sentiment on the same pipeline, see Google Play Store Reviews Scraper and Apple App Store Review Scraper.
How the dupe score is computed
dupeScore is this Actor's own computation, not a Fragrantica figure. Fragrantica publishes recommendation lists; it does not publish a dupe score, a similarity percentage, or any endorsement that two fragrances match. The Actor takes the candidate list Fragrantica renders and measures the overlap between the two perfumes' public note and accord lists itself.
The features used are notes and accords only. Brand is carried through the output but never scored. Launch year is not extracted at all. Rating, gender, perfumer, price and review sentiment play no part in the number.
The pipeline for one parent perfume:
- Collect the recommendation cards named by
similarSource—peopleWhoLikeThisAlsoLike,thisPerfumeRemindsMeOf, or both merged and de-duplicated by URL — and cut the list tomaxSimilarPerItem. - Fetch each neighbour's own perfume page fresh and extract its real
mainAccordsand note pyramid. The bare recommendation card is overwritten in place with that data. - Score the parent against each neighbour.
The note vector is a flat presence set. Every note from topNotes, middleNotes, baseNotes and allNotes is lowercased, trimmed and put into one set. Pyramid position is discarded — a base note and a top note of the same name are the same element, and there is no top/middle/base weighting. The accord vector is a weighted map, {accord name: value}, where value is the accord bar's width percentage as published on the page.
noteJaccard = |A intersect B| / |A union B| over the flat note setsaccordCosine = cosine(Wa, Wb) over accord-weight vectors,union of accord names, missing = 0overlapScore = 0.55 * noteJaccard + 0.45 * accordCosine rounded to 4 decimalsdupeScore = overlapScore * 100 rounded to 1 decimal
The blend is fixed at 55% Jaccard over notes and 45% cosine over accord weights. overlapScore runs 0 to 1, dupeScore runs 0.0 to 100.0. Both are deterministic — no model, no AI, no randomness.
Is the score comparable across runs? The pair score is. Because it is a pure function of two perfumes' published note and accord lists, the same pair scores identically in any run, on any input, so you can accumulate dupeScore values across many runs and compare them directly. What is not stable across runs is which pairs get scored, and therefore rank and topDupe. The candidate set is whatever Fragrantica currently recommends on that page, truncated by your maxSimilarPerItem and filtered by your similarSource. Change either input, or wait for Fragrantica to reshuffle its recommendations, and the same perfume can get a different topDupe — not because any score moved, but because a different set of candidates was measured. Treat rank as "best of what was fetched this run", never as "best dupe that exists". Nothing is scored against a fixed reference catalogue, and parent perfumes are never scored against each other.
When a perfume has too few notes to score. There is no minimum-note threshold and nothing is skipped. If either side's note set is empty, noteJaccard is 0.0; if either side's accord map is empty, accordCosine is 0.0. A perfume with neither parsed therefore scores dupeScore: 0.0 — which is indistinguishable from "measured properly, nothing in common" unless you look at sharedNoteCount, notes and mainAccords on the row. If a neighbour's page fails to fetch after retries, it is excluded from similarityGraph and from the child rows entirely, and its card in peopleWhoLikeThisAlsoLike is flagged "resolved": false. similarResolvedCount counts only the neighbours that were actually scored.
What this number is, and is not. It is a heuristic overlap measure over two public note lists — useful for shortlisting candidates that a human or a downstream model then judges. It does not measure how two fragrances smell: note lists are marketing copy as much as formulation, concentration and materials quality are invisible to it, and two perfumes sharing a note name can share very little in the bottle. No accuracy figure is claimed for it, because none has been measured. Fragrantica does not endorse these pairings, and neither does any brand named in the output.
⚠️ Limits you should know before your first run
An empty API run fails. The schema marks nothing as required, but the Actor has nothing to work with unless one of startUrls or query is set. If both are missing it logs Either 'startUrls' or 'query' must be provided, fails the run with the status message Missing startUrls and query., and exits. It does not fall back to a hidden default search. What masks this in the Console is that startUrls carries a prefill of ["https://www.fragrantica.com/perfume/Creed/Aventus-9828.html"] — a prefill populates the Console form only, it is not a schema default. So a Console user who clicks Start without editing scrapes Creed Aventus and sees a working run, while an API caller who posts {} gets a failed run. Always send startUrls or query explicitly.
maxItems caps parent perfumes only. Child rows are extra. With maxItems: 10 and maxSimilarPerItem: 8, a run can write up to 10 parent rows and up to 80 child rows into the same dataset. Filter on type or isChild before counting anything.
maxSimilarPerItem: 0 means unlimited, not none. Zero disables the cut, so every card the page offers gets fetched and scored. To turn the feature off, set resolveSimilar: false. To keep the scores but suppress the extra rows, set includeSimilarChildren: false — the graph then lives only inside each parent's similarityGraph.
maxItemsReviews only applies when allReviews is on. With allReviews: false the Actor skips review pagination and the per-perfume review cap is internally raised to 10,000, so it keeps whatever reviews are already present in the rendered page rather than the number you set. Set allReviews: true for the cap to mean what it says.
Choosing remindsMeOf depends on the browser layer. peopleWhoLikeThisAlsoLike is parsed from the static HTML and is the reliable source. thisPerfumeRemindsMeOf is Vue-rendered and only exists after the headless-browser pass; if that pass fails on a page, choosing similarSource: "remindsMeOf" yields no candidates and the perfume comes back with similarityGraph: [], similarResolvedCount: 0 and topDupe: null.
The search key is harvested at runtime. Keyword search goes through Fragrantica's own Algolia index, whose secured key rotates. The Actor harvests the live key and app ID from the site on each run; the hardcoded pair in the source is a last-ditch fallback and expired on 2026-05-25. If harvesting fails and the fallback is rejected, search returns nothing, the Actor logs the HTTP status, sets the status message No URLs collected from inputs. and ends the run without writing rows. Passing perfume URLs in startUrls does not touch this path at all.
Why not build this yourself?
Fragrantica publishes no public API, so a DIY scraper is the alternative — and the cost is not the parsing, it is everything around it.
Access comes first. Fragrantica sits behind Cloudflare, and the fingerprint check is the part that breaks scripts: this Actor was moved off its original HTTP engine because that engine's TLS and HTTP/2 fingerprint started returning 403 on both HTTP/2 and HTTP/3, and it now issues every static request through curl_cffi with Chrome impersonation. A challenge page returns HTTP 200 with a "Just a moment" body, so a naive scraper records a successful empty page rather than a block. On top of that you need a proxy ladder, a shared cooldown gate so parallel workers do not retry into the same throttle, and sticky tier selection once something works.
Then the page itself splits in two. Notes, accords, ratings, brand and the "people who like this also like" list are in the static HTML and can be parsed directly. Longevity, sillage, price-value and gender breakdowns, the pros and cons chips, "reminds me of" and the reviews are components that do not exist until a browser renders them, scrolls the page and clicks through "show more" — which is why this Actor ships a headless Chromium pass alongside the HTTP pass.
The note pyramid is its own problem: perfumes come in two shapes, a three-tier pyramid and a flat all-notes list, and a parser that only knows the first silently returns zero notes for the second — which, in a scoring pipeline, becomes a confident 0.0 similarity rather than an error. And keyword search runs on a secured search key that rotates, so any hardcoded key has a short shelf life.
How to use Fragrantica.com Scraper With Dupe & Similarity Scoring
The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API with your Apify token — there is no separate signup and no Fragrantica credential.
- Open the Actor on Apify and click Try for free
- Paste one or more perfume URLs into Perfume URLs or Search URLs (
startUrls), or type a keyword into Search query (query) — nothing is marked required, but the run fails without one of them - Set Maximum number of perfumes (
maxItems) — this counts parent perfumes only - Under Dupe & similarity scoring, leave Resolve & score similar perfumes (
resolveSimilar) on, pick a Similar-perfume source (similarSource) and set Max similar perfumes per item (maxSimilarPerItem) - Decide whether scored neighbours should also become their own rows (
includeSimilarChildren) - Click Start, then export as JSON, CSV, Excel or XML, or read the dataset through the Apify API
The dataset ships with seven prepared views — Overview, Dupes & Similarity, Similar Perfume Rows, Accords, Note Pyramid, Ratings & Breakouts and Reviews — so you can inspect the similarity output without writing a query.
How to scale to bulk perfume extraction
startUrls is a list and every entry is processed in one run. Both plain strings and {"url": "..."} objects are accepted. Entries are classified by shape: a URL containing /perfume/ and ending in .html is scraped directly, a URL containing /search has its query= parameter pulled out and expanded through the catalogue search, and anything else is logged as unsupported and skipped. If your URLs come to fewer than maxItems and query is also set, the search tops the list up to the cap.
Two scoping notes. De-duplication is by exact URL string within a single run, so two URLs pointing at the same perfume with different query strings both get scraped, and nothing is remembered between runs. And a perfume can legitimately appear more than once in one dataset — as a parent row and as a child of another perfume, or as a child of several parents — so de-duplicate on id after the run if you need a unique set.
What can you do with Fragrantica perfume data?
- 🧬 A category manager building a "smells like" shelf sorts child rows by
dupeScoreand keeps the ones wheresharedAccordCountis high, then readssharedNotesto write the on-site comparison copy from the actual overlap rather than from guesswork. - 🛒 A niche retailer sourcing affordable alternatives filters child rows on
dupeScoreabove a threshold andbrandNameoutside the luxury houses, usingperfumeRatingon the same row to drop poorly reviewed candidates before anything reaches a buying decision. - 📊 A market researcher mapping a category runs a keyword search with
resolveSimilar: trueand clusters the results onmainAccordsweights, usingrelationBreakout.wantagainstrelationBreakout.haveto separate aspirational fragrances from owned ones. - 🧪 A product analyst studying performance complaints reads
longevityAverageandsillageAveragealongsidepriceValueAverageto find fragrances the community rates highly on scent but poorly on value. - 🤖 An AI engineer building a fragrance recommender indexes
description,reviews[].commentand the flattenednotesarray into a vector store, keepingdupeScore,noteJaccard,accordCosineandperfumeRatingas retrieval metadata so the agent can justify a recommendation with the shared notes rather than asserting a match.
How does Fragrantica.com Scraper With Dupe & Similarity Scoring handle rate limits and blocking?
Requests are paced by a shared limiter: a minimum 1.5-second interval plus up to 0.8 seconds of jitter between every static fetch, applied run-wide rather than per worker, with perfume pages fetched two at a time and neighbour pages at most four at a time.
Static requests go out through curl_cffi with Chrome impersonation so the TLS and HTTP/2 fingerprint matches a real browser. A response is treated as blocked on HTTP 403, 429 or 503, or when the body contains a Cloudflare challenge marker such as "Just a moment" or cf_chl — the HTTP 200 soft block that otherwise reads as an empty page. On a block the Actor engages a cooldown gate that holds every in-flight worker back (the server's Retry-After if present, otherwise exponential backoff, with a 30-second floor), then escalates the proxy tier.
The proxy ladder is direct → Apify datacenter → Apify residential, with up to three residential rotations, and it sticks with whatever tier works for the rest of the run. Setting proxyConfiguration to an explicit RESIDENTIAL or DATACENTER group locks the tier and disables automatic escalation. The Actor does not solve CAPTCHAs — a challenge page is detected, cooled down and retried from a different tier.
A perfume page gets up to 5 attempts with exponential backoff and jitter; a neighbour page gets up to 3. If a perfume still fails, it is logged as an error and simply produces no row, and the run continues with the rest.
⬇️ Input
Every parameter is optional per the schema. In practice one of startUrls or query must be present or the run fails — see the limits section above.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
startUrls | No | array | Fragrantica perfume URLs or search URLs. Perfume URLs must contain /perfume/ and end in .html; search URLs must contain /search and carry a query= parameter. Plain strings and {"url": "..."} objects both work. Anything else is skipped with a warning. Console prefill is the Creed Aventus page; there is no schema default. | ["https://www.fragrantica.com/perfume/Creed/Aventus-9828.html"] |
query | No | string | Free-text catalogue search, used when no startUrls are given or to top the list up to maxItems. Runs against Fragrantica's search index with a key harvested at runtime. Default "". | "aventus" |
maxItems | No | integer | Cap on top-level perfumes across all inputs. Similar-perfume child rows do not count toward it. Minimum 1, maximum 10000. Default 10. | 5 |
allReviews | No | boolean | Scroll and paginate through the reviews on each perfume page. When off, maxItemsReviews is not applied. Default true. | true |
maxItemsReviews | No | integer | Reviews to keep per perfume, applied only when allReviews is on. Minimum 0, maximum 10000. Default 5. | 2 |
resolveSimilar | No | boolean | Fetch each perfume's recommended fragrances, extract their real accords and notes, and score them. Adds similarityGraph, topDupe and similarResolvedCount. Default true. | true |
similarSource | No | string | Which recommendation list to score: peopleAlsoLike (static HTML, reliable), remindsMeOf (browser-rendered), or both (merged and de-duplicated by URL). Default "peopleAlsoLike". | "peopleAlsoLike" |
maxSimilarPerItem | No | integer | Neighbours fetched and scored per perfume. 0 means unlimited — every card the page offers. Minimum 0, maximum 40. Default 8. | 3 |
includeSimilarChildren | No | boolean | Also push each scored neighbour as its own row (type: "similarPerfume", isChild: true, parentId set). When off, scores live only inside the parent's similarityGraph. Default true. | true |
omitFields | No | array | Top-level keys to drop from every row, parent and child alike. Applied as a plain key removal, so identity keys such as type can be dropped too. Listing reviews also skips review pagination entirely. Default []. | ["images", "reviews"] |
proxyConfiguration | No | object | Apify Proxy settings. Default {"useApifyProxy": false}, which enables the built-in direct → datacenter → residential fallback. Naming an explicit group locks the tier. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Inputs accepted but not in the schema. The Actor merges your input over an internal defaults table that holds more knobs than the Console exposes: concurrency (2), maxRetries (5), requestTimeoutSeconds (30), minRequestIntervalSeconds (1.5), requestJitterSeconds (0.8), retryBackoffSeconds (4.0), rateLimitCooldownSeconds (30.0), useBrowser (true), browserHeadless (true), blockHeavyResources (false), similarConcurrency (3, clamped to 1–4) and debugDumpHtml (false, which saves each rendered page into the key-value store). They work if you post them through the API, but they are unsupported and can change. logLevel is accepted and then never read.
One default mismatch worth knowing. The Actor's internal fallbacks for maxItems and maxItemsReviews are 50 and 20, while the schema defaults shown in the Console are 10 and 5. Send both explicitly if the exact count matters to you.
Example input
{"startUrls": ["https://www.fragrantica.com/perfume/Creed/Aventus-9828.html"],"query": "","maxItems": 5,"allReviews": true,"maxItemsReviews": 2,"resolveSimilar": true,"similarSource": "peopleAlsoLike","maxSimilarPerItem": 3,"includeSimilarChildren": true,"omitFields": [],"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
⬆️ Output
Typed, normalized JSON with a consistent schema across runs: 52 keys on every parent row and 23 on every child row, written to the dataset as each perfume finishes so a long run never loses partial output. Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API. Missing values come back as null or as an empty array rather than as a fabricated 0 — the one exception is dupeScore, which is a real 0.0 when there is nothing to measure.
There are no error rows and no accounting rows. A perfume that fails every retry is a log line only — nothing is written, so there is no errorReason, status or isError marker to filter on, and no diagnostic row inflating your counts. The flip side is that a failure leaves no trace in the dataset, so check the run log when a run returns fewer perfumes than you asked for.
The only row-type split is parent versus child. Parent rows are pushed under the row_result event; scored neighbour rows are pushed with no event name attached, so only parent perfumes are counted as result events. Separate them with type or isChild:
parents = [r for r in rows if r.get("type") == "perfume"]children = [r for r in rows if r.get("isChild")]
One type caveat when you join the two: id is a string on parent rows, because it is parsed out of the URL, but a scored neighbour that came from the "reminds me of" list carries an integer id taken from the card. Cast before joining parentId to id.
Example output
Parent row:
{"type": "perfume","isChild": false,"parentId": null,"id": "9828","url": "https://www.fragrantica.com/perfume/Creed/Aventus-9828.html","title": "Aventus Creed cologne - a fragrance for men 2010","description": "Aventus by Creed is a Chypre Fruity fragrance for men. Aventus was launched in 2010. Top notes are Bergamot, Blackcurrant, Apple and Pineapple; middle notes are Birch, Patchouli, Moroccan Jasmine and Rose; base notes are Musk, Oakmoss, Ambergris and Vanilla.","primaryImageUrl": "https://fimgs.net/mdimg/perfume-thumbs/375x500.9828.jpg","images": ["https://fimgs.net/mdimg/perfume-social-cards/en-p_c_9828.jpeg","https://fimgs.net/mdimg/secundar/o.14522.jpg"],"brandName": "Creed","brandUrl": "https://www.fragrantica.com/designers/Creed.html","brandLogo": "https://fimgs.net/mdimg/dizajneri/m.86.jpg","mainAccords": [{ "accord": "fruity", "color": "Pale Red", "rgb": "rgb(242, 114, 115)", "hex": "#f27273", "value": 100 },{ "accord": "woody", "color": "Dark Brown", "rgb": "rgb(119, 68, 20)", "hex": "#774414", "value": 78.412 },{ "accord": "fresh", "color": "Sushi", "rgb": "rgb(145, 176, 70)", "hex": "#91b046", "value": 64.907 },{ "accord": "smoky", "color": "Cocoa Brown", "rgb": "rgb(59, 45, 41)", "hex": "#3b2d29", "value": 41.203 }],"pros": [{ "label": "Excellent projection", "like": 412, "dislike": 18, "score": 806, "userVote": 0 }],"cons": [{ "label": "Batch variation", "like": 297, "dislike": 44, "score": 550, "userVote": 0 }],"pyramid": {"type": "full","topNotes": [{ "name": "Bergamot", "img": "https://fimgs.net/mdimg/sastojci/t.75.jpg", "url": "https://www.fragrantica.com/notes/Bergamot-75.html" },{ "name": "Blackcurrant", "img": "https://fimgs.net/mdimg/sastojci/t.132.jpg", "url": "https://www.fragrantica.com/notes/Black-Currant-132.html" },{ "name": "Apple", "img": "https://fimgs.net/mdimg/sastojci/t.146.jpg", "url": "https://www.fragrantica.com/notes/Apple-146.html" },{ "name": "Pineapple", "img": "https://fimgs.net/mdimg/sastojci/t.170.jpg", "url": "https://www.fragrantica.com/notes/Pineapple-170.html" }],"middleNotes": [{ "name": "Birch", "img": "https://fimgs.net/mdimg/sastojci/t.202.jpg", "url": "https://www.fragrantica.com/notes/Birch-202.html" },{ "name": "Patchouli", "img": "https://fimgs.net/mdimg/sastojci/t.83.jpg", "url": "https://www.fragrantica.com/notes/Patchouli-83.html" },{ "name": "Moroccan Jasmine", "img": "https://fimgs.net/mdimg/sastojci/t.412.jpg", "url": "https://www.fragrantica.com/notes/Jasmine-412.html" },{ "name": "Rose", "img": "https://fimgs.net/mdimg/sastojci/t.6.jpg", "url": "https://www.fragrantica.com/notes/Rose-6.html" }],"baseNotes": [{ "name": "Musk", "img": "https://fimgs.net/mdimg/sastojci/t.4.jpg", "url": "https://www.fragrantica.com/notes/Musk-4.html" },{ "name": "Oakmoss", "img": "https://fimgs.net/mdimg/sastojci/t.24.jpg", "url": "https://www.fragrantica.com/notes/Oakmoss-24.html" },{ "name": "Ambergris", "img": "https://fimgs.net/mdimg/sastojci/t.51.jpg", "url": "https://www.fragrantica.com/notes/Ambergris-51.html" },{ "name": "Vanilla", "img": "https://fimgs.net/mdimg/sastojci/t.10.jpg", "url": "https://www.fragrantica.com/notes/Vanilla-10.html" }]},"longevityBreakout": [{ "very weak": 120 }, { "weak": 210 }, { "moderate": 640 }, { "long lasting": 2100 }, { "eternal": 1400 }],"longevityAverage": 3.9955,"longevitySum": 4470,"longevityMax": 2100,"sillageBreakout": [{ "intimate": 300 }, { "moderate": 900 }, { "strong": 1600 }, { "enormous": 700 }],"sillageAverage": 2.7714,"sillageSum": 3500,"sillageMax": 1600,"priceValueBreakout": [{ "way overpriced": 400 }, { "overpriced": 900 }, { "ok": 700 }, { "good value": 500 }, { "great value": 600 }],"priceValueAverage": 3.0,"priceValueSum": 3100,"priceValueMax": 900,"perfumeRating": 4.34,"ratingBreakout": [{ "hate": 300 }, { "dislike": 500 }, { "ok": 900 }, { "like": 3200 }, { "love": 6800 }],"ratingAverage": 4.3419,"ratingSum": 11700,"ratingMax": 6800,"bestRating": 5,"ratingCount": 11700,"reviewCount": 2,"gender": "male","genderBreakout": {"female": 200,"femaleUnisex": 150,"unisex": 900,"maleUnisex": 1800,"male": 5200},"genderSum": 8250,"genderMax": 5200,"thisPerfumeRemindsMeOf": [],"seasonBreakout": {"winter": 4100,"spring": 3200,"summer": 2900,"autumn": 4300,"day": 5100,"night": 4600},"relationBreakout": { "have": 9800, "had": 2400, "want": 14500 },"relationSum": 26700,"relationMax": 14500,"perfumers": ["Olivier Creed", "Erwin Creed"],"peopleWhoLikeThisAlsoLike": [{"url": "https://www.fragrantica.com/perfume/Armaf/Club-de-Nuit-Intense-Man-30754.html","brand": "Armaf","title": "Club de Nuit Intense Man","image": "https://fimgs.net/mdimg/perfume-thumbs/s.30754.jpg","resolved": true,"accords": [{ "accord": "fruity", "value": 100 },{ "accord": "woody", "value": 82.115 },{ "accord": "smoky", "value": 55.44 }],"notes": ["ambergris", "apple", "bergamot", "birch", "black currant", "jasmine", "lemon", "musk", "patchouli", "pineapple", "rose", "vanilla"],"neighborRating": 4.02,"sharedNotes": ["ambergris", "apple", "bergamot", "birch", "musk", "patchouli", "pineapple"],"sharedAccords": ["fruity", "smoky", "woody"],"sharedNoteCount": 7,"sharedAccordCount": 3,"noteJaccard": 0.4118,"accordCosine": 0.9312,"overlapScore": 0.6455,"dupeScore": 64.6}],"reviews": [{"name": "notesandnoses","published": "2026-05-18","comment": "Smoky pineapple over birch, exactly as advertised. Projection is strong for the first two hours and then it settles close to the skin.","votes": {"rating": 5,"winter": 0,"spring": 1,"summer": 0,"autumn": 0,"day": 1,"night": 0,"longevity": 4,"sillage": 3,"gender": "male","relation": "have"}},{"name": "sillagechaser","published": "2026-03-02","comment": "Batch dependent. My 2023 bottle is noticeably smokier than the one I finished last year.","votes": {"rating": 4,"winter": 1,"spring": 0,"summer": 0,"autumn": 1,"day": 0,"night": 1,"longevity": 3,"sillage": 2,"gender": null,"relation": "had"}}],"similarityGraph": [{"id": "30754","title": "Club de Nuit Intense Man","brand": "Armaf","url": "https://www.fragrantica.com/perfume/Armaf/Club-de-Nuit-Intense-Man-30754.html","image": "https://fimgs.net/mdimg/perfume-thumbs/s.30754.jpg","neighborRating": 4.02,"accords": [{ "accord": "fruity", "value": 100 },{ "accord": "woody", "value": 82.115 },{ "accord": "smoky", "value": 55.44 }],"notes": ["ambergris", "apple", "bergamot", "birch", "black currant", "jasmine", "lemon", "musk", "patchouli", "pineapple", "rose", "vanilla"],"sharedNotes": ["ambergris", "apple", "bergamot", "birch", "musk", "patchouli", "pineapple"],"sharedAccords": ["fruity", "smoky", "woody"],"sharedNoteCount": 7,"sharedAccordCount": 3,"noteJaccard": 0.4118,"accordCosine": 0.9312,"overlapScore": 0.6455,"dupeScore": 64.6,"rank": 1},{"id": "53169","title": "Explorer","brand": "Montblanc","url": "https://www.fragrantica.com/perfume/Montblanc/Explorer-53169.html","image": "https://fimgs.net/mdimg/perfume-thumbs/s.53169.jpg","neighborRating": 4.11,"accords": [{ "accord": "woody", "value": 100 },{ "accord": "fruity", "value": 71.508 },{ "accord": "fresh", "value": 60.22 }],"notes": ["ambergris", "bergamot", "clary sage", "leather", "patchouli", "pink pepper", "vanilla", "vetiver"],"sharedNotes": ["ambergris", "bergamot", "patchouli", "vanilla"],"sharedAccords": ["fresh", "fruity", "woody"],"sharedNoteCount": 4,"sharedAccordCount": 3,"noteJaccard": 0.3333,"accordCosine": 0.7208,"overlapScore": 0.5077,"dupeScore": 50.8,"rank": 2},{"id": "31861","title": "Sauvage","brand": "Dior","url": "https://www.fragrantica.com/perfume/Christian-Dior/Sauvage-31861.html","image": "https://fimgs.net/mdimg/perfume-thumbs/s.31861.jpg","neighborRating": 4.07,"accords": [{ "accord": "fresh spicy", "value": 100 },{ "accord": "woody", "value": 74.63 },{ "accord": "fruity", "value": 48.91 }],"notes": ["ambroxan", "bergamot", "cedar", "elemi", "geranium", "lavender", "patchouli", "pepper", "vetiver"],"sharedNotes": ["bergamot", "patchouli"],"sharedAccords": ["fruity", "woody"],"sharedNoteCount": 2,"sharedAccordCount": 2,"noteJaccard": 0.24,"accordCosine": 0.69,"overlapScore": 0.4425,"dupeScore": 44.3,"rank": 3}],"similarResolvedCount": 3,"topDupe": {"id": "30754","title": "Club de Nuit Intense Man","brand": "Armaf","url": "https://www.fragrantica.com/perfume/Armaf/Club-de-Nuit-Intense-Man-30754.html","dupeScore": 64.6,"overlapScore": 0.6455,"sharedAccords": ["fruity", "smoky", "woody"],"sharedNotes": ["ambergris", "apple", "bergamot", "birch", "musk", "patchouli", "pineapple"]},"scrapedAt": "2026-07-25T09:14:02Z"}
Child row, written when includeSimilarChildren is on:
{"type": "similarPerfume","isChild": true,"parentId": "9828","parentTitle": "Aventus Creed cologne - a fragrance for men 2010","parentBrand": "Creed","rank": 1,"id": "30754","title": "Club de Nuit Intense Man","brandName": "Armaf","url": "https://www.fragrantica.com/perfume/Armaf/Club-de-Nuit-Intense-Man-30754.html","primaryImageUrl": "https://fimgs.net/mdimg/perfume-thumbs/s.30754.jpg","perfumeRating": 4.02,"mainAccords": [{ "accord": "fruity", "value": 100 },{ "accord": "woody", "value": 82.115 },{ "accord": "smoky", "value": 55.44 }],"notes": ["ambergris", "apple", "bergamot", "birch", "black currant", "jasmine", "lemon", "musk", "patchouli", "pineapple", "rose", "vanilla"],"sharedAccords": ["fruity", "smoky", "woody"],"sharedNotes": ["ambergris", "apple", "bergamot", "birch", "musk", "patchouli", "pineapple"],"sharedAccordCount": 3,"sharedNoteCount": 7,"noteJaccard": 0.4118,"accordCosine": 0.9312,"overlapScore": 0.6455,"dupeScore": 64.6,"scrapedAt": "2026-07-25T09:14:02Z"}
How does it work?
Each run builds its URL list first — perfume URLs are taken as given, search URLs and query are expanded through Fragrantica's own catalogue search index using a key harvested from the site at runtime, and the list is trimmed to maxItems.
Every perfume is then read twice over. First a static HTTP request through curl_cffi with Chrome impersonation, which yields the notes, accords, brand, rating and the "people who like this also like" list. Then, if the browser layer is on, a headless Chromium pass loads the same page, scrolls it, expands the reviews and reads the dynamically rendered sections — longevity, sillage, price-value and gender breakdowns, pros and cons, "reminds me of" and the reviews themselves. The two results are merged, with rendered values only overwriting a static field when they are non-empty.
If similarity scoring is on, each recommended fragrance is fetched the same way and scored locally. Rows are pushed to the dataset as each perfume completes, so a run that stops early keeps everything already collected. Only publicly visible data is touched — no login, no cookie jar. Field names are held stable across Actor updates, so an integration reading pyramid, mainAccords or dupeScore keeps working when Fragrantica changes its front end.
Integrations
The Actor works with what you already use: call it from your own code through the Apify API, wire it into a no-code automation, or schedule it and receive results by webhook.
Calling the Actor programmatically
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/fragrantica-com-scraper-with-dupe-similarity-scoring").call(run_input={"startUrls": ["https://www.fragrantica.com/perfume/Creed/Aventus-9828.html"],"maxItems": 5,"allReviews": True,"maxItemsReviews": 2,"resolveSimilar": True,"similarSource": "peopleAlsoLike","maxSimilarPerItem": 3,"includeSimilarChildren": True,})rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())parents = [r for r in rows if r.get("type") == "perfume"]for p in parents:top = p.get("topDupe") or {}print(p["title"], p["perfumeRating"], top.get("title"), top.get("dupeScore"))
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request.
No-code tools (n8n, Make, LangChain)
In n8n, use the HTTP Request node pointed at the Actor's run endpoint, then a second node against the dataset endpoint to pull the rows; filter on type before the data reaches a spreadsheet or database node so child rows do not double your counts. In Make, the Apify app's Run an Actor module takes the same input JSON and hands the dataset to downstream modules unchanged. In LangChain or any agent framework, wrap the run-and-fetch call as a tool — rows are typed JSON with stable keys, so description, reviews[].comment and the flattened notes array can be chunked into a vector store with no HTML-stripping step.
Is it legal to scrape Fragrantica perfume data?
Collecting publicly rendered product records — perfume names, brands, note pyramids, accords and aggregate community ratings — is broadly treated as permissible where no authentication is bypassed, and this Actor collects only that.
Keep the questions apart. Terms of service: Fragrantica's terms restrict automated access. That is a contractual matter, distinct from legality, and it is a real consideration. Database rights: the EU sui generis database right and comparable regimes elsewhere can apply if you systematically reproduce a substantial part of the catalogue as a competing encyclopaedia. Data protection: the default output is product data. Perfume names, brands, note lists, accord weights and vote tallies are product records and aggregate figures, not personal data — GDPR and equivalent personal-data regimes do not attach to that output the way they would to a profile scraper.
One narrow exception. When reviews are collected, reviews[].name is a reviewer's display name and reviews[].comment is text an individual wrote, which can contain details they chose to disclose; the attached reviews[].votes object is that individual's own voting record. That is personal data. If you store it for EU or UK data subjects you need a lawful basis, and minimisation applies — drop the field with omitFields: ["reviews"] if your analysis does not need it, or strip name at ingestion and keep comment and votes.
One further point specific to this Actor's output: perfume names and brand names are registered trademarks, and publishing a "dupe" claim commercially raises trademark and comparative-advertising questions that are separate from the question of whether the data may be collected. Consult legal counsel if your use case involves bulk storage of personal data or publishing similarity claims about named products.
❓ Frequently asked questions
What Fragrantica fields does this Actor return?
The five you will use most are title, brandName, pyramid, mainAccords and perfumeRating, plus dupeScore and topDupe from the similarity layer. Fifty-two keys are written on every parent perfume row and 23 on every scored similar-perfume row. See the full list in the data fields section above.
Is the dupe score published by Fragrantica?
No. Fragrantica publishes recommendation lists; the score is computed by this Actor from the two perfumes' public note and accord lists — a Jaccard overlap on the flattened note sets blended 55/45 with a cosine similarity on the accord-weight vectors. It is a heuristic for shortlisting, not a verdict, and neither Fragrantica nor any brand endorses the pairings it produces.
Does the Actor require a Fragrantica account or login?
No. No account, no login, no cookie. The only credential is your Apify token. Every field returned is visible to an anonymous visitor.
How many perfumes can I extract in one run?
maxItems accepts 1 to 10000 and caps parent perfumes only. Child rows are additional: with includeSimilarChildren on, a run can write up to maxItems × maxSimilarPerItem extra rows on top. Setting maxSimilarPerItem to 0 removes that cap entirely and fetches every card the page offers, so raise it deliberately.
What happens if a perfume has no notes listed, or its page is unavailable?
Two different outcomes. If the page loads but the note pyramid cannot be parsed, the row is still written with pyramid holding empty arrays — and because the note set is then empty, noteJaccard comes back 0.0 for every comparison, capping dupeScore at 45 even where the accords match perfectly. Check sharedNoteCount and the notes array before trusting a low score. If the page cannot be fetched at all after retries, the perfume produces no row, no error marker and no partial record — just an error line in the run log. When it is a neighbour page that fails, the neighbour is dropped from similarityGraph and similarResolvedCount, and its card in peopleWhoLikeThisAlsoLike is flagged "resolved": false.
Can I scrape multiple perfumes at once?
Yes. startUrls is a list and every entry is processed in one run, mixing perfume URLs and search URLs freely, and query tops the list up to maxItems if the URLs come to fewer. De-duplication is by exact URL string within a run only — the same perfume can also appear as a child row under several parents, so de-duplicate on id if you need a unique set.
Does it work with Claude, ChatGPT, and other AI agent tools?
Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a custom tool definition can invoke it and receive typed JSON. There is no separate agent surface to configure — one API call in, structured rows out.
How does this compare to other Fragrantica scrapers?
Checked on the Apify Store on 25 July 2026: lexis-solutions/fragrantica is the most-used Fragrantica Actor and the closest in shape — its listing documents the same perfume detail fields including pyramid, mainAccords with colour metadata, the vote breakouts, peopleWhoLikeThisAlsoLike and reviews, and it advises using residential proxies to avoid blocking. shahidirfan/fragrantica-com-scraper works from designer pages rather than perfume pages, returns one flat snake_case row per perfume with popularity and pageview signals plus locale-flattened url_* and reviews_* fields, and takes a single startUrl with results_wanted. Neither listing documents a similarity or dupe score, a fresh fetch of each recommended fragrance, or child rows — capabilities not stated on a competitor's listing are described here as "not documented" rather than assumed absent. What this Actor adds on top of the shared field set is that the recommendation lists stop being bare cards: each neighbour is fetched for its real accords and notes and scored against the parent.
Does it return data in a format LLMs can use directly?
Yes. Typed, normalized JSON with consistent field names across runs. No HTML parsing, no selectors. description, reviews[].comment and the flattened notes array go straight into a context window or a vector store, with dupeScore and perfumeRating usable as retrieval metadata.
What happens when Fragrantica changes its layout or anti-bot system?
The Actor is maintained and the output field names are held stable, so an integration reading pyramid, mainAccords or dupeScore keeps working through front-end changes. This has already been exercised twice: the HTTP engine was replaced when Fragrantica's Cloudflare configuration began rejecting the original client's TLS fingerprint, and the catalogue search key was moved from a hardcoded value to a runtime harvest after the old key expired — both without changing the output shape.
Which Fragrantica fields work best for AI training data and RAG indexing?
For RAG, the high-information text fields are description, reviews[].comment, the note names inside pyramid, and topDupe.sharedNotes — all clean text needing no HTML stripping. For training data, the structurally consistent fields across every record are perfumeRating, ratingCount, longevityAverage, sillageAverage, priceValueAverage, the mainAccords weight vector and dupeScore. All arrive as typed primitives, arrays or nested objects. Use id as the record key, and remember it is a string on parent rows.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Amazon Top-Rated Products Scraper | Products screened by star rating and review count, re-ranked with a Bayesian weighted rating |
| Amazon Product Scraper By Bestseller & Demand Filter | Product data with monthly-purchase counts and Best Sellers Rank |
| Goodreads Review Scraper With Reviewer Lead Enrichment | Book reviews, ratings and enriched reviewer detail |
| Google Play Store Reviews Scraper | Android app review text, ratings and app versions |
| Apple App Store Review Scraper | iOS review text, ratings and rating summaries |
| Google Maps Scraper With Reviews Sentiment Analysis | Place listings with review text and sentiment scoring |
💬 Your feedback
Found a bug, or missing a field you need on the row? Open an issue from the Actor's page in the Apify Console — include the run URL and the input you used, and it goes straight to the Scraper Engine maintainers. Field requests are welcome, and so are reports of a Fragrantica markup change that broke an extraction or a scoring case that looks wrong.