$ cat case-study/farmaciradar.md
FarmaciRadar: five pharmacy catalogues, one honest price list
How I collect 26,000+ offers twice a day, fold them into 25,000+ comparable products — and why a broken scrape can never reach the live site.
PythonPublic store APIsFuzzy matchingNetlify FunctionsPush alertsPWA
Compare medicine prices across 5 Albanian online pharmacies — plus a map of 810 physical pharmacies with live open-now status.
01 The problem
The same box of medicine can cost noticeably different amounts from one pharmacy to the next — but finding out means opening five shops, searching five times and doing the maths yourself. I wanted one search that answers: where is this cheapest today?
02 The pipeline
Twice a day a scheduled job runs four steps, each in its own script:
scrape.pyCollect
normalize.pyClean
match.pyGroup
build + deployPublish
- Collect — only public store APIs. One pharmacy runs on Shopify (its product feed, 250 items per page); the other four run WooCommerce and expose the Store API (100 per page). No HTML scraping, no logins.
- The stubborn one. One store's API answers
410 Gone to any request that sets a page size. So it is paged the slow way — ?page=N, with the total read from the X-WP-TotalPages header — about 670 pages fetched by 6 parallel workers.
- Clean. Every source is reshaped into one schema: title, brand, price and old price in lek, stock, image, volume, category. Prices under 10 L are clearly wrong at the source — they are not deleted, but flagged
price_unverified, and the site says “check at the pharmacy” instead of promising a fake bargain.
03 Matching: the heart of it
Comparing prices only works if two listings are truly the same product. The matcher is deliberately conservative:
# a pair is grouped only if ALL of these hold
brand (normalized) must be identical
title similarity ≥ 0.90 (≥ 0.95 on sorted words)
quantity tokens ml / g / tabs must match exactly
a content word swapped → not grouped
no brand at all → never grouped
store SKUs ignored — internal codes, not barcodes
A wrong match is worse than a missing one. Merging 30 tablets with 60, or one active ingredient with another, would show a price that doesn't exist — so when in doubt, FarmaciRadar keeps them apart.
04 The safety net
- Every pharmacy fails alone. Each source runs in its own try/except. If one shop is down, the others carry on and its last good data is kept, marked
STALE with a date.
- A circuit breaker for bad days. If every source fails, or the total number of products drops by more than 30%, the scan is cancelled and the live site is left untouched.
- Then — and only then — publish. scrape → normalize → match → build → deploy together with the serverless functions, and a push check sends alerts for price drops.
05 In numbers
Launch · 12 Oct24,890 products
Today · 15 Oct25,003 products
Offers tracked26,758
On the map810 pharmacies
Need data pulled, cleaned and compared like this?
Let's talk →
$ cat case-study/farmaciradar.md
FarmaciRadar: pesë katalogë farmacish, një listë e ndershme çmimesh
Si mbledh mbi 26,000 oferta dy herë në ditë, i bashkoj në mbi 25,000 produkte të krahasueshme — dhe pse një skanim i prishur nuk arrin kurrë te faqja live.
PythonAPI publike dyqaneshFuzzy matchingNetlify FunctionsNjoftime pushPWA
Krahaso çmimet e barnave në 5 farmaci online shqiptare — plus një hartë me 810 farmaci fizike dhe statusin live “hapur tani”.
01 Problemi
E njëjta kuti ilaçi mund të kushtojë dukshëm ndryshe nga një farmaci te tjetra — por për ta zbuluar duhet të hapësh pesë dyqane, të kërkosh pesë herë dhe të bësh vetë llogaritë. Doja një kërkim të vetëm që përgjigjet: ku është më lirë sot?
02 Pipeline-i
Dy herë në ditë, një punë e planifikuar kalon nëpër katër hapa, secili në skriptin e vet:
scrape.pyMbledhja
normalize.pyPastrimi
match.pyGrupimi
build + deployPublikimi
- Mbledhja — vetëm nga API-të publike. Një farmaci punon me Shopify (feed-i i produkteve, 250 për faqe); katër të tjerat me WooCommerce dhe Store API-në e tij (100 për faqe). Pa lexim HTML, pa hyrje me llogari.
- Ajo kokëfortja. API-ja e një dyqani kthen
410 Gone për çdo kërkesë që cakton madhësinë e faqes. Ndaj atje faqosja bëhet me ?page=N, totali lexohet nga header-i X-WP-TotalPages — rreth 670 faqe me 6 workerë paralelë.
- Pastrimi. Çdo burim kthehet në një strukturë të vetme: titull, brand, çmim dhe çmim i vjetër në lekë, stok, foto, volum, kategori. Çmimet nën 10 L janë qartë gabime të burimit — nuk fshihen, por shënohen
price_unverified, dhe faqja thotë “shiko në farmaci” në vend që të premtojë një ofertë të rreme.
03 Grupimi: zemra e projektit
Krahasimi i çmimeve ka kuptim vetëm kur dy listime janë vërtet i njëjti produkt. Algoritmi është qëllimisht i kujdesshëm:
# një çift grupohet vetëm nëse vlejnë TË GJITHA
brand-i (i normalizuar) duhet të jetë i njëjtë
ngjashmëria e titullit ≥ 0.90 (≥ 0.95 me fjalët e renditura)
masa ml / g / tableta duhet të përputhet saktë
një fjalë përmbajtje ndryshon → nuk grupohet
pa brand fare → nuk grupohet kurrë
SKU-të e dyqaneve injorohen — kode të brendshme, jo barkode
Një bashkim i gabuar është më i keq se një që mungon. Të bashkosh 30 tableta me 60, ose një substancë me një tjetër, do të tregonte një çmim që nuk ekziston — ndaj kur ka dyshim, FarmaciRadar i mban veç.
04 Rrjeta e sigurisë
- Çdo farmaci bie e vetme. Çdo burim punon në try/except më vete. Nëse një dyqan nuk përgjigjet, të tjerët vazhdojnë dhe ruhen të dhënat e tij të fundit të mira, të shënuara
STALE me datë.
- Një siguresë për ditët e këqija. Nëse bien të gjitha burimet, ose numri total i produkteve bie mbi 30%, skanimi anulohet dhe faqja live nuk preket.
- Vetëm atëherë — publikimi. scrape → normalize → match → build → deploy bashkë me funksionet serverless, dhe një kontroll push dërgon njoftimet për uljet e çmimeve.
05 Në shifra
Nisja · 12 tetor24,890 produkte
Sot · 15 tetor25,003 produkte
Oferta të ndjekura26,758
Në hartë810 farmaci
Të duhen të dhëna të mbledhura, të pastruara dhe të krahasuara kështu?
Le të flasim →