(数据科学学习手札59)从抓取数据到生成shp文件并展示

2019-06-11 16:57:56 浏览数 (2)

一、简介

  shp格式的文件是地理信息领域最常见的文件格式之一,很好的结合了矢量数据与对应的标量数据,而在Python中我们可以使用pyshp来完成创建shp文件的过程,本文将从如何从高德地图获取矢量信息开始,最终构造出相应的shp文件,并利用R中的leaflet进行可视化;

二、数据获取及清洗

2.1 数据获取

  首先我们需要从高德地图获取所关注对象的矢量信息,这里点数据我们选择重庆轨道交通站点,线我们选择重庆轨道交通线路,面我们选择重庆市三峡博物馆,考虑到只是简单演示小规模采集数据,因此选择selenium作为数据爬取的工具,首先我们需要操纵模拟浏览器打开高德地图查找内容的页面(即query带有关键词),这样做的目的是让我们的浏览器加载所需接口对应的cookies,方便之后直接进行矢量信息的采集,如下面这个页面:

https://www.amap.com/search?query=中国三峡博物馆&city=500000&geoobj=106.548805|29.559976|106.552163|29.564269&zoom=18

  运行下面的代码启动浏览器,接着观察会不会出现滑块验证码(笔者亲测有很大的概率触发验证码):

代码语言:javascript复制
from selenium import webdriver
from tqdm import tqdm
import time

option = webdriver.ChromeOptions()
'''这个实验参数用于避免被高德检测到为driver驱动的浏览器'''
option.add_experimental_option('excludeSwitches', ['enable-automation'])
browser = webdriver.Chrome(options=option)
'''访问指定网址拿到cookies'''
browser.get('https://www.amap.com/search?query=轨道交通3号线&city=500000&geoobj=106.477496|29.407019|106.642291|29.665101&zoom=12')

  这时若出现下列验证码则手动接触即可(考虑到爬虫并不是本文重点因此没有花费时间编写模拟滑动滑块的代码):

  在滑块解除后,我们就可以批量获取轨道线路矢量信息,代码如下,注意每轮运行间隔调久一些防止被ban:

代码语言:javascript复制
'''这个字典存放所有原始的json数据'''
rawSHP = {}
crtLines = ['轨道交通1号线','轨道交通2号线','轨道交通3号线','轨道交通4号线','轨道交通5号线','轨道交通6号线','轨道交通10号线',
            '轨道交通3号线北延伸段(空港线)','轨道交通6号线支线','轨道交通环线']

for line in tqdm(crtLines):
    browser.get(f'https://www.amap.com/service/poiInfo?query_type=TQUERY&pagesize=20&pagenum=1&qii=true&cluster_state=5&need_utd=true&utd_sceneid=1000&div=PC1000&addr_poi_merge=true&is_classify=true&zoom=12&city=500000&geoobj=106.477496|29.394307|106.642291|29.677779&keywords={line}')
    '''这里从网页内容标签中抽取json部分内容'''
    rawSHP[line] = eval(browser.find_elements_by_xpath("//pre")[0].text)
    time.sleep(8)

  这样我们就得到对应重庆轨道交通线路和站点的原始json数据,接下来类似上面的做法,获取中国三峡博物馆矢量信息:

代码语言:javascript复制
browser.get('https://www.amap.com/service/poiInfo?query_type=TQUERY&pagesize=20&pagenum=1&qii=true&cluster_state=5&need_utd=true&utd_sceneid=1000&div=PC1000&addr_poi_merge=true&is_classify=true&zoom=12&city=500000&geoobj=106.477496|29.394307|106.642291|29.677779&keywords=中国三峡博物馆')
'''这里从网页内容标签中抽取json部分内容'''
museumSX = eval(browser.find_elements_by_xpath("//pre")[0].text)

  经过上面的步骤我们就得到了所需内容的原始格式,接下来进行清洗;

2.2 数据清洗

  首先提取点数据,rawSHP为字典,键为线路名称,值为所对应包含的全部内容,我们需要的经纬度信息就包含在其中,以环线为例:

  按照上图箭头所指的路径便可找到对应的站点名称name和经纬度xy_coords,而对于线数据,如下图:

   同样可以找到对应每个折点的经度xs与纬度ys,对于面数据,在museumSX变量下data->poi_list->domain_list中name属性为'aoi'的元素中可以找到其对应的面矢量信息:

  获悉所需数据的位置之后,接下来我们在写入shp文件的过程中同时完成清洗过程,在此之间首先需要介绍pyshp中写出shp文件相关的用法;

三、写出shp文件

3.1 用pyshp写出shp文件

