Kleinanzeigen Flächen-Preisrechner (Aktualisiert 2025)

Zeigt €/m² für neue Kleinanzeigen-Struktur, färbt nach regionalen Preisen (3-stellig PLZ), Navigation a/d

// ==UserScript==
// @name         Kleinanzeigen Flächen-Preisrechner (Aktualisiert 2025)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Zeigt €/m² für neue Kleinanzeigen-Struktur, färbt nach regionalen Preisen (3-stellig PLZ), Navigation a/d
// @author       Du
// @license      MIT
// @icon         https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://kleinanzeigen.de&size=64
// @match        https://www.kleinanzeigen.de/s-wohnung-mieten/*
// @match        https://www.kleinanzeigen.de/s-haus-mieten/*
// @match        https://www.kleinanzeigen.de/s-wohnung-kaufen/*
// @match        https://www.kleinanzeigen.de/s-haus-kaufen/*
// @match        https://www.kleinanzeigen.de/s-wg-zimmer/*
// @match        https://www.kleinanzeigen.de/s-immobilien/*
// @match        https://www.kleinanzeigen.de/s-grundstuecke-gaerten/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const REGIONAL_STORAGE_KEY = 'kleinanzeigenRegionalSqmPrices_3digit';
    const PLZ_PREFIX_LENGTH = 3;
    let globalSqmPricesByRegion = loadRegionalSqmPrices();

    GM_addStyle(`
        .ka-sqm-price-display { 
            font-size: 1.3em; 
            color: #2a67ca; 
            font-weight: bold; 
            margin-left: 8px;
            display: inline-block;
            background: rgba(42, 103, 202, 0.1);
            padding: 2px 6px;
            border-radius: 4px;
        }
        
        .ka-sqm-container {
            margin: 4px 0;
        }
        
        /* Farbkodierung basierend auf regionalen Preisen */
        .ka-price-low { 
            background-color: rgba(144, 238, 144, 0.3) !important;
            border-left: 4px solid #90ee90;
        }
        .ka-price-medium { 
            background-color: rgba(255, 210, 100, 0.3) !important;
            border-left: 4px solid #ffd264;
        }
        .ka-price-high { 
            background-color: rgba(255, 127, 127, 0.3) !important;
            border-left: 4px solid #ff7f7f;
        }
        .ka-price-uniform { 
            background-color: rgba(200, 200, 200, 0.3) !important;
            border-left: 4px solid #c8c8c8;
        }
        
        /* Artikelstil verbessern */
        article {
            padding: 8px !important;
            margin: 4px 0 !important;
            border-radius: 6px;
            transition: all 0.2s ease;
        }
    `);

    function loadRegionalSqmPrices() {
        return GM_getValue(REGIONAL_STORAGE_KEY, {});
    }

    function saveRegionalSqmPrices() {
        GM_setValue(REGIONAL_STORAGE_KEY, globalSqmPricesByRegion);
    }

    function calculateStorageInfo(data) {
        const jsonString = JSON.stringify(data);
        const sizeBytes = new TextEncoder().encode(jsonString).length;
        const sizeMegabytes = (sizeBytes / (1024 * 1024)).toFixed(4);

        let totalAdEntries = 0;
        for (const region in data) {
            if (data.hasOwnProperty(region)) {
                totalAdEntries += Object.keys(data[region]).length;
            }
        }
        return { sizeMegabytes, totalAdEntries };
    }

    function confirmAndClearAllRegionalSqmPrices() {
        const currentData = GM_getValue(REGIONAL_STORAGE_KEY, {});
        const { sizeMegabytes, totalAdEntries } = calculateStorageInfo(currentData);

        const confirmationMessage = `Möchten Sie wirklich alle gespeicherten regionalen m² Preisdaten löschen?\n\n` +
                                  `Anzahl erfasste Anzeigen: ${totalAdEntries}\n` +
                                  `Speichergröße: ${sizeMegabytes} MB\n\n` +
                                  `Diese Aktion kann nicht rückgängig gemacht werden.`;

        if (confirm(confirmationMessage)) {
            GM_deleteValue(REGIONAL_STORAGE_KEY);
            globalSqmPricesByRegion = {};
            alert('Alle regionalen m² Preise gelöscht. Seite wird neu eingefärbt.');
            processAdItems();
        } else {
            alert('Löschvorgang abgebrochen.');
        }
    }

    GM_registerMenuCommand("Alle regionalen m² Preise löschen...", confirmAndClearAllRegionalSqmPrices, "L");

    function parsePrice(priceString) {
        if (!priceString) return null;
        const cleanedPrice = priceString.replace(/\s*VB\s*/i, '')
                                       .replace(/\s*€\s*/g, '')
                                       .replace(/\./g, '')
                                       .replace(/,/g, '.')
                                       .trim();
        
        if (cleanedPrice.toLowerCase() === 'zu verschenken') return 0;
        if (cleanedPrice.toLowerCase().includes('anfrage')) return null;
        
        const price = parseFloat(cleanedPrice);
        return isNaN(price) ? null : price;
    }

    function parseArea(areaString) {
        if (!areaString) return null;
        const match = areaString.match(/([\d\.,]+)\s*m²/i);
        if (match && match[1]) {
            const cleanedArea = match[1].replace(/\./g, '').replace(/,/g, '.');
            const area = parseFloat(cleanedArea);
            return isNaN(area) || area === 0 ? null : area;
        }
        return null;
    }

    function removeColorClasses(element) {
        element.classList.remove('ka-price-low', 'ka-price-medium', 'ka-price-high', 'ka-price-uniform');
    }

    function getPlzPrefixFromText(text) {
        if (!text) return null;
        const plzMatch = text.match(/\b(\d{5})\b/);
        if (plzMatch && plzMatch[1]) {
            return plzMatch[1].substring(0, PLZ_PREFIX_LENGTH);
        }
        return null;
    }

    function getAdIdFromArticle(article) {
        // Versuche Ad-ID aus href oder anderen Attributen zu extrahieren
        const links = article.querySelectorAll('a[href*="/s-anzeige/"]');
        if (links.length > 0) {
            const href = links[0].href;
            const match = href.match(/\/(\d+)-\d+-\d+$/);
            if (match) return match[1];
        }
        // Fallback: Generiere eine ID basierend auf Inhalt
        return 'ad_' + btoa(article.textContent.substring(0, 50)).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
    }

    function processAdItems() {
        globalSqmPricesByRegion = loadRegionalSqmPrices();
        const articles = document.querySelectorAll('article');
        let mainStorageHasChanged = false;
        const itemsByPlzOnPage = {};

        console.log(`[Kleinanzeigen Script] Verarbeite ${articles.length} Artikel`);

        articles.forEach(article => {
            removeColorClasses(article);
            
            // Entferne bereits vorhandene Preisanzeigen
            const existingSqmDisplay = article.querySelector('.ka-sqm-container');
            if (existingSqmDisplay) existingSqmDisplay.remove();

            const adId = getAdIdFromArticle(article);
            const articleText = article.textContent;
            
            // PLZ aus Text extrahieren
            const plzPrefix = getPlzPrefixFromText(articleText);
            if (!plzPrefix) return;

            // Fläche aus Text extrahieren (Format: "XX m² · X Zi.")
            const areaMatch = articleText.match(/(\d+(?:,\d+)?)\s*m²\s*·/);
            const area = areaMatch ? parseArea(areaMatch[0]) : null;

            // Preis extrahieren (kommt normalerweise nach der Fläche)
            const priceMatches = articleText.match(/(\d{1,3}(?:\.\d{3})*(?:,\d{2})?)\s*€/g);
            let price = null;
            
            if (priceMatches && priceMatches.length > 0) {
                // Nehme den ersten gefundenen Preis (Hauptpreis)
                price = parsePrice(priceMatches[0]);
            }

            if (price !== null && area !== null && area > 0 && price > 0) {
                const pricePerSqm = price / area;
                const roundedPricePerSqm = Math.round(pricePerSqm);

                // Regional storage initialisieren
                if (!globalSqmPricesByRegion[plzPrefix]) {
                    globalSqmPricesByRegion[plzPrefix] = {};
                }
                if (!itemsByPlzOnPage[plzPrefix]) {
                    itemsByPlzOnPage[plzPrefix] = [];
                }

                // Preis speichern
                if (globalSqmPricesByRegion[plzPrefix][adId] !== roundedPricePerSqm) {
                    globalSqmPricesByRegion[plzPrefix][adId] = roundedPricePerSqm;
                    mainStorageHasChanged = true;
                }

                itemsByPlzOnPage[plzPrefix].push({ element: article, adId: adId, precisePricePerSqm: pricePerSqm });

                // Preis/m² Display hinzufügen
                const sqmContainer = document.createElement('div');
                sqmContainer.classList.add('ka-sqm-container');
                
                const sqmPriceElement = document.createElement('span');
                sqmPriceElement.classList.add('ka-sqm-price-display');
                sqmPriceElement.textContent = `${pricePerSqm.toFixed(2).replace('.', ',')} €/m²`;
                
                sqmContainer.appendChild(sqmPriceElement);
                
                // Einfügen nach dem letzten Link im Artikel
                const lastLink = article.querySelector('a[href*="/s-anzeige/"]:last-of-type');
                if (lastLink) {
                    lastLink.parentNode.insertBefore(sqmContainer, lastLink.nextSibling);
                } else {
                    article.appendChild(sqmContainer);
                }

                console.log(`[Kleinanzeigen Script] PLZ: ${plzPrefix}, Preis: ${price}€, Fläche: ${area}m², €/m²: ${pricePerSqm.toFixed(2)}`);
            }
        });

        if (mainStorageHasChanged) {
            saveRegionalSqmPrices();
        }

        // Farbkodierung anwenden
        for (const plzKey in itemsByPlzOnPage) {
            const itemsInThisRegionOnPage = itemsByPlzOnPage[plzKey];
            const allStoredPricesInThisRegion = Object.values(globalSqmPricesByRegion[plzKey] || {}).filter(p => typeof p === 'number');

            if (allStoredPricesInThisRegion.length < 1) continue;

            const minRegionalPrice = Math.min(...allStoredPricesInThisRegion);
            const maxRegionalPrice = Math.max(...allStoredPricesInThisRegion);
            const regionalRange = maxRegionalPrice - minRegionalPrice;

            if (regionalRange === 0) {
                itemsInThisRegionOnPage.forEach(data => data.element.classList.add('ka-price-uniform'));
                continue;
            }

            const lowerBound = minRegionalPrice + regionalRange / 3;
            const upperBound = minRegionalPrice + (regionalRange * 2) / 3;

            itemsInThisRegionOnPage.forEach(data => {
                const itemElement = data.element;
                const roundedPriceForThisAd = globalSqmPricesByRegion[plzKey][data.adId];

                if (typeof roundedPriceForThisAd !== 'number') return;

                if (roundedPriceForThisAd <= lowerBound) {
                    itemElement.classList.add('ka-price-low');
                } else if (roundedPriceForThisAd <= upperBound) {
                    itemElement.classList.add('ka-price-medium');
                } else {
                    itemElement.classList.add('ka-price-high');
                }
            });

            console.log(`[Kleinanzeigen Script] PLZ ${plzKey}: ${allStoredPricesInThisRegion.length} gespeicherte Preise, Range: ${minRegionalPrice}-${maxRegionalPrice} €/m²`);
        }
    }

    // Hauptverarbeitung starten
    processAdItems();

    // MutationObserver für dynamische Inhalte
    const observer = new MutationObserver(function(mutationsList) {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                let hasNewArticles = false;
                for (const addedNode of mutationsList) {
                    if (addedNode.nodeType === Node.ELEMENT_NODE && 
                        (addedNode.matches && (addedNode.matches('article') || addedNode.querySelector('article')))) {
                        hasNewArticles = true;
                        break;
                    }
                }
                if (hasNewArticles) {
                    setTimeout(processAdItems, 350);
                    return;
                }
            }
        }
    });

    const observerTarget = document.body;
    observer.observe(observerTarget, { childList: true, subtree: true });

    // Tastaturnavigation (a/d)
    document.addEventListener('keydown', function(event) {
        const targetTagName = event.target.tagName.toLowerCase();
        if (targetTagName === 'input' || targetTagName === 'textarea' || event.target.isContentEditable) {
            return;
        }

        const key = event.key.toLowerCase();
        if (key === 'a') {
            // Vorherige Seite
            const prevLinks = document.querySelectorAll('a[href*="seite:"]:not([href*="seite:2"]), a[href*="/c203"]:not([href*="seite:"])');
            if (prevLinks.length > 0) {
                prevLinks[prevLinks.length - 1].click(); // Letzter Link ist meist "Vorherige"
            }
        } else if (key === 'd') {
            // Nächste Seite
            const nextLinks = document.querySelectorAll('a[href*="seite:"]');
            if (nextLinks.length > 0) {
                // Finde den Link mit der nächsthöheren Seitenzahl
                const currentPage = window.location.href.match(/seite:(\d+)/) ? parseInt(window.location.href.match(/seite:(\d+)/)[1]) : 1;
                const targetPage = currentPage + 1;
                const nextLink = Array.from(nextLinks).find(link => link.href.includes(`seite:${targetPage}`));
                if (nextLink) {
                    nextLink.click();
                } else if (nextLinks.length > 0) {
                    nextLinks[0].click(); // Fallback: erster Link
                }
            }
        }
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址