IT / 地理/GIS · 2022年4月7日 12

cnmaps新升级:区县级数据现已加入豪华午餐

在 《如何用Python优雅地绘制中国的地图》文章发出之后,陆续收到了一些朋友的反馈,其中有一部分建议是希望行政边界数据能够支持到市级。当时在0.2.*版本时,由于获取的shp数据源并非直接从高德开放API获取,而是从网络上一个博主分享的网站上下载的,县市级数据原作者未提供。

后来尝试使用阿里云提供的后处理版本,发现他们的版本中相邻行政边界的多边形之间有缝隙,由于我比较执着于多边形合并拼接的功能,用这种数据会导致两个多边形合并以后会出现明显的裂缝,因此我放弃了阿里云。

之所以我没有直接从高德开放API下载,主要是因为懒。高德开放API的行政边界数据抓取容易,但后处理比较麻烦,尤其是遇到有洞、有岛的情况,需要做专门的解析和调试,我本着”懒惰即美德“的精神,在GitHub上不断找轮子。最后还真让我找到了,有一个声称是高德API后处理版本的shapefile仓库,数据支持到了区县级,我fork完下载下来在QGIS里检查,发现质量很高,不存在缝隙的情况,将部分样例数据放进cnmaps的程序中测试,结果完全OK。于是我决定立刻马上开始写支持区县的cnmaps,撸了一个清明节,终于把它搞出来了。

cnmaps:1.0.1版本,现已支持全国近3000多个区县的快速检索,同时也新增加了一些实用功能,让我们来看一看吧。

如何安装新版本的cnmaps

关于cnmaps的安装,目前来说还是需要你预先将cartopy装好,再安装cnmaps,虽然前段时间我也曾向conda-forge组织提交过recipe,希望通过conda-forge发布cnmaps版本以避免手动安装cartopy,但是由于conda-forge发布包是需要人工review的,现在至少一周过去了一点动静都没有。我想着再等等,如果真的没有人理我的话我再直接用conda构建。

安装cartopy: $ conda install -c conda-forge -y cartopy==0.19.0

在cartopy已经安装好的情况下,在终端执行$ pip install -U cnmaps会安装最新版本的cnmaps。由于新版本在一些函数上有取舍,无法做到完全向下兼容,因此本轮升级使用的是大版本升级,从0.*.*升级到1.*.*。若有小伙伴在代码中使用了cnmaps又担心升级会影响已写代码功能的话,可以在安装时指定旧版本,比如$ pip install cnmaps==0.2.1

如何查找你想要的行政边界

安装好新版本的cnmaps之后,我们就可以来感受一下新版本在搜索行政边界时的丝滑体验了。

新版本引入了一个新的函数叫get_adm_maps,该函数主要用于对行政区边界的查找。

比如当我们需要查找北京市的行政边界时,可以将'北京市'传入city参数:

>>> from cnmaps import get_adm_maps
>>> get_adm_maps(city='北京市')

我们会得到一个由字典组成的列表,里面记录着查询到的北京市的结果,包含了数据一些元信息,以及多边形对象:

[{'国家': '中华人民共和国',  '省/直辖市': '北京市',  '市': '北京市',  '区/县': None,  '级别': '市',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fd03db64810>}]

同样地,如果我们要查询海淀区的行政边界时,将'海淀区'传入district参数即可:

>>> get_adm_maps(district='海淀区')

获取的结果为:

[{'国家': '中华人民共和国',  '省/直辖市': '北京市',  '市': '北京市',  '区/县': '海淀区',  '级别': '区县',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fd03e4e75d0>}]

当传入的参数没有歧义的时候,它会仅返回一条结果,而当数据存在歧义的时候,它会返回不止一条,例如当我们直接查找朝阳区的时候:

>>> get_adm_maps(district='朝阳区')

它会返回两条记录:

[{'国家': '中华人民共和国',  '省/直辖市': '北京市',  '市': '北京市',  '区/县': '朝阳区',  '级别': '区县',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fced86cf510>}, 
{'国家': '中华人民共和国',  '省/直辖市': '吉林省',  '市': '长春市',  '区/县': '朝阳区',  '级别': '区县',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fced86cfc10>}]

也就是说全国范围内,有不止一个“朝阳区”,北京和长春各有一个,这个时候就需要你对它的上级行政区做限制:

>>> get_adm_maps(city='北京市', district='朝阳区')

这个时候返回的就只有北京的朝阳区了。

当我们要查询省级数据的时候,可以向province参数传入省的全称,例如:

>>> get_adm_maps(province='山西省')
[{'国家': '中华人民共和国',  '省/直辖市': '山西省',  '市': None,  '区/县': None,  '级别': '省',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fd03e4e7250>}]

如果我们要查询山西省下辖的所有城市怎么办呢?很简单,只需要通过level参数来调整查找的行政级别即可,例如:

>>> get_adm_maps(province='山西省', level='市')
[{'国家': '中华人民共和国',  '省/直辖市': '山西省',  '市': '太原市',  '区/县': None,  '级别': '市',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fd03f019fd0>},  
… # 为了节约篇幅,省略中间的记录 
{'国家': '中华人民共和国',  '省/直辖市': '山西省',  '市': '吕梁市',  '区/县': None,  '级别': '市',  '来源': '高德',  '类型': '陆地',  'geometry': <cnmaps.maps.MapPolygon at 0x7fd03e4e7990>}]

