如今年輕人到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 }
結束語:
這只是個初步的版本,很簡單的展現天天都的銷售狀況,全部的代碼都託管在了GITHUB上,項目地址爲:https://github.com/react-map/HangzhouRealEstate,各路小夥伴若是有新的思路,新的想法,能夠直接在Issues上提出來,一塊兒作一個房產銷售數據可視化的平臺。