您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在浏览贝壳(ke.com)时,自动收集成交列表页和在售详情页的房源信息,并提供独立的、动态命名的CSV下载功能。
// ==UserScript== // @name 贝壳房源信息收集器 (成交/在售双模式) // @namespace http://tampermonkey.net/ // @version 2.1 // @description 在浏览贝壳(ke.com)时,自动收集成交列表页和在售详情页的房源信息,并提供独立的、动态命名的CSV下载功能。 // @author CodeDust // @match https://*.ke.com/chengjiao/* // @match https://*.ke.com/ershoufang/*.html* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 全局配置 --- const STORAGE_KEYS = { CHENGJIAO: 'beike_chengjiao_data', ERSHOUFANG: 'beike_ershoufang_data' }; /** * 主函数,脚本的入口 */ function main() { console.log('贝壳房源信息收集脚本 (v2.1) 已启动!'); createUI(); routePage(); } /** * 页面路由,根据当前URL决定执行哪个函数 */ function routePage() { const url = window.location.href; if (url.includes('/chengjiao/')) { console.log('进入成交列表页模式'); handleChengjiaoListPage(); } else if (url.includes('/ershoufang/') && url.endsWith('.html')) { console.log('进入在售详情页模式'); // 在售详情页,通过点击按钮来保存 } } // ================================================================== // 在售(ershoufang)页面处理逻辑 // ================================================================== /** * 点击“一键保存”按钮时触发的函数 */ function saveErshoufangDetail() { console.log('开始抓取在售房源详情...'); // 辅助函数,用于安全地获取元素文本 const getText = (selector) => document.querySelector(selector)?.innerText.trim() || ''; // 辅助函数,用于从“基本属性”和“交易属性”列表中提取信息 const getFromInfoList = (label) => { const allLi = document.querySelectorAll('.base .content li, .transaction .content li'); for (const li of allLi) { if (li.querySelector('.label')?.innerText === label) { const clone = li.cloneNode(true); clone.querySelector('.label').remove(); return clone.innerText.trim(); } } return ''; }; // 1. 抓取原始数据 const rawData = { title: getText('h1.main'), totalPrice: getText('.price .total'), unitPrice: getText('.unitPriceValue'), community: getText('.communityName > a.info'), fullArea: getText('.areaName > .info'), tags: Array.from(document.querySelectorAll('.tags .content .tag')).map(el => el.innerText.trim()).join(' | '), followerCount: getText('#favCount'), // 从基本属性列表中提取 layout: getFromInfoList('房屋户型'), floor: getFromInfoList('所在楼层'), grossArea: getFromInfoList('建筑面积'), structure: getFromInfoList('户型结构'), buildingType: getFromInfoList('建筑类型'), direction: getFromInfoList('房屋朝向'), decoration: getFromInfoList('装修情况'), elevatorRatio: getFromInfoList('梯户比例'), // 从交易属性列表中提取 listDate: getFromInfoList('挂牌时间'), ownership: getFromInfoList('交易权属'), lastTrade: getFromInfoList('上次交易'), usage: getFromInfoList('房屋用途'), propertyAge: getFromInfoList('房屋年限'), propertyRight: getFromInfoList('产权所属'), mortgage: getFromInfoList('抵押信息'), // 从另一个位置获取更准确的年代和建筑类型 yearAndBuildTypeFromSubInfo: getText('.houseInfo .area .subInfo'), }; // 2. 解析和格式化数据 const formatted = {}; formatted['标题'] = rawData.title; formatted['小区'] = rawData.community; const areaParts = rawData.fullArea.split(/\s+/).filter(Boolean); formatted['区域'] = areaParts[0] || 'N/A'; formatted['商圈'] = areaParts[1] || 'N/A'; formatted['总价(万)'] = parseFloat(rawData.totalPrice) || 'N/A'; formatted['单价(元/平)'] = parseInt(rawData.unitPrice) || 'N/A'; formatted['户型'] = rawData.layout; formatted['建筑面积(㎡)'] = parseFloat(rawData.grossArea) || 'N/A'; formatted['朝向'] = rawData.direction; formatted['装修'] = rawData.decoration; formatted['楼层'] = rawData.floor ? rawData.floor.split('咨询楼层')[0].trim() : 'N/A'; if (rawData.yearAndBuildTypeFromSubInfo) { const yearMatch = rawData.yearAndBuildTypeFromSubInfo.match(/(\d{4})年建/); formatted['年代'] = yearMatch ? parseInt(yearMatch[1]) : 'N/A'; const buildTypeMatch = rawData.yearAndBuildTypeFromSubInfo.match(/建\/(.+)/); formatted['建筑类型'] = buildTypeMatch ? buildTypeMatch[1].trim() : 'N/A'; } else { formatted['年代'] = 'N/A'; formatted['建筑类型'] = rawData.buildingType; } formatted['户型结构'] = rawData.structure; formatted['梯户比例'] = rawData.elevatorRatio; formatted['挂牌时间'] = rawData.listDate; formatted['交易权属'] = rawData.ownership; formatted['上次交易'] = rawData.lastTrade; formatted['房屋用途'] = rawData.usage; formatted['房屋年限'] = rawData.propertyAge; formatted['产权所属'] = rawData.propertyRight; formatted['抵押信息'] = rawData.mortgage.replace(/\s*查看详情\s*/g, '').trim(); formatted['房源标签'] = rawData.tags; formatted['关注人数'] = parseInt(rawData.followerCount) || 0; formatted['详情链接'] = window.location.href; // 3. 保存数据 let allData = JSON.parse(GM_getValue(STORAGE_KEYS.ERSHOUFANG) || '{}'); allData[window.location.href] = formatted; GM_setValue(STORAGE_KEYS.ERSHOUFANG, JSON.stringify(allData)); // 4. 更新UI反馈 const count = Object.keys(allData).length; updateButtonCount('ershoufang', count); const saveBtn = document.getElementById('gemini-save-ershoufang-btn'); saveBtn.innerText = '已保存!'; saveBtn.style.backgroundColor = '#67c23a'; // 绿色表示成功 setTimeout(() => { saveBtn.innerText = '一键保存本页信息'; saveBtn.style.backgroundColor = '#409EFF'; }, 1500); console.log('在售房源保存成功:', formatted); } // ================================================================== // 成交(chengjiao)页面处理逻辑 (无变动) // ================================================================== function handleChengjiaoListPage() { let allCollectedData = JSON.parse(GM_getValue(STORAGE_KEYS.CHENGJIAO) || '{}'); const items = document.querySelectorAll('ul.listContent > li'); if (items.length === 0) return; console.log(`在成交列表找到 ${items.length} 个房源,开始处理...`); items.forEach(item => { const titleElement = item.querySelector('div.info > div.title > a'); if (!titleElement) return; const getText = (selector) => item.querySelector(selector)?.innerText.trim() || ''; const rawHouseData = { title: getText('div.info > div.title > a'), detailUrl: titleElement.href, houseInfo: getText('div.houseInfo'), positionInfo: getText('div.positionInfo'), dealDate: getText('div.dealDate'), totalPrice: getText('div.totalPrice span.number'), unitPrice: getText('div.unitPrice span.number'), dealCycleInfo: getText('div.dealCycleeInfo .dealCycleTxt') }; const formattedData = parseChengjiaoData(rawHouseData); allCollectedData[formattedData.详情链接] = formattedData; }); GM_setValue(STORAGE_KEYS.CHENGJIAO, JSON.stringify(allCollectedData)); const finalCount = Object.keys(allCollectedData).length; console.log(`处理完毕!目前总共收集了 ${finalCount} 条成交房源信息。`); updateButtonCount('chengjiao', finalCount); } function parseChengjiaoData(rawData) { const formatted = { '小区名称': 'N/A', '户型': 'N/A', '面积(㎡)': 'N/A', '详情链接': rawData.detailUrl, '成交日期': rawData.dealDate, '成交总价(万)': rawData.totalPrice, '成交单价(元/平)': rawData.unitPrice, '朝向': 'N/A', '装修': 'N/A', '楼层信息': 'N/A', '建成年代': 'N/A', '房屋结构': 'N/A', '挂牌价(万)': 'N/A', '成交周期(天)': 'N/A' }; if (rawData.title) { const titleParts = rawData.title.split(/\s+/).filter(Boolean); if (titleParts.length >= 3) { formatted['面积(㎡)'] = parseFloat(titleParts[titleParts.length - 1]) || 'N/A'; formatted['户型'] = titleParts[titleParts.length - 2]; formatted['小区名称'] = titleParts.slice(0, -2).join(' '); } else { formatted['小区名称'] = rawData.title; } } if (rawData.houseInfo && rawData.houseInfo.includes('|')) { const parts = rawData.houseInfo.split('|'); formatted['朝向'] = parts[0] ? parts[0].trim() : 'N/A'; formatted['装修'] = parts[1] ? parts[1].trim() : 'N/A'; } else { formatted['朝向'] = rawData.houseInfo; } if (rawData.positionInfo) { const parts = rawData.positionInfo.split(/\s+/).filter(Boolean); formatted['楼层信息'] = parts[0] || 'N/A'; const yearAndStructurePart = parts.find(p => p.includes('年')); if (yearAndStructurePart) { const yearMatch = yearAndStructurePart.match(/(\d{4})年/); if (yearMatch) formatted['建成年代'] = parseInt(yearMatch[1]); const structureMatch = yearAndStructurePart.match(/年(.+)/); if (structureMatch) formatted['房屋结构'] = structureMatch[1].trim(); } } if (rawData.dealCycleInfo) { let match; match = rawData.dealCycleInfo.match(/挂牌(\d+\.?\d*)万/); if (match) formatted['挂牌价(万)'] = parseFloat(match[1]); match = rawData.dealCycleInfo.match(/成交周期(\d+)天/); if (match) formatted['成交周期(天)'] = parseInt(match[1]); } return formatted; } // ================================================================== // UI 和通用功能函数 // ================================================================== /** * 创建界面元素 */ function createUI() { const url = window.location.href; const container = document.createElement('div'); let buttonsHtml = ''; // 根据页面类型显示不同的按钮组合 if (url.includes('/ershoufang/')) { buttonsHtml = ` <div id="gemini-ershoufang-panel"> <button class="gemini-save-btn" id="gemini-save-ershoufang-btn">一键保存本页信息</button> <div class="gemini-main-btn" id="gemini-download-ershoufang-btn" title="点击下载已收集的在售信息"> <span>在售</span> <span class="gemini-data-count" id="gemini-ershoufang-count">0</span> </div> <button class="gemini-clear-btn" id="gemini-clear-ershoufang-btn" title="清空所有已收集的在售数据">清空</button> </div>`; } else if (url.includes('/chengjiao/')) { buttonsHtml = ` <div id="gemini-chengjiao-panel"> <div class="gemini-main-btn" id="gemini-download-chengjiao-btn" title="点击下载已收集的成交信息"> <span>成交</span> <span class="gemini-data-count" id="gemini-chengjiao-count">0</span> </div> <button class="gemini-clear-btn" id="gemini-clear-chengjiao-btn" title="清空所有已收集的成交数据">清空</button> </div>`; } container.innerHTML = buttonsHtml; document.body.appendChild(container); // 动态绑定事件 if (url.includes('/ershoufang/')) { document.getElementById('gemini-save-ershoufang-btn').addEventListener('click', saveErshoufangDetail); document.getElementById('gemini-download-ershoufang-btn').addEventListener('click', () => downloadData('ershoufang')); document.getElementById('gemini-clear-ershoufang-btn').addEventListener('click', () => clearData('ershoufang')); updateButtonCount('ershoufang', Object.keys(JSON.parse(GM_getValue(STORAGE_KEYS.ERSHOUFANG) || '{}')).length); } else if (url.includes('/chengjiao/')) { document.getElementById('gemini-download-chengjiao-btn').addEventListener('click', () => downloadData('chengjiao')); document.getElementById('gemini-clear-chengjiao-btn').addEventListener('click', () => clearData('chengjiao')); updateButtonCount('chengjiao', Object.keys(JSON.parse(GM_getValue(STORAGE_KEYS.CHENGJIAO) || '{}')).length); } GM_addStyle(` #gemini-chengjiao-panel, #gemini-ershoufang-panel { display: flex; align-items: center; position: fixed; right: 20px; bottom: 20px; z-index: 9999; } .gemini-main-btn, .gemini-clear-btn, .gemini-save-btn { border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; margin-left: 10px; height: 40px; color: white; padding: 0 15px; } .gemini-main-btn { background-color: #00AE66; } .gemini-main-btn:hover { background-color: #00995a; } .gemini-clear-btn { background-color: #F56C6C; width: 50px; } .gemini-clear-btn:hover { background-color: #d32f2f; } .gemini-save-btn { background-color: #409EFF; font-weight: bold; } .gemini-save-btn:hover { background-color: #3a8ee6; } .gemini-data-count { background-color: white; color: #00AE66; padding: 2px 6px; border-radius: 10px; margin-left: 8px; font-weight: bold; font-size: 12px; } `); } function updateButtonCount(type, count) { const countElement = document.getElementById(`gemini-${type}-count`); if (countElement) countElement.innerText = count; } function getAreaName() { const url = window.location.href; let areaName = '未知区域'; if (url.includes('/chengjiao/')) { areaName = document.querySelector('div.deal-bread a:nth-last-child(2)')?.innerText.replace('二手房成交', '') || '成交房源'; } else if (url.includes('/ershoufang/')) { const areaElements = document.querySelectorAll('.areaName .info a'); if (areaElements.length > 1) { areaName = areaElements[areaElements.length - 1].innerText; } else if (areaElements.length === 1) { areaName = areaElements[0].innerText; } } return areaName; } function downloadData(type) { const storageKey = type === 'chengjiao' ? STORAGE_KEYS.CHENGJIAO : STORAGE_KEYS.ERSHOUFANG; const typeName = type === 'chengjiao' ? '成交房源' : '在售房源'; const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const areaName = getAreaName(); const fileName = `${date}_${areaName}_${typeName}.csv`; const rawData = GM_getValue(storageKey); if (!rawData || rawData === '{}') { alert(`尚未收集到任何“${typeName}”信息!`); return; } const data = Object.values(JSON.parse(rawData)); if (data.length === 0) { alert('数据为空,无法下载。'); return; } const headers = Object.keys(data[0]); let csvContent = headers.join(',') + '\n'; data.forEach(row => { const values = headers.map(header => { let value = row[header] === undefined || row[header] === null ? '' : row[header]; return `"${String(value).replace(/"/g, '""')}"`; }); csvContent += values.join(',') + '\n'; }); const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function clearData(type) { const storageKey = type === 'chengjiao' ? STORAGE_KEYS.CHENGJIAO : STORAGE_KEYS.ERSHOUFANG; const typeName = type === 'chengjiao' ? '成交' : '在售'; if (confirm(`您确定要清空所有已收集的“${typeName}”房源信息吗?此操作不可撤销。`)) { GM_setValue(storageKey, '{}'); updateButtonCount(type, 0); alert(`“${typeName}”数据已清空!`); } } main(); })();