pyshp是以纯Python代码的方式对ESRI shapefiles文件进行读写、编辑等操作的模块,以用法方便快捷功能高效强大著称,写出时使用到其中的Writer类,其主要有三个参数:

  target:文件最终存出的具体位置及文件名称

  shapeType:int型,用于决定文件类型,类型与数字对应关系如下:

  autoBalance:int型,建议传入1,即定义的属性有秩序的自动跟随定义的要素之后,避免出现错乱;

  而pyshp中的Writer对象有如下常用方法:

  field:用于创建跟随矢量要素的属性表字段,其name参数用于定义字段名;fieldType参数用于控制数据类型,'C'代表字符串,‘N’代表数值型,‘F’代表浮点型,‘L’代表bool型,‘D’代表日期;参数size为字符型,用于控制数据长度,最大限制为‘2046’

  point:传入点的经度与纬度

  line:传入单条或多条线每个折点的经纬度

  poly:传入面中对应每个边界点的经纬度

  除了上述三种最基本的,还有很多传入其他格式矢量信息的方法,本文未使用到不再赘述;

  record:传入属性表对应字段的值

  close:在最后存出文件时调用

  因为我们爬取的数据来自高德地图,因此如果有转换坐标系的需求,可以使用下列代码完成百度坐标、火星坐标系、wgs84之间的互转:

代码语言:javascript复制
import math
x_pi = 3.14159265358979324 * 3000.0 / 180.0
pi = 3.1415926535897932384626  # π
a = 6378245.0  # 长半轴
ee = 0.00669342162296594323  # 偏心率平方


