Fragrantica.com Scraper — Reviews & Sentiment Analysis
Pricing
from $4.99 / 1,000 results
Fragrantica.com Scraper — Reviews & Sentiment Analysis
Scrape Fragrantica perfume reviews, ratings, fragrance details, notes, and reviewer insights. Analyze review text and sentiment to identify customer opinions, trends, strengths, and weaknesses for perfume research, product analysis, and market intelligence.
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Fragrantica Scraper — Perfumes, Notes, Reviews and Sentiment JSON
The Fragrantica scraper extracts perfume records, note pyramids, community vote breakouts and user reviews from Fragrantica.com, then computes review sentiment locally — per-review label and score, aspect sentiment for longevity, sillage, scent, value and packaging, note-mention mining, and a per-perfume rollup. Every run returns typed, normalized JSON — no HTML, no selectors, no parsing step. Sentiment comes from a deterministic lexicon engine that needs no API key, so the same reviews always produce the same numbers. By the end of this page you will know every input, every output key, and exactly how the score is derived.
What is the Fragrantica.com Scraper — Reviews & Sentiment Analysis?
The Fragrantica.com Scraper — Reviews & Sentiment Analysis is an Apify Actor that turns public Fragrantica perfume pages into structured datasets. It returns four structurally distinct things per run: the perfume record (brand, description, images, main accords, note pyramid, perfumers), the community vote breakouts (longevity, sillage, price value, rating, gender, seasonality, ownership), the user reviews (author, date, comment text, structured vote tags), and a computed sentiment layer attached to each review and to the perfume as a whole.
No Fragrantica account, login, cookie or API key is required — the Actor reads only publicly served pages. A key is needed only if you switch on the optional AI enrichment layer, and even then it is your own, passed in the masked aiApiKey field or read from a provider environment variable.
- Scrape perfumes —
title,brandName,description,mainAccords,pyramid,perfumers,images, pluspeopleWhoLikeThisAlsoLikeandthisPerfumeRemindsMeOf - Scrape community vote breakouts —
longevityBreakout,sillageBreakout,priceValueBreakout,ratingBreakout,genderBreakout,seasonBreakout,relationBreakout, plusprosandcons - Scrape user reviews —
reviews[]withname,published,commentand an 11-keyvotesobject - Compute review sentiment —
sentiment,aspectsandmentionedNotesper review, plus areviewSentimentrollup per perfume - Export as JSON, CSV, Excel or XML from the Apify dataset; no proxy management and no HTML parsing on your side
What data does the Fragrantica Scraper collect?
One dataset item is pushed per perfume. Inside it sit seven distinct data types sharing one stable schema — the perfume record, notes and accords, ratings and vote breakouts, audience votes, reviews, computed sentiment, and related perfumes.
| Data Type | Key Fields | JSON Field Names |
|---|---|---|
| Perfume record | Perfume ID, page title, description, brand, hero image, gallery, gender read from the title | id, url, title, description, brandName, brandUrl, brandLogo, primaryImageUrl, images, gender, perfumers |
| Notes & accords | Main accords with colour and strength, top/middle/base note pyramid with note images | mainAccords (accord, color, rgb, hex, value), pyramid (type, topNotes, middleNotes, baseNotes, allNotes) |
| Ratings & vote breakouts | Aggregate rating plus vote buckets and derived average/sum/max for rating, longevity, sillage and price value | perfumeRating, bestRating, ratingCount, and ratingBreakout / ratingAverage / ratingSum / ratingMax — same four-key pattern for longevity*, sillage* and priceValue* |
| Audience & occasion votes | Gender perception split, seasonality and day/night votes, ownership intent, community pros and cons | genderBreakout, genderSum, genderMax, seasonBreakout, relationBreakout, relationSum, relationMax, pros, cons |
| User reviews | Reviewer name, publish date, full comment text, structured vote tags left with the review | reviews[] → name, published, comment, votes (rating, winter, spring, summer, autumn, day, night, longevity, sillage, gender, relation), reviewCount |
| Computed sentiment | Per-review label/score/confidence, aspect sentiment, note-level praise vs criticism, per-perfume rollup | reviews[].sentiment, reviews[].aspects, reviews[].mentionedNotes, reviewSentiment (overall, reviewsAnalyzed, distribution, byAspect, mentionedNotes, topPraisedNotes, topCriticizedNotes) |
| Related perfumes | Recommendation cards from two different Fragrantica modules | peopleWhoLikeThisAlsoLike (url, brand, title, image), thisPerfumeRemindsMeOf (id, title, brand, gender, url, image) |
The dataset ships with seven prebuilt views — Overview, Review Sentiment, Reviews, Accords, Note Pyramid, Ratings & Breakouts and Similar Perfumes — so you can flip between them in the Console without exporting. Views are display filters only; the full item always contains every key above.
Need more review and sentiment data?
If you are building a cross-platform voice-of-customer dataset, API Empire publishes review-and-sentiment Actors for other catalogs on Apify: a Goodreads review scraper with rating-distribution analytics, an Apple App Store review scraper with version quality trends, and a Google Play Store review scraper split by country and language. They emit the same style of typed review rows, so one downstream pipeline can ingest perfume, book and app reviews without per-source parsing code.
🧠 How is review sentiment computed?
Sentiment is computed inside the Actor — it is not scraped from Fragrantica. Fragrantica does not publish sentiment labels, so src/sentiment.py derives them from the review text and the review's own vote tags. The engine is a lexicon-and-rules scorer, not a machine-learning model: pure Python, no network call, no API key, no randomness. Given the same review text the output is byte-for-byte identical on every run.
The scoring pipeline
- Phrase collapsing. Before tokenizing, 14 multi-word expressions are rewritten to single tokens so they score as units —
long lasting/long-lasting/all day→long_lasting,worth every penny/worth it→worth,waste of money→waste,not worth/over-priced→overpriced,not good→bad,beast mode→beast. - Tokenizing. Text is lowercased and split on
[a-z_']+, so punctuation, digits and non-Latin characters are dropped. - Lexicon lookup. Each token is matched against a fragrance-tuned positive list (~90 entries) and negative list (~70 entries). Entries carry weight 2 = strong (
love,masterpiece,flawless,hate,disgusting,overpriced,waste) or 1 = normal (nice,elegant,versatile,harsh,synthetic,faint). - Negation. The three tokens before each hit are scanned for one of 22 negators (
not,no,never,hardly,barely,without, the contraction formsdont/isnt/wasnt/cant,lacks,lacking, …). A negator flips the polarity of that hit. - Intensification. The same three-token lookback also applies the strongest intensifier found:
quite×1.2,too×1.3,really/so×1.4,very/super×1.5,utterly×1.6,incredibly/insanely×1.7,extremely/absolutely×1.8. - Text score. Positive and negative contributions are summed, then
score = (positive − negative) / (positive + negative), rounded to 4 decimals. The scale is −1.0 to +1.0. A review with no lexicon hits scores0.0. - Vote blending. Fragrantica reviews carry structured vote tags. The
ratingtag (1 = hate … 5 = love) is mapped onto the same −1…+1 scale by(value − mid) / (high − mid). When a review has both text hits and a rating vote, the final score is 0.6 × text + 0.4 × vote. With only text hits, the text score is used; with only a vote, the vote score is used. - Labeling. The neutral band is ±0.15:
score > 0.15→positive,score < −0.15→negative, everything in between →neutral. - Confidence.
confidence = min(0.6, lexicon_hits × 0.2) + 0.4 if a rating vote exists, capped at1.0. A one-word review with no vote lands at0.2; a detailed review with a vote reaches1.0.
Aspect sentiment
Five aspects are scored independently: longevity, sillage, scent, value, packaging. Each has its own keyword set (sillage, for example, matches sillage, projection, projects, trail, wafts, beast, throw, presence, intimate, enormous, diffusion, cloud). Every keyword occurrence becomes a window centre, the same lexicon scorer runs over a ±6-token window around it, and window scores are averaged. For longevity and sillage only, the review's own longevity vote (1–5) and sillage vote (1–4) are blended in at 0.6 text / 0.4 vote. An aspect with neither text evidence nor a vote is omitted rather than reported as neutral.
Note-mention mining
The note vocabulary per perfume is that perfume's own pyramid (each note tagged top, middle, base, or unspecified) plus a base lexicon of ~110 generic fragrance notes — vanilla, oud, rose, patchouli, tonka, oakmoss, ambroxan, iso e super, saffron and so on — with perfume-specific notes winning their tier label. For each note found in a review, the scorer runs a ±5-token window around every mention and averages, producing { note, tier, label, score, mentions }. The rollup then counts how many reviews praised versus criticized each note and ranks them into topPraisedNotes / topCriticizedNotes (top 5 each).
What the engine does not do — read this before trusting the numbers
- No sarcasm or irony detection. "Great, another blue fragrance" scores positive. There is no clause-level or contrastive parsing.
- English-only lexicon. A non-English review yields zero lexicon hits, so its text score is
0.0; with a rating vote the sentiment falls back entirely to that vote, and without one the review landsneutralatconfidence: 0.0. Filter onsentiment.confidencebefore aggregating. - Perfume jargon is ambiguous, and the lexicon takes a side. Descriptive words are scored as evaluative:
sweet,fresh,clean,warm,rich,cozycount positive;sharp,synthetic,chemical,cheap,faint,plasticcount negative. "A sharp synthetic aldehyde opening" scores negative even when the reviewer meant it as praise. (powderysits in the negative list at weight0, so it is inert.) longandlastingalso score separately as weight-1 positives, so "it does not last long" produces one negated and one non-negated hit from the same clause.- The rollup is an unweighted mean.
reviewSentiment.overall.scoreaverages per-review scores without weighting by confidence or length, so twenty low-confidence one-liners move it as much as twenty essays.
Optional AI enrichment (off by default)
Setting aiEnhancement to true layers an LLM on top of the deterministic engine — it never replaces it. Reviews go out in batches of 20, each comment truncated to 1,500 characters, and the model returns aiSentiment (positive/neutral/negative), aiSentimentScore (−1.0…1.0), aiThemes (2–5 lowercase tags) and a one-sentence aiSummary. The provider is inferred from the model name: claude-* uses the Anthropic SDK, everything else an OpenAI-compatible chat.completions endpoint (OpenAI, Gemini, xAI, DeepSeek, Perplexity, Mistral). Any failure — missing key, bad key, malformed JSON, provider outage — leaves the four AI fields null. They are never guessed and never back-filled from the deterministic score.
Why not build this yourself?
Fragrantica publishes no public developer API — no documented endpoint, no OAuth application, no rate-limit contract, no versioned schema. Every field in this Actor comes from parsing the public site, which leaves a DIY scraper owning three moving problems permanently: Cloudflare (a plain requests call gets a challenge interstitial, not HTML), client-side rendering (the perfume page hydrates with Vue and replaces the server markup that carries the pyramid, the accord bars and the review cards), and rotating search credentials (the catalog search key is a secured Algolia key with an embedded expiry — when it lapses, search returns nothing, silently).
This Actor absorbs all three: a Chrome-impersonating TLS client with Cloudflare challenge detection, sticky proxy escalation that only fires on real blocks, a Chromium pass that earns the clearance cookie and re-fetches the raw server HTML same-origin, and a per-run Algolia key harvest with a last-known fallback — each detailed under What changed in Fragrantica scraping recently below.
Rebuilding all three, then keeping them alive through frontend redeploys, is the real cost — not the parser.
Why do developers and teams scrape Fragrantica?
Fragrantica is the largest public fragrance encyclopedia, and its value to data teams is that opinion and composition sit on the same page: a note pyramid next to thousands of votes next to free-text reviews. Four groups use that combination in very different ways.
For AI engineers and agent builders
Perfume reviews are ideal RAG material because each chunk arrives pre-labelled. Index reviews[].comment into a vector store with sentiment.label, sentiment.confidence, aspects and mentionedNotes as metadata, and a query like "why do people dislike this fragrance's longevity" can filter to aspects.longevity.label == "negative" before it ever hits the LLM. reviewSentiment.topCriticizedNotes gives an agent a one-hop answer without reading a single review, and because the scores are deterministic, re-indexing never drifts your labels away from your embeddings.
For fragrance brands and marketing teams
Track how a launch is actually received rather than how its rating trends. Run the Actor across your own SKUs and a competitor set, then compare reviewSentiment.byAspect row by row: a perfume can hold a strong perfumeRating while byAspect.longevity.score sits negative — a reformulation complaint, not a scent complaint. topPraisedNotes and topCriticizedNotes name which accord is carrying the product and which one reviewers keep flagging, in their own vocabulary, with pros and cons adding the community's upvoted tags alongside.
For researchers and analysts
Everything returned is publicly visible on the page — no login, no private profile, no gated content. That makes Fragrantica a workable corpus for consumer-perception research: genderBreakout gives the perceived-gender split, seasonBreakout the seasonality and day/night votes, relationBreakout have/had/want ownership intent, and longevityAverage / sillageAverage / priceValueAverage convert vote buckets into comparable scalars. Pair those votes with the computed aspect sentiment to test whether stated votes and written opinion actually agree.
For developers building data products
The output is a stable, flat-topped JSON document per perfume, so it maps cleanly onto a table or a document store. Build a recommendation engine on mainAccords and pyramid similarity, a price-perception tracker on priceValueBreakout, or a discovery graph on peopleWhoLikeThisAlsoLike and thisPerfumeRemindsMeOf — both carry canonical perfume URLs you can feed straight back in as startUrls. Schedule the run and diff reviewSentiment between snapshots to alert on reception shifts.
🚀 How to scrape Fragrantica (step by step)
This Actor runs on the Apify platform. You start it from the Apify Console or by calling the Apify API with your Apify API token. There is no separate signup, no vendor key and no self-hosted binary.
- Open the Actor on its Apify Store listing and click Try for free, or open it from your Console if you have already added it.
- Provide input. Nothing is strictly required by the schema, but the run needs something to scrape: put one or more Fragrantica perfume URLs (or search URLs) into
startUrls, or type a search term intoquery. If both are empty the run fails immediately withMissing startUrls and query. - Set the caps and the sentiment switches.
maxItemscaps perfumes (default10),maxItemsReviewscaps reviews per perfume (default20),allReviewscontrols review pagination (defaulttrue), andanalyzeSentimentcontrols the deterministic engine (defaulttrue). LeaveaiEnhancementoff unless you have a provider key. - Start the run. Each perfume is pushed to the dataset the moment it finishes, so partial results survive an interrupted run. The log reports the title, brand, rating and accord count per page, then the sentiment label, score, distribution and note count.
- Download results from the Storage → Dataset tab as JSON, CSV, Excel or XML, or switch between the seven built-in dataset views to inspect accords, the pyramid, reviews or the sentiment rollup separately.
What to do when Fragrantica changes its structure
Nothing on your side. The Actor is maintained, and the output schema is the contract: field names, nesting and types stay stable even when the parsers behind them are rewritten for a new frontend. If a section stops parsing, the affected keys come back empty or null rather than disappearing, so downstream jobs keep running instead of throwing key errors.
What changed in Fragrantica scraping recently?
The most disruptive change is the Vue hydration rewrite on the perfume page: the frontend now mounts over and destroys the server-rendered markup that carries the note pyramid custom elements, the hex-coloured accord bars and the review cards. This Actor's browser_enricher.py documents and works around it — navigate with Chromium to earn the Cloudflare clearance cookie, re-fetch the raw server HTML same-origin from inside the page, verify it before parsing, and fall back to the hydrated DOM only if the raw fetch is unavailable.
Three consequences follow, all handled in this Actor's source:
- Review vote tags moved into a JSON blob on the Vue element
<user-perfume-votes-new :perfume-votes="{...}">.dynamic_parser.pyreads that blob first and keeps the legacy labelled-badge parser as a fallback, so both markup generations work. - Reviews are lazy-mounted and render only once the review trigger scrolls into the viewport, so a naive
page.content()returns zero reviews. The browser layer polls forschema.org/Reviewelements before reading. - The Algolia search key rotates. The site's InstantSearch key is secured with an embedded
validUntiltimestamp; when it lapses, search silently returns nothing — soalgolia.pyharvests a fresh key from the live bundle each run rather than trusting the shipped fallback.
For DIY scrapers this means breakage arrives without an error: a 200 response with an empty result set. For users of this Actor, no action is required — these paths are maintained behind the same output keys.
⬇️ Input
Every parameter below comes from .actor/actor.json. No parameter is marked required ("required": []), but a run with an empty startUrls and an empty query fails immediately — supply at least one of them.
| Parameter | Required | Type | Default | Constraints | Description | Example Value |
|---|---|---|---|---|---|---|
startUrls | No | array | — (prefilled with one Dior Sauvage URL) | editor: stringList; accepts plain strings or { "url": "…" } objects | Fragrantica perfume URLs (containing /perfume/ and ending in .html) or search URLs (containing /search). Search URLs are read for their query= parameter and expanded through the catalog search. Anything else is logged as Skipping unsupported startUrl. | ["https://www.fragrantica.com/perfume/Dior/Sauvage-31861.html"] |
query | No | string | "" | editor: textfield | Free-text catalog search. Used when startUrls is empty, and also as a top-up when startUrls yields fewer perfumes than maxItems. | "Tom Ford oud" |
maxItems | No | integer | 10 | minimum: 1, maximum: 10000 | Hard cap on perfumes scraped across all inputs, applied after de-duplication of the collected URL list. | 25 |
allReviews | No | boolean | true | — | Controls review pagination. When true, the browser layer clicks "show more"-style controls until maxItemsReviews is reached. See the pitfall note below for what false actually does. | true |
maxItemsReviews | No | integer | 20 | minimum: 0, maximum: 10000 | Cap on reviews collected per perfume page. With maxItems: 5 and maxItemsReviews: 20 you get up to 5 perfumes and up to 100 reviews. Do not set this to 0 — see the pitfall note. | 50 |
analyzeSentiment | No | boolean | true | — | Turns the deterministic sentiment engine on or off. When false, reviews are returned raw: no sentiment, no aspects, no mentionedNotes, and reviewSentiment stays null. | true |
aiEnhancement | No | boolean | false | — | Adds the optional LLM layer (aiSentiment, aiSentimentScore, aiThemes, aiSummary) on top of the deterministic engine. Requires aiApiKey or the provider's environment variable. Ignored entirely when analyzeSentiment is false. | false |
aiModel | No | string | "claude-haiku-4-5" | enum of 13 values: claude-haiku-4-5, claude-sonnet-5, claude-opus-4-8, gpt-4o-mini, gpt-4o, gpt-4.1-mini, gemini-2.0-flash-lite, gemini-2.0-flash, gemini-2.5-flash, grok-3-mini, deepseek-chat, sonar, mistral-small-latest | Provider is auto-detected from the model name prefix: claude-* → Anthropic, gemini-* → Google, grok-* → xAI, deepseek-* → DeepSeek, sonar* → Perplexity, mistral* → Mistral, everything else → OpenAI. Cheap mini/flash/haiku tiers are recommended for classification. | "gpt-4o-mini" |
aiApiKey | No | string | — | editor: textfield, isSecret: true (masked and stored encrypted) | Your own provider API key, used only when aiEnhancement is on. Falls back to ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY / GOOGLE_API_KEY, XAI_API_KEY / GROK_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY / PPLX_API_KEY or MISTRAL_API_KEY. If no key resolves, the AI fields are null and a warning is logged. | "sk-…" |
maxReviewsForAI | No | integer | 40 | minimum: 0, maximum: 10000 | Caps how many reviews per perfume are sent to the LLM, to control token cost. 0 means no limit. Affects only the AI layer — the deterministic engine always covers every scraped review. | 40 |
omitFields | No | array | [] | editor: stringList | Top-level keys to drop from each output item. Applied after extraction. Adding "reviews" here also skips review collection in the browser layer entirely. | ["images", "peopleWhoLikeThisAlsoLike"] |
proxyConfiguration | No | object | { "useApifyProxy": false } | editor: proxy | Apify Proxy or your own proxy URLs. Left at the default, the Actor runs the built-in escalation chain. Selecting RESIDENTIAL or a datacenter group locks that tier and disables automatic escalation. | { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] } |
Runtime defaults not exposed in the input schema
src/main.py merges a defaults map before running. These keys are not in the input schema, so the Console form cannot set them, but they are read from the input object if present — relevant when calling the Actor over the Apify API. Documented for transparency, not offered as a supported interface:
concurrency = 2 · minRequestIntervalSeconds = 1.5 · requestJitterSeconds = 0.8 · requestTimeoutSeconds = 30 · maxRetries = 5 · retryBackoffSeconds = 4.0 · rateLimitCooldownSeconds = 30.0 · useBrowser = true · browserHeadless = true · blockHeavyResources = true · logLevel = "INFO" (accepted but not applied) · debugDumpHtml = false (dumps rendered HTML to the key-value store).
That map carries maxItems: 50 while the schema default is 10. Console runs submit the schema default and cap at 10; an API call that omits maxItems falls through to 50. Always send maxItems explicitly from the API.
Example JSON input
{"startUrls": ["https://www.fragrantica.com/perfume/Dior/Sauvage-31861.html","https://www.fragrantica.com/perfume/Tom-Ford/Tobacco-Vanille-1825.html","https://www.fragrantica.com/search/?query=oud%20wood"],"query": "amber woody niche","maxItems": 25,"allReviews": true,"maxItemsReviews": 50,"analyzeSentiment": true,"aiEnhancement": false,"aiModel": "claude-haiku-4-5","aiApiKey": "","maxReviewsForAI": 40,"omitFields": ["images"],"proxyConfiguration": { "useApifyProxy": false }}
Common pitfalls
maxItemsReviews: 0 does not disable reviews — it uncaps them to 200. The value is read as int(config.get("maxItemsReviews") or 200), and 0 is falsy in Python, so it falls through to the fallback. To collect no reviews at all, put "reviews" in omitFields instead.
allReviews: false does not disable reviews either. It disables pagination and simultaneously raises the per-page cap to 10000, so the Actor still parses every review already present in the initial HTML. Use maxItemsReviews to limit volume and omitFields: ["reviews"] to switch reviews off.
⬆️ Output
Every run writes typed, normalized JSON to an Apify dataset — one item per perfume, pushed live as each page completes, so a cancelled run keeps everything finished so far. Export from the Console or the API as JSON, CSV, Excel or XML. The schema is stable: keys stay present with null or [] when a section is missing, never silently dropped (except keys you remove yourself via omitFields).
Semantics worth knowing before you build on the fields:
titleis the raw page<title>—"Sauvage Dior cologne - a fragrance for men 2015", not a clean product name.genderis parsed from that same string (unisex/female/male, ornull).idis the numeric Fragrantica perfume ID from the URL suffix, returned as a string.reviewCountis the number of reviews this run collected, capped bymaxItemsReviews— not Fragrantica's total review count.*Breakoutfields are ordered arrays of one-key objects, one per vote bucket, in the site's canonical order.*Averageis a weighted mean of the 1-based bucket index, solongevityAverage,priceValueAverageandratingAverageland on a 1–5 scale andsillageAverageon a 1–4 scale.*Sumis total votes cast,*Maxthe largest bucket.pyramid.typeis"full"withtopNotes/middleNotes/baseNotes, or"single"with a flatallNotesarray.ratingSumfalls back toratingCountwhen the rating breakout could not be parsed — treat it as "total rating votes, best effort".
Scraped perfume (full item)
{"id": "31861","url": "https://www.fragrantica.com/perfume/Dior/Sauvage-31861.html","title": "Sauvage Dior cologne - a fragrance for men 2015","description": "Sauvage is a new fragrance by Dior, a radically fresh composition...","primaryImageUrl": "https://fimgs.net/mdimg/perfume/375x500.31861.jpg","images": ["https://fimgs.net/mdimg/secretnotes/o.31861.jpg"],"brandName": "Dior","brandUrl": "https://www.fragrantica.com/designers/Dior.html","brandLogo": "https://fimgs.net/mdimg/dizajneri/o.99.jpg","mainAccords": [{ "accord": "fresh spicy", "color": "Sushi", "rgb": "rgb(145, 176, 70)", "hex": "#91b046", "value": 100.0 },{ "accord": "amber", "color": "Festival", "rgb": "rgb(244, 206, 99)", "hex": "#f4ce63", "value": 71.4213 },{ "accord": "citrus", "color": "Golden Dream", "rgb": "rgb(231, 210, 79)", "hex": "#e7d24f", "value": 63.8899 }],"pros": [{ "label": "Long lasting", "like": 412, "dislike": 38, "score": 786, "userVote": 0 }],"cons": [{ "label": "Too common", "like": 265, "dislike": 51, "score": 479, "userVote": 0 }],"pyramid": {"type": "full","topNotes": [{ "name": "Calabrian bergamot", "img": "https://fimgs.net/mdimg/sastojci/t.75.jpg", "url": "https://www.fragrantica.com/notes/Bergamot-75.html" }],"middleNotes": [{ "name": "Sichuan Pepper", "img": "https://fimgs.net/mdimg/sastojci/t.223.jpg", "url": "https://www.fragrantica.com/notes/Sichuan-Pepper-223.html" }],"baseNotes": [{ "name": "Ambroxan", "img": "https://fimgs.net/mdimg/sastojci/t.328.jpg", "url": "https://www.fragrantica.com/notes/Ambroxan-328.html" }]},"longevityBreakout": [{ "very weak": 214 }, { "weak": 611 }, { "moderate": 1902 }, { "long lasting": 2740 }, { "eternal": 883 }],"longevityAverage": 3.5182,"longevitySum": 6350,"longevityMax": 2740,"sillageBreakout": [{ "intimate": 402 }, { "moderate": 1611 }, { "strong": 2088 }, { "enormous": 741 }],"sillageAverage": 2.6114,"sillageSum": 4842,"sillageMax": 2088,"priceValueBreakout": [{ "way overpriced": 288 }, { "overpriced": 640 }, { "ok": 1503 }, { "good value": 1188 }, { "great value": 511 }],"priceValueAverage": 3.2003,"priceValueSum": 4130,"priceValueMax": 1503,"perfumeRating": 4.05,"ratingBreakout": [{ "hate": 1204 }, { "dislike": 1477 }, { "ok": 3390 }, { "like": 6612 }, { "love": 9004 }],"ratingAverage": 3.8419,"ratingSum": 21687,"ratingMax": 9004,"bestRating": 5,"ratingCount": 21687,"reviewCount": 20,"gender": "male","genderBreakout": { "female": 331, "femaleUnisex": 502, "unisex": 1640, "maleUnisex": 2214, "male": 5388 },"genderSum": 10075,"genderMax": 5388,"seasonBreakout": { "winter": 1502, "spring": 3944, "summer": 3120, "autumn": 2711, "day": 4402, "night": 2988 },"relationBreakout": { "have": 8412, "had": 1204, "want": 3377 },"relationSum": 12993,"relationMax": 8412,"perfumers": ["Francois Demachy"],"peopleWhoLikeThisAlsoLike": [{ "url": "https://www.fragrantica.com/perfume/Dior/Dior-Homme-Intense-2011-11145.html", "brand": "Dior", "title": "Dior Homme Intense 2011", "image": "https://fimgs.net/mdimg/perfume/375x500.11145.jpg" }],"thisPerfumeRemindsMeOf": [{ "id": 1825, "title": "Tobacco Vanille", "brand": "Tom Ford", "gender": null, "url": "https://www.fragrantica.com/perfume/Tom-Ford/Tobacco-Vanille-1825.html", "image": "https://fimgs.net/mdimg/perfume/375x500.1825.jpg" }],"reviews": [],"reviewSentiment": null}
Scraped review (one element of reviews[])
Each review carries the scraped fields plus, when analyzeSentiment is on, the three computed objects. The votes object always has all eleven keys — numeric tags default to 0, and gender and relation default to null.
{"name": "AmberDrift","published": "2026-03-04","comment": "The bergamot opening is gorgeous and it lasts all day on my skin. Sillage is a beast for the first two hours, then it settles into a very warm ambroxan hum. Not cheap, but absolutely worth it.","votes": {"rating": 5,"winter": 0,"spring": 1,"summer": 0,"autumn": 0,"day": 1,"night": 0,"longevity": 5,"sillage": 3,"gender": "male","relation": "have"},"sentiment": { "label": "positive", "score": 0.856, "confidence": 1.0 },"aspects": {"longevity": { "label": "positive", "score": 0.88, "mentions": 2 },"sillage": { "label": "positive", "score": 0.6067, "mentions": 2 },"scent": { "label": "positive", "score": 0.7143, "mentions": 1 },"value": { "label": "positive", "score": 0.3333, "mentions": 2 }},"mentionedNotes": [{ "note": "Calabrian bergamot", "tier": "top", "label": "positive", "score": 1.0, "mentions": 1 },{ "note": "Ambroxan", "tier": "base", "label": "positive", "score": 0.5714, "mentions": 1 }],"aiSentiment": null,"aiSentimentScore": null,"aiThemes": null,"aiSummary": null}
When aiEnhancement is on and a key resolves, the last four keys fill in — for example "aiSentiment": "positive", "aiSentimentScore": 0.9, "aiThemes": ["longevity", "sillage", "bergamot note", "value"], "aiSummary": "A long-lasting, bergamot-forward scent the reviewer considers worth the price."
Computed sentiment rollup (reviewSentiment)
One object per perfume, aggregated across every review that received a sentiment object. It is null when analyzeSentiment is off, when no reviews were collected, or when no review could be analyzed.
{"overall": { "label": "positive", "score": 0.3241, "positiveRatio": 0.7 },"reviewsAnalyzed": 20,"distribution": { "positive": 14, "neutral": 3, "negative": 3 },"byAspect": {"longevity": { "label": "positive", "score": 0.4108, "mentions": 31, "reviewCount": 16 },"sillage": { "label": "neutral", "score": 0.0512, "mentions": 18, "reviewCount": 11 },"scent": { "label": "positive", "score": 0.5522, "mentions": 44, "reviewCount": 19 },"value": { "label": "negative", "score": -0.2874, "mentions": 12, "reviewCount": 9 },"packaging": { "label": "neutral", "score": 0.1102, "mentions": 5, "reviewCount": 4 }},"mentionedNotes": [{ "note": "Ambroxan", "tier": "base", "label": "positive", "score": 0.4411, "mentions": 9, "praisedBy": 6, "criticizedBy": 1, "reviewCount": 8 },{ "note": "Calabrian bergamot", "tier": "top", "label": "positive", "score": 0.621, "mentions": 7, "praisedBy": 5, "criticizedBy": 0, "reviewCount": 7 },{ "note": "pepper", "tier": "unspecified", "label": "negative", "score": -0.3333, "mentions": 4, "praisedBy": 0, "criticizedBy": 3, "reviewCount": 4 }],"topPraisedNotes": ["Calabrian bergamot", "Ambroxan", "vanilla"],"topCriticizedNotes": ["pepper", "synthetic musk"]}
mentionedNotes is sorted by mention count first, then score. topPraisedNotes takes the highest-scoring positive notes and topCriticizedNotes the lowest-scoring negative ones, capped at five names each.
How does this Fragrantica scraper compare to other review scrapers?
No other Apify Actor targets Fragrantica reviews, so the honest comparison is against the review-sentiment scrapers ranking for the same intent. Every competitor statement below was read from that Actor's own Apify Store listing on 2026-07-25 and is not measured here.
| Feature | Fragrantica.com Scraper — Reviews & Sentiment Analysis | Generic review-sentiment scraper |
|---|---|---|
| Where sentiment comes from | Computed in-Actor by the deterministic lexicon engine documented above; reproducible with no key | Commonly re-exported from the platform's own labels — memo23/hotels-scraper's listing states "Sentiment Labels: Extracted from the GraphQL API's sentiment analysis", and memo23/expedia-scraper's listing describes surfacing "Expedia's AI-generated guest-sentiment summary" |
| Sentiment granularity | Per review (label, score, confidence), per aspect (5), per fragrance note, plus a perfume rollup | Usually one label per review; parseforge/youtube-comments-scraper's listing offers "Optional AI sentiment analysis and topic categorization" as an add-on |
| API key for sentiment | None. The default engine runs offline; the LLM layer is opt-in with your own key across 7 providers | AI sentiment layers typically depend on the Actor's own model access or an add-on step |
| Entity coverage per run | Perfume record, accords, note pyramid, 7 vote breakouts, reviews, related perfumes and the rollup in one item | Review rows only, with a parent aggregate attached |
| Input flexibility | Perfume URLs, search URLs and free-text query in one run, the query topping up to maxItems | Typically URL lists only |
| Anti-bot handling | Chrome-impersonating TLS client, Cloudflare challenge detection, sticky direct → datacenter → residential escalation, Chromium fallback | Varies; API-backed scrapers avoid the problem, HTML scrapers inherit it |
If you're building an AI agent or a RAG pipeline, the output-format row is the decision-maker — parsing HTML inside an agent loop is a reliability failure mode, not a feature. If reproducibility matters, read the "where sentiment comes from" row next: a deterministic score can be recomputed and audited; a model-generated label cannot be reproduced from the dataset alone.
How many results can you scrape with this Fragrantica scraper?
maxItems accepts 1 to 10,000 perfumes per run and defaults to 10; maxItemsReviews accepts 0 to 10,000 reviews per perfume and defaults to 20. Those two schema ceilings are the only hard caps the Actor imposes on volume.
Pagination works in two places. For catalog search, the Actor queries Fragrantica's Algolia index with hitsPerPage = min(60, maxItems) and requests ceil(maxItems / hitsPerPage) pages, stopping early on an empty page or a non-200 status; hits become canonical perfume URLs, are de-duplicated against startUrls, and the combined list is truncated to maxItems. For reviews, the browser layer polls for review elements, then clicks "show more" / "load more" / "show next" controls up to 80 times, stopping once the count reaches maxItemsReviews or stops growing across two consecutive passes.
Platform-side, retrievable reviews are bounded by what Fragrantica renders for that perfume — a page with 8 reviews returns 8 however high you set the cap. Perfumes run at a concurrency of 2 with a minimum ~1.5 s spacing plus jitter between requests, so large runs are paced rather than bursty. No throughput benchmark is published for this Actor, and none is claimed here.
Integrate the Fragrantica Scraper and automate your workflow
The Fragrantica Scraper works with any language or tool that can send an HTTP request. It is published as an Apify Actor, so it is started from the Apify Console or by calling the Apify API with your Apify API token — there is no separate service endpoint, no vendor-specific header and no self-hosted deployment.
REST API integration
Run the Actor synchronously and read the dataset items back in one call. Replace <YOUR_USERNAME> with the account the Actor is published under and set APIFY_TOKEN in your environment.
import os, requestsACTOR = "<YOUR_USERNAME>/fragrantica-com-scraper-reviews-sentiment-analysis"url = f"https://api.apify.com/v2/acts/{ACTOR.replace('/', '~')}/run-sync-get-dataset-items"resp = requests.post(url,params={"token": os.environ["APIFY_TOKEN"]},json={"startUrls": ["https://www.fragrantica.com/perfume/Dior/Sauvage-31861.html"],"maxItems": 3,"maxItemsReviews": 50,"analyzeSentiment": True,},timeout=900,)resp.raise_for_status()for perfume in resp.json():rollup = perfume.get("reviewSentiment") or {}overall = rollup.get("overall") or {}print(perfume["brandName"], "|", perfume["title"])print(" sentiment:", overall.get("label"), overall.get("score"))print(" praised notes:", rollup.get("topPraisedNotes"))
Works in Python, Node.js, Go, Ruby and cURL — it is a plain HTTPS POST with a JSON body. For long runs, start the Actor asynchronously with POST /v2/acts/{actorId}/runs and page through GET /v2/datasets/{datasetId}/items instead, so you are not holding a connection open.
Automation platforms (n8n, Make, LangChain)
In n8n, use the official Apify node: pick the Run Actor operation, select this Actor, paste the input JSON, and chain a Get dataset items operation. A common pattern is Schedule Trigger → Apify Run Actor → filter on reviewSentiment.overall.label == "negative" → notification node.
In Make, the Apify app exposes Run an Actor and Watch Actor runs modules. Point Run an Actor at this Actor, map the scenario's input into startUrls or query, and route the dataset items into Google Sheets, Airtable or a webhook.
In LangChain, the Apify integration's dataset loader pulls a finished run's items straight into Document objects. Map reviews[].comment to page_content and keep sentiment.label, sentiment.confidence, aspects and mentionedNotes as metadata — a retriever then reaches negative-longevity reviews or note-specific criticism without an extra classification pass.
Is it legal to scrape Fragrantica?
Scraping publicly accessible web pages is generally lawful in the US and EU, and this Actor collects only publicly available Fragrantica pages — no login, no session cookie, no paywalled or member-restricted content.
Two regimes apply. The perfume, accord, note and vote data is product and catalog data: use it in line with Fragrantica's Terms of Service, and note that compilations of factual data can attract database rights in the EU. The reviews[] array is different — reviewer display names and free-text comments are personal data under GDPR and CCPA, so storing, processing or publishing them requires a lawful basis, minimized retention and honoured deletion requests. The computed sentiment fields are derived personal data and inherit the same obligations.
Consult legal counsel for commercial use cases involving bulk personal data.
❓ Frequently asked questions
Does the Fragrantica Scraper work without a Fragrantica account?
Yes. No Fragrantica account, login or cookie is required — the Actor requests public perfume pages and the public catalog search index. The only credential involved is the optional aiApiKey, your own LLM provider key, needed only if you turn aiEnhancement on. To run the Actor you need an Apify account and its API token, nothing more.
How often is the scraped data updated?
Every run fetches live pages. There is no cache layer: each perfume URL is fetched fresh, the browser layer re-navigates for the hydrated sections, and reviews are read from that run's HTML. ratingCount and the vote breakouts therefore reflect the moment of the run. Schedule the Actor if you want a time series.
What happens if a perfume page is missing, blocked or has no reviews?
Failures are isolated per perfume, not per run. If both the static fetch and the browser fallback fail for a URL, that item is logged as Failed and skipped while the rest of the run continues; nothing partial is pushed for it. If the page loads but the review section is empty or did not mount, you get a complete perfume record with reviews: [] and reviewSentiment: null — the rollup returns null rather than fabricating a neutral score. An empty catalog search ends the run cleanly with the status message No URLs collected from inputs. rather than an error.
Can I scrape private or member-restricted Fragrantica content?
No. Only publicly rendered page content is returned. Reviewer display names and comment text are collected exactly as Fragrantica shows them to an anonymous visitor; member-only areas, private messages, email addresses and account settings are never in the output.
How do I run it, and what does a run cost?
You run it on Apify — open the Actor in the Apify Console and click Start, or call the Apify API with your Apify token as shown above. Each successfully scraped perfume is pushed as one charged result row, so cost scales with perfumes returned, not URLs submitted; perfumes that fail are not pushed. The current price and any trial terms are shown on the Actor's Apify Store listing, which is authoritative.
Does the Fragrantica Scraper work for AI agent workflows and LLM pipelines?
Yes. The Actor is callable as an HTTPS endpoint by any agent framework — LangChain, LlamaIndex, CrewAI, a custom tool-calling loop — via the Apify API shown above. Every response is typed, normalized JSON with stable field names, so there is no parsing step before passing it to a model: send an item straight into a context window, index reviews[].comment into a vector store with the sentiment objects as metadata, or route the dataset through an agent tool. The pre-computed sentiment, aspects and mentionedNotes fields act as retrieval metadata that lets an agent filter reviews before spending context on them, and because the scores are deterministic, re-running a scrape does not invalidate labels you already indexed.
How does the scraper handle Fragrantica's anti-bot protection?
With four layers, all in the source. TLS impersonation: static fetches go through an HTTP client that impersonates Chrome, including HTTP/3 and realistic client-hint headers. Challenge detection: responses are scanned for Cloudflare markers in their first 4,000 characters, so a challenge page is treated as a block rather than parsed as content. Sticky proxy escalation: the run starts direct, escalates to datacenter on a block, then to residential with up to 3 retries, and stays on the first tier that works. Browser fallback: headless Chromium navigates the page (blocking media, fonts, images and ~20 ad/analytics hosts), then re-fetches the raw server HTML from inside the page using the clearance cookies the navigation just earned. On blocks, a shared cooldown gate pauses every concurrent worker — honouring Retry-After when present — instead of retrying into the same throttle.
How does this compare with other sentiment scrapers on the Apify Store?
The observable difference is where the sentiment comes from and how deep it goes: this Actor computes it locally with a documented lexicon and exposes it at four levels — review, aspect, note and perfume. The closest ranked alternatives target other platforms and describe sentiment sourced elsewhere; see the comparison table above for what their listings said on 2026-07-25. None of them target Fragrantica.
Can I use the Fragrantica Scraper without managing proxies?
Yes. Leave proxyConfiguration at its default { "useApifyProxy": false } and the Actor handles it: direct connection first, escalating to Apify's automatic datacenter proxy the first time it sees a block or network error, then to the RESIDENTIAL group with up to 3 retries, sticking with whichever tier works for the rest of the run. Browser sessions relaunch against the current tier automatically. If you prefer to pin a tier, select it in the proxy input and automatic escalation is disabled.
What happens when Fragrantica changes its structure or blocks the scraper?
The Actor is maintained and the output schema stays stable — field names and types do not change on your end. Parsers are updated behind the same keys, and where a section cannot be parsed the corresponding fields come back empty or null instead of vanishing, so your integrations keep running while the fix lands. No numeric turnaround is promised here.
💬 Your feedback
Found a bug, hit a perfume page that parses oddly, or need a field that isn't in the output yet? We want to know. Open an Issue on the Actor's Apify Store listing — every report is read, and field requests that fit the schema are the fastest kind of improvement to ship. If your use case needs a different slice of Fragrantica, tell us what the ideal dataset row would look like and we will take it from there.