这样就可以把山西省下辖的所有市都列举出来了。如果我们想要以geopandas的GeoDataFrame格式返回,可以向engine参数传入'goepandas'

>>> get_adm_maps(province='山西省', level='市', engine='geopanda')       
 国家 省/直辖市    市   区/县 级别  来源  类型                                           geometry
0   中华人民共和国   山西省  太原市  None  市  高德  陆地  MULTIPOLYGON (((113.06683 38.05646, 113.06708 …
1   中华人民共和国   山西省  大同市  None  市  高德  陆地  MULTIPOLYGON (((113.57727 39.43812, 113.57460 …
2   中华人民共和国   山西省  阳泉市  None  市  高德  陆地  MULTIPOLYGON (((113.99691 37.70448, 113.99567 …
3   中华人民共和国   山西省  长治市  None  市  高德  陆地  MULTIPOLYGON (((111.99642 36.68713, 111.99480 …
4   中华人民共和国   山西省  晋城市  None  市  高德  陆地  MULTIPOLYGON (((113.46543 35.51493, 113.46300 …
5   中华人民共和国   山西省  朔州市  None  市  高德  陆地  MULTIPOLYGON (((112.62431 40.23685, 112.62429 …
6   中华人民共和国   山西省  晋中市  None  市  高德  陆地  MULTIPOLYGON (((113.06683 38.05646, 113.06903 …
7   中华人民共和国   山西省  运城市  None  市  高德  陆地  MULTIPOLYGON (((110.90373 34.66882, 110.89349 …
8   中华人民共和国   山西省  忻州市  None  市  高德  陆地  MULTIPOLYGON (((111.26944 39.42373, 111.27091 ...
9   中华人民共和国   山西省  临汾市  None  市  高德  陆地  MULTIPOLYGON (((110.41054 36.89947, 110.41487 ...
10  中华人民共和国   山西省  吕梁市  None  市  高德  陆地  MULTIPOLYGON (((111.41469 36.80403, 111.41071 …

返回的对象就是GeoDataFrame,并且GeoDataFrame是支持导出为文件的,默认为shapefile,但也可以导出为geojson,例如:

>>> gdf = get_adm_maps(province='山西省', level='市', engine='geopandas')>>> gdf.to_file('./shanxi.shp', encoding='utf-8')>>> gdf.to_file('./shanxi.geojson', engine='GeoJSON')

即可将山西省的市级行政边界分别保存为一个shp和一个geojson文件,在QGIS下查看可以发现它的元信息都完整地记录下来了。

QGIS查看输出的边界文件

以上例子基本说明了get_adm_maps的基本使用方法,但是它们传入的地名都是全称,是不是不支持模糊查询呢?答案是:不支持,目前我们没有能力进行模糊查询,所以当你查询的时候必须在对应的参数中传入完整的名称,那么问题来了,如果对于一些地名我们拿不准甚至不知道怎么办呢?比如说成都市有一个“双流”,它以前是“双流县”,后来成了“双流区”,我们不知道究竟应该传入那个值怎么办?这时候就可以使用get_adm_names函数了,请看例子:

>>> from cnmaps import get_adm_names
>>> get_adm_names(city='成都市', level='区县')
['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市']

当我们知道“双流”的上级“成都市”的准确名字的时候,就可以在对应的city参数中传入'成都市',然后把level设为'区县',这个时候我们就可以看到成都市下辖的所有区县的名称了,可以看到在我们这里双流的正式名称时“双流区”。

一次性绘制多个行政区

还记得《如何用Python优雅地绘制中国的地图》中,我们在绘制中国地图的时候,需要把南海单独拿出来画吗?如果按旧版的写法,如果你要同时绘制多个行政边界,你就需要自己写代码遍历绘图,显然这样做不够优雅。因此我在新版中增加了一个函数:draw_maps,它可以与get_adm_names配合使用,用于同时绘制多个地图边界,而原版的draw_map没有变化。

看下面的例子:

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
from cnmaps import get_adm_maps, draw_maps

fig = plt.figure(figsize(10,10))
ax = fig.add_subplot(111, projection=ccrs.PlateCarree())
draw_maps(get_adm_maps(level='国')) 

plt.show()

结果如图,可以看到我们无需再单独绘制南海,只需要在查找行政边界时,返回的列表中包含南海,get_adm_maps就可以直接帮我们画好了。

国界线

同理我们可以画出省、市、区县,以区县为例:

fig = plt.figure(figsize=(20,20))
ax = fig.add_subplot(111, projection=ccrs.PlateCarree())
draw_maps(get_adm_maps(level='区县'), linewidth=0.3, color='b') 

plt.show()
区县级边界图

哦对了,高德的地图并不支持台湾省的市、区县级别的行政边界,这个后续我会去找别的源补充。

详细的使用指南已发布

一个合格的开源工具包必然需要一个合格的文档指南,cnmaps该有的都会有,文档也不例外。目前我已经基于python的sphinx框架,构建了一个readthedocs风格的文档项目,托管在readthedocs网站上:

cnmaps使用指南

该文档是该项目最完整的cnmaps使用指南,内部还包含了主要的函数接口的详细参数说明,以及各种示例,同时readthedocs还支持多版本构建,即使更新了版本,旧版本的文档仍然可以查看,此外还支持直接输出为PDF版下载,可以说非常实用。

cnmaps文档链接

你也可以去github的cnmaps 仓库里,通过图示中的链接进入:

GitHub仓库的文档入口

往期回顾:

如何用Python优雅地绘制中国的地图