Spanish Pharmacy Price Comparison Scraper (EAN-13 by Barcode)
Pricing
from $2.50 / 1,000 comparison row delivereds
Spanish Pharmacy Price Comparison Scraper (EAN-13 by Barcode)
Compare EAN-13 codes across 7 online pharmacies serving Spain: price, availability, brand, SKU and URL per store (Farmaciasdirect, Farmaline, Farmaten, El Negrito, Cuesta La Villa, Vistabella, Farmacia Principal). For price tracking and market research. Sitemap-only; no login, no CAPTCHA bypass.
Pricing
from $2.50 / 1,000 comparison row delivereds
Rating
0.0
(0)
Developer
Steadfetch
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 hours ago
Last modified
Categories
Share
Compare a list of EAN-13 codes across Spanish online pharmacies and get one row per (EAN, store) with price, stock, brand, SKU and the public product URL. v3 roster: Farmaciasdirect, Farmaline, Farmaten, El Negrito, Cuesta La Villa, Vistabella and Farmacia Principal. HTTP + JSON-LD only: no browser, no login, no proxy and no CAPTCHA bypass.
What it does
The Actor resolves each requested EAN against the public catalogue of every selected store:
- Discovery: reads each store's
robots.txtand uses only the sitemaps declared there. Farmaciasdirect exposes/sitemaps/products-index.xml(8 product shards), Farmaline/sitemap/sitemap.xml(31 Dutch product sitemaps), Farmaten/1_index_sitemap.xml(the Spanish child is the only one used), the three Shopify storefronts expose/sitemap/sitemap_web.xmldirectly, and Farmacia Principal exposes a PrestaShoplgsitemapsindex whose product child is the only one used. No on-site search route, no category crawling, no undocumented endpoint. - Extraction: fetches a product page and reads its schema.org
ProductJSON-LD:gtin13->ean,name,sku,brand.name,offers.price,offers.priceCurrencyandoffers.availability. Pages without a valid EAN, name or positive price are skipped, never invented. EAN is mandatory: a store that does not publish a 13-digit EAN or GTIN stays out of the roster. - Comparison: writes one row per (EAN, store). If the same EAN appears twice on a page or an EAN is repeated in the input, it is written once.
- Schema validation: every row is validated against
.actor/dataset_schema.jsonbefore reaching the dataset. WithstrictSchema: true(the default) an invalid row fails the run instead of publishing malformed data. - Politeness: concurrency is always 1, every request waits at least
delayMs(default 1,500 ms) and a custom Steadfetch User-Agent identifies the Actor. A 401/403/451 or a WAF/CAPTCHA challenge page stops the run with a clear error; nothing is evaded. - Credit guard:
maxScanPagescaps how many product pages a run may fetch per store. Product sitemaps can hold tens of thousands of URLs, so a cold lookup of an old product may need a larger budget than the default.
v3 roster
| Store | Id | Discovery | Verified on 2026-10-21 |
|---|---|---|---|
| Farmaciasdirect (ES) | farmaciasdirect | robots.txt -> /sitemaps/products-index.xml -> 8 product shards | robots 200; product page with schema.org Product (gtin13, price, availability); legal notice has zero robot/spider/crawler/scraping clauses |
| Farmaline (BE, Dutch) | farmaline | robots.txt -> /sitemap/sitemap.xml -> products-*-nl-*.xml | robots 200 (only admin/cart/account and search routes disallowed); Product + Offer JSON-LD with price, availability and gtin13; terms have zero robot/spider/crawler/scraping clauses |
| Farmaten (ES) | farmaten | robots.txt -> /1_index_sitemap.xml -> 1_es_0_sitemap.xml | robots 200; 12/12 sampled product pages publish gtin13 in Product JSON-LD; terms and purchase conditions have zero robot/spider/crawler/scraping clauses |
| Farmacia El Negrito (ES) | negrito | robots.txt -> /sitemap/sitemap_web.xml (6,981 product URLs) | robots 200; 9/12 sampled product pages publish gtin13 in Product JSON-LD; terms have zero robot/spider/crawler/scraping clauses |
| Farmacia Cuesta La Villa (ES) | cuestavilla | robots.txt -> /sitemap/sitemap_web.xml (2,063 product URLs) | robots 200; 9/12 sampled product pages publish gtin13 in Product JSON-LD; terms have zero robot/spider/crawler/scraping clauses |
| Farmacia Vistabella (ES) | vistabella | robots.txt -> /sitemap/sitemap_web.xml (1,323 product URLs) | robots 200; 12/12 sampled product pages publish gtin13 in Product JSON-LD; terms have zero robot/spider/crawler/scraping clauses |
| Farmacia Principal (ES) | farmaprincipal | robots.txt -> /module/lgsitemaps/sitemap?name=sitemap_1 -> product_1_1 (9,180 product URLs) | robots 200 (product pages and the declared sitemap allowed); 7/7 sampled product pages publish schema.org Product with 13-digit gtin13, price and availability; legal notice and terms have zero robot/spider/crawler/scraping clauses |
A store enters only if it publishes a real 13-digit EAN or GTIN. Pages that do not publish one are skipped; nothing is invented.
Evaluated for v3 and left out
These sources were checked with plain HTTP on 2026-10-22 and are not requested at runtime:
- farmaciaetayo.com (
etayo) - out: the declared sitemap is a single urlset with 1,794 URLs (products and categories) and every sampled product page publishes aProductJSON-LD node, but it carriesskuonly: 0 of 6 sampled product pages publish a 13-digit EAN/GTIN in the JSON-LD or anywhere in the HTML (gtinandeanappear zero times). Fails the mandatory-EAN gate, so it is not scraped even though discovery and terms would allow it. - farmaciamartinezsegura.es (
martinez_segura) - out:robots.txtdeclares noSitemap:line, product discovery would require crawling the/es/2-iniciocategory, and the sampled product page publishes noProductJSON-LD and no 13-digit EAN/GTIN (the donated code itself documents that this PrestaShop installation has no EAN available). Fails the mandatory-EAN gate and needs no proxy.
These v2 sources were checked with plain HTTP on 2026-10-21 and are not requested at runtime:
- farmaciasantacruz.es (
santa_cruz) - out: 0 of 12 sampled product pages publish a 13-digit EAN/GTIN (JSON-LD only carriessku/mpn). Fails the EAN gate. - farmaciaevoracentro.es (
evora) - out: 0 of 10 sampled product pages publish a 13-digit EAN/GTIN. The donated code used the WooCommerce product id as "ean", which is not a barcode and is not accepted. - farmaciacanariasonline.com (
canariasonline) - out: the sitemap declared inrobots.txt(113 URLs) contains no product URLs, only categories, blog and information pages, and sampled product pages publish no 13-digit EAN/GTIN in JSON-LD (checked 2026-10-21). Both discovery and the mandatory-EAN gate fail, so the source stays out. - farmaciaelcardonal.es (
cardonal) - out: product pages publish no EAN/GTIN (only the PrestaShop product id) and category discovery by numeric id returned 404. Fails the EAN gate and the discovery gate.
Stores deliberately out of scope
These sources are not part of the Actor and are not requested at runtime:
- farmaciabarata.es - out under company rule 3 (legal risk). Its legal notice states: "usted se obliga a indemnizar a la EMPRESA frente a cualesquiera daños ... que se deriven del uso por su parte de "robots", "spiders", "crawlers" o herramientas similares empleadas con el fin de recabar o extraer datos" (
/content/aviso-legal.html). - Dosfarma, Promofarma, Mifarma/Atida - out: anti-bot/CAPTCHA or HTTP 405; Steadfetch never evades anti-bot protection.
- Dr. Max - out:
drmax.esdoes not resolve (NXDOMAIN). - Other shops from the donated
reference/farmacias/folder - not included until their robots.txt and terms are verified in writing. This Actor does not build or publish that reference folder.
Example output
[{"ean": "4015110035732","store": "farmaciasdirect","storeProductUrl": "https://www.farmaciasdirect.es/products/esquiladora-wahl-admire","url": "https://www.farmaciasdirect.es/products/esquiladora-wahl-admire","name": "Esquiladora Wahl Admire","sku": "WA4015110035732","brand": "WAHL","price": 128.9,"currency": "EUR","availability": "OutOfStock","scrapedAt": "2026-10-20T23:55:00.000Z"},{"ean": "4031683919460","store": "farmaline","storeProductUrl": "https://www.farmaline.be/nl/artikel/4805261/erbe-klappfeile-lila.htm","url": "https://www.farmaline.be/nl/artikel/4805261/erbe-klappfeile-lila.htm","name": "ERBE Klappfeile lila","sku": "04805261","brand": null,"price": 30.99,"currency": "EUR","availability": "InStock","scrapedAt": "2026-10-20T23:55:00.000Z"}]
sku and brand are null when the page does not publish them; the Actor never invents a value. url is the common Steadfetch envelope and is the same public URL as storeProductUrl. availability uses the schema.org short form (InStock, OutOfStock, ...); a value outside that list is reported as Unknown.
Input
Minimal run:
{"eans": ["4015110035732"],"maxResults": 10}
One store only, with a wider scan budget for an older product:
{"eans": ["4015110035732", "4031683919460"],"stores": ["farmaciasdirect", "vistabella"],"maxResults": 100,"delayMs": 1500,"maxScanPages": 5000}
| Field | Type | Description |
|---|---|---|
eans | array of strings (required) | Non-empty list of 13-digit EAN codes. EAN-13 only; free-text product names are not supported yet. |
stores | array of strings | Subset of ["farmaciasdirect", "farmaline", "farmaten", "negrito", "cuestavilla", "vistabella", "farmaprincipal"]. Omit it or send an empty list to use the full v3 roster. |
maxResults | integer | Maximum rows (one per EAN and store) saved to the dataset. Default 500, maximum 5,000. |
delayMs | integer | Minimum delay between HTTP requests. Default and minimum 1,000 ms; default 1,500 ms. Concurrency is always 1. |
maxScanPages | integer | Credit guard: maximum product pages fetched per store while looking for the EANs. Default 2,000, maximum 200,000. |
strictSchema | boolean | Default true; fail the run on schema violations. |
Why a scan budget?
The spec requires discovery only through the sitemaps declared in robots.txt. Those sitemaps list the whole catalogue (Farmaciasdirect: 8 shards x 10,000 URLs; Farmaline: 31 product sitemaps), and they do not include the EAN. The Actor therefore opens product pages in sitemap order and stops as soon as every requested EAN is found or maxScanPages is reached. For a large batch of EANs, send them in one run: the same page scan serves every EAN in the list, and only matching rows are pushed and charged.
Project structure
.actor/actor.json Actor definition (payPerEvent, dataset schema, output schema).actor/input_schema.json INPUT options (eans, stores, maxResults, delayMs, maxScanPages).actor/dataset_schema.json Item JSON Schema (additionalProperties: false) + table view.actor/output_schema.json Output schema: links the default dataset as the results output.actor/pay_per_event.json PPE pricing (two synthetic platform events)src/stores.ts v3 roster: origins, robots URLs, declared sitemap URLs and URL patternssrc/sitemaps.ts Pure robots.txt and sitemap parsingsrc/discovery.ts robots.txt -> declared sitemaps -> product URLssrc/extract.ts Pure schema.org JSON-LD extraction and normalizationsrc/http.ts Throttled HTTP client with retries and block detectionsrc/input.ts Input validation and normalizationsrc/compare.ts Framework-free orchestration (also used by the offline demo)src/main.ts Apify entry point: run, validate, pushsrc/demo.ts Offline fixture run (no network, no token, no credit)src/validate-output.ts Ajv validator shared by the run and the teststest/ Real saved pages/sitemaps and Vitest suites
Pay-per-event pricing
The Actor uses Apify pay-per-event pricing with the two synthetic platform events:
| Event | Charged when | Price |
|---|---|---|
apify-actor-start | a run starts (synthetic, automatic) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute) |
apify-default-dataset-item | a validated (EAN, store) row is saved to the dataset (synthetic, automatic) | 0.0025 USD per row = 2.50 USD per 1,000 rows |
There is no custom per-item event and no Actor.charge() call: the platform already charges apify-default-dataset-item per pushed row, so another charge would bill users twice. EANs with no result, 404s, retries and errors are never pushed, so they are never charged.
Reference and margin
- Direct single-store competitors on the Apify Store price around 2.00 USD per 1,000 results (six of eight rivals checked on 2026-10-20). Promofarma is the high outlier at 20.00 USD per 1,000 and is deliberately not imitated.
- This comparator charges 2.50 USD per 1,000 rows, a small premium for comparing several stores per EAN. It sits inside Steadfetch's 1-10 USD band.
- Estimated cost: 0.01-0.05 USD per 1,000 delivered rows for HTTP + schema.org extraction (profile A: no proxy, no browser). The dominant cost is the catalogue scan needed to find an EAN;
maxScanPagesbounds it and the run log reports the pages fetched. The platform figure has not been measured yet (no platform run has been spent on this Actor); it will be published here once the first minimal run is done.
Development
npm installnpm test # lint + typecheck + offline Vitest suites (no credit)npm run publish:check # Steadfetch publish gatenpm run schemas:validate # apify-cli schema validationnpm run demo:fixtures # full discovery + extraction against saved real pages (no network)
The default test suite runs against committed real pages and sitemap excerpts and never touches the network or the Apify platform. It covers robots.txt/sitemap parsing, the declared-sitemap gate, JSON-LD extraction for every store, throttling and retries, CAPTCHA/403 stop behaviour, input validation and an end-to-end comparison that validates the OUTPUT schema.
FAQ
Does the Actor need a login, API key or paid subscription? No. Only public product pages are fetched, with no login and no platform key.
Which stores are supported? Farmaciasdirect, Farmaline, Farmaten, El Negrito, Cuesta La Villa, Vistabella and Farmacia Principal in v3. Stores that forbid scraping, use anti-bot/CAPTCHA, publish no EAN or are pending legal verification are documented above and are not requested at runtime.
Why does an EAN sometimes come back empty?
The product sitemaps do not expose the EAN, so the Actor must open product pages in sitemap order. If the EAN is beyond maxScanPages, the run reports it as not found and does not charge for it. Raise maxScanPages (and keep delayMs at 1,500 ms or higher) or batch more EANs into one run.
When am I charged?
Once per run start (apify-actor-start) and once per validated row saved to the dataset (apify-default-dataset-item). EANs without a result do not generate a row and are not charged.
Is the output schema stable?
Yes. New fields are added additively, required fields never change, and additionalProperties is false so consumers can rely on the exact shape documented above.
What about personal data? Nothing in this Actor targets people. It only reads product pages: EAN, product name, brand, SKU, price, currency, availability and product URL. No reviews, authors, sellers or contact data are collected.
Can I request a new store? Yes. Open an issue in this repository with the public store URL. A store only enters after its robots.txt and terms of use are verified and documented; anti-bot/CAPTCHA sources are rejected.
Maintenance
This Actor is tested daily: automated runs keep the sources reachable and the output schema valid. Failures open an issue in this repository. Schema changes are announced through repository releases.
Legal and attribution
Product data is published by each store. Prices are public price information that distance-selling pharmacies must publish (RD 870/2013, Spain). The Actor follows each store's robots.txt, sends an identifying User-Agent, keeps concurrency at 1 and honours a delay of at least one second. It does not bypass logins, paywalls, CAPTCHAs or WAF challenges; if a store blocks a request, the run stops and reports it. The Actor code itself is Steadfetch proprietary (UNLICENSED).