您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Erfasst sichtbare Inhalte und Metadaten als JSONL für LLM-Verarbeitung. Unterstützt kontinuierliches Sammeln über mehrere Seiten (persistenter Modus).
当前为
// ==UserScript== // @name Kleinanzeigen-Datenexport (TO LLM) 🚀 // @namespace http://tampermonkey.net/ // @version 19.0 // @description Erfasst sichtbare Inhalte und Metadaten als JSONL für LLM-Verarbeitung. Unterstützt kontinuierliches Sammeln über mehrere Seiten (persistenter Modus). // @author Assistant & User // @license MIT // @match https://www.kleinanzeigen.de/* // @icon https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://kleinanzeigen.de&size=64 // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // Globale Variablen let pickerButton = null; let longPressTimer = null; const LONG_PRESS_THRESHOLD = 500; // ms, Wie lange der Klick gehalten werden muss für Langklick let isLockedMode = false; // true wenn im kontinuierlichen Sammel-Modus let allCollectedData = []; // Speichert JSONL-Objekte aus allen gesammelten Seiten let scriptErrors = []; // Speichert Skript-interne Fehler let countdownInterval = null; // Für den Countdown-Timer let lastKnownUrl = window.location.href; // Für die Erkennung von Seitenwechseln let mutationObserver = null; // Für DOM-Änderungen in SPAs const defaultStylesCache = new Map(); // Cache für Standard-CSS-Eigenschaften let processingPage = false; // Flag, um mehrfache Verarbeitung bei schnellen DOM-Änderungen zu verhindern // Schlüssel für die Speicherung des Modus-Status const STORAGE_KEY_LOCKED_MODE = 'kleinanzeigen_picker_locked_mode'; // Startzeit für Skript-Laufzeitmessung (für Metadaten) const scriptStartTime = performance.now(); // CSS-Stile für den Button und Benachrichtigungen GM_addStyle(` #element-picker-btn { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); width: 100px; /* Länglicher */ height: 50px; /* Länglicher */ background: #007185; border: none; border-radius: 8px; /* Leicht abgerundet */ cursor: pointer; z-index: 9999; box-shadow: 0 2px 10px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; transition: background-color 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease; color: white; font-size: 16px; font-weight: bold; text-align: center; line-height: 1.2; padding: 5px; } #element-picker-btn:hover { background-color: #005a6b; opacity: 0.9; } #element-picker-btn.active { background: #dc3545; animation: pulse 1s infinite; } #element-picker-btn.locked { background: #17a2b8; animation: pulse-locked 1s infinite; } #element-picker-btn.processing { opacity: 0.6; cursor: wait; } /* Visueller Hinweis während der Datenverarbeitung */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } } @keyframes pulse-locked { 0% { box-shadow: 0 0 0 0 rgba(23, 162, 184, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(23, 162, 184, 0); } 100% { box-shadow: 0 0 0 0 rgba(23, 162, 184, 0); } } .picker-notification { position: fixed; top: 20px; right: 20px; background: #28a745; color: white; padding: 10px 20px; border-radius: 5px; z-index: 10000; font-family: Arial, sans-serif; font-size: 14px; animation: slideIn 0.3s ease-out; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .picker-status { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #343a40; color: white; padding: 8px 16px; border-radius: 5px; z-index: 10000; font-family: Arial, sans-serif; font-size: 12px; font-weight: bold; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } `); // --- FEHLERPROTOKOLLIERUNG --- function logScriptError(error, context) { scriptErrors.push({ timestamp: new Date().toISOString(), context: context, message: error.message, stack: error.stack ? error.stack.split('\n') : 'No stack trace available' }); console.error(`Kleinanzeigen-Skript Fehler [${context}]:`, error); } // --- HILFSFUNKTIONEN FÜR DATENEXTRAKTION --- function parseLocation(locationString) { try { if (!locationString) return { plz: null, stadt: null, entfernung_km: null }; const cleanString = locationString.replace(/\s+/g, ' ').trim(); const plzMatch = cleanString.match(/^(\d{5})/); const cityMatch = cleanString.match(/^\d{5}\s(.+?)(?:\s\(|$)/); // Verbessert, um Städte ohne Entfernungsangabe zu erfassen const distanceMatch = cleanString.match(/\((\d+)\s*km\)/); return { plz: plzMatch ? plzMatch[1] : null, stadt: cityMatch ? cityMatch[1].trim() : cleanString.replace(/^\d{5}\s*/, '').trim(), entfernung_km: distanceMatch ? parseInt(distanceMatch[1], 10) : null }; } catch (e) { logScriptError(e, 'parseLocation'); return { plz: null, stadt: null, entfernung_km: null }; } } function parsePrice(priceString) { try { if (!priceString) return { betrag: null, zusatz: null }; const cleanString = priceString.replace(/\s+/g, ' ').trim(); const betragMatch = cleanString.match(/(\d[\d\.]*)/); let betrag = null; if (betragMatch) { betrag = parseFloat(betragMatch[1].replace(/\./g, '').replace(/,/g, '.')); // Auch Kommas für Dezimalzahlen beachten } const zusatzMatch = cleanString.match(/VB/i); return { betrag: betrag, zusatz: zusatzMatch ? 'VB' : null }; } catch (e) { logScriptError(e, 'parsePrice'); return { betrag: null, zusatz: null }; } } function getCompressedCss(element) { try { const tagName = element.tagName; if (!defaultStylesCache.has(tagName)) { const dummy = document.createElement(tagName); document.body.appendChild(dummy); defaultStylesCache.set(tagName, window.getComputedStyle(dummy)); document.body.removeChild(dummy); } const styles = window.getComputedStyle(element); const defaultStyles = defaultStylesCache.get(tagName); const cssProps = {}; const relevantProperties = [ 'display', 'position', 'flex-direction', 'justify-content', 'align-items', 'flex-wrap', 'gap', 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height', 'margin', 'padding', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-radius', 'box-sizing', 'font-family', 'font-size', 'font-weight', 'font-style', 'color', 'background-color', 'text-align', 'line-height', 'text-decoration', 'text-transform', 'letter-spacing', 'word-spacing', 'box-shadow', 'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y', 'z-index', 'cursor', 'pointer-events', 'white-space', 'word-break', 'float', 'clear', 'top', 'bottom', 'left', 'right', 'background-image', 'background-size', 'background-position', 'background-repeat', 'background-attachment', 'transform', 'transform-origin', 'transition', 'animation', 'outline', 'outline-offset', 'filter', 'clip-path', 'mask', 'content', 'vertical-align', 'writing-mode', 'direction', 'grid-template-columns', 'grid-template-rows', 'grid-gap', 'grid-column', 'grid-row' ]; for (const prop of relevantProperties) { const value = styles.getPropertyValue(prop); // Nur Werte hinzufügen, die sich vom Standard unterscheiden oder nicht "auto"/"normal" sind (falls relevanter) if (value && value !== defaultStyles.getPropertyValue(prop) && value !== 'auto' && value !== 'normal' && value !== '0px') { cssProps[prop] = value; } } // Spezielle Handhabung für 'border' um Redundanz zu vermeiden if (cssProps.border && cssProps.border.includes('none')) { delete cssProps.border; delete cssProps['border-top']; delete cssProps['border-right']; delete cssProps['border-bottom']; delete cssProps['border-left']; } return Object.keys(cssProps).length > 0 ? cssProps : undefined; } catch (e) { logScriptError(e, 'getCompressedCss'); return undefined; } } function getElementGeometry(element) { try { const rect = element.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, inViewport: ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) }; } catch (e) { logScriptError(e, 'getElementGeometry'); return undefined; } } function compactHTML(html) { try { return html.replace(/\s+/g, ' ') .replace(/>\s*</g, '><') .trim(); } catch (e) { logScriptError(e, 'compactHTML'); return html; } } function buildRecursiveElementTree(element, level = 0, maxDepth = 3) { if (level > maxDepth) return []; let tree = []; for (const child of element.children) { if (child.tagName === 'SCRIPT' || child.tagName === 'NOSCRIPT' || child.tagName === 'STYLE' || child.tagName === 'META') continue; const childData = { tagName: child.tagName.toLowerCase(), classes: Array.from(child.classList).filter(cls => !cls.startsWith('picker-')).join(' ') || undefined, id: child.id || undefined, textContentSnippet: (child.textContent || '').trim().substring(0, 100) + ((child.textContent || '').trim().length > 100 ? '...' : '') || undefined, }; if (child.hasAttribute('href')) { childData.href = child.getAttribute('href'); } if (child.hasAttribute('src')) { childData.src = child.getAttribute('src'); } if (child.hasAttribute('alt')) { childData.alt = child.getAttribute('alt'); } if (child.hasAttribute('data-adid')) { childData.dataAttributes = { adid: child.getAttribute('data-adid') }; } if (child.children.length > 0 && level + 1 <= maxDepth) { childData.children = buildRecursiveElementTree(child, level + 1, maxDepth); } tree.push(childData); } return tree.length > 0 ? tree : undefined; } function extractElementDetails(element, isAdItem = false) { try { const tagName = element.tagName.toLowerCase(); const classes = Array.from(element.classList).filter(cls => !cls.startsWith('picker-')).join(' ') || undefined; const textContent = element.textContent ? element.textContent.trim().replace(/\s+/g, ' ') : undefined; const domPath = getDomPath(element); const geometry = getElementGeometry(element); const css = getCompressedCss(element); const data = { tagName: tagName, domPath: domPath, id: element.id || undefined, classes: classes || undefined, dataAttributes: Array.from(element.attributes) .filter(attr => attr.name.startsWith('data-')) .reduce((acc, attr) => { acc[attr.name.substring(5)] = attr.value; return acc; }, {}) || undefined, isInteractive: ['a', 'button', 'input', 'select', 'textarea'].includes(tagName) || element.hasAttribute('onclick') || element.hasAttribute('tabindex') || (element.hasAttribute('role') && element.getAttribute('role').includes('button')) || undefined, geometry: geometry || undefined, textContent: textContent || undefined, css: css || undefined, }; if (tagName === 'a' && element.href) { data.href = element.href; } if (tagName === 'img' && element.src) { data.src = element.src; if (element.alt) data.alt = element.alt; } // Strukturbaum nur für wichtige oder Ad-Elemente if (isAdItem || (['h1', 'h2', 'h3', 'p'].includes(tagName) && textContent && textContent.length > 20)) { const structureTree = buildRecursiveElementTree(element); if (structureTree) data.structureTree = structureTree; } if (isAdItem) { const rawChildrenHTML = compactHTML(element.innerHTML); if (rawChildrenHTML) data.rawChildrenHTML = rawChildrenHTML; } // Entferne undefined-Werte Object.keys(data).forEach(key => data[key] === undefined && delete data[key]); return data; } catch (e) { logScriptError(e, `extractElementDetails for ${element.tagName}`); return null; } } function extractAdItemData(adListItem) { try { const baseData = extractElementDetails(adListItem, true); // True for isAdItem if (!baseData) return null; const keyData = {}; const titleElement = adListItem.querySelector('h2.text-module-begin a.ellipsis, h2.text-module-begin span.ellipsis'); if (titleElement) keyData.title = titleElement.textContent.trim(); const descElement = adListItem.querySelector('.aditem-main--middle--description'); if (descElement) keyData.description = descElement.textContent.trim().replace(/\s+/g, ' '); const priceElement = adListItem.querySelector('.aditem-main--middle--price-shipping--price'); if (priceElement) keyData.price = parsePrice(priceElement.textContent); const locationElement = adListItem.querySelector('.aditem-main--top--left'); if (locationElement) keyData.location = parseLocation(locationElement.textContent); const dateElement = adListItem.querySelector('.aditem-main--top--right'); if (dateElement) keyData.date = dateElement.textContent.trim(); const linkElement = adListItem.querySelector('a[href^="/s-anzeige/"]'); if(linkElement) keyData.link = `https://www.kleinanzeigen.de${linkElement.getAttribute('href')}`; const imageCountElement = adListItem.querySelector('.galleryimage--counter'); if (imageCountElement) keyData.image_count = parseInt(imageCountElement.textContent.trim(), 10); const isShippingPossible = adListItem.querySelector('.simpletag.tag-with-icon .icon-package'); if (isShippingPossible) keyData.shipping_possible = true; const isDirectBuy = adListItem.querySelector('.simpletag.tag-with-icon .icon-send-money'); if (isDirectBuy) keyData.direct_buy_possible = true; baseData.key_data = Object.keys(keyData).length > 0 ? keyData : undefined; return baseData; } catch (e) { logScriptError(e, 'extractAdItemData'); return null; } } function extractPageMetadata() { try { const url = window.location.href; const title = document.title; const timestamp_utc = new Date().toISOString(); const html_lang = document.documentElement.lang || 'de'; const faviconLink = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]'); const favicon_url = faviconLink ? faviconLink.href : 'https://www.kleinanzeigen.de/favicon.ico'; const metaDescriptionTag = document.querySelector('meta[name="description"]'); const metaDescription = metaDescriptionTag ? metaDescriptionTag.content : null; const canonicalLink = document.querySelector('link[rel="canonical"]'); const canonicalUrl = canonicalLink ? canonicalLink.href : null; const urlParams = new URLSearchParams(window.location.search); const url_parameters = {}; for (const [key, value] of urlParams.entries()) { url_parameters[key] = value; } const schemaOrgData = []; document.querySelectorAll('script[type="application/ld+json"]').forEach(script => { try { const json = JSON.parse(script.textContent); if (json['@type']) { // If it's an array of types, take them all if (Array.isArray(json['@type'])) { schemaOrgData.push(...json['@type']); } else { schemaOrgData.push(json['@type']); } } } catch (e) { logScriptError(e, 'Parsing Schema.org JSON-LD'); } }); return { metadata: { url: url, title: title, timestamp_utc: timestamp_utc, html_lang: html_lang, favicon_url: favicon_url, url_parameters: Object.keys(url_parameters).length > 0 ? url_parameters : null, metaDescription: metaDescription, canonicalUrl: canonicalUrl, schemaOrgData: schemaOrgData.length > 0 ? Array.from(new Set(schemaOrgData)) : null, // Unique types script_runtime: ((performance.now() - scriptStartTime) / 1000).toFixed(3) + 's', script_version: GM_info.script.version, page_load_errors: scriptErrors.length > 0 ? scriptErrors : null } }; } catch (e) { logScriptError(e, 'extractPageMetadata'); return { metadata: { url: window.location.href, error: e.message, page_load_errors: scriptErrors } }; } } function getDomPath(el) { if (!(el instanceof Element)) return; const path = []; let current = el; while (current.nodeType === Node.ELEMENT_NODE) { let selector = current.tagName.toLowerCase(); if (current.id) { selector += '#' + current.id; path.unshift(selector); break; } else { let sib = current, nth = 1; while (sib.previousElementSibling) { sib = sib.previousElementSibling; if (sib.tagName.toLowerCase() === selector) nth++; } if (nth !== 1) selector += `:nth-of-type(${nth})`; } path.unshift(selector); current = current.parentNode; if (current && current.tagName === 'BODY') { // Stop at body to avoid overly long paths path.unshift('body'); break; } } return path.join(' > '); } // --- HAUPTFUNKTIONEN & MODUS-STEUERUNG --- function collectCurrentPageData() { scriptErrors = []; // Fehler für jede neue Seite zurücksetzen const pageData = [extractPageMetadata()]; // Beginne immer mit Metadaten // Kleinanzeigen spezifische Ad-Items immer sammeln document.querySelectorAll('article.aditem, li.ad-listitem > article.aditem').forEach(item => { const data = extractAdItemData(item); if (data) pageData.push(data); }); // Allgemeine sichtbare und lesbare Elemente, SEHR selektiv // Ziel: Haupt-Überschriften, Suchfelder, Filter, Paginierung, etc., die den Kontext der Seite beschreiben const significantSelectors = [ 'h1', // Hauptüberschrift der Seite 'h2.headline-module', // Wichtige Unterüberschriften 'input[name="keywords"]', // Suchfeld 'button[data-e2e-id="search-button"]', // Such-Button '.pagination a', // Paginierungs-Links '.currentpage', // Aktuelle Seite in der Paginierung '.sortby-box', // Sortier-Dropdown '.filter-box' // Filter-Container (könnte weitere wichtige Elemente enthalten) ]; document.querySelectorAll(significantSelectors.join(', ')).forEach(element => { // Ignoriere Elemente, die bereits Teil eines ad-listitem sind if (element.closest('.aditem')) return; const data = extractElementDetails(element); if (data && data.geometry && data.geometry.inViewport) { // Nur Elemente mit relevantem Textinhalt oder spezifischen Attributen sammeln if (data.textContent && data.textContent.trim().length > 0) { pageData.push(data); } else if (element.tagName.toLowerCase() === 'input' && element.value) { // Für Input-Felder den Wert erfassen data.value = element.value; pageData.push(data); } else if (element.tagName.toLowerCase() === 'a' && element.href && (element.textContent || element.getAttribute('title') || element.getAttribute('aria-label'))) { // Links, die entweder Text haben oder durch Title/Aria-Label beschrieben sind pageData.push(data); } else if (element.tagName.toLowerCase() === 'button' && (element.textContent || element.getAttribute('title') || element.getAttribute('aria-label'))) { // Buttons, die entweder Text haben oder durch Title/Aria-Label beschrieben sind pageData.push(data); } } }); // Aktualisiere Metadaten mit tatsächlicher Anzahl gesammelter Elemente pageData[0].metadata.selected_elements_count = pageData.length - 1; // Metadaten selbst nicht zählen console.log(`Kleinanzeigen-Skript: Gesammelte Elemente auf aktueller Seite: ${pageData.length - 1}`); return pageData; } function copyDataToClipboard(data) { try { const jsonlOutput = data.map(obj => JSON.stringify(obj)).join('\n'); GM_setClipboard(jsonlOutput, 'text/plain'); showNotification(`${data.length - 1} Objekt(e) als JSONL kopiert!`); } catch (e) { logScriptError(e, 'copyDataToClipboard'); showNotification('Fehler beim Kopieren der Daten!'); } } // --- BUTTON INTERAKTION --- function handleButtonPress(e) { if (e.button !== 0) return; // Nur linke Maustaste if (isLockedMode) { // Wenn im Sammel-Modus, beende den Modus und kopiere alles console.log("Kleinanzeigen-Skript: Sammel-Modus beenden durch Kurz-Klick."); deactivateLockedMode(); return; } // Startet Timer für Langklick-Erkennung console.log("Kleinanzeigen-Skript: Maustaste gedrückt, starte Langklick-Timer."); longPressTimer = setTimeout(() => { console.log("Kleinanzeigen-Skript: Langklick-Schwelle erreicht, starte Countdown."); startCountdown(3); // Startet den visuellen Countdown longPressTimer = null; // Setzt Timer zurück, da Langklick erkannt }, LONG_PRESS_THRESHOLD); // Listener, um Langklick zu beenden, falls die Maustaste losgelassen wird // Oder wenn die Maus den Button verlässt, bevor der Langklick registriert wird pickerButton.addEventListener('mouseup', handleButtonRelease, { once: true }); pickerButton.addEventListener('mouseleave', handleButtonRelease, { once: true }); } function handleButtonRelease() { console.log("Kleinanzeigen-Skript: Maustaste losgelassen."); if (countdownInterval) { // Wenn der Countdown aktiv ist, wurde der Langklick abgebrochen console.log("Kleinanzeigen-Skript: Langklick-Countdown abgebrochen."); clearInterval(countdownInterval); countdownInterval = null; pickerButton.textContent = 'TO LLM'; // Text zurücksetzen pickerButton.classList.remove('active'); // Puls entfernen showNotification('Langklick abgebrochen.'); } else if (longPressTimer) { // Wenn der Timer noch läuft, war es ein kurzer Klick console.log("Kleinanzeigen-Skript: Kurzer Klick erkannt."); clearTimeout(longPressTimer); longPressTimer = null; // Kurzer Klick: Sammle Daten der aktuellen Seite und kopiere sie const currentPageData = collectCurrentPageData(); copyDataToClipboard(currentPageData); showNotification('Aktuelle Seite als JSONL kopiert!'); pickerButton.classList.remove('active'); // Setze Zustand zurück } // Wenn weder countdownInterval noch longPressTimer aktiv sind, war es ein gültiger Langklick // oder ein Klick im LockedMode, der bereits von deactivateLockedMode behandelt wurde. } function startCountdown(count) { let currentCount = count; pickerButton.textContent = currentCount; pickerButton.classList.add('active'); // Visuell als "aktiv" markieren countdownInterval = setInterval(() => { currentCount--; if (currentCount > 0) { pickerButton.textContent = currentCount; } else { clearInterval(countdownInterval); countdownInterval = null; pickerButton.textContent = 'STOP'; // Text für Sammel-Modus activateLockedMode(); // Aktiviere den kontinuierlichen Sammel-Modus showNotification('Sammel-Modus aktiv. Navigieren Sie durch Seiten.'); } }, 1000); } function activateLockedMode() { console.log("Kleinanzeigen-Skript: Sammel-Modus aktiviert."); isLockedMode = true; GM_setValue(STORAGE_KEY_LOCKED_MODE, true); // Status speichern allCollectedData = []; // Alte Daten löschen allCollectedData.push(...collectCurrentPageData()); // Daten der Startseite sammeln pickerButton.classList.remove('active'); // Entferne "active" Puls pickerButton.classList.add('locked'); // Füge "locked" Puls hinzu pickerButton.title = 'Klicken, um Sammel-Modus zu beenden und alle gesammelten Daten zu kopieren'; // Starte den MutationObserver für Seitenwechsel in SPAs observeDOMChanges(); updateStatusDisplay(); } function deactivateLockedMode() { console.log("Kleinanzeigen-Skript: Sammel-Modus deaktiviert."); isLockedMode = false; GM_setValue(STORAGE_KEY_LOCKED_MODE, false); // Status löschen if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; console.log("Kleinanzeigen-Skript: MutationObserver gestoppt."); } // Kopiere alle gesammelten Daten vor dem Löschen copyDataToClipboard(allCollectedData); allCollectedData = []; // Gesammelte Daten leeren nach dem Kopieren/Beenden pickerButton.classList.remove('locked', 'active', 'processing'); pickerButton.textContent = 'TO LLM'; // Zurück zum Standardtext pickerButton.title = 'Daten der aktuellen Seite erfassen (Kurz-Klick) / Kontinuierlich sammeln (Langklick)'; removeStatus(); showNotification('Sammel-Modus beendet.'); } // --- SEITENWECHSEL-ERKENNUNG (Primär über MutationObserver) --- function processNewPage() { if (processingPage) { console.log("Kleinanzeigen-Skript: Seitenverarbeitung bereits im Gange, überspringe."); return; } processingPage = true; pickerButton.classList.add('processing'); // Visueller Hinweis console.log(`Kleinanzeigen-Skript: Starte Datensammlung für aktuelle Seite: ${window.location.href}`); // Kleine Verzögerung, damit die Seite vollständig gerendert werden kann // und um Doppel-Sammlungen bei schnellen DOM-Updates zu vermeiden setTimeout(() => { // Die URL wird beim Hinzufügen der Metadaten erfasst. // Die Entscheidung, ob eine Seite gesammelt wird, liegt nun allein beim MutationObserver. allCollectedData.push(...collectCurrentPageData()); updateStatusDisplay(); showNotification(`Seite gesammelt: ${window.location.pathname.split('/').pop() || 'Startseite'}`); console.log(`Kleinanzeigen-Skript: Datensammlung für ${window.location.href} abgeschlossen. Total Datenpunkte: ${allCollectedData.length}`); pickerButton.classList.remove('processing'); processingPage = false; }, 800); // Erhöhte Verzögerung für stabilere Erfassung } function observeDOMChanges() { if (mutationObserver) { mutationObserver.disconnect(); // Vorherigen Observer trennen } const targetNode = document.body; // childList: Änderungen an den direkten Kindern // subtree: Änderungen in der gesamten Unterbaumstruktur // attributes: Änderungen an Attributen (z.B. style, class) const config = { childList: true, subtree: true, attributes: false }; mutationObserver = new MutationObserver(mutationsList => { const newUrl = window.location.href; if (newUrl !== lastKnownUrl) { // URL hat sich geändert (Browser-Nav, PushState), behandle als Seitenwechsel console.log(`Kleinanzeigen-Skript: URL-Wechsel erkannt: ${lastKnownUrl} -> ${newUrl}`); if (isLockedMode) { processNewPage(); } } else { // URL ist gleich, aber DOM hat sich geändert (AJAX-Load, Filter) const adListContainer = document.getElementById('srchrslt-adtable'); // Der Container, der die Anzeigenliste hält const paginationContainer = document.querySelector('.pagination'); // Paginierungsbereich const relevantChange = mutationsList.some(mutation => { // Prüfen, ob Nodes hinzugefügt wurden, die Anzeigen enthalten könnten const addedRelevantNodes = Array.from(mutation.addedNodes).some(node => node.nodeType === Node.ELEMENT_NODE && (node.matches('.ad-listitem, .aditem') || node.querySelector('.ad-listitem, .aditem')) // Einzelne Anzeige oder Container mit Anzeigen ); // Prüfen, ob im Anzeigen- oder Paginierungscontainer etwas entfernt oder hinzugefügt wurde const changeInKeyContainer = (adListContainer && adListContainer.contains(mutation.target) && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) || (paginationContainer && paginationContainer.contains(mutation.target) && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)); // Wenn eine Änderung im Anzeigen- oder Paginierungsbereich stattgefunden hat, ist es relevant. // Oder wenn direkt relevante Nodes hinzugefügt wurden (z.B. neue Ad-Items). if (addedRelevantNodes || changeInKeyContainer) { console.log("Kleinanzeigen-Skript: Relevant Change Detected: ", mutation.type, " target: ", mutation.target); if (mutation.addedNodes.length > 0) console.log("Added Nodes: ", mutation.addedNodes); if (mutation.removedNodes.length > 0) console.log("Removed Nodes: ", mutation.removedNodes); return true; } return false; }); if (isLockedMode && relevantChange) { console.log("Kleinanzeigen-Skript: DOM-Änderung erkannt, die auf neue Inhalte hindeutet (URL unverändert)."); processNewPage(); } } lastKnownUrl = newUrl; // Aktualisiere die zuletzt bekannte URL nach der Verarbeitung }); mutationObserver.observe(targetNode, config); console.log("Kleinanzeigen-Skript: MutationObserver gestartet."); } // --- UI-FUNKTIONEN --- function createPickerButton() { const button = document.createElement('button'); button.id = 'element-picker-btn'; button.textContent = 'TO LLM'; // Standardtext button.title = 'Daten der aktuellen Seite erfassen (Kurz-Klick) / Kontinuierlich sammeln (Langklick)'; button.addEventListener('mousedown', handleButtonPress); document.body.appendChild(button); return button; } function showNotification(message) { const n = document.createElement('div'); n.className = 'picker-notification'; n.textContent = message; document.body.appendChild(n); setTimeout(() => { if (n.parentNode) n.parentNode.removeChild(n) }, 3000); } function updateStatusDisplay() { if (isLockedMode) { // Filtere nur die Metadaten-Objekte, um die URLs zu extrahieren const collectedPageUrls = new Set(allCollectedData.filter(d => d.metadata).map(d => d.metadata.url)); showStatus(`Sammel-Modus aktiv. Gesammelte Seiten: ${collectedPageUrls.size}. Datenpunkte: ${allCollectedData.length}`); } else { removeStatus(); } } function showStatus(message) { let s = document.getElementById('picker-status'); if (!s) { s = document.createElement('div'); s.id = 'picker-status'; s.className = 'picker-status'; document.body.appendChild(s); } s.textContent = message; } function removeStatus() { const e = document.getElementById('picker-status'); if (e) e.parentNode.removeChild(e); } // Initialisierung async function init() { pickerButton = createPickerButton(); console.log('Kleinanzeigen-Datenexport (TO LLM) v19.0 geladen.'); // Prüfen, ob der Sammel-Modus zuvor aktiv war const storedLockedMode = await GM_getValue(STORAGE_KEY_LOCKED_MODE, false); if (storedLockedMode) { console.log('Kleinanzeigen-Skript: Sammel-Modus aus Speicher wiederhergestellt.'); isLockedMode = true; pickerButton.classList.add('locked'); pickerButton.textContent = 'STOP'; pickerButton.title = 'Klicken, um Sammel-Modus zu beenden und alle gesammelten Daten zu kopieren'; showNotification('Sammel-Modus wiederhergestellt.'); // Daten der aktuellen Seite sammeln, wenn der Modus wiederhergestellt wird processNewPage(); observeDOMChanges(); // Observer neu starten updateStatusDisplay(); } } // Ausführung nach DOMContentLoaded oder sofort, falls schon geladen if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址