2025-10-18 03:26:42 +00:00
|
|
|
import re
|
|
|
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
|
|
|
|
import httpx
|
2024-09-29 05:04:10 +00:00
|
|
|
|
|
|
|
|
HEADERS = {
|
2025-10-18 03:26:42 +00:00
|
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
|
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
|
|
|
"Accept-Language": "en-US,en;q=0.5",
|
|
|
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
|
|
|
"DNT": "1",
|
|
|
|
|
"Sec-GPC": "1",
|
|
|
|
|
"Connection": "keep-alive",
|
|
|
|
|
"Upgrade-Insecure-Requests": "1",
|
|
|
|
|
"Sec-Fetch-Dest": "document",
|
|
|
|
|
"Sec-Fetch-Mode": "navigate",
|
|
|
|
|
"Sec-Fetch-Site": "none",
|
|
|
|
|
"Sec-Fetch-User": "?1",
|
|
|
|
|
"Pragma": "no-cache",
|
|
|
|
|
"Cache-Control": "no-cache",
|
2024-09-29 05:04:10 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
2024-09-29 05:04:10 +00:00
|
|
|
async def _get_cookies(client):
|
|
|
|
|
# Make a request to https://www.coles.com.au/ as if we were a normal browser, then return the cookies
|
2025-10-18 03:26:42 +00:00
|
|
|
response = await client.get(
|
|
|
|
|
"https://www.coles.com.au/product/coles-strawberries-250g-5191256",
|
|
|
|
|
headers=HEADERS,
|
|
|
|
|
follow_redirects=True,
|
|
|
|
|
)
|
|
|
|
|
version = re.findall(r"202[4-9][01]\d[0-2]\d.02_v\d.\d\d.\d", response.text)
|
2024-10-24 11:14:05 +00:00
|
|
|
|
|
|
|
|
if len(version) == 0:
|
2025-10-18 03:26:42 +00:00
|
|
|
raise Exception("Could not find the Coles API")
|
|
|
|
|
|
2024-10-24 11:14:05 +00:00
|
|
|
return dict(response.cookies), version[0]
|
2024-09-29 05:04:10 +00:00
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
|
|
|
|
def _get_package_size(size: str) -> Tuple[int, str]:
|
2024-09-29 05:04:10 +00:00
|
|
|
if size:
|
2025-10-18 03:26:42 +00:00
|
|
|
match = re.match(r"(\d+)(.*)", size)
|
2024-09-29 05:04:10 +00:00
|
|
|
if match:
|
|
|
|
|
return int(match.group(1)), match.group(2)
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
return 1, "items"
|
|
|
|
|
|
2024-09-29 05:04:10 +00:00
|
|
|
|
|
|
|
|
def _get_client():
|
|
|
|
|
return httpx.AsyncClient()
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
2024-10-24 11:14:05 +00:00
|
|
|
api_details = None
|
2025-10-18 03:26:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _request_details(product_id: str) -> Optional[dict]:
|
2024-10-24 11:14:05 +00:00
|
|
|
global api_details
|
2024-09-29 05:04:10 +00:00
|
|
|
|
|
|
|
|
async with _get_client() as client:
|
2024-10-24 11:14:05 +00:00
|
|
|
if not api_details:
|
|
|
|
|
api_details = await _get_cookies(client)
|
2024-09-29 05:04:10 +00:00
|
|
|
|
2024-10-24 11:14:05 +00:00
|
|
|
cookies, api_version = api_details
|
|
|
|
|
|
|
|
|
|
url = _get_product_details_url(api_version, product_id)
|
2024-09-29 05:04:10 +00:00
|
|
|
try:
|
2025-10-18 03:26:42 +00:00
|
|
|
response = await client.get(
|
|
|
|
|
url, headers=HEADERS, follow_redirects=True, cookies=cookies
|
|
|
|
|
)
|
2024-09-29 05:04:10 +00:00
|
|
|
response.raise_for_status()
|
|
|
|
|
return response.json()
|
2024-10-28 06:14:16 +00:00
|
|
|
except httpx.HTTPError as ne:
|
2024-09-29 05:04:10 +00:00
|
|
|
print(ne)
|
2024-10-24 11:14:05 +00:00
|
|
|
api_details = None
|
|
|
|
|
|
2024-09-29 05:04:10 +00:00
|
|
|
return None
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
2024-10-24 11:14:05 +00:00
|
|
|
def _get_product_details_url(version: str, product_id: str) -> str:
|
2024-09-29 05:04:10 +00:00
|
|
|
# https://www.coles.com.au/_next/data/20240926.02_v4.18.0/en/product/cadbury-favourites-boxed-chocolate-340g-3571992.json?slug=cadbury-favourites-boxed-chocolate-340g-3571992
|
|
|
|
|
# https://www.coles.com.au/_next/data/20240926.02_v4.18.0/en/product/coles-blueberries-170g-3571948.json?slug=coles-blueberries-170g-3571948
|
2025-10-18 03:26:42 +00:00
|
|
|
#
|
|
|
|
|
return f"https://www.coles.com.au/_next/data/{version}/en/product/{product_id}.json"
|
2024-09-29 05:04:10 +00:00
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
|
|
|
|
def get_product_id(url: str) -> Optional[str]:
|
2024-09-29 05:04:10 +00:00
|
|
|
# https://www.coles.com.au/product/cadbury-favourites-boxed-chocolate-340g-3571992
|
2025-10-18 03:26:42 +00:00
|
|
|
regex = r"https://www.coles.com.au/product/([^/]+)/?.*"
|
2024-09-29 05:04:10 +00:00
|
|
|
match = re.match(regex, url)
|
|
|
|
|
if match:
|
|
|
|
|
return match.group(1)
|
|
|
|
|
return None
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
|
|
|
|
|
async def scrape(product_id: str) -> Tuple[dict, dict]:
|
2024-10-24 11:14:05 +00:00
|
|
|
raw_data = await _request_details(product_id)
|
2025-10-18 03:26:42 +00:00
|
|
|
if raw_data is None:
|
|
|
|
|
raw_data = {"pageProps": {"product": {"size": "", "images": []}}}
|
|
|
|
|
|
|
|
|
|
product = raw_data["pageProps"]["product"]
|
2024-09-29 05:04:10 +00:00
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
quantity, unit = _get_package_size(product["size"])
|
2024-09-29 05:04:10 +00:00
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
img_prefix = "https://shop.coles.com.au"
|
|
|
|
|
images = product["images"][0]
|
2024-09-29 05:04:10 +00:00
|
|
|
product_data = {
|
2025-10-18 03:26:42 +00:00
|
|
|
"name": product["name"],
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
"unit": unit,
|
|
|
|
|
"img_small": (img_prefix + images["thumb"]["path"]) if images else None,
|
|
|
|
|
"img_large": (img_prefix + images["full"]["path"]) if images else None,
|
2024-09-29 05:04:10 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-18 03:26:42 +00:00
|
|
|
return product_data, raw_data
|