因此,本文的目標是用一個rails應用配合js腳本來實現這種自動化抓取和儲存,思路是js腳本負責與百度地圖Api交互,rails服務器端負責儲存抓取的數據,js和rails服務器用ajax方式傳遞數據. 前提是rails服務器里已經有相應的房屋數據,如房屋的街道地址,小區名字等. 接下來需要做的就是為周邊信息數據建表以及相應的關聯表(因為它們為多對多關系)

流程詳解

js代碼在用戶瀏覽器中執行,因此爬取的主要部分邏輯都需要寫在js腳本里,而rails服務器端需要完成的是獲得當前需要抓取的房屋數據以及儲存js抓取的數據。下圖為對id=1的房屋周邊數據抓取的分解過程:

  1. 首先由用戶在瀏覽器中點擊開始按鈕,激活GetDataFromServer()方法,瀏覽器向rails服務器發送請求,服務器的return_next()方法返回當前需要抓取的房屋數據(主要是街道或者小區的位置信息);
  2. 通過getPoint方法,瀏覽器向Baidu API 發送請求查找房屋坐標,若有結果則繼續,否則直接遞歸調用GetDataFromServer();
  3. 使用查詢到的房屋坐標搜索周邊的信息:對于每一類信息(如地鐵,醫院等),在查詢到結果后立即向服務器發送查詢結果以及房屋信息,并標記當前的數據類型(地鐵,醫院..).服務器在接收到數據后,先判斷數據類型,然后根據類別再對房屋的周邊信息進行儲存;
  4. 如果完成當前房屋所有的周邊數據的查詢后, 再次調用GetDataFromServer()來獲得下一個房屋的數據。

  代碼實現

1. 瀏覽器端(js)

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')
}
});
}

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);}

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;
}
});
}

2. 服務器端(rails controller)

return_next:?通過類變量@@house_id確定當前需要查詢的房屋id,這個全局id變量隨著return_text的調用而自增. 為了避免重復抓取, 跳過已經有相關記錄的,最后以json格式返回房屋數據。

@@house_id=0def 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
endend

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_jsonend

HousesHelper

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])

Demo

https://house-pricing.herokuapp.com/

房屋的基礎數據可以去房天下,鏈家等房價網站爬取, 參考這個scrapy-HousePricing,

https://github.com/PENGZhaoqing/scrapy-HousePricing?爬取后導入到rails的數據庫,就可以使用上面的方法便利抓取地理位置信息。

文章轉自微信公眾號@數據派THU

上一篇:

百度地圖API使用總結(一)

下一篇:

大牛Python庫中的云服務集成專家!Google API的Python使者!
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費