• 基于 Golang 完整获取百度地图POI数据的方案


    百度地图为web开发者提供了基于HTTP/HTTPS协议的丰富接口,其中包括地点检索服务,web开发者通过此接口可以检索区域内的POI数据。百度地图处于数据保护对接口做了限制,每次访问服务,最多只能检索到400条数据,这样开发者就无法轻易的扒光收录的POI数据。作者基于 Golang 编写程序,完整获取百度地图POI数据。

    百度地图WEB服务API基于HTTP/HTTPS协议,用户按照API文档要求的格式发送HTTP请求来获取POI数据,请求获取的数据格式可以为xml或json。

    地点检索接口提供了3种检索方式:行政区划区域检索,圆形区域检索,矩形区域检索。详见接口文档。web 开发者在不同的场景可以采用不同的检索方式。

    我们的目的是完整的获取POI数据,针对这一使用场景,最先想到是按照行政区划区域检索,先建立区域字典,然后一个区域一个区域的去获取数据。但是,有些区域POI的密度很大,一个市可能有数千家餐馆,而每次检索最多检索到400条数据,显然无法完整的拿到数据。

    为突破数据保护的限制,这里采用矩形检索的方式。首先把要抓取的区域放到两经线和两纬线之间,构成一个大矩形(把地球看成方的),然后从大矩形的角落开始,逐个检索一个个小矩形,同时根据上一个小矩形的POI数量动态调整小矩形的面积。当所有小矩形组合覆盖完成大矩形之后,即可认为数据抓取完整。

    OK,动手写代码!

    需要考虑如下问题:

    • 如何设置小矩形的初始面积?如何动态调整小矩形的面积?
    • 地图API每日调用次数有限制, 超出限制之后如何继续上次的抓取?

    提出这些问题之后,程序的大致结构有了,首先加载抓取状态数据,判断用户上次是否抓取完整,如果已经抓取完整,则让用户输入新的大矩形参数(两个经度,两个纬度);否则让用户决定是否继续抓取。

    那么,状态数据应该包含哪些?该怎么存储状态数据呢?显然这么小的一个程序不需要用到数据库,存到文件中即可,而Golang处理JSON格式的数据很方便,所以状态数据可以以JSON格式存到文件中。存储的内容应该包含大矩形信息,小矩形信息,当前POI类别信息(POI文档),小矩形区域最近一次抓取的页号(每页最多20条),API调用信息。于是状态格式可以设计为:

    {
        "upperLatitude": 40.15,
        "lowerLatitude": 38.33,
        "leftLongitude": 116.42,
        "rightLongitude": 118.04,
        "apiAvailableTimes": 2000,
        "lastUseDate": "20181219",
        "apiKey": "申请的API Key",
        "lastLongitudePosition": 118.98,
        "lastLatitudePosition": 40.109,
        "lastCategoryIndex": 0,
        "lastLongitudeLength": 1.28,
        "lastLatitudeLength": 0.001,
        "lastPageIndex": -1
    }
    View Code

    对应的golang结构体:

    /**
     * 状态结构体
     */
    type Status struct {
        UpperLatitude         float64 `json:"upperLatitude"`         //上纬度
        LowerLatitude         float64 `json:"lowerLatitude"`         //下纬度
        LeftLongitude         float64 `json:"leftLongitude"`         //左经度
        RightLongitude        float64 `json:"rightLongitude"`        //右经度
        ApiAvailableTimes     int     `json:"apiAvailableTimes"`     //API可用次数
        LastUseDate           string  `json:"lastUseDate"`           //上次使用日期
        ApiKey                string  `json:"apiKey"`                //API Key
        LastLongitudePosition float64 `json:"lastLongitudePosition"` //上次抓取经度位置
        LastLatitudePosition  float64 `json:"lastLatitudePosition"`  //上次抓取纬度位置
        LastCategoryIndex     int     `json:"lastCategoryIndex"`     //上次抓取类别索引
        LastLongitudeLength   float64 `json:"lastLongitudeLength"`   //上次抓取矩形区域横向宽度(经度位移)
        LastLatitudeLength    float64 `json:"lastLatitudeLength"`    //上次抓取矩形区域纵向高度(纬度位移)
        LastPageIndex         int     `json:"lastPageIndex"`         //上次抓取页索引
    }

     为使程序模块化,我们将保存,读取状态文件,保存状态等函数挂到此结构体下(为阅读方便,省略了函数体)。

     

    /*
     * 保存状态
     */
    func (s *Status) Save(path string) error 
    
    /**
     * 加载状态
     */
    func (s *Status) Load(path string) error 
    
    var category = []string{
        "酒店", "生活服务", "房地产", "汽车服务",
        "教育培训", "丽人", "运动健身", "美食",
        "政府机构", "自然地物", "公司企业", "交通设施",
        "旅游景点", "医疗", "文化传媒", "出入口", "购物",
        "休闲娱乐", "金融"}
    
    /**
     * 获取类别
     */
    func (s Status) GetCategory() string 
    
    /**
     * 类别数量
     */
    func (s Status) CategorySize() int 
    
    /**
     * 重置API
     */
    func (s *Status) Reset() 
    /**
     * 更新API的使用次数
     */
    func (s *Status) RefreshApiAvairableTimes() 
    
    func today() string 

    完成了抓取状态控制代码之后,接下来是设计解析POI数据的代码,golang天生处理JSON非常方便,所以我们需要接口返回JSON格式数据,然后按照返回的数据格式设计对应的结构体即可。

    首先我们在浏览器中输入如下url,注意要先申请API Key,获取一手样本数据,然后写出对应的golang结构体。

    http://api.map.baidu.com/place/v2/search?query=银行&bounds=39.915,116.404,39.975,116.414&output=json&ak={您的密钥}

    样例数据

    {
        "status":0,
        "message":"ok",
        "total":400,
        "results":[
            {
                "name":"华信半岛酒店(天津百货大楼)",
                "location":{
                    "lat":39.135317,
                    "lng":117.199346
                },
                "address":"天津市和平区和平路172号天津百货大楼F1",
                "province":"天津市",
                "city":"天津市",
                "area":"和平区",
                "street_id":"546b796f38537107d518b782",
                "telephone":"(022)59063666",
                "detail":1,
                "uid":"546b796f38537107d518b782"
            },
            {
                "name":"维也纳酒店(贵州路店)",
                "location":{
                    "lat":39.118757,
                    "lng":117.200051
                },
                "address":"天津市和平区贵州路16号",
                "province":"天津市",
                "city":"天津市",
                "area":"和平区",
                "street_id":"c92e659a2a785d188c55ec32",
                "telephone":"(022)85588888",
                "detail":1,
                "uid":"58c3f6ea68ee0da917c8f6f8"
            },
            {
                "name":"鑫茂天财酒店",
                "location":{
                    "lat":39.10411,
                    "lng":117.125587
                },
                "address":"天津市西青区华苑新技术产业园区榕苑路1号(临近复康路)",
                "province":"天津市",
                "city":"天津市",
                "area":"西青区",
                "street_id":"4edf9fbf49ade88dcc06a459",
                "detail":1,
                "uid":"4edf9fbf49ade88dcc06a459"
            }
        ]
    }
    View Code

    对应的结构体

    // POI 位置
    type PoiLocation struct {
        Lat float64 `json:"lat"` //纬度
        Lgt float64 `json:"lng"` //经度
    }
    
    // POI 信息
    type Poi struct {
        Name      string      `json:"name"`
        Location  PoiLocation `json:"location"`
        Address   string      `json:"address"`
        Province  string      `json:"province"`
        City      string      `json:"city"`
        Area      string      `json:"area"`
        Street_id string      `json:"street_id"`
        Telephone string      `json:"telephone"`
        //Detail    string      `json:"detail"`
        Uid string `json:"uid"`
    }
    
    type PoiResponse struct {
        Status  int    `json:"status"`
        Message string `json:"message"`
        Total   int    `json:"total"`
        Results []Poi  `json:"results"`
    }

    最后是流程控制,简单起见,用户交互采用控制台输入的方式。流程控制较为复杂,有三层循环,分别对应类别切换,纬度切换,经度切换。代码如下:

    for ; s.LastCategoryIndex < s.CategorySize(); s.LastCategoryIndex++ {
            f, err := os.OpenFile(fmt.Sprintf("%f_%f_%f_%f_%s.txt", s.LowerLatitude, s.LeftLongitude, s.UpperLatitude, s.RightLongitude, s.GetCategory()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
            if err != nil {
                log.Panic("打开文件出错。", err)
            }
            defer f.Close()
    
            for ; s.LastLatitudePosition > s.LowerLatitude; s.LastLatitudePosition -= s.LastLatitudeLength {
                s.LastLatitudePosition = math.Round(s.LastLatitudePosition*100000) / 100000
                for s.LastLongitudePosition < s.RightLongitude {
                    upper := s.LastLatitudePosition
                    lower := upper - s.LastLatitudeLength
                    left := s.LastLongitudePosition
                    right := left + s.LastLongitudeLength
    
                NEXT_PAGE:
                    if s.ApiAvailableTimes < 1 {
                        log.Println("今日API次数用完了")
                        os.Exit(0)
                    }
                    log.Printf("开始抓取[%.5f,%.5f,%.5f,%.5f], %s, 页号:%d", lower, left, upper, right, s.GetCategory(), s.LastPageIndex+1)
                    time.Sleep(500000000) //避免超出频率限制
                    s.ApiAvailableTimes--
                    url := fmt.Sprintf("http://api.map.baidu.com/place/v2/search?output=json&page_size=20&scope=1&coord_type=1&query=%s&bounds=%.5f,%.5f,%.5f,%.5f&ak=%s&page_num=%d",
                        s.GetCategory(), lower, left, upper, right, s.ApiKey, s.LastPageIndex+1)
    
                    resp, err := http.Get(url)
                    if nil != err {
                        log.Panic("网络出现错误", err)
                    } else {
                        defer resp.Body.Close()
                        body, err := ioutil.ReadAll(resp.Body)
                        if err != nil {
                            log.Panic("读取HTTP Body数据时出错", err)
                        }
                        poiResponse := &poi.PoiResponse{}
                        err = json.Unmarshal(body, poiResponse)
                        if nil != err {
                            log.Println("解析服务器返回的数据出错,5秒后重新获取", err)
                            time.Sleep(5000000000)
                        } else if poiResponse.Status != 0 {
                            log.Printf("未能成功获取POI数据,服务器返回:%s
    ", poiResponse.Message)
                            time.Sleep(5000000000)
                        } else {
                            if poiResponse.Total == 0 { //没有获取到数据
                                //扩大矩形面积
                                log.Printf("未能获取到数据,区域宽度(经度)由%f调整为%f
    ", s.LastLongitudeLength, s.LastLongitudeLength*2)
                                s.LastLongitudePosition += s.LastLongitudeLength
                                //加个判断,不能让它无限膨胀
                                if s.LastLongitudeLength*2 < s.RightLongitude-s.LeftLongitude {
                                    s.LastLongitudeLength *= 2
                                }
                                s.Save(statusFileName)
                            } else if poiResponse.Total == 400 { //获取的数据达到400上限,可能不完整
                                log.Printf("获取的数据总数达到400条,可能不完整,区域宽度(经度)由%f调整为%f
    ", s.LastLongitudeLength, s.LastLongitudeLength/2)
                                //缩小矩形面积
                                s.LastLongitudeLength /= 2
                            } else { // 获取的数据在 0 到 400之间,有效
                                log.Printf("获取的数据总条数为%d,有效!
    ", poiResponse.Total)
    
                                for _, p := range poiResponse.Results {
                                    record := fmt.Sprintf("%s,%f,%f,%s,%s,%s,%s,%s
    ", p.Name, p.Location.Lgt, p.Location.Lat, p.Telephone, p.Province, p.City, p.Area, p.Address)
                                    log.Print(record)
                                    if _, err = f.WriteString(record); err != nil {
                                        panic(err)
                                    }
                                }
    
                                size := len(poiResponse.Results)
                                if size == 20 { //表示还有下一页
                                    log.Printf("当前页数据条数为20条,表示不是末尾页,页号+1")
                                    s.LastPageIndex++
                                    s.Save(statusFileName)
                                    goto NEXT_PAGE
                                } else {
                                    log.Printf("当前页数据为%d条,是末尾页,页号重置", size)
                                    s.LastPageIndex = -1
                                    s.LastLongitudePosition += s.LastLongitudeLength
                                    s.Save(statusFileName)
                                }
                            }
                        }
                    }
                }
                log.Printf("抓取完一行,纬度由%f调整为%f", s.LastLatitudePosition, s.LastLatitudePosition+s.LastLatitudeLength)
                //抓完一行了,经度回到原点
                s.LastLongitudePosition = s.LeftLongitude
                s.Save(statusFileName)
            }
            f.Close()
            //抓完一个类别了,回到原点
            s.LastLatitudeLength = s.UpperLatitude
            s.LastCategoryIndex++
            s.Save(statusFileName)
        }

    完整代码可点击这里下载

  • 相关阅读:
    Scrum Meeting Alpha
    Scrum Meeting Alpha
    Scrum Meeting Alpha
    你连自律都做不到,还奢谈什么自由?
    改变这个世界
    这世界没有人能随随便便成功
    “沙堆实验”
    解读那些年我们见过的“富人思维”
    心存希望,面朝大海
    闻香识女人 演讲台词
  • 原文地址:https://www.cnblogs.com/robothy/p/10144063.html
Copyright © 2020-2023  润新知