现在年轻人到25岁+,总的要考虑买房结婚的问题,2016年的一波房价大涨,小伙伴们纷纷表示再也买不起上海的房产了,博主也得考虑考虑未来的发展了,思考了很久,决定去杭州工作、买房、定居、生活,之前去过很多次杭州,很喜欢这个城市,于是例行每天晚上都要花一点时间关注杭州的房产销售情况,以及价格,起初我每天都在杭州的本地论坛,透明售房网上查看,每一天的房产销售数据,但是无奈博主不是杭州本地人,看了网页上展示的很多楼盘,但是我不知道都在什么地方啊,于是乎,看到价格合适的,总是到高德地图去搜索地理位置,每次非常麻烦,于是我想是不是可以,写一个小的爬虫工具,每天抓取透明售房网上的销售记录,直接展示在地图上,直观明了的看看都是哪些地方的楼盘地理位置不错,同时价格也在能接受的范围内,同时最近在学习node.js,正好可以练练手。说干就干,一个下午时间,有了初步的成果如下,后期在加入每天的销售数据,加入到mongoDB中,用于分析每周、每月的销售数据,用于自己买房的参考,要学以致用嘛!

先说下基本思路:

第一步:利用nodejs,技术抓取透明售房网的实时的数据(http://www.tmsf.com/daily.htm),存储在后台;

第二步:页面请求后台数据,然后借助高德地图提供的按照名称查询地理位置的服务,展示在地图上,并绑定每个楼盘的销售详情;

ok,有了基本思路,下面一步一步的开干:

一:后台爬虫

1.抓取在线网络数据

这里先介绍一个利器,cheerio(https://github.com/cheeriojs/cheerio),可以说是位服务器特别定制的,快速,灵活,实施的jQuery核心实现,或者说是后台解析html的;安装nodejs 模块这里不再说明,抓取html页面逻辑比较简单,直接上代码:

 //定义爬虫数据源网络地址
var url = 'http://www.tmsf.com/daily.htm'; /**
* 请求网络地址抓取数据
* @param {function} callBack 传回爬虫数据处理之后的最终结果
*/
function getHzfcSaleInfo(callBack) {
var hzfcSaleInfo = [];
http.get(url, function(res) {
var html = '';
res.on('data', function(data) {
html += data;
});
res.on('end', function() {
hzfcSaleInfo = filterData(html);
callBack(hzfcSaleInfo);
});
res.on('error', function() {
console.log('获取数据出错');
});
})
}

2.解析获取的数据

已经抓取整个网页的数据,在这一步中要根据网页的DOM,结构来分析应该怎么解析:首先我们可以看到,每日房产销售情况的数据是分行政区展示在并列的几个div中,通过display控制显示哪一个行政区,所以思路就是首先获取这个外层container,然后不停一层一层的循环解析数据;

其中解析到每一行的数据的时候,发现了一个有点奇葩的网页展示,每一行后面数字竟然不是直接用数字来表示的,而是用css的图片来代替,可能就是为了防止我这种爬虫的吧,不过不管了,有了css,还不能转成数字吗,哈哈

具体代码如下:

/**
* 解析DOM节点,提取核心数据
* @param {string} html 页面整体html
* @returns {array} 最终处理之后的数据
*/
function filterData(html) {
var $ = cheerio.load(html);
var data = [];
var container = $('#myCont2')
var districts = container.find('table');
districts.each(function() {
var district = $(this);
var trs = district.find('tr');
trs.each(function() {
var tr = $(this);
var tds = tr.find('td');
var i = 0;
var estateName;
var estateSite;
var estateSign;
var estateReserve;
var estateArea;
var estatePrice;
tds.each(function() {
var col = $(this);
if (i == 0) {
estateName = col.find('a').text();
} else if (i == 1) {
estateSite = col.text().replace(/[^\u4e00-\u9fa5]/gi, "");
} else if (i == 2) {
var spanClass = '';
var spans = col.find('span');
spans.each(function(a) {
var span = $(this);
var cssName = classNameToNumb(span.attr('class'));
spanClass = spanClass + cssName;
});
estateSign = spanClass;
} else if (i == 3) {
var spanClass = '';
var spans = col.find('span');
spans.each(function(a) {
var span = $(this);
var cssName = classNameToNumb(span.attr('class'));
spanClass = spanClass + cssName;
});
estateReserve = spanClass;
} else if (i == 4) {
var spanClass = '';
var spans = col.find('span');
spans.each(function(a) {
var span = $(this);
var cssName = classNameToNumb(span.attr('class'));
spanClass = spanClass + cssName;
});
estateArea = spanClass + '㎡';
} else if (i == 5) {
var spanClass = '';
var spans = col.find('span');
spans.each(function(a) {
var span = $(this);
var cssName = classNameToNumb(span.attr('class'));
spanClass = spanClass + cssName;
});
estatePrice = spanClass + '元/㎡';
}
i++;
})
var estateData = {
estateName: estateName,
estateSite: estateSite,
estateSign: estateSign,
estateReserve: estateReserve,
estateArea: estateArea,
estatePrice: estatePrice
}
if (estateData.estateName) {
data.push(estateData);
}
})
})
return data;
}
/**
* 根据class name 提取数值
* @param {string} className 节点class name
* @returns 数值
*/
function classNameToNumb(className) {
var numb;
if (className == 'numbzero') {
numb = '0';
} else if (className == 'numbone') {
numb = '1';
} else if (className == 'numbtwo') {
numb = '2';
} else if (className == 'numbthree') {
numb = '3';
} else if (className == 'numbfour') {
numb = '4';
} else if (className == 'numbfive') {
numb = '5';
} else if (className == 'numbsix') {
numb = '6';
} else if (className == 'numbseven') {
numb = '7';
} else if (className == 'numbeight') {
numb = '8';
} else if (className == 'numbnine') {
numb = '9';
} else if (className == 'numbdor') {
numb = '.';
}
return numb;
}

  

数据抓取的最终结果,先做个简单的展示:

  

二:页面展示

1.搭建基本的web server,为了方便使用的是express(http://www.expressjs.com.cn/)框架,直接上代码:

var express = require('express');
var getHzfcSaleInfo = require('./hzfc'); var app = express(); app.use(express.static('public')); //处理前台页面的数据请求
app.get('/getHzfcSaleInfo', function(req, res) {
/**
* 处理前台页面ajax请求
* 返回给前台全部的处理数据
* @param {any} data
*/
var hzfcSaleInfo = getHzfcSaleInfo(function(data) {
res.end(JSON.stringify({ data: data }));
// data.forEach(function(item) {
// if (item.estateName) {
// console.log(item.estateName + ' ' + item.estateSite + ' ' + item.estateSign + ' ' + item.estateReserve + ' ' + item.estateArea + ' ' + item.estatePrice + '\n');
// }
// })
}); //res.end(hzfcSaleInfo);
}); /**
* 启动web server
*/
var server = app.listen(8081, function() {
console.log('web server start success', '访问地址为:http://localhost:8081/index.html');
})

其中app.get方法用来处理前台页面的请求

2.前台页面展示:

首先利用高德地图API(http://lbs.amap.com/api/javascript-api/summary/),在网页中展示黑色的地图底图,然后页面发送请求给后台请求数据,然后利用高德api的由名称查询地理位置的方法,递归请求每个楼盘的地理位置,然后用marker添加到地图上,

代码如下:

 var map = new AMap.Map('map', {
resizeEnable: true,
zoom: 11,
center: [120.197428, 30.20923],
mapStyle: 'dark',
});
$.ajax({
url: 'http://localhost:8081/getHzfcSaleInfo',
type: 'GET',
cache: false,
contentType: false,
processData: false,
success: function(data) {
var hzfcSaleInfo = JSON.parse(data).data;
showInfo(hzfcSaleInfo);
},
error: function() {
console.log('后台抓取数据失败!')
}
}) function showInfo(data) {
var saleTotal = document.getElementsByClassName('total')[0];
var d = new Date();
var str = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
saleTotal.innerHTML = str + '日杭州房产销售总量:' + data.length;
//console.log(saleTotal)
AMap.plugin('AMap.Geocoder', function() {
var len = data.length;
var geocoder = new AMap.Geocoder({
city: "杭州" //城市
});
showSingle(data, 0) function showSingle(data, n) {
if (n >= len) {
return;
}
geocoder.getLocation(data[n].estateName, function(status, result) {
if (status == 'complete' && result.geocodes.length) {
//var price = parseInt(data[n].estatePrice)
var marker = priceMarker(data[n].estatePrice, result)
var title = result.geocodes[0].formattedAddress.replace("浙江省杭州市", "") + '<br/><span style="font-size:11px;color:#F00;">价格:' + data[n].estatePrice + '</span>',
content = [];
content.push("小区名称:" + data[n].estateName);
content.push("所在区:" + data[n].estateSite);
content.push("销售套数:" + data[n].estateSign);
content.push("销售总面积:" + data[n].estateArea);
content.push("预定套数:" + data[n].estateReserve);
var infoWindow = new AMap.InfoWindow({
isCustom: true, //使用自定义窗体
content: createInfoWindow(title, content.join("<br/>")),
offset: new AMap.Pixel(16, -45)
});
AMap.event.addListener(marker, 'click', function() {
infoWindow.open(map, marker.getPosition());
});
showSingle(data, n + 1);
} else {
showSingle(data, n + 1);
}
})
}
})
} function priceMarker(estatePrice, result) {
var price = parseInt(estatePrice);
var iconUrl;
if (price <= 10000) {
iconUrl = 'http://localhost:8081/img/icon0.png';
} else if (price > 10000 && price <= 15000) {
iconUrl = 'http://localhost:8081/img/icon1.png';
} else if (price > 15000 && price <= 20000) {
iconUrl = 'http://localhost:8081/img/icon2.png';
} else if (price > 20000 && price <= 25000) {
iconUrl = 'http://localhost:8081/img/icon3.png';
} else if (price > 25000 && price <= 30000) {
iconUrl = 'http://localhost:8081/img/icon4.png';
} else if (price > 30000) {
iconUrl = 'http://localhost:8081/img/icon5.png';
}
var marker = new AMap.Marker({
offset: new AMap.Pixel(-22, -42),
map: map,
bubble: true,
icon: iconUrl,
position: result.geocodes[0].location,
title: result.geocodes[0].formattedAddress
});
return marker
} function createInfoWindow(title, content) {
var info = document.createElement("div");
info.className = "info"; //可以通过下面的方式修改自定义窗体的宽高
//info.style.width = "400px";
// 定义顶部标题
var top = document.createElement("div");
var titleD = document.createElement("div");
var closeX = document.createElement("img");
top.className = "info-top";
titleD.innerHTML = title;
closeX.src = "http://webapi.amap.com/images/close2.gif";
closeX.onclick = closeInfoWindow; top.appendChild(titleD);
top.appendChild(closeX);
info.appendChild(top); // 定义中部内容
var middle = document.createElement("div");
middle.className = "info-middle";
middle.style.backgroundColor = 'white';
middle.innerHTML = content;
info.appendChild(middle); // 定义底部内容
var bottom = document.createElement("div");
bottom.className = "info-bottom";
bottom.style.position = 'relative';
bottom.style.top = '0px';
bottom.style.margin = '0 auto';
var sharp = document.createElement("img");
sharp.src = "http://webapi.amap.com/images/sharp.png";
bottom.appendChild(sharp);
info.appendChild(bottom);
return info;
} //关闭信息窗体
function closeInfoWindow() {
map.clearInfoWindow();
} function refresh(e) {
map.setMapStyle(e);
}

结束语:

这只是个初步的版本,很简单的展示每天都的销售情况,所有的代码都托管在了GITHUB上,项目地址为:https://github.com/react-map/HangzhouRealEstate,各路小伙伴如果有新的思路,新的想法,可以直接在Issues上提出来,一起做一个房产销售数据可视化的平台。

node.js爬虫杭州房产销售及数据可视化的更多相关文章

  1. Node.js爬虫-爬取慕课网课程信息

    第一次学习Node.js爬虫,所以这时一个简单的爬虫,Node.js的好处就是可以并发的执行 这个爬虫主要就是获取慕课网的课程信息,并把获得的信息存储到一个文件中,其中要用到cheerio库,它可以让 ...

  2. Node JS爬虫:爬取瀑布流网页高清图

    原文链接:Node JS爬虫:爬取瀑布流网页高清图 静态为主的网页往往用get方法就能获取页面所有内容.动态网页即异步请求数据的网页则需要用浏览器加载完成后再进行抓取.本文介绍了如何连续爬取瀑布流网页 ...

  3. node.js爬虫

    这是一个简单的node.js爬虫项目,麻雀虽小五脏俱全. 本项目主要包含一下技术: 发送http抓取页面(http).分析页面(cheerio).中文乱码处理(bufferhelper).异步并发流程 ...

  4. Node.js aitaotu图片批量下载Node.js爬虫1.00版

    即使是https网页,解析的方式也不是一致的,需要多试试. 代码: //====================================================== // aitaot ...

  5. Node.js umei图片批量下载Node.js爬虫1.00

    这个爬虫在abaike爬虫的基础上改改图片路径和下一页路径就出来了,代码如下: //====================================================== // ...

  6. Node.js abaike图片批量下载Node.js爬虫1.01版

    //====================================================== // abaike图片批量下载Node.js爬虫1.01 // 1.01 修正了输出目 ...

  7. Node.js abaike图片批量下载Node.js爬虫1.00版

    这个与前作的差别在于地址的不规律性,需要找到下一页的地址再爬过去找. //====================================================== // abaik ...

  8. Node.js 爬虫爬取电影信息

    Node.js 爬虫爬取电影信息 我的CSDN地址:https://blog.csdn.net/weixin_45580251/article/details/107669713 爬取的是1905电影 ...

  9. 手把手教你用Node.js爬虫爬取网站数据

    个人网站 https://iiter.cn 程序员导航站 开业啦,欢迎各位观众姥爷赏脸参观,如有意见或建议希望能够不吝赐教! 开始之前请先确保自己安装了Node.js环境,还没有安装的的童鞋请自行百度 ...

随机推荐

  1. sharedPreferences存储数据

    sharedPreferences使用的是键值对的方式存储数据. 1.Android中三种获取sharedPreferences的方式 1)Context 类中的getSharedPreference ...

  2. Laravel 目录结构分析

    根目录结构 /app/bootstrap/public/vendorartisancomposer.jsonserver.php 1./app 整个Laravel 目录中最需要我们注意的地方,包含设置 ...

  3. Delphi 数据类型的说明

    简单类型包括实数类型(Real) 和有序类型(Ordinal),有序类型又包括整数类型,字符类型,布尔类型,枚举类型和子界类型等. 数据类型                       范围      ...

  4. 关于ios 推送功能的终极解决

    刚刚做了一个使用推送功能的应用 遇到了一些问题整的很郁闷 搞了两天总算是弄明白了 特此分享给大家 本帖 主要是针对产品发布版本的一些问题 综合了网上一些资料根据自己实践写的 不过测试也可以看看 首先要 ...

  5. web前端面试第三波~

    快来测试测试自己掌握能力吧! 1. class.forname的作用?为什么要用? 1).获取Class对象的方式:类名.class.对象.getClass().Class.forName(" ...

  6. IOC:AutoFac使用demo

    使用autofac 实现依赖注入 1.引用 autofac.dll 和 autofac.configuration.dll 2.新增接口 IDAL using System; using System ...

  7. .NET运行机制

    .NET运行机制   .NET框架是一个多语言组件开发和执行环境,它提供了一个跨语言的统一编程环境..NET框架的目的是便于开发人员更容易地建立Web应用程序和Web服务,使得Internet上的各应 ...

  8. java系列--JDBC连接oracle

    <oracle开发实战经典><oracle DBA从入门到精通> JDBC连接数据库 JNDI连接池 oracle.jdbc.driver.OracleDriver 其实就是一 ...

  9. Leetcode 181. Employees Earning More Than Their Managers

    The Employee table holds all employees including their managers. Every employee has an Id, and there ...

  10. dev repositoryItem 手工定义

    一.打开设计界面 二.定义Repository 事件定义 三.把repositoryItemTextEdit1邦定存在的列