Readme

Scrapes offers from SAGA

This services queries from 3 PM to 5 PM all offers listed on SAGA and filters offers cheaper than 700 EUR and sends it via mail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { distance } from "https://esm.town/v/matthiasraimann/street_distance";
import { blob } from "https://esm.town/v/std/blob";
import { JSDOM } from "npm:jsdom";
export async function scrapeSAGA(
areas: Area[],
maxPrice: number,
maxQM: number,
): Promise<Immo[]> {
const html = await fetch("https://www.saga.hamburg/immobiliensuche?Kategorie=APARTMENT").then(res => res.text());
const exploredBefore = (await blob.getJSON("immos") || []) as string[];
const document = new JSDOM(html).window.document;
const immosHTML = Array.from(document.querySelectorAll(".immo-item")).filter(i => i.id.startsWith("APARTMENT"));
const immos = await Promise.all(immosHTML.map(async immo => {
const detailURL = immo.querySelector("a")?.getAttribute("href") ?? "Kein Link";
if (exploredBefore.includes(detailURL)) return undefined;
else exploredBefore.push(detailURL);
console.log(`looking at: ${detailURL}`);
const price = parsePriceToNumber(immo.querySelector("p[data-fullcosts]")?.textContent ?? "Kein Preis");
if (price > maxPrice) return;
const qm = parsePriceToNumber(immo.querySelector("p[data-livingspace]")?.textContent ?? "Kein Preis");
if (qm > maxQM) return;
const title = immo.querySelector("h3.text-primary")?.textContent ?? "Kein Titel";
const rooms = parsePriceToNumber(immo.querySelector("p[data-rooms]")?.textContent ?? "Kein Preis");
const location = immo.querySelector("p.pb-3")?.textContent ?? "Adresse nicht gefunden";
const detailsHTML = await fetch("https://www.saga.hamburg" + detailURL).then(res => res.text());
const details = new JSDOM(detailsHTML).window.document;
const exposeURL = details.querySelector("a.bg-primary-light[rel=\"noopener noreferrer\"]")?.getAttribute("href");
const isInsideArea = await Promise.all(areas.map(async a => {
return (await distance(a.center, location).catch(() => 0)) <= a.meters;
})).then(bools => bools.some(b => b));
if (!isInsideArea) {
console.log(`Is too far off: ${exposeURL}`);
return undefined;
}
const renderImmo = () => {
return `\n${title}\nAdresse: ${location}\nPreis: ${price}\nBewerbung: ${exposeURL}`;
};
return { title, price, qm, rooms, exposeURL, renderImmo, detailURL };
})).then(mms => mms.filter(i => i !== undefined));
blob.setJSON("immos", exploredBefore);
return immos.length
? immos
: Promise.reject("No immos found!");
}
function parsePriceToNumber(priceString: string): number {
const val = +priceString.replace(/[^0-9,]/g, "").replace(",", ".");
return isNaN(val) ? 0 : val;
}
type Immo = {
title: any;
price: number;
qm: number;
rooms: number;
exposeURL: string;
detailURL: string;
renderImmo: () => string;
};
type Area = { center: string; meters: number };
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
Nobody has commented on this val yet: be the first!
July 18, 2024