// ==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();
})();