1. 程式人生 > >資料探勘:如何通過百度地圖API抓取物體周邊地理位置資訊

資料探勘:如何通過百度地圖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的房屋周邊資料抓取的分解過程:

這裡寫圖片描述

  1. 首先由使用者在瀏覽器中點選開始按鈕,啟用GetDataFromServer()方法,瀏覽器向rails伺服器傳送請求,伺服器的return_next()方法返回當前需要抓取的房屋資料(主要是街道或者小區的位置資訊)
  2. 通過getPoint方法,瀏覽器向Baidu API 傳送請求查詢房屋座標,若有結果則繼續,否則直接遞迴呼叫GetDataFromServer()
  3. 使用查詢到的房屋座標搜尋周邊的資訊:對於每一類資訊(如地鐵,醫院等),在查詢到結果後立即向伺服器傳送查詢結果以及房屋資訊,並標記當前的資料型別(地鐵,醫院..).伺服器在接收到資料後,先判斷資料型別,然後根據類別再對房屋的周邊資訊進行儲存.
  4. 如果完成當前房屋所有的周邊資料的查詢後, 再次呼叫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.SearchStartSearchNearby: 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的資料庫,就可以使用上面的方法便利抓取地理位置資訊

這裡寫圖片描述

這裡寫圖片描述