Fork me on GitHub

node.js爬虫杭州房产销售及数据可视化

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

                                                                

 

        先说下基本思路:

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

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

 

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

        一:后台爬虫

          1.抓取在线网络数据

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

 1 //定义爬虫数据源网络地址
 2 var url = 'http://www.tmsf.com/daily.htm';
 3 
 4 /**
 5  * 请求网络地址抓取数据
 6  * @param {function} callBack 传回爬虫数据处理之后的最终结果
 7  */
 8 function getHzfcSaleInfo(callBack) {
 9     var hzfcSaleInfo = [];
10     http.get(url, function(res) {
11         var html = '';
12         res.on('data', function(data) {
13             html += data;
14         });
15         res.on('end', function() {
16             hzfcSaleInfo = filterData(html);
17             callBack(hzfcSaleInfo);
18         });
19         res.on('error', function() {
20             console.log('获取数据出错');
21         });
22     })
23 }

           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添加到地图上,

     代码如下:

  1 var map = new AMap.Map('map', {
  2     resizeEnable: true,
  3     zoom: 11,
  4     center: [120.197428, 30.20923],
  5     mapStyle: 'dark',
  6 });
  7 $.ajax({
  8     url: 'http://localhost:8081/getHzfcSaleInfo',
  9     type: 'GET',
 10     cache: false,
 11     contentType: false,
 12     processData: false,
 13     success: function(data) {
 14         var hzfcSaleInfo = JSON.parse(data).data;
 15         showInfo(hzfcSaleInfo);
 16     },
 17     error: function() {
 18         console.log('后台抓取数据失败!')
 19     }
 20 })
 21 
 22 function showInfo(data) {
 23     var saleTotal = document.getElementsByClassName('total')[0];
 24     var d = new Date();
 25     var str = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
 26     saleTotal.innerHTML = str + '日杭州房产销售总量:' + data.length;
 27     //console.log(saleTotal)
 28     AMap.plugin('AMap.Geocoder', function() {
 29         var len = data.length;
 30         var geocoder = new AMap.Geocoder({
 31             city: "杭州" //城市
 32         });
 33         showSingle(data, 0)
 34 
 35         function showSingle(data, n) {
 36             if (n >= len) {
 37                 return;
 38             }
 39             geocoder.getLocation(data[n].estateName, function(status, result) {
 40                 if (status == 'complete' && result.geocodes.length) {
 41                     //var price = parseInt(data[n].estatePrice)
 42                     var marker = priceMarker(data[n].estatePrice, result)
 43                     var title = result.geocodes[0].formattedAddress.replace("浙江省杭州市", "") + '<br/><span style="font-size:11px;color:#F00;">价格:' + data[n].estatePrice + '</span>',
 44                         content = [];
 45                     content.push("小区名称:" + data[n].estateName);
 46                     content.push("所在区:" + data[n].estateSite);
 47                     content.push("销售套数:" + data[n].estateSign);
 48                     content.push("销售总面积:" + data[n].estateArea);
 49                     content.push("预定套数:" + data[n].estateReserve);
 50                     var infoWindow = new AMap.InfoWindow({
 51                         isCustom: true, //使用自定义窗体
 52                         content: createInfoWindow(title, content.join("<br/>")),
 53                         offset: new AMap.Pixel(16, -45)
 54                     });
 55                     AMap.event.addListener(marker, 'click', function() {
 56                         infoWindow.open(map, marker.getPosition());
 57                     });
 58                     showSingle(data, n + 1);
 59                 } else {
 60                     showSingle(data, n + 1);
 61                 }
 62             })
 63         }
 64     })
 65 }
 66 
 67 function priceMarker(estatePrice, result) {
 68     var price = parseInt(estatePrice);
 69     var iconUrl;
 70     if (price <= 10000) {
 71         iconUrl = 'http://localhost:8081/img/icon0.png';
 72     } else if (price > 10000 && price <= 15000) {
 73         iconUrl = 'http://localhost:8081/img/icon1.png';
 74     } else if (price > 15000 && price <= 20000) {
 75         iconUrl = 'http://localhost:8081/img/icon2.png';
 76     } else if (price > 20000 && price <= 25000) {
 77         iconUrl = 'http://localhost:8081/img/icon3.png';
 78     } else if (price > 25000 && price <= 30000) {
 79         iconUrl = 'http://localhost:8081/img/icon4.png';
 80     } else if (price > 30000) {
 81         iconUrl = 'http://localhost:8081/img/icon5.png';
 82     }
 83     var marker = new AMap.Marker({
 84         offset: new AMap.Pixel(-22, -42),
 85         map: map,
 86         bubble: true,
 87         icon: iconUrl,
 88         position: result.geocodes[0].location,
 89         title: result.geocodes[0].formattedAddress
 90     });
 91     return marker
 92 }
 93 
 94 function createInfoWindow(title, content) {
 95     var info = document.createElement("div");
 96     info.className = "info";
 97 
 98     //可以通过下面的方式修改自定义窗体的宽高
 99     //info.style.width = "400px";
100     // 定义顶部标题
101     var top = document.createElement("div");
102     var titleD = document.createElement("div");
103     var closeX = document.createElement("img");
104     top.className = "info-top";
105     titleD.innerHTML = title;
106     closeX.src = "http://webapi.amap.com/images/close2.gif";
107     closeX.onclick = closeInfoWindow;
108 
109     top.appendChild(titleD);
110     top.appendChild(closeX);
111     info.appendChild(top);
112 
113     // 定义中部内容
114     var middle = document.createElement("div");
115     middle.className = "info-middle";
116     middle.style.backgroundColor = 'white';
117     middle.innerHTML = content;
118     info.appendChild(middle);
119 
120     // 定义底部内容
121     var bottom = document.createElement("div");
122     bottom.className = "info-bottom";
123     bottom.style.position = 'relative';
124     bottom.style.top = '0px';
125     bottom.style.margin = '0 auto';
126     var sharp = document.createElement("img");
127     sharp.src = "http://webapi.amap.com/images/sharp.png";
128     bottom.appendChild(sharp);
129     info.appendChild(bottom);
130     return info;
131 }
132 
133 //关闭信息窗体
134 function closeInfoWindow() {
135     map.clearInfoWindow();
136 }
137 
138 function refresh(e) {
139     map.setMapStyle(e);
140 }
View Code

 

    结束语:

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

     

         

 

posted @ 2017-02-08 15:10  sylven'as  阅读(1450)  评论(0编辑  收藏  举报