GeoHash在LBS的应用,看完这篇就什么都懂了

今天在做项目时,遇到这么一个小小场景:对于用户的一条行为数据信息,我需要通过他的地理坐标实时的得到他所在地附近商圈信息,并且给他打上相关标签以方便向他实时推送广告。问题是:如何根据用户的地理坐标获得他附近的商圈信息呢?怎样控制获得商圈信息的地理坐标范围呢? 怎样更精确的获得附近商圈的信息呢?
这里有一个很关键的GeoHash算法解决了这些问题,下面带着这三个问题来阅读这篇文章,你就会收获很多。
在这之前,先给大家介绍一下GeoHash算法

1、GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。

GeoHash在LBS的应用,看完这篇就什么都懂了_第1张图片

2、字符串越长,表示的范围越精确。如图所示,5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)

GeoHash在LBS的应用,看完这篇就什么都懂了_第2张图片

3、字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,一个在城区,一个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串
相似程度要低些。

GeoHash在LBS的应用,看完这篇就什么都懂了_第3张图片

通过上面的介绍我们知道了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的插入顺序排序;post请使用TreeMap保存,
    // 该方法会自动将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)
  }
}

看到这里,相信篇头的三个问题大家都迎刃而解。好了,今天的分享就到这里,希望大家看完都各有收获!!

你可能感兴趣的:(Spark,GeoHash,LBS)