資料探勘:如何通過百度地圖API抓取物體周邊地理位置資訊
1.需求描述
對於資料探勘工程師來說,有時候需要抓取地理位置資訊,比如統計房子周邊基礎設施資訊,比如醫院、公交車站、寫字樓、地鐵站、商場等,一般的爬蟲可以採用python指令碼爬取,有很多成型的框架如scrapy,但是想要爬百度地圖就必須遵循它的JavaScript Api,那麼肯定需要自己寫JavaScript指令碼與百度API進行互動,問題是:這種互動下來的資料如何儲存(直接寫進文字or使用sql資料庫?),如何自動化這種互動方式。
因此,本文的目標是用一個rails應用配合js指令碼來實現這種自動化抓取和儲存,思路是js指令碼負責與百度地圖Api互動,rails伺服器端負責儲存抓取的資料,js和rails伺服器用ajax方式傳遞資料. 前提是rails伺服器裡已經有相應的房屋資料,如房屋的街道地址,小區名字等. 接下來需要做的就是為周邊資訊資料建表以及相應的關聯表(因為它們為多對多關係)
2.流程詳解
js程式碼在使用者瀏覽器中執行,因此爬取的主要部分邏輯都需要寫在js腳本里,而rails伺服器端需要完成的是獲得當前需要抓取的房屋資料以及儲存js抓取的資料。下圖為對id=1的房屋周邊資料抓取的分解過程:
- 首先由使用者在瀏覽器中點選開始按鈕,啟用GetDataFromServer()方法,瀏覽器向rails伺服器傳送請求,伺服器的return_next()方法返回當前需要抓取的房屋資料(主要是街道或者小區的位置資訊)
- 通過getPoint方法,瀏覽器向Baidu API 傳送請求查詢房屋座標,若有結果則繼續,否則直接遞迴呼叫GetDataFromServer()
- 使用查詢到的房屋座標搜尋周邊的資訊:對於每一類資訊(如地鐵,醫院等),在查詢到結果後立即向伺服器傳送查詢結果以及房屋資訊,並標記當前的資料型別(地鐵,醫院..).伺服器在接收到資料後,先判斷資料型別,然後根據類別再對房屋的周邊資訊進行儲存.
- 如果完成當前房屋所有的周邊資料的查詢後, 再次呼叫GetDataFromServer()來獲得下一個房屋的資料
3. 程式碼實現
3.1 瀏覽器端(js)
1.GetDataFromServer
: ajax向get_data_url地址以get方法請求json格式的資料, 成功拿到資料後先用小區來匹配房屋座標, 如果失敗再用街道匹配,若兩者都沒找到結果,那麼此房屋的地理資訊為空,則查詢下一個房屋;若能找到房屋座標,呼叫SearchStart()開始搜尋周邊資料
function GetDataFromServer() {
$.ajax({
type: "GET",
url: get_data_url,
dataType: 'json',
success: function (house_data) {
// 拿到房屋資料後先顯示出來
displayHouseData(house_data);
// 然後先用街道去查座標
myGeo.getPoint(house_data.street, function (point) {
if (point) {
// 如果查到座標,開始檢索周圍資訊
SearchStart(point, house_data);
} else {
// 如果街道沒查搭配,再用小區去查座標
myGeo.getPoint(house_data.community, function (repoint) {
if (repoint) {
// 如果查到座標,開始檢索周圍資訊
SearchStart(repoint, house_data)
} else {
setTimeout(function () {
console.log("Error: no address of " + " id: " + data.id + " community: " + data.community + " street: " + data.street);
// 如果還沒查到座標,繼續查詢下一個房屋,延遲timeInterval秒
GetDataFromServer();
}, timeInterval);
}
}, "北京市");
}
}, "北京市");
},
error: function () {
alert('error')
},
timeout: function () {
alert('time out')
}
});
}
2.SearchStart
和SearchNearby
: SearchStart為SearchNearby的入口, SearchNearby方法構建了一個BMap.LocalSearch物件的函式變數,呼叫searchNearby並傳入關鍵詞就可以查詢house_loc附近的所有的包含關鍵詞的位置資訊, search_range能指定查詢附近的範圍. BMap.LocalSearch通過onSearchComplete指定了查詢完成後的回撥函式:這裡我們對查詢的結果做一個遍歷,計算出這個查詢結果與房屋的距離,然後將這些資訊整合到一個數組裡,傳給sendData()來發送資料
function SearchStart(point, house_data) {
// 先在地圖上標記出來
map.centerAndZoom(point, 16);
map.addOverlay(new BMap.Marker(point));
// 首先查詢此房屋的第一個關鍵詞資訊(公交車站,idx=0)
setTimeout(function () {
SearchNearby(point, house_data, 0);
}, timeInterval);
}
function SearchNearby(house_loc, house_data, keyword_idx) {
var nearby_info = [];
// 清除地圖覆蓋物
map.clearOverlays();
var local = new BMap.LocalSearch(map, {
renderOptions: {map: map, autoViewport: false},
pageCapacity: 50,
onSearchComplete: function (results) {
DisplayClear();
if (local.getStatus() == BMAP_STATUS_SUCCESS) {
// 百度地圖成功返回,將每個周邊資訊儲存到nearby_info裡
for (var i = 0; i < results.getCurrentNumPois(); i++) {
var locate = results.getPoi(i);
if (locate != null) {
// 查詢結果與房屋的距離
var distance = parseFloat(map.getDistance(locate.point, house_loc)).toFixed(1);
nearby_info.push(locate.title + "/" + locate.point.lng + '/' + locate.point.lat + '/' + distance);
DisplayNearbyData(nearby_info, locate, distance)
}
}
// 獲得百度地圖查詢結果後立即傳送給伺服器
return sendData(keywords_en[keyword_idx], nearby_info, house_data, house_loc, keyword_idx)
} else {
GetDataFromServer();
console.log("No records with baiduAPI:", local.getStatus());
return false;
}
}
});
local.searchNearby(keywords[keyword_idx], house_loc, search_range);
}
3.sendData
: sendData負責傳送查詢資料nearby_info, 周邊資料型別由nearby_type指定,房子本身的資料資訊由house_data提供而座標由house_loc給出, idx記錄著現在查詢的關鍵詞的索引. sendData使用ajax post方法提交資料, 當提交成功後, 通過呼叫SearchNearby並傳遞下一個關鍵詞的id來檢索這個房子其他周邊資訊;如果當前關鍵詞已經是最後一個,那麼呼叫GetDataFromServer來啟動下一輪的查詢
function sendData(nearby_type, nearby_info, house_data, house_loc, idx) {
data = "nearby_type=" + nearby_type + "&nearby_info=" + nearby_info + "&id=" + house_data.id + "&lat=" + house_loc.lat + "&lng=" + house_loc.lng;
$.ajax({
type: "POST",
url: post_data_url,
data: data,
dataType: "JSON",
success: function (data) {
if (flag) {
console.log("warning", 'pause');
} else {
// 當查詢到最後一個kewords時,請求伺服器獲得下一個房屋資訊
if (idx == keywords.length - 1) {
GetDataFromServer();
} else {
// 查詢此房屋的下一個關鍵詞資訊
setTimeout(function () {
SearchNearby(house_loc, house_data, idx + 1);
}, timeInterval);
}
console.log("success", data);
}
return true;
},
error: function () {
alert('error in post');
return false;
},
timeout: function () {
alert('time out in post');
return false;
}
});
}
3.2 伺服器端(rails controller)
SpidersController
1.return_next
: 通過類變數@@house_id確定當前需要查詢的房屋id,這個全域性id變數隨著return_text的呼叫而自增. 為了避免重複抓取, 跳過已經有相關記錄的,最後以json格式返回房屋資料
@@house_id=0
def return_next
# 查詢下一個房屋資訊
house=House.next_record(@@house_id)
@@house_id=house.id
# 避免重複抓取,跳過已經有相關資訊的
while not house.buses_houses.nil? and not house.buses_houses.blank?
house=House.next_record(@@house_id)
@@house_id=house.id
break if house == House.last
end
# TODO
# 避免重複抓取,現在只能靠bus資訊進行判斷,希望更全面的資訊判斷
# END
if house == House.last
redirect_to buses_path, flash: {:success => "抓取完畢"}
else
respond_to do |format|
format.json { render :json => house }
end
end
end
2.create
: 接受抓取的周邊資料,判斷資料型別並交給insert處理
def create
house=House.find_by(id: params[:id])
house.latitude=params[:lat]
house.longitude=params[:lng]
house.save
insert(house, params, Bus, BusesHouses, 'bus') if params[:nearby_type] == 'bus'
insert(house, params, Hospital, HospitalsHouses, 'hospital') if params[:nearby_type] == 'hospital'
insert(house, params, Work, WorksHouses, 'work') if params[:nearby_type] == 'work'
insert(house, params, School, SchoolsHouses, 'school') if params[:nearby_type] == 'school'
insert(house, params, Subway, SubwaysHouses, 'subway') if params[:nearby_type] == 'subway'
insert(house, params, Shop, ShopsHouses, 'shop') if params[:nearby_type] == 'shop'
render json: params.as_json
end
HousesHelper
3.insert
: 解析sendData()傳送來的資料, 建立相應的記錄. attr中存放著每條周邊資料經度,維度以及名字; obj為類名,如Bus, Subway, 通過find_by()方法查詢這個經度和維度是否已經存在,若已經存在此記錄, 說明之前存過了,因為同一片區域的房子可能會有公有的基礎設施; 若不存在,則建立新的記錄. asso_obj為關聯表,如BusesHouses, 這是由於bus和house為多對多關係: 一個公交車站附近有多個房屋,一個房屋附近也有多個公交車站, 所以需要這個關聯表來儲存bus和house的對應關係(由三個欄位表示: house_id, bus_id和distance, 表示這個house_id與這個bus_id是附近關係,而且相距distance), 因此有幾類周邊型別,就需要多少個關聯表,本文共有Bus, Hospital, Work, School, Subway, Shop 6類周邊資料, 所以需要維護6個關聯表,BusHouses是其中的一種.
def insert(house, params, obj, asso_obj, asso_type)
params[:nearby_info].split(',').each do |row|
attr=row.split('/')
bus=obj.find_by(longitude: attr[1], latitude: attr[2])
if bus.nil?
bus=obj.new(name: attr[0], longitude: attr[1], latitude: attr[2])
bus.save
end
asso_obj.create("#{asso_type}_id": bus.id, house_id: house.id, distance: attr[3])
end
end
Demo
先給出一些截圖, 目前基礎資料只爬取了部分北京二手房的資料,所以資料可能已經過時了, 完整的程式碼在HousePricing, 房屋的基礎資料可以去房天下,鏈家等房價網站爬取, 參考這個scrapy-HousePricing, 爬取後匯入到rails的資料庫,就可以使用上面的方法便利抓取地理位置資訊