爬取中国行政区划边界数据(GeoJSON格式)

前言

行政区划相关信息在GIS中算是不可或缺的基础数据,然而由于行政区划经常会进行调整,所以许多网上搜集的已经存在了不同程度的过时。而如果用国外的数据,又会有领土的问题,让人很膈应。偶然一次,在GitHub上看到了分享来自阿里云的数据,那么如果能够自动从网上抓取的话,岂不是可以获得较为准确的资料,并可以及时更新?

准备

数据的来源就是阿里云的“DATAV.GeoAtlas”(其实我并不知道这是个什么东西),具体如下图所示。根据其提供的json API可以发现,其返回值为GeoJSON,那么我们可以考虑用python的geopandas库直接利用url进行读取并保存到本地,跳过requests的请求环节。

datav_aliyun

1
2
3
4
5
import requests
import geopandas as gpd
import os
import threading
import time

网站上提供的数据分为两种:不包含子区域的(称之为“simple”)和包含子区域的(称之为“full”),依据行政区划的精细程度分为国家、省、地级市和县区三级,所以先准备后存储数据的文件路径:

1
2
3
4
5
6
7
8
type_list = ['simple', 'full']
level_list = ['country', 'province', 'city', 'district']

for tl in type_list:
for level in level_list:
if not os.path.exists(f'./{tl}/{level}'):
os.makedirs(f'./{tl}/{level}')

递归获取GeoJSON

本来我是可以从网上找一张行政区划的编码表,然后遍历访问就可以。但是,由于我们的行政区划数据存在很多版本,而我并不知道这个网站上提供的是哪个版本,所以可能会存在不匹配的问题。在“full”版本的json中,包含了子区域的信息,假设这个网站上的数据是“自洽”的,那么我们可以从“中华人民共和国”出发,递归地找下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def recur_fetch(adcode, threads):
simple_file = f'{adcode}.json'
full_file = f'{adcode}_full.json'
prefix = 'https://geo.datav.aliyun.com/areas/bound/'

# 对于出现异常的直接跳过
try:
# 先获取simple版的
gdf = gpd.read_file(prefix+simple_file)
level = gdf.level[0]
# 在Windows环境下,没有encoding参数会报错
gdf.to_file(f'./simple/{level}/{simple_file}', driver='GeoJSON', encoding='UTF-8')

# 未必有full版的
try:
gdf = gpd.read_file(prefix+full_file)
for index in gdf.index:
sub_adcode = str(gdf.loc[index].adcode)
# 避免父区域也包含在子区域中,造成无限循环
if sub_adcode != adcode:
t = threading.Thread(target=recur_fetch, args=(sub_adcode,threads, ))
t.start()
threads.append(t)
gdf.to_file(f'./full/{level}/{full_file}', driver='GeoJSON', encoding='UTF-8')
except:
pass
except:
pass

Escher_hands

递归多线程

每次提到递归,都会感觉头大。在这个问题里,其实递归并不是理想的方案,但是由于不同递归深度之间是独立的,所以可以通过多线程的方式,并发从网站上获取数据。对比发现,多线程的方式比单线程的效率大约提高在5倍以上,效果还是比较明显的。

但是,我这个写法有一个问题就是:不知道什么时候所有数据下载完毕,也就是递归产生的线程什么时候运行结束。一开始,我考虑轮询threading.activeCount() ,当数字不再变化就认为已经结束,但是这个的可靠性存在问题;后来从网上找到了用join*()阻塞主线程的方法,由于我们用的是递归的,所以通过传递参数的方式维护线程的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
adcode = '100000'

start = time.time()
threads = []
t = threading.Thread(target=recur_fetch, args=(adcode,threads,))
t.start()
threads.append(t)
for proc in threads:
proc.join()
end = time.time()
print(f'Elapsed time:{end-start}')


Elapsed time:301.6022205352783

最后上一张带九段线的中国地图:

1
2
3
4
5
6
7
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['figure.figsize']=(25,20)
plt.style.use('ggplot')
gdf = gpd.read_file('./full/country/100000_full.json', encoding='UTF-8')
gdf.plot()
plt.show()

output_14_0

残留问题

这个小程序看上去非常简单,但是实际上还存在两个问题,只不过运行中抛出的异常并没有处理:

  1. 数据集并不是“自洽”的,具体而言,在“simple”中的区划信息与“full”中的信息并不一致,例如在天津的“full json”中,有16年撤县设区的“蓟州区”,但是却没有响应的“simple json”,而是原先的“蓟县”,如此的情况也出现在西安的户县等区域,对于这个问题,要解决的话,可以找出来不一致的,然后一个一个修正;
  2. 有些"simple json"有对应的"full json",但是“full json”其实是空的,会造成写文件会报错,最简单的方法就是忽略。

考虑到这两个问题的存在,我建议用“国家、省、市”三级的“full json”数据。

小结

用一个很简单的程序,我们可以不定期从网上更新中国行政区划的GeoJSON数据,为了进一步提高数据的使用效率,接下来考虑用数据库如PostgreSQL进行管理。

tintin-snowy-moon

参考资料

  1. https://github.com/adyliu/china_area
  2. http://datav.aliyun.com/tools/atlas/
  3. https://github.com/cn/GB2260
  4. https://github.com/linwrui/geo-map-fetcher