Morrisons publishes product titles, prices, unit prices, More Card prices, Price Lock flags, own-label tier, category paths and availability on its public grocery pages. What makes Morrisons technically distinct from Tesco, Sainsbury's or ASDA is its fresh food weighting. Morrisons is vertically integrated and runs Market Street counters, which means a large share of the catalogue is priced by weight rather than per item — loose produce, butcher and fishmonger lines, and variable-weight packs. A schema built for fixed-unit packaged goods will mangle that data. This guide covers the field schema including variable-weight handling, a sample dataset, the specific failure points, and the build-versus-buy maths.
Morrisons is one of the traditional big four UK grocers by heritage and store estate, though by 2026 Kantar Worldpanel's 12-week grocery share readings put it at around 8.4% of the market — behind Tesco, Sainsbury's, Asda and Aldi, and now also behind Lidl following Lidl's overtaking of Morrisons in the rankings in May 2026, but its operating model differs from the others in a way that shows up directly in the data.
Morrisons manufactures a substantial proportion of the fresh food it sells, operating its own abattoirs, bakeries and produce packing facilities — it remains the only major UK supermarket to own its manufacturing sites and abattoirs outright, and trade reporting puts the share of fresh food it makes itself at around half of what it sells. It also runs Market Street, the in-store counter format covering butcher, fishmonger, bakery and deli. The online catalogue reflects this: fresh and own-manufactured lines make up a larger share of the assortment than at a comparable competitor.
The practical consequence for a data team is that Morrisons has more variable-weight products than the other big four. A packaged good has one price and one pack size. A Market Street or loose produce line has a price per kilogram, an estimated or minimum weight, and a final price that depends on what the picker actually selects. Loose bananas, a whole chicken, a piece of cheese cut at the counter — none of these behave like a tin of beans.
Most scraping schemas are built for tins of beans. Point one at Morrisons and it will do one of two things: drop the variable-weight lines silently because the price field does not parse, or capture a per-kg price into a price column that everything downstream assumes is per item. The second failure is worse, because it produces a dataset that looks complete and is quietly wrong — your fresh category averages will be off by a factor that varies by product.
There is a second distinguishing feature. Morrisons runs two overlapping price mechanics: More Card loyalty pricing, similar in structure to Clubcard and Nectar, and Price Lock, a commitment to hold selected lines at a fixed price for an extended period. Price Lock is not a discount — it is a duration commitment. Tracking which lines are locked and for how long tells you where Morrisons has chosen to hold margin steady, which is a different and more strategic signal than a weekly promotion.
| Field | Description | Example |
|---|---|---|
| product_id | Morrisons internal SKU identifier | 108234567 |
| product_url | Canonical product page URL | https://groceries.morrisons.com/products/... |
| product_name | Full product title as displayed | Morrisons British Semi Skimmed Milk 2.27L |
| brand | Brand name, parsed or from structured data | Morrisons |
| own_label_tier | Own-brand tier where applicable | Morrisons Savers / Morrisons / The Best |
| is_market_street | Boolean — counter or fresh-service line | true |
| category_path | Full breadcrumb hierarchy | Fresh > Meat & Poultry > Chicken |
| gtin_ean | Barcode identifier, where published | 05010251000000 |
| image_urls | Array of product image URLs | ["https://groceries.morrisons.com/..."] |
Morrisons own-label runs from Morrisons Savers at the value end, through the core Morrisons brand, to The Best at the premium end. Capture it explicitly. Savers is the range aimed squarely at Aldi and Lidl, and its price movements are a useful discounter-pressure indicator.
The is_market_street flag is worth carrying as its own boolean. Market Street lines behave differently on availability, pricing and substitution, and analysts almost always want to filter them in or out rather than blend them.
| Field | Description | Example |
|---|---|---|
| price | Price as charged for this listing | 4.50 |
| currency | ISO currency code | GBP |
| pricing_basis | Critical — is the price per item or per weight | per_item / per_kg |
| price_per_kg | Price per kilogram for weight-priced lines | 6.50 |
| pack_size | Size, weight or volume as listed | 2.27L |
| estimated_weight_kg | Approximate weight for variable-weight lines | 1.40 |
| weight_range_min_kg | Lower bound of the stated weight range | 1.20 |
| weight_range_max_kg | Upper bound of the stated weight range | 1.60 |
| unit_price | Normalised price per standard unit | 1.98 |
| unit_of_measure | Basis for the unit price | per 100g |
| was_price | Previous price where a reduction is shown | 5.25 |
| more_card_price | More Card loyalty price where offered | 3.75 |
| more_card_valid_until | End date of the More Card offer | 2026-04-11 |
| is_price_lock | Boolean — is this line under Price Lock | true |
| price_lock_until | Published end of the Price Lock period | 2026-06-30 |
| promo_type | Nature of the offer | more_card / price_lock / multibuy / price_drop |
| promo_text | Raw offer text exactly as displayed | More Card Price £3.75. Was £4.50 |
The pricing_basis field is the single most important column in a Morrisons schema, and it is the one most commonly missing. Every downstream calculation — category averages, basket comparisons, inflation indices — has to branch on it. A dataset where price sometimes means "per item" and sometimes means "per kilogram", with no field distinguishing them, is not usable for analysis at all.
| Field | Description | Example |
|---|---|---|
| availability_status | Stock state at time of capture | in_stock / out_of_stock / unavailable |
| delivery_postcode | Postcode context used for this capture | BD3 7DL |
| store_id | Store identifier where resolvable | 221 |
| store_format | Format of the store context | supermarket / daily |
| fulfilment_type | Collection method for this capture | delivery / click_collect |
| rating_average | Average customer rating | 4.2 |
| review_count | Number of reviews | 417 |
| captured_at | UTC timestamp of the capture | 2026-03-04T07:05:18Z |
Morrisons operates convenience sites under Morrisons Daily alongside its supermarkets. As with ASDA Express, convenience formats generally price above large stores, so store_format has to be stamped on every row or your price series will contain movements that never happened.
Product description, ingredients, allergen statement, nutrition per 100g, storage and usage instructions, country of origin, and dietary flags. For fresh and own-manufactured lines, country of origin and provenance claims carry more weight at Morrisons than elsewhere — British sourcing is central to its positioning, and brands and researchers actively track those claims.
This is the Morrisons-specific problem, and it is worth being precise about how it fails.
Consider a whole chicken listed at £6.50 per kg, with a typical weight around 1.4kg, so roughly £9.10 at checkout. A parser that grabs the first price element it finds might record 6.50. Downstream, that number sits in a column next to a tin of beans at 1.20, and every average, index and comparison built on that column is now wrong — not by a fixed amount, but by a different factor for every weight-priced line in the dataset.
The failure is invisible. Nothing errors. The file validates. Category averages just drift low, and if nobody has a reference point they may never notice.
Handling it correctly requires three things:
For cross-retailer comparison this matters even more. A Morrisons Market Street chicken and a packaged chicken at Tesco are only comparable on normalised price per kg. Compare the headline prices and you are comparing a rate to a total.
More Card price is a price — a lower number available to loyalty members, captured as a price field with a validity window, structurally similar to Clubcard or Nectar.
Price Lock is a commitment — a statement that a line will hold at its current price for an extended period. It does not necessarily lower today's price at all. Its value in a dataset is as a duration and stability signal, not a discount.
Collapsing these into one promo_type string loses the distinction. Model them as separate fields: more_card_price with its validity date, and is_price_lock with price_lock_until. A product can carry both simultaneously, and that combination is commercially meaningful — a locked line also running a loyalty offer is a line Morrisons is defending hard.
For analysts, the interesting Price Lock question is not "what is locked today" but "which categories has Morrisons chosen to lock, and has that set changed since last quarter". That question only becomes answerable if you store the flag historically with continuity.
As with every UK online grocer, availability and some pricing resolve against a delivery location, and Morrisons Daily convenience sites price differently from supermarkets.
Every capture must be pinned to an explicit, recorded location and format context, stamped as fields on every row at collection time. For national benchmarking, fix one reference context and hold it constant for the life of the dataset. For regional work, run a defined panel in parallel. Never blend formats into a single price series — a supermarket series and a Daily series are two datasets that happen to share SKUs.
The Morrisons online catalogue runs to tens of thousands of active SKUs (third-party catalogue crawls consistently put the full range at around 30,000 products), and fresh lines churn faster than packaged goods — seasonal produce appears and disappears, Market Street ranges shift, and availability fluctuates through the day far more than in ambient categories.
That churn has a scheduling implication. For fresh and Market Street categories, a single daily capture at 6am tells you almost nothing about afternoon availability. If availability tracking is the use case, fresh categories need at least twice-daily capture; ambient categories do not.
Morrisons also sells clothing under Nutmeg. As with George at ASDA, scope it explicitly. Clothing needs size, colour and variant fields that grocery does not, and letting it bleed into a grocery feed distorts SKU counts and category averages.
Beyond that, the standard requirements apply: category-tree discovery that re-walks the hierarchy rather than trusting a static seed list; delisting detection that records disappearance as delisted; change reconciliation on a stable key; and pack-size change detection for the shrinkflation signal.
An illustrative record showing the output schema. Values are synthetic, shown to demonstrate field shape and types — they do not represent live Morrisons pricing. Request a live sample for real current data.
{
"product_id": "108234567",
"product_url": "https://groceries.morrisons.com/products/example-product",
"product_name": "Example Whole British Chicken",
"brand": "Morrisons",
"own_label_tier": "Morrisons",
"is_market_street": true,
"category_path": "Fresh > Meat & Poultry > Chicken",
"gtin_ean": null,
"price": 9.10,
"currency": "GBP",
"pricing_basis": "per_kg",
"price_per_kg": 6.50,
"pack_size": "approx 1.4kg",
"estimated_weight_kg": 1.40,
"weight_range_min_kg": 1.20,
"weight_range_max_kg": 1.60,
"unit_price": 0.65,
"unit_of_measure": "per 100g",
"was_price": null,
"more_card_price": null,
"more_card_valid_until": null,
"is_price_lock": true,
"price_lock_until": "2026-06-30",
"promo_type": "price_lock",
"promo_text": "Price Lock",
"availability_status": "in_stock",
"delivery_postcode": "BD3 7DL",
"store_id": "221",
"store_format": "supermarket",
"rating_average": 4.2,
"review_count": 417,
"captured_at": "2026-03-04T07:05:18Z"
}
Flattened to CSV:
| product_id | product_name | tier | basis | price | per_kg | unit_price | more_card | lock | format | captured_at |
|---|---|---|---|---|---|---|---|---|---|---|
| 108234567 | Whole British Chicken | Morrisons | per_kg | 9.10 | 6.50 | 0.65/100g | — | true | supermarket | 2026-03-04 |
| 108234568 | Baked Beans 410g | Savers | per_item | 0.42 | — | 0.10/100g | — | true | supermarket | 2026-03-04 |
| 108234569 | Mature Cheddar 400g | The Best | per_item | 5.00 | — | 1.25/100g | 4.00 | false | supermarket | 2026-03-04 |
| 108234570 | Loose Bananas | Morrisons | per_kg | — | 1.10 | 0.11/100g | — | false | supermarket | 2026-03-04 |
| 108234568 | Baked Beans 410g | Savers | per_item | 0.55 | — | 0.13/100g | — | true | daily | 2026-03-04 |
Three things this table demonstrates that a simpler schema cannot.
Row one and row four both have pricing_basis: per_kg, but row four has no item price at all — loose bananas have no fixed pack, so only the per-kg rate and normalised unit price exist. A schema requiring a non-null price would have dropped that row entirely.
Rows two and five are the same SKU on the same day at £0.42 in a supermarket and £0.55 in a Morrisons Daily. Without store_format, that reads as a 31% price rise that never occurred.
Row two also shows a Savers line under Price Lock with no loyalty offer — a value line held stable rather than promoted. Row three shows a premium line with a More Card offer and no lock. Those are two opposite commercial strategies, and only a schema that keeps the mechanics separate can distinguish them.
Read https://groceries.morrisons.com/robots.txt and honour it. Confine collection to publicly accessible pages — no logged-in areas, no account data, no More Card account information, no personal data. If a path is disallowed, it is out of scope.
Extract from structured data where it exists rather than CSS selectors. Many retail product pages publish Product schema in JSON-LD, giving name, brand, identifiers, images and price in a form that changes far less often than front-end class names.
A simplified, courteous pattern with pricing-basis handling made explicit:
import json, time, requests
from bs4 import BeautifulSoup
HEADERS = {"User-Agent": "ActowizDataBot/1.0 (+https://actowizsolutions.com/bot)"}
DELAY_SECONDS = 3 # conservative; stay well inside courteous limits
def detect_pricing_basis(unit_text: str | None) -> str | None:
"""Return per_kg or per_item. Never guess — unknown must fail validation."""
if not unit_text:
return None
t = unit_text.lower()
if "kg" in t or "per kilo" in t:
return "per_kg"
if "each" in t or "per item" in t:
return "per_item"
return None
def parse_product(url: str) -> dict | None:
resp = requests.get(url, headers=HEADERS, timeout=30)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
for tag in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "")
except json.JSONDecodeError:
continue
if isinstance(data, dict) and data.get("@type") == "Product":
offer = data.get("offers") or {}
return {
"product_name": data.get("name"),
"brand": (data.get("brand") or {}).get("name"),
"gtin_ean": data.get("gtin13"),
"price": offer.get("price"),
"currency": offer.get("priceCurrency"),
"availability_status": offer.get("availability"),
"pricing_basis": None, # resolved from page-specific parsing
"product_url": url,
}
return None
def crawl(urls: list[str], store_context: dict) -> list[dict]:
out = []
for u in urls:
if (record := parse_product(u)):
record.update(store_context) # stamp context at collection time
out.append(record)
time.sleep(DELAY_SECONDS) # rate limiting is not optional
return out
Note detect_pricing_basis returns None rather than defaulting. That is deliberate. A row with an unresolved pricing basis should fail validation and be flagged for review, because a wrong default silently corrupts every downstream calculation. Failing loudly on 200 rows is far cheaper than quietly mispricing them.
What this does not do: it makes no attempt to evade any protective measure, and it does not handle More Card or Price Lock parsing, which sit in the promotional presentation layer and need page-specific logic — the part requiring ongoing maintenance.
Generic schema checks will not catch the failures that matter here. The validation layer needs:
Formats. CSV and Excel for category and merchandising teams. JSON or JSONL for engineering pipelines. Parquet where volume is high and query cost matters.
Destinations. S3, Google Cloud Storage or Azure Blob; SFTP; direct load into BigQuery, Snowflake or Redshift; or a REST endpoint for on-demand querying.
Delivery shape. A full snapshot ships the entire catalogue state each run. A change log ships only what moved, with change type recorded (price_change, more_card_started, more_card_ended, price_lock_added, price_lock_removed, stock_change, new_listing, delisted). Most mature programmes take a weekly full snapshot for reconciliation plus a daily change log for alerting.
Fresh-specific scheduling. Worth configuring separately at Morrisons: twice-daily capture on fresh and Market Street categories for availability tracking, daily on ambient. Paying for intraday capture across the whole catalogue when only fresh needs it is wasted spend.
Fresh food suppliers and producers are a larger share of the audience here than at other retailers, precisely because of Morrisons' vertical integration. A supplier competing against Morrisons' own manufactured lines needs to see where those lines are priced, and normalised per-kg comparison is the only way to do it.
CPG and FMCG brands monitor their SKUs for price compliance, promotional execution, availability and content accuracy — and specifically for whether an agreed More Card promotion went live as agreed.
Competing grocers benchmark baskets like-for-like. Morrisons is the key benchmark for fresh and butchery pricing among the big four.
Analysts and researchers track food inflation at SKU level. Morrisons fresh lines are particularly useful for meat and produce inflation series, because the per-kg pricing basis makes them directly comparable across time without pack-size confounds.
Price comparison platforms need broad coverage with correct pricing basis, or their fresh comparisons will be visibly wrong to users.
Public data only. Collect what any visitor can see without authenticating. No logged-in pages, no account areas, no More Card account data, no basket data.
No personal data. Prices are not personal data. Customer reviews may contain reviewer names or identifiable content — if you collect reviews, UK GDPR applies and you need a lawful basis, retention policy and minimisation. For most price monitoring, collect review counts and averages only, never review text or author identity.
Database rights. The UK retains a sui generis database right, separate from copyright, protecting substantial investment in obtaining, verifying or presenting database contents. Extracting a substantial part can infringe it. The defensible position is factual price monitoring for analysis and comparison — not republishing a retailer's catalogue as your own product.
Provenance claims. If you are capturing country-of-origin or British sourcing claims for research or compliance purposes, record them verbatim with a timestamp. Paraphrased provenance data is useless as evidence and potentially misleading.
Terms of service. Site terms are contractual and enforceability against non-account-holders varies. Treat them as a real consideration.
Rate limiting as a legal posture. Conduct that impairs a service is where scraping disputes escalate. Conservative volumes are a risk control.
Not legal advice. Take advice from a qualified UK solicitor for your specific programme.
Build in-house if you need one or two ambient categories, refresh weekly, have engineering capacity spare, and can tolerate gaps.
Buy a managed feed if your scope includes fresh. That is the honest dividing line at Morrisons. Ambient packaged goods are a manageable in-house build. Fresh and Market Street lines require pricing-basis detection, weight-range capture, cross-basis normalisation and validation that catches a silent basis failure — and getting any of those wrong produces a dataset that looks fine and is wrong in ways nobody spots until a decision has already been made on it.
Model the cost over three years, not three months, and include the cost of the fresh category analysis that was wrong for a quarter before anyone checked it against a receipt.
Capture the pricing basis as an explicit required field, store both the per-kg rate and the estimated or ranged weight where published, and compute a normalised unit price at ingestion. Never default an unresolved basis to per-item — flag the row for review instead, because a wrong default corrupts every downstream average.
More Card price is a lower price available to loyalty members, captured as a price field with a validity window. Price Lock is a commitment to hold a line at its current price for an extended period, captured as a boolean with an end date. A product can carry both, and that combination signals a line Morrisons is actively defending.
Yes, where they are listed on the public grocery site. They should carry a dedicated flag, because their availability, pricing basis and substitution behaviour differ from packaged lines and analysts usually want to filter them in or out.
Because Morrisons Daily convenience sites generally price above supermarkets, and availability resolves against a delivery location. Without a recorded store format and location context on every row, these appear as unexplained price movements.
Morrisons has not historically offered an open public product API for commercial monitoring, and that remains the position as of 2026 — there is no self-service, publicly documented product or pricing API. Morrisons.com runs on the Ocado Smart Platform and exposes an internal storefront API that third-party tools call directly, but that interface is undocumented and unsupported for external use, not an official developer offering. Structured extraction from public pages is the practical route. If an official data partnership is available for your use case, pursue that first.
Yes, with a product matching layer — EAN where published, fuzzy matching on brand, title and pack size where not. For fresh and variable-weight lines the comparison must be made on normalised price per kg, never on headline price, or you will be comparing a rate against a total.
Collecting publicly displayed factual pricing for analysis is a widely practised commercial activity. The risk areas are personal data, database rights, contractual site terms, and conduct that impairs the service. Stay on public pages, avoid personal data, rate-limit conservatively, and take legal advice for your programme.
Actowiz Solutions delivers UK grocery datasets across Morrisons and the other major UK retailers, with correct variable-weight and per-kg handling, More Card and Price Lock captured as separate fields, store-format context on every row, own-label tier classification, validated schemas and scheduled delivery to S3, SFTP, BigQuery or API.
You can also reach us for all your mobile app scraping, data collection, web scraping , and instant data scraper service requirements!
Our web scraping expertise is relied on by 4,000+ global enterprises including Zomato, Tata Consumer, Subway, and Expedia — helping them turn web data into growth.
Watch how businesses like yours are using Actowiz data to drive growth.
From Zomato to Expedia — see why global leaders trust us with their data.
Backed by automation, data volume, and enterprise-grade scale — we help businesses from startups to Fortune 500s extract competitive insights across the USA, UK, UAE, and beyond.
We partner with agencies, system integrators, and technology platforms to deliver end-to-end solutions across the retail and digital shelf ecosystem.
Explore the 2026 guide to UK supermarket price and product data, covering product catalogs, pricing, promotions, availability, and competitive insights.
Multi-Channel Marketplace Inventory Scraping API helps brands monitor product stock, availability, and inventory changes across Amazon, Flipkart, and Myntra.
Sephora & Trendyol Arabic Market Data Report 2026 delivers UAE e-commerce intelligence on products, pricing, trends, and customer demand.
Whether you're a startup or a Fortune 500 — we have the right plan for your data needs.