class LngLatTransfer():
    def __init__(self):
        pass

    def gcj02_to_bd09(self, lng, lat):
        """
        火星坐标系(GCJ-02)转百度坐标系(BD-09)
        谷歌、高德——>百度
        :param lng:火星坐标经度
        :param lat:火星坐标纬度
        :return:
        """
        z = math.sqrt(lng * lng   lat * lat)   0.00002 * math.sin(lat * x_pi)
        theta = math.atan2(lat, lng)   0.000003 * math.cos(lng * x_pi)
        bd_lng = z * math.cos(theta)   0.0065
        bd_lat = z * math.sin(theta)   0.006
        return [bd_lng, bd_lat]


    def bd09_to_gcj02(self, bd_lon, bd_lat):
        """
        百度坐标系(BD-09)转火星坐标系(GCJ-02)
        百度——>谷歌、高德
        :param bd_lat:百度坐标纬度
        :param bd_lon:百度坐标经度
        :return:转换后的坐标列表形式
        """
        x = bd_lon - 0.0065
        y = bd_lat - 0.006
        z = math.sqrt(x * x   y * y) - 0.00002 * math.sin(y * x_pi)
        theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
        gg_lng = z * math.cos(theta)
        gg_lat = z * math.sin(theta)
        return [gg_lng, gg_lat]


    def wgs84_to_gcj02(self, lng, lat):
        """
        WGS84转GCJ02(火星坐标系)
        :param lng:WGS84坐标系的经度
        :param lat:WGS84坐标系的纬度
        :return:
        """
        if self.out_of_china(lng, lat):  # 判断是否在国内
            return [lng, lat]
        dlat = self._transformlat(lng - 105.0, lat - 35.0)
        dlng = self._transformlng(lng - 105.0, lat - 35.0)
        radlat = lat / 180.0 * pi
        magic = math.sin(radlat)
        magic = 1 - ee * magic * magic
        sqrtmagic = math.sqrt(magic)
        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
        dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
        mglat = lat   dlat
        mglng = lng   dlng
        return [mglng, mglat]


    def gcj02_to_wgs84(self, lng, lat):
        """
        GCJ02(火星坐标系)转GPS84
        :param lng:火星坐标系的经度
        :param lat:火星坐标系纬度
        :return:
        """
        if self.out_of_china(lng, lat):
            return [lng, lat]
        dlat = self._transformlat(lng - 105.0, lat - 35.0)
        dlng = self._transformlng(lng - 105.0, lat - 35.0)
        radlat = lat / 180.0 * pi
        magic = math.sin(radlat)
        magic = 1 - ee * magic * magic
        sqrtmagic = math.sqrt(magic)
        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
        dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
        mglat = lat   dlat
        mglng = lng   dlng
        return [lng * 2 - mglng, lat * 2 - mglat]


    def bd09_to_wgs84(self, bd_lon, bd_lat):
        lon, lat = self.bd09_to_gcj02(bd_lon, bd_lat)
        return self.gcj02_to_wgs84(lon, lat)


    def wgs84_to_bd09(self, lon, lat):
        lon, lat = self.wgs84_to_gcj02(lon, lat)
        return self.gcj02_to_bd09(lon, lat)


    def _transformlat(self, lng, lat):
        ret = -100.0   2.0 * lng   3.0 * lat   0.2 * lat * lat   
              0.1 * lng * lat   0.2 * math.sqrt(math.fabs(lng))
        ret  = (20.0 * math.sin(6.0 * lng * pi)   20.0 *
                math.sin(2.0 * lng * pi)) * 2.0 / 3.0
        ret  = (20.0 * math.sin(lat * pi)   40.0 *
                math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
        ret  = (160.0 * math.sin(lat / 12.0 * pi)   320 *
                math.sin(lat * pi / 30.0)) * 2.0 / 3.0
        return ret


    def _transformlng(self, lng, lat):
        ret = 300.0   lng   2.0 * lat   0.1 * lng * lng   
              0.1 * lng * lat   0.1 * math.sqrt(math.fabs(lng))
        ret  = (20.0 * math.sin(6.0 * lng * pi)   20.0 *
                math.sin(2.0 * lng * pi)) * 2.0 / 3.0
        ret  = (20.0 * math.sin(lng * pi)   40.0 *
                math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
        ret  = (150.0 * math.sin(lng / 12.0 * pi)   300.0 *
                math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
        return ret


    def out_of_china(self, lng, lat):
        """
        判断是否在国内,不在国内不做偏移
        :param lng:
        :param lat:
        :return:
        """
        return not (lng > 73.66 and lng < 135.05 and lat > 3.86 and lat < 53.55)

3.2 写出shp文件

点文件:

思路是初始化Writer对象之后,利用循环从rawSHP字典中抽取所有的站点名称、经纬度以及对应线路,因此属性表中创建字段name用于保存站点名称,route字段用于存放线路信息,具体代码如下(注意导入名需为shapefile,即pyshp):

代码语言:javascript复制

  输出目录中也包含了我们所需的文件:

  在arcgis中查看:

  成功~

  接下来是线文件:

代码语言:javascript复制
'''shp文件写出部分'''
import shapefile

w_line = shapefile.Writer(r'C:UsershpDesktopshp写出重庆轨道交通线路矢量数据',
                     autoBalance=1)

w_line.field('name','C')
for key in rawSHP.keys():
    lines = []
    for idx in range(len(rawSHP[key]['data']['busline_list'])):
        lines.append([])
        for lng,lat in zip(rawSHP[key]['data']['busline_list'][idx]['xs'].split(','),rawSHP[key]['data']['busline_list'][idx]['ys'].split(',')):
            lines[-1].append(transfer.gcj02_to_wgs84(lng=float(lng),lat=float(lat)))

    for l in lines:
        '''每段线路要素单独写出'''
        w_line.line([l])
        '''追加对应的线路名称'''
        w_line.record(name=key)

w_line.close()

  在arcgis中查看线文件:

  面文件

代码语言:javascript复制
rawPoly = museumSX['data']['poi_list'][0]['domain_list'][14]['value'].split('_')
rawPoly = [transfer.gcj02_to_wgs84(float(rawPoly[i].split(',')[0]),float(rawPoly[i].split(',')[1])) for i in range(len(rawPoly))]

w_polygon = shapefile.Writer(r'C:UsershpDesktopshp写出三峡博物馆面矢量数据',
                     autoBalance=shapefile.POLYGON)
w_polygon.field('name','C')
w_polygon.poly([rawPoly])
w_polygon.record(name='中国三峡博物馆')
w_polygon.close()

  在arcgis中查看:

  可以与高德网页上的形状对比,非常吻合,至此,我们就完成了shp文件的生成,下面我们简单的在R中用leaflet进行可视化,这里选用Carto的底图(WGS84坐标系),对应的R代码如下:

代码语言:javascript复制
rm(list=ls())
library(rgdal)
library(leaflet)
library(ggplot2)
library(readxl)
setwd('C:\Users\hp\Desktop\shp写出')

crt <- readOGR('重庆轨道交通线路矢量数据.shp')
crt_station <- readOGR('重庆轨道交通站点矢量数据.shp')
museum <- readOGR('三峡博物馆面矢量数据.shp')

#用循环的方式叠加线
m <- leaflet() %>%
  addTiles(urlTemplate = '//{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png')
for(i in 1:length(crt@lines)){
  m <- m %>% addPolylines(fortify(crt@lines[[i]])$long,lat=fortify(crt@lines[[i]])$lat,fillColor = sample(colors(),1),color = sample(colors(),1),weight=2)
}

#叠加点
m <- m %>% addCircleMarkers(lng=data.frame(crt_station@coords)$coords.x1,lat=data.frame(crt_station@coords)$coords.x2,
                       radius=1,
                       weight = 1,
                       opacity = 1,
                       color = 'red',
                       fillOpacity = 1,
                       label=crt_station@data
                 )
#叠加面
m <- m %>% addPolygons(lng=fortify(museum)$long,
                       lat=fortify(museum)$lat)
m

放大后可以看到位于中山四路附近的三峡博物馆,跟高德地图上的对比一下,还是我们的底图比较素雅~:

  以上就是本文的全部内容,如有疏漏之处望指出。

0 人点赞