GeoHash在LBS的應用,看完這篇就什麼都懂了
今天在做專案時,遇到這麼一個小小場景:對於使用者的一條行為資料資訊,我需要通過他的地理座標實時的得到他所在地附近商圈資訊,並且給他打上相關標籤以方便向他實時推送廣告。問題是:如何根據使用者的地理座標獲得他附近的商圈資訊呢?怎樣控制獲得商圈資訊的地理座標範圍呢? 怎樣更精確的獲得附近商圈的資訊呢?
這裡有一個很關鍵的GeoHash演算法解決了這些問題,下面帶著這三個問題來閱讀這篇文章,你就會收穫很多。
在這之前,先給大家介紹一下GeoHash演算法
1、GeoHash將二維的經緯度轉換成字串,比如下圖展示了北京9個區域的GeoHash字串,分別是WX4ER,WX4G2、WX4G3等等,每一個字串代表了某一矩形區域。也就是說,這個矩形區域內所有的點(經緯度座標)都共享相同的GeoHash字串,這樣既可以保護隱私(只表示大概區域位置而不是具體的點),又比較容易做快取,比如左上角這個區域內的使用者不斷髮送位置資訊請求餐館資料,由於這些使用者的GeoHash字串都是WX4ER,所以可以把WX4ER當作key,把該區域的餐館資訊當作value來進行快取,而如果不使用GeoHash的話,由於區域內的使用者傳來的經緯度是各不相同的,很難做快取。
2、字串越長,表示的範圍越精確。如圖所示,5位的編碼能表示10平方千米範圍的矩形區域,而6位編碼能表示更精細的區域(約0.34平方千米)
3、字串相似的表示距離相近(特殊情況後文闡述),這樣可以利用字串的字首匹配來查詢附近的POI資訊。如下兩個圖所示,一個在城區,一個在郊區,城區的GeoHash字串之間比較相似,郊區的字串之間也比較相似,而城區和郊區的GeoHash字串
相似程度要低些。
通過上面的介紹我們知道了GeoHash就是一種將經緯度轉換成字串的方法,並且使得在大部分情況下,字串字首匹配越多的距離越近,回到我們的案例,根據所在位置查詢來查詢附近餐館時,只需要將所在位置經緯度轉換成GeoHash字串,並與各個餐館的GeoHash字串進行字首匹配,匹配越多的距離越近。
現在相信你對GeoHash演算法已經有了一些瞭解,下面我們通過程式碼通過地理座標獲取它的商圈資訊
當然在這之前你需要在百度地圖開放平臺申請金鑰,然後申請應用獲取AK和SK的資訊,個人就可以申請,並且每天免費有0.6萬次的配額上限,具體的申請過程這裡就不再贅述,下面來看這段用scala來寫的獲取商圈的程式碼
package com.utils import java.io.UnsupportedEncodingException import java.net.URLEncoder import java.security.NoSuchAlgorithmException import java.util import com.google.gson.{JsonObject, JsonParser} import org.apache.commons.httpclient.HttpClient import org.apache.commons.httpclient.methods.GetMethod import org.apache.commons.lang3.StringUtils /** * 請求百度LBS(位置服務),解析經緯度對應的商圈資訊 * */ object BaiduLBSHandler { /** * 對外提供的解析經緯度對應的商圈資訊 * * @param lng 經度 * @param lat 緯度 */ def parseBusinessTagBy(lng: String, lat: String) = { var business: String = "" val requestParams = requetParams(lng, lat) val requestURL = "http://api.map.baidu.com/geocoder/v2/?" + requestParams // 使用HttpClient 模擬瀏覽器傳送請求 val httpClient = new HttpClient() val getMethod = new GetMethod(requestURL) val statusCode = httpClient.executeMethod(getMethod) if (statusCode == 200) { // HTTP.OK val response = getMethod.getResponseBodyAsString // 判斷是否是合法的json字元換 var str = response.replaceAll("renderReverse&&renderReverse\\(", "") if (!response.startsWith("{")) { str = str.substring(0, str.length - 1) } // 解析這個json字串,取出business節點資料 val returnData = new JsonParser().parse(str).getAsJsonObject // 伺服器返回來的json資料,status表示伺服器是否正常(0)處理了我的請求 val status = returnData.get("status").getAsInt if (status == 0) { val resultObject = returnData.getAsJsonObject("result") business = resultObject.get("business").getAsString.replaceAll(",", ";") // 判斷business是否為空,如果為空,接著解析改座標點附近的標籤資訊pois if (StringUtils.isEmpty(business)) { val pois = resultObject.getAsJsonArray("pois") var tagSet = Set[String]() for (i <- 0 until pois.size()) { val elemObject: JsonObject = pois.get(i).getAsJsonObject val tag = elemObject.get("tag").getAsString if (StringUtils.isNotEmpty(tag)) tagSet += tag } business = tagSet.mkString(";") } } } business } private def requetParams(lng: String, lat: String) = { //3eWFUfbFLTMopRpY1xk9BRD3iFdxo3r4 //rvGCL2H2iEScXwNgZvGplcyRsnDC2x9j // ak , sk val list ="y3sWrdIWEjAMMti4i4klRtZzzRDPgwl7,82pKQOUGkjGcuESghMndR00PmQwQSxIS" val Array(ak, sk) = list.split(",") // 計算sn跟引數對出現順序有關,get請求請使用LinkedHashMap儲存<key,value>, // 該方法根據key的插入順序排序;post請使用TreeMap儲存<key,value>, // 該方法會自動將key按照字母a-z順序排序。所以get請求可自定義引數順序(sn引數必須在最後)傳送請求, // 但是post請求必須按照字母a-z順序填充body(sn引數必須在最後)。 // 以get請求為例:http://api.map.baidu.com/geocoder/v2/?address=百度大廈&output=json&ak=yourak, // paramsMap中先放入address,再放output,然後放ak,放入順序必須跟get請求中對應引數的出現順序保持一致。 val paramsMap = new util.LinkedHashMap[String, String](); paramsMap.put("callback", "renderReverse") // paramsMap.put("location", "39.343424,116.452987") paramsMap.put("location", lat.concat(",").concat(lng)) paramsMap.put("output", "json") paramsMap.put("pois", "1") paramsMap.put("ak", ak) // 請求的引數 val paramsStr = toQueryString(paramsMap) // 生成SN val wholeStr = new String("/geocoder/v2/?" + paramsStr + sk) val tempStr = URLEncoder.encode(wholeStr, "UTF-8") val sn = MD5(tempStr) paramsStr + "&sn=" + sn } // 對Map內所有value作utf8編碼,拼接返回結果 @throws[UnsupportedEncodingException] private def toQueryString(data: util.LinkedHashMap[String, String]): String = { val queryString = new StringBuffer import scala.collection.JavaConversions._ for (pair <- data.entrySet) { queryString.append(pair.getKey + "=") queryString.append(URLEncoder.encode(pair.getValue.asInstanceOf[String], "UTF-8") + "&") } if (queryString.length > 0) queryString.deleteCharAt(queryString.length - 1) queryString.toString } // 來自stackoverflow的MD5計算方法,呼叫了MessageDigest庫函式,並把byte陣列結果轉換成16進位制 private def MD5(md5: String): String = { try { val md = java.security.MessageDigest.getInstance("MD5") val array = md.digest(md5.getBytes) val sb = new StringBuffer var i = 0 while ( { i < array.length }) { sb.append(Integer.toHexString((array(i) & 0xFF) | 0x100).substring(1, 3)) { i += 1 i } } return sb.toString } catch { case e: NoSuchAlgorithmException => } null } }
下面的是通過呼叫上面的方法傳入地理座標,獲取到相關商圈,其實也可以把獲取到的商圈資訊儲存在redis(程式碼中已註釋掉)中,由於百度地圖提供的介面是按條收費的,這樣我們就可以直接從redis中讀取商圈資訊,從而減少開發成本。當然嘍,這種方式也是有弊端的,比如說,使用者的某個地理座標附近的商圈位置或資訊發生了變化,而redis中的資料卻沒有改變,這樣就導致使用者獲得了錯誤的資訊,這裡其實也是有解決方案的,對redis中的資料用expire進行過期時間處理,根據地理資訊每過一段時間清除redis中的資料,然後使用者獲得新的商圈資訊。個人認為這樣可行,有誤的地方或者有更好的見解,歡迎大家踴躍指出
package com.Tag
import ch.hsr.geohash.GeoHash
import com.utils.{BaiduLBSHandler}
import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}
object ExtractLongandLat2Business {
def main(args: Array[String]): Unit = {
if(args.length!=1){
println("argument is wrong!!")
sys.exit()
}
val Array(inputPath) = args
val conf = new SparkConf().setMaster("local[*]")
.setAppName(s"${this.getClass.getSimpleName}")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val sc = new SparkContext(conf)
val sqlContext = new SQLContext(sc)
sqlContext.setConf("spark.sql.parquet.compression.codec","snappy")
sqlContext.read.parquet(inputPath).select("lat","long").filter(
"""
|cast(long as double) >=73 and cast(long as double) <=136 and
|cast(lat as double) >=3 and cast(lat as double) <=54
""".stripMargin).distinct()
//還可以把獲得的商圈資訊存放在redis中,減少成本
// .foreachPartition(t=>{
// val jedis = JedisConnectionPool.getConnection()
// t.foreach(t=>{
// val long = t.getAs[String]("long")
// val lat = t.getAs[String]("lat")
// //通過百度的逆地址解析,獲取到商圈資訊
// val geoHashs = GeoHash.geoHashStringWithCharacterPrecision(long.toDouble,lat.toDouble,8)
// //進行sn驗證
// val business = BaiduLBSHandler.parseBusinessTagBy(long,lat)
// jedis.set(geoHashs,business)
// })
// jedis.close()
// })
.map(t=>{
val long =t.getAs[String]("long")
val lat = t.getAs[String]("lat")
//8代表返回geoHash的值為8位 字串的長度越長,獲得的地理位置越精確
val geoHash = GeoHash.geoHashStringWithCharacterPrecision(lat.toDouble,long.toDouble,8)
val b = BaiduLBSHandler.parseBusinessTagBy(long,lat)
(geoHash,b)
}).foreach(println)
}
}
看到這裡,相信篇頭的三個問題大家都迎刃而解。好了,今天的分享就到這裡,希望大家看完都各有收穫!!