nodejs数据处理实践之百度poi数据获取

任务描述

获取百度上关于深圳市的所有POI数据。

百度POI类型描述

百度POI行业分类

这个链接给出了百度的POI分类标准,包括17个一级类别,每个一级类别下面有多个二级类别。

这次实验我们希望按照一级类别分类来获取数据。

百度POI接口介绍

Place API

这个链接介绍了百度的POI接口。文档介绍的很详细,这里就不赘述了。但是有几个要点希望读者注意:

  1. 百度POI接口,虽然可以指定页码和每页的显示数目,但无论如何一个请求,最多也只能返回400条数据。
  2. 可以把region设置为省级行政区来获得每个城市的POI数目
  3. coord_type字段控制的是,参数的坐标系统,返回值的坐标系统是BD09LL,可以使用数值拟合的方法把该坐标系统的坐标转换到WGS84

    eviltransform

方法

因为一个请求最多只能返回400条数据,所以不可能使用城市内检索api来获取整个城市的所有POI数据。一个可行的想法是,使用矩形检索API。首先,我们把城市等间距划分成多个矩形,使得每个矩形都足够小以至其内部的POI数据条目少于400.然后在分别获取每个矩形区域内的所有数据,就可以实现整个城市的POI数据获取。

这个方法虽然可行但存在一下缺点需要克服:

  1. 城市是不规则的多边形,简单的使用城市的bottomLeft和topRigh点来圈定城市的范围,将会包含很多城市外的区域。以深圳这样一个狭长的城市为例,它的外包矩形中大概包含了一半城市外区域。这导致我们发起了很多不必要的请求。

    对于这个问题,我们可以使用Arcgis的fishnet工具来生成矩形格网,然后使用select by location,选出和城市相交的矩形。

  2. POI数据的分布是很不均匀的,简单地均匀划分城市为多个矩形导致了,很多矩形内是没有POI数据的,而某些矩形内的POI数据仍然是远大于400的,无法完全获取。

    对于这个问题,我们可以只对total == 400的矩形进行进一步划分。

  3. 请求数目过多,导致网络连接错误

代码

// main
const config = require('./config')
const superagent = require('superagent')
const co = require('co')
const fs = require('fs')
const transform = require('./transform')
const parallel = require('co-parallel')
const async = require('async')
let type = process.argv[2]
let outputFileName = `./${type}.csv`
let writer = fs.createWriteStream(outputFileName)


const coordinateSplit = require('./coordinateSplit')
// 获得坐标块
let coordArr = coordinateSplit(100, config.bound.bottomLeft, config.bound.topRight)



let qBase = {
    ak: config.ak,
    q: type,
    bounds: ``,
    output: 'json',
    coord_type: 1,
    page_size: 20,
    page_num: 0,
}

//  深圳 政府机构的poi数量 10827


function getPoiPromise(q) {
    return new Promise((resolve, reject) => {

        async.retry(5, (cb) => {
            superagent.get(config.url)
                .query(q)
                .timeout({
                    response: 500
                })
                .end((err, res) => {
                    if (err) {
                        cb(err)
                    } else {
                        cb(null, res)
                    }
                })
        }, (err, res) => {
            if (err) {
                resolve({err,q}) // 重连5次后,仍然错误, 也不要抛出错误,避免程序终止
            } else {
                let obj = JSON.parse(res.text)
                resolve(obj)

            }
        })

    })
}

let numOfPoints = 0
let numOfQuery = 0
let numOfError = 0
function* thunnkGet(q) {
    return yield getPoiPromise(q)
}


co(function* () {

    let queryArr = []
    let prePromiseArr = []
    // 这个循环, 对每个小块发起一个请求,来获得小块内的POI数目。
    for (let i = 0; i < coordArr.length; i++) {
        let coord = coordArr[i]
        let q = Object.assign({}, qBase, {
            bounds: `${coord.bl.lat},${coord.bl.lng},${coord.tr.lat},${coord.tr.lng}`,
            page_num: 0
        })
        queryArr.push(q)
        prePromiseArr.push(thunnkGet(q))
    }

    let preResArr = yield parallel(prePromiseArr, 80)

    for (let i = 0; i < preResArr.length; i++) {
        numOfPoints += preResArr[i].total
    }


    // 这个循环, 针对前一步获得的,块内POI数目,根据块内的POI数目,并发的发出多个请求,来获得具体的POI
    let promiseArr = []
    for (let i = 0; i < queryArr.length; i++) {
        let q = queryArr[i]
        let total = preResArr[i].total
        if (total > 0) {

            let pageCount = Math.ceil(total / 20)

            console.log('页数:' + pageCount)

            for (let i = 0; i < pageCount; i++) {
                let pageQuery = Object.assign({}, q, {
                    page_num: i
                })

                // console.log(pageQuery)
                numOfQuery++
                promiseArr.push(thunnkGet(pageQuery))
            }
        }
    }

    let resArr = yield parallel(promiseArr, 100)
    
    for (let i = 0; i < resArr.length; i++) {

        let results = resArr[i].results
        if (!results) {
            numOfError++
            console.log(resArr[i])
            continue
        }


        console.log(`获得 ${results.length} 条`)
        for (let j = 0; j < results.length; j++) {
            let item = results[j]
            let wgsLnglat = transform.bd2wgs(item.location.lat, item.location.lng)
            writer.write(`${item.name},${wgsLnglat.lat},${wgsLnglat.lng},${item.address},${item.uid}\n`)
        }
    }

})
    .catch(err => {
        console.log(err)
    })
    .then(() => {
        console.log('兴趣点数目:' + numOfPoints)
        console.log('请求数目:' + numOfQuery)
        console.log('错误数目:' + numOfError)
    })


// console.log(lngArr.length)
// console.log(lngArr)
// console.log(latArr.length)
// console.log(latArr)
// console.log(coordArr.length)


/**
 * 把一个由bottomLeft和topRight指定区域均匀划分为numOfCell块
 * 
 */
module.exports = function (numOfCell=100, bl, tr) {
    
    let numOfRowOrCoL = Math.sqrt(numOfCell)



    let spanLng = tr.lng - bl.lng
    let spanLat = tr.lat - bl.lat



    // 经度的步长
    let stepLng = spanLng / numOfRowOrCoL
    // 纬度的步长
    let stepLat = spanLat / numOfRowOrCoL

    let lngArr = []
    let latArr = []


    let beginLng = bl.lng
    let beginLat = bl.lat
    for (let i = 0; i < numOfRowOrCoL; i++) {
        lngArr.push(beginLng + i * stepLng)
        latArr.push(beginLat + i * stepLat)
    }
    lngArr.push(tr.lng)
    latArr.push(tr.lat)


    let coordArr = []
    for (let row = 0; row < numOfRowOrCoL; row++) {
        for (let col = 0; col < numOfRowOrCoL; col++) {

            let bl = {
                lat: latArr[row],
                lng: lngArr[col],
            }
            let tr = {
                lat: latArr[row + 1],
                lng: lngArr[col + 1],
            }
            coordArr.push({
                bl,
                tr,
            })
        }
    }

    return coordArr
}



参考文献:

  1. Place API
  2. eviltransform
  3. 我这样抓取百度地图的数据a