- Reset master to upstream/main (16,697 commits) - Overlay 2,271 local-only files (skills, tools, workspace, configs, apps) - Restore IDENTITY.md and USER.md templates - Build verified, gateway running, Discord working Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Scrape en pris fra en URL via Scrapling. Returnerer heltal eller N/A."""
|
|
import sys, re, json
|
|
|
|
if len(sys.argv) < 2:
|
|
print("N/A"); sys.exit(1)
|
|
|
|
url = sys.argv[1]
|
|
|
|
try:
|
|
from scrapling import StealthyFetcher
|
|
except ImportError:
|
|
print("ERROR:scrapling-not-installed"); sys.exit(1)
|
|
|
|
try:
|
|
page = StealthyFetcher.fetch(url, stealthy_headers=True, auto_match=False)
|
|
except Exception as e:
|
|
print(f"ERROR:{e}"); sys.exit(1)
|
|
|
|
# Metode 1: JSON-LD structured data
|
|
for el in page.css('script[type="application/ld+json"]'):
|
|
try:
|
|
obj = json.loads(el.get().extract())
|
|
offers = obj.get('offers', {})
|
|
if isinstance(offers, list): offers = offers[0] if offers else {}
|
|
p = offers.get('price') or offers.get('lowPrice')
|
|
if p:
|
|
price = float(str(p).replace(',', '.'))
|
|
if 100 < price < 100000:
|
|
print(int(price)); sys.exit(0)
|
|
except Exception: pass
|
|
|
|
# Metode 2: itemprop="price" content attribut
|
|
for el in page.css('[itemprop="price"]'):
|
|
try:
|
|
content = el.get().extract()
|
|
m = re.search(r'content=["\']([0-9.,]+)["\']', content)
|
|
if m:
|
|
p = float(m.group(1).replace(',', '.'))
|
|
if 100 < p < 100000:
|
|
print(int(p)); sys.exit(0)
|
|
except Exception: pass
|
|
|
|
# Metode 3: Next.js __NEXT_DATA__
|
|
for el in page.css('script#__NEXT_DATA__'):
|
|
try:
|
|
raw = el.get().extract()
|
|
data = json.loads(raw.split('>', 1)[1].rsplit('<', 1)[0])
|
|
prices = []
|
|
def find_prices(obj, depth=0):
|
|
if depth > 15: return
|
|
if isinstance(obj, dict):
|
|
for k, v in obj.items():
|
|
if k in ('price', 'lowestPrice', 'minPrice', 'amount'):
|
|
try:
|
|
p = float(str(v).replace(',', '.'))
|
|
if 100 < p < 100000: prices.append(p)
|
|
except Exception: pass
|
|
else: find_prices(v, depth+1)
|
|
elif isinstance(obj, list):
|
|
for item in obj: find_prices(item, depth+1)
|
|
find_prices(data)
|
|
if prices: print(int(min(prices))); sys.exit(0)
|
|
except Exception: pass
|
|
|
|
# Metode 4: Synlig tekst (get_all_text) — undgår afbetalingspriser og SVG-data
|
|
visible = page.get_all_text(separator="\n")
|
|
# Ekskluder linjer med afdrag (kr./md, kr/måned, betalinger af)
|
|
lines = [l for l in visible.split("\n")
|
|
if not re.search(r'kr\./md|kr/m[åa]ned|betalinger\s+af', l, re.I)]
|
|
text4 = "\n".join(lines)
|
|
matches4 = re.findall(r'(\d{1,2}[.\s]?\d{3}|\d{3,5})\s*(?:\xa0|[\s\u202f])*(?:kr\.?|DKK|,-)', text4)
|
|
prices4 = []
|
|
for raw in matches4:
|
|
try:
|
|
p = float(re.sub(r'[\s.]', '', raw))
|
|
if 100 < p < 100000: prices4.append(p)
|
|
except: pass
|
|
if prices4: print(int(min(prices4))); sys.exit(0)
|
|
|
|
print("N/A")
|