在 《如何用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下查看可以发现它的元信息都完整地记录下来了。
以上例子基本说明了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使用指南,内部还包含了主要的函数接口的详细参数说明,以及各种示例,同时readthedocs还支持多版本构建,即使更新了版本,旧版本的文档仍然可以查看,此外还支持直接输出为PDF版下载,可以说非常实用。
你也可以去github的cnmaps 仓库里,通过图示中的链接进入:
往期回顾:
多次尝试,也没安装成功。
具体报什么错呢?
cnmaps安装报错了
报的什么错呢?
报错多次,Collecting fiona>=1.8 (from geopandas->cnmaps)
Using cached https://files.pythonhosted.org/packages/67/5c/4e028e84a1f0cb3f8a994217cf2366360ca984dfc1433f6171de527d0dca/Fiona-1.8.21.tar.gz
ERROR: Complete output from command python setup.py egg_info:
ERROR: A GDAL API version must be specified. Provide a path to gdal-config using a GDAL_CONFIG environment variable or use a GDAL_VERSION environment variable.
使用pip安装报错,麻烦支持一下。
环境:win11,python 3.9.8 x64
Cartopy 0.20.2,GDAL 3.4.3已经安装好。
安装cnmaps一致报错:
Preparing metadata (setup.py) … error
error: subprocess-exited-with-error
× python setup.py egg_info did not run successfully.
│ exit code: 1
╰─> [1 lines of output]
A GDAL API version must be specified. Provide a path to gdal-config using a GDAL_CONFIG environment variable or use a GDAL_VERSION environment variable.
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed
创建一个虚拟环境在不安装GDAL的情况下先安装cartopy再安装cnmaps试试呢?
我没有使用虚拟环境,新安装的python 3.9.8 x64,然后使用pip方式从清华大学镜像来源安装的,一直报这个错误,回头我使用虚拟环境试试。
似乎发现错误了,使用pip自动安装的geos和proj版本很低,都是0.2左右,但是你这包要求一个是>3.7,一个>8.0。
你这里有geos和proj的python安装库whl文件吗,从官网下载的源代码实在安装不了。
pip安装包依赖的时候会按照依赖包的版本号需求自动从高到低进行匹配安装,除非是有其他包对依赖有版本冲突,所以需要开一个干净的虚拟环境来安装依赖,否则其他环境因素太复杂,很难处理。
最近研究cnmaps时发现自带的geojson数据的命名格式跟阿里的DataV.GeoAtlas很像,以为数据源是那个网站。但测试后发现分辨率低,多边形存在自相交,省份间还有缝隙。看了博客才发现是用的别的数据。请问您是将那个仓库里的shp文件的记录一条条提取出来生成的geojson文件吗,还是说是通过高德的API得到的?
另外不知道draw_maps能不能显式引入ax参数,源码里的ax=plt.gca()似乎会在画多子图时产生影响。
DataV.GeoAtlas 的地图边界数据源也是高德开放API的数据,cnmaps使用的数据源是这个:https://github.com/Clarmy/ChinaAdminDivisonSHP