node.js爬蟲杭州房產銷售及數據可視化

     如今年輕人到25歲+,總的要考慮買房結婚的問題,2016年的一波房價大漲,小夥伴們紛紛表示再也買不起上海的房產了,博主也得考慮考慮將來的發展了,思考了好久,決定去杭州工做、買房、定居、生活,以前去過不少次杭州,很喜歡這個城市,因而例行天天晚上都要花一點時間關注杭州的房產銷售狀況,以及價格,起初我天天都在杭州的本地論壇,透明售房網上查看,每一天的房產銷售數據,可是無奈博主不是杭州本地人,看了網頁上展現的不少樓盤,可是我不知道都在什麼地方啊,因而乎,看到價格合適的,老是到高德地圖去搜索地理位置,每次很是麻煩,因而我想是否是能夠,寫一個小的爬蟲工具,天天抓取透明售房網上的銷售記錄,直接展現在地圖上,直觀明瞭的看看都是哪些地方的樓盤地理位置不錯,同時價格也在能接受的範圍內,同時最近在學習node.js,正好能夠練練手。說幹就幹,一個下午時間,有了初步的成果以下,後期在加入天天的銷售數據,加入到mongoDB中,用於分析每週、每個月的銷售數據,用於本身買房的參考,要學以至用嘛!javascript

                                                                

 

        先說下基本思路:css

        第一步:利用nodejs,技術抓取透明售房網的實時的數據(http://www.tmsf.com/daily.htm),存儲在後臺;html

        第二步:頁面請求後臺數據,而後藉助高德地圖提供的按照名稱查詢地理位置的服務,展現在地圖上,並綁定每一個樓盤的銷售詳情;java

 

       ok,有了基本思路,下面一步一步的開幹:node

        一:後臺爬蟲react

          1.抓取在線網絡數據git

          這裏先介紹一個利器,cheerio(https://github.com/cheeriojs/cheerio),能夠說是位服務器特別定製的,快速,靈活,實施的jQuery核心實現,或者說是後臺解析html的;安裝nodejs 模塊這裏再也不說明,抓取html頁面邏輯比較簡單,直接上代碼:                     github

 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.解析獲取的數據web

            已經抓取整個網頁的數據,在這一步中要根據網頁的DOM,結構來分析應該怎麼解析:首先咱們能夠看到,每日房產銷售狀況的數據是分行政區展現在並列的幾個div中,經過display控制顯示哪個行政區,因此思路就是首先獲取這個外層container,而後不停一層一層的循環解析數據;ajax

           

 

          其中解析到每一行的數據的時候,發現了一個有點奇葩的網頁展現,每一行後面數字居然不是直接用數字來表示的,而是用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上提出來,一塊兒作一個房產銷售數據可視化的平臺。

相關文章
相關標籤/搜索