DEV Community

Russell Yapp
Russell Yapp

Posted on

How to get Morrisons prices as rows, and match them without barcodes

Morrisons publishes no barcode on its website. That is the main thing to know if you want to compare its products with other shops. Most price data is joined on the barcode (the EAN). Without one you need another way to say that two rows are the same product.

This post shows how to get Morrisons prices as rows with an Apify Actor I publish, and how the Actor's match fields work. It says how well the matching has been checked, and where it is weak.

Everything below is shelf prices as shown on the shops' websites when the run reads them. They are not a promise of the price in a store.

The Actor is UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi, Morrisons, published by Yappman Data: https://apify.com/yappman/uk-supermarket-price-scraper

Morrisons became one of its default shops on 9 November 2026, in version 0.2.32. A run with no shop list now reads all five shops.

An example Task that searches baked beans at all five, Morrisons included, is here: https://apify.com/yappman/uk-supermarket-price-scraper/examples/uk-baked-beans-five-shops

Get Morrisons rows

Start with a small run. This input searches one product at Morrisons and Tesco and saves at most 20 rows:

{
  "mode": "search",
  "queries": ["baked beans"],
  "retailers": ["morrisons", "tesco"],
  "maxItems": 20
}
Enter fullscreen mode Exit fullscreen mode

You get one row per product. The fields that matter for Morrisons:

  • price: the current shelf price in pounds.
  • loyaltyPrice: the More Card price, when there is one. price stays the price any shopper pays.
  • wasPrice and promotionText: other reductions and offers, such as Buy 2 for £4.
  • packSize and unitPrice: the pack size and the price per litre, kilo or each.
  • inStock: Morrisons' own availability flag.
  • url: the product page.
  • scrapedAt: when the row was read.

Items sold by weight, such as loose fruit and meat, are priced for their typical weight. packSize shows it, for example 0.25kg, and the per-kilo figure is in unitPrice.

What it costs

The Actor charges $0.005 for each run and $0.0025 for each product row saved. Those are Free plan prices. Higher Apify plans pay less per product.

The input above costs at most $0.055 ($0.005 plus 20 rows). A 20-row test run across all five shops on 9 November 2026 cost exactly that. A default run, which searches milk at all five shops and saves up to 100 rows, costs at most $0.255.

Set maxItems every time. The Actor also has a catalogue mode that reads the whole Morrisons category tree. A check run on 29 September 2026 returned about 26,000 rows, which would cost about $66 on the Free plan. That is why I keep maxItems small until the first rows look right.

What no barcode means

For Tesco, Sainsbury's and Asda, ean is filled on nearly every row. For Morrisons it is an empty array on every row. So you cannot join Morrisons to another shop on ean.

The Actor gives you three fields for this instead:

  • matchKey: a normalised version of brand, name and pack size.
  • matchBasis: which parts went into the key. The values are brand-name-size, name-size, brand-name and name.
  • matchConfidence: a number from 0 to 1.

Here is a row from the README, a Sainsbury's one, trimmed to the main match fields:

{
  "retailer": "sainsburys",
  "name": "Sainsbury's British Semi Skimmed Milk 2.27L (4 pint)",
  "packSize": "2.27L",
  "matchKey": "sainsburys british semi skimmed milk 2.27l 4 pint 4pt",
  "matchBasis": "brand-name-size",
  "matchConfidence": 0.97
}
Enter fullscreen mode Exit fullscreen mode

matchConfidence is not a score for the row. It is a fixed value for each matchBasis. In the sample above, 0.97 is the value for brand-name-size. The name basis, at 0.50, is an assumed value, not a measured one.

How well it has been checked

I checked matchKey against barcodes on the 4 October 2026 weekly snapshot. Among the pairs of products at different shops that shared a matchKey and could be checked against a barcode, 97.6% were the same product (4,124 of 4,225).

Two limits on that figure:

  • It was measured on Tesco, Sainsbury's and Asda rows, the shops that publish a barcode. Morrisons rows use the same per-basis values, but they are unmeasured, because there is no barcode to check against.
  • It only counts pairs that the key found. matchKey joins products that are named the same way, so it finds only some of the pairs a barcode would. Shops word the same product differently.

Two products that differ only in something the name leaves out, such as a scent or a flavour, can share a matchKey. matchConfidence does not account for that. Before you rely on a name-only join, check unitPrice and packSize.

Match in a few lines

This joins a run's rows on matchKey and keeps the pairs where one row is Morrisons. It reads a JSON file of rows downloaded from the run.

import json
from collections import defaultdict

with open("rows.json", encoding="utf-8") as f:
    rows = json.load(f)

by_key = defaultdict(list)
for r in rows:
    if r.get("matchKey"):
        by_key[r["matchKey"]].append(r)

for key, group in by_key.items():
    shops = {r["retailer"] for r in group}
    if "morrisons" in shops and len(shops) > 1:
        for r in group:
            unit = r.get("unitPrice") or {}
            print(r["retailer"], r.get("brand"), r["name"], r["packSize"],
                  unit.get("value"), unit.get("measure"), r["price"])
        print()
Enter fullscreen mode Exit fullscreen mode

Some shops leave the brand out of the product name, so the snippet prints brand too.

Read the printed pairs before you trust them. Look at packSize and unitPrice first. If two rows have the same key but different pack sizes, one of them is wrong for your purpose.

Things that catch people out

  • Two ids for one item. Morrisons sometimes lists the same item twice, under two product ids with slightly different names, for example "British Semi Skimmed Milk 2 Pint" and "... 2 Pints". Both are returned and both are charged. Since 4 November 2026 the matchKey treats unit words as singular, so these two give the same key. Check for duplicates before you count products.
  • The key changed on 4 November 2026. If you join older rows to newer ones, join on matchKeyV1, which carries the key as it was built before. It is on every row for one release and is then removed, with a notice. Older rows, including the earlier rows of my free daily dataset, have the old key in matchKey and no matchKeyV1.
  • 50 products per page. A Morrisons listing page carries data for only its first 50 products. Search returns at most 50 products per query. In category mode the run summary has a listingShortfall count of the products that were not reached.
  • Aldi. Aldi also publishes no barcode and sells mostly its own labels. In the 4 October snapshot no Aldi matchKey equalled another shop's. For Aldi, compare by unitPrice.

Using the data

If you pay to run the Actor you may show the prices in your own app and keep a copy for up to about a week, as long as you show when the prices were read; you may not resell or pass the data on in bulk; the shops' own content is not ours, and their own terms apply to it. Product images, descriptions and ingredient text belong to the shops, and I do not license them.

Limits

  • The Actor reads what Morrisons shows on its groceries website. It does not log in, and it does not read baskets, accounts or delivery slots.
  • A run returns that day's prices. It does not compare them with earlier runs.
  • The Actor's basket route on the Standby URL is separate, and it does not cover Morrisons.

This is an independent tool. It is not affiliated with or endorsed by Morrisons, Tesco, Sainsbury's, Asda or Aldi.

Top comments (1)

Collapse
 
0xgollum profile image
0xGollum •

Nice that you publish the 97.6% figure with its limits instead of just a confidence badge.

One way to get a partial measure for Morrisons without its barcodes: go through the other shops. If a Morrisons matchKey equals a key that Tesco and Sainsbury's rows also share, and those two agree on the same EAN, the Morrisons row inherits that EAN with decent odds. For branded goods that should cover a good chunk, and the disagreements are a cheap sample of where the key fails.

For the scent/flavour trap, a second signal might help: branded products often reuse the manufacturer's packshot across retailers, so a small perceptual hash of the image could catch "same name, different variant". Own labels stay the blind spot, same as Aldi.