Python自动化爬取全国行政区划GeoJSON数据实战指南
1. 为什么你需要一份最新的行政区划GeoJSON数据?
做数据可视化的朋友,尤其是和地图打交道的,肯定对GeoJSON这个格式不陌生。简单来说,GeoJSON就是一种用JSON格式来描述地理空间数据的标准。它能把一个省、一个市、甚至一条街道的边界轮廓,用一串坐标点精确地“画”出来。当你用ECharts、Mapbox、Leaflet这些酷炫的可视化库做中国地图时,背后支撑的“骨架”往往就是一份份GeoJSON数据。
那么问题来了,数据从哪来?你可能听说过阿里云的DataV地图选择器,它确实提供了一个非常方便的在线工具,可以手动下载单个省市的GeoJSON。但如果你和我一样,是个“懒人”程序员,或者项目需要全国所有省、市、区县三级完整的数据集,手动一个个点下载,不仅耗时费力,还容易出错漏。更关键的是,行政区划并非一成不变,每年都可能会有调整,比如某个县变成了区,或者新设立了一个市级行政区。手动维护这套数据,简直是场噩梦。
这就是为什么我们需要自动化。用Python写个脚本,一键爬取、整理、存储全国最新的行政区划GeoJSON数据,把我们从重复劳动中解放出来。今天,我就把自己在实际项目中多次使用的这套自动化方案分享给你,从环境搭建、代码逐行解析,到数据存储设计、错误处理,再到如何无缝对接ECharts,手把手带你走完全流程。你会发现,原来获取一份权威、完整、结构清晰的地图底图数据,可以如此简单高效。
2. 动手之前:环境与工具准备
工欲善其事,必先利其器。在开始写爬虫代码之前,我们需要把“厨房”收拾好。别担心,这个过程非常简单,几乎就是“开箱即用”。
2.1 Python环境与核心库
首先,确保你安装了Python,我推荐使用Python 3.7或以上版本,兼容性更好。我们这次用到的库都是Python标准库或非常轻量的第三方库,不需要复杂的科学计算环境。
- urllib: 这是Python内置的HTTP请求库,我们用它来从网站获取数据。虽然现在
requests库更流行,但为了减少外部依赖,让脚本在任何纯净的Python环境都能运行,我们选择标准库。你不需要额外安装。 - json: 同样是Python内置库,用于解析服务器返回的JSON格式数据,以及将我们处理好的数据保存为JSON文件。
- ssl: 用于处理HTTPS请求的SSL证书验证。有时候目标网站的证书可能不被本地环境完全信任,我们会用到它来创建一个“不验证”的上下文,但这仅用于示例和学习,在生产环境中请务必谨慎评估安全风险。
- os: 用于文件和目录操作,比如创建文件夹来分类存放我们爬取到的各省市数据。
你只需要一个能写代码的文本编辑器(比如VS Code、PyCharm)或者Jupyter Notebook,就可以开始了。我习惯在项目根目录下新建一个geo_json_crawler.py的Python文件,所有代码都写在这里。
2.2 理解数据源结构
我们的数据来自阿里云DataV提供的一个公共服务接口。在写代码之前,我们先花两分钟搞清楚我们要“爬”什么,以及对方是怎么组织这些数据的。这能让你后面的代码理解起来毫不费力。
核心的入口地址是这个:https://geo.datav.aliyun.com/areas_v2/bound/infos.json。你用浏览器打开这个链接,会看到一大串JSON数据。它就像一个“总目录”或“元数据索引”。里面以行政区划代码(比如110000代表北京市)为键,存储了每个区域的信息,主要包括:
name: 行政区划名称,如“北京市”。level: 层级,如province(省)、city(市)、district(区县)。parent: 父级区域的行政区划代码。通过这个字段,我们可以构建出省-市-区县的树形关系。centroid: 一个包含lat(纬度)和lng(经度)的数组,代表该区域的中心点坐标。这个在ECharts中做标记点(如标注城市位置)时极其有用。
有了这个索引文件,我们就知道了全国所有行政区划的代码和层级。真正的GeoJSON几何数据,则通过另一个URL模式获取:
- 不包含子区域的边界(外轮廓):
https://geo.datav.aliyun.com/areas_v2/bound/{行政区划代码}.json - 包含子区域的边界(比如广东省的GeoJSON里包含了所有市级的边界):
https://geo.datav.aliyun.com/areas_v2/bound/{行政区划代码}_full.json
注意,区县级(district)通常没有_full版本,因为它已经是最细的层级了。理解了这个结构,我们的爬虫逻辑就清晰了:先抓“目录”(infos.json),再根据目录里的列表,去批量下载每一个具体的“地图文件”(.json和_full.json)。
3. 核心爬虫代码逐行详解
接下来,我们进入最核心的部分——代码。我会把完整的脚本拆解开,一段一段解释其作用和背后的思考,你完全可以跟着我的注释和说明,自己敲一遍,印象会更深刻。
3.1 目录创建与数据写入函数
任何好的程序都应该结构清晰。我们首先定义两个工具函数,专门负责把爬取到的GeoJSON数据漂亮地保存到本地。
import json
import urllib.request
import urllib.parse
import ssl
import os
# 工具函数:将不包含子区域的geojson写入文件
def write_json_to_file(area_code, geojson_data, level):
"""
保存单个区域的轮廓数据。
:param area_code: 行政区划代码,如 '110000'
:param geojson_data: 爬取到的GeoJSON字典数据
:param level: 层级,如 'province', 'city', 'district'
"""
# 按层级创建文件夹,例如 data/province/
level_dir = os.path.join("data", level)
if not os.path.isdir(level_dir):
os.makedirs(level_dir) # 使用makedirs可以创建多级目录,更安全
# 构建文件路径,并以UTF-8编码写入,确保中文不乱码
file_path = os.path.join(level_dir, f"{area_code}.json")
with open(file_path, "w", encoding='utf-8') as f:
json.dump(geojson_data, f, ensure_ascii=False, indent=2) # indent参数让生成的json文件有缩进,便于阅读
print(f"已保存: {file_path}")
# 工具函数:将包含子区域的geojson写入文件
def write_full_json_to_file(area_code, geojson_data, level):
"""
保存包含子区域的合并数据。
:param area_code: 行政区划代码
:param geojson_data: 爬取到的GeoJSON字典数据
:param level: 层级
"""
# 为包含子区域的数据创建单独的文件夹,如 data/province_full/
level_dir = os.path.join("data", f"{level}_full")
if not os.path.isdir(level_dir):
os.makedirs(level_dir)
file_path = os.path.join(level_dir, f"{area_code}.json")
with open(file_path, "w", encoding='utf-8') as f:
json.dump(geojson_data, f, ensure_ascii=False, indent=2)
print(f"已保存(全): {file_path}")
这里有几个我踩过坑后总结的细节:
- 使用
os.makedirs()代替os.mkdir():mkdir只能创建单级目录,如果data文件夹不存在,它会报错。而makedirs会递归创建所有需要的父目录,更省心。 os.path.join()构建路径:直接用字符串拼接路径(如"data/" + level + "/")在Windows和Linux系统上可能因为斜杠方向不同而出问题。os.path.join()是跨平台的正确姿势。ensure_ascii=False:这是关键!如果不设置这个参数,json.dump会把所有非ASCII字符(比如中文)转义成\uXXXX的形式,文件内容会变得难以阅读。设置为False后,中文就能原样保存了。indent=2:让生成的JSON文件有整齐的缩进,虽然会稍微增加文件体积,但对于我们后续可能的查阅和调试来说,可读性大大提升。
3.2 主流程:获取索引并遍历下载
准备好工具函数后,我们开始编写主逻辑。这部分代码就像乐高说明书,一步步把零件组装起来。
def main():
# 1. 创建不验证SSL证书的上下文(处理某些HTTPS站点)
context = ssl._create_unverified_context()
# 2. 定义数据索引的URL
index_url = "https://geo.datav.aliyun.com/areas_v2/bound/infos.json"
print("开始获取全国行政区划索引...")
# 3. 发起网络请求,获取索引JSON
with urllib.request.urlopen(index_url, context=context) as response:
# 读取响应内容,解码为UTF-8字符串,然后解析为Python字典
index_data = json.loads(response.read().decode("UTF-8"))
print(f"索引获取成功,共包含 {len(index_data)} 个区域条目。")
# 4. 将索引元数据也保存一份,后续可视化定位非常有用
with open("data/location_index.json", "w", encoding='utf-8') as f:
json.dump(index_data, f, ensure_ascii=False, indent=2)
print("索引元数据已保存至 data/location_index.json")
# 5. 遍历索引中的每一个区域
for area_code, area_info in index_data.items():
level = area_info.get('level')
name = area_info.get('name', '未知')
print(f"正在处理 [{level}] {name} ({area_code})...")
# 5.1 下载并保存不包含子区域的轮廓数据
try:
geo_url = f"https://geo.datav.aliyun.com/areas_v2/bound/{area_code}.json"
with urllib.request.urlopen(geo_url, context=context) as geo_res:
geo_data = json.loads(geo_res.read().decode("UTF-8"))
write_json_to_file(area_code, geo_data, level)
except Exception as e:
# 网络超时、404错误等都可能发生,捕获异常避免程序崩溃
print(f" 警告:下载轮廓数据失败 - {area_code}, 错误: {e}")
# 5.2 下载并保存包含子区域的完整数据(区县级没有_full数据)
if level != "district": # 关键判断!
try:
full_geo_url = f"https://geo.datav.aliyun.com/areas_v2/bound/{area_code}_full.json"
with urllib.request.urlopen(full_geo_url, context=context) as full_res:
full_geo_data = json.loads(full_res.read().decode("UTF-8"))
write_full_json_to_file(area_code, full_geo_data, level)
except Exception as e:
print(f" 警告:下载完整数据失败 - {area_code}, 错误: {e}")
print("\n全部数据爬取完成!")
if __name__ == "__main__":
# 确保data根目录存在
if not os.path.isdir("data"):
os.makedirs("data")
main()
这段主流程代码有几个精髓点,值得你特别注意:
- 异常处理(try-except):网络请求是极不稳定的操作。服务器可能临时下线、某个地区的GeoJSON文件可能缺失、你的网络可能波动。用
try-except把每一次下载包裹起来,即使某个文件失败,也不会影响整个脚本的运行,它会打印一条警告信息后继续处理下一个区域。这是自动化脚本健壮性的体现。 - 条件判断
if level != "district":这是根据数据源特性做的优化。区县级是最细粒度,没有子区域,所以自然不存在_full.json文件。如果强行请求,只会得到一个404错误,浪费时间和资源。这个判断能避免大量无效请求。 - 进度反馈:在循环中打印当前正在处理的区域名称和层级,能让你在运行脚本时心里有数,知道进度到了哪里,万一中途出错也容易定位。
- 入口检查
if __name__ == "__main__":这是Python脚本的标准写法。它保证了当你直接运行这个.py文件时,main()函数会被执行;而当这个文件被作为模块导入到其他程序中时,main()不会自动运行,提供了灵活性。
4. 数据存储结构设计与实战建议
脚本跑完后,你的项目目录下会生成一个data文件夹,里面的结构会非常清晰,就像下面这样:
data/
├── location_index.json # 全国行政区划元数据总表
├── province/ # 各省外轮廓
│ ├── 110000.json # 北京市
│ ├── 440000.json # 广东省
│ └── ...
├── province_full/ # 各省包含下属市
│ ├── 110000.json
│ ├── 440000.json
│ └── ...
├── city/ # 各市外轮廓
│ ├── 110100.json # 北京市市辖区
│ ├── 440300.json # 深圳市
│ └── ...
├── city_full/ # 各市包含下属区县
│ ├── 110100.json
│ ├── 440300.json
│ └── ...
└── district/ # 各区县外轮廓(无_full)
├── 110101.json # 东城区
├── 440304.json # 福田区
└── ...
这种结构设计的好处是一目了然。无论你是想获取单个省份的地图,还是需要某个省下所有市的合并地图,都能快速找到对应的文件。location_index.json这个文件更是宝藏,它包含了所有区域的名称、层级、父级关系和中心点坐标。当你用ECharts做地图时,如果想在某个城市的中心点显示一个标记或者数值,直接从这个文件里查坐标就行了,无需再去计算几何中心。
给新手的几个实战建议:
- 首次运行先测试:你可以先修改主循环,用
list(index_data.items())[:5]只遍历前5个区域,测试一下脚本是否能正常工作,避免一开始就发起大量请求。 - 处理网络问题:如果网络环境不稳定,可以考虑在请求之间增加短暂的延时,比如
time.sleep(0.5),以减轻对目标服务器的压力,也降低自己被屏蔽的风险。 - 数据更新策略:行政区划数据不会天天变。你可以每月或每季度运行一次这个脚本,更新本地数据。可以把脚本设置为定时任务(如Linux的cron或Windows的任务计划程序),实现完全自动化更新。
- 备用数据源:正如原始资料提到的,如果这个源的数据不够全面(例如某些特别新的行政区划调整尚未收录),国家地理信息公共服务平台是一个更权威的官方来源。不过,其数据获取方式可能更复杂,通常需要注册、申请API密钥,数据格式也可能需要额外转换。可以将它作为我们当前方案的补充和验证。
5. 在ECharts中无缝使用爬取的数据
数据爬下来不是目的,用起来才是。下面我以最流行的ECharts为例,展示如何将我们爬取的GeoJSON数据变成一张交互式地图。
假设我们有一个简单的HTML页面,引入了ECharts库。核心步骤是两步:注册地图和配置图表选项。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>全国行政区划地图示例</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
</head>
<body>
<div id="main" style="width: 1000px;height:800px;"></div>
<script>
// 初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));
// 使用Fetch API异步加载我们爬取好的GeoJSON数据
fetch('./data/province_full/440000.json') // 这里以广东省(包含下属市)为例
.then(response => response.json())
.then(guangdongGeoJSON => {
// 关键步骤:注册地图
echarts.registerMap('GuangDong', guangdongGeoJSON);
// 配置项
var option = {
title: {
text: '广东省行政区划地图',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{b}' // 鼠标悬停时显示区域名称
},
visualMap: { // 可以配合假数据做颜色映射
min: 0,
max: 100,
text: ['高', '低'],
realtime: false,
calculable: true,
inRange: {
color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695']
}
},
series: [{
name: '示例数据',
type: 'map',
map: 'GuangDong', // 使用刚才注册的地图名
roam: true, // 允许缩放和平移
label: {
show: true // 显示地名标签
},
// 这里可以绑定真实的数据,例如各市的GDP
data: [
{name: '深圳市', value: 95},
{name: '广州市', value: 88},
{name: '东莞市', value: 75},
// ... 其他城市数据
],
emphasis: { // 高亮状态样式
label: {
color: '#fff',
fontWeight: 'bold'
},
itemStyle: {
areaColor: '#ff7b5a' // 高亮颜色
}
}
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
})
.catch(error => console.error('加载GeoJSON数据失败:', error));
// 响应窗口大小变化
window.addEventListener('resize', function() {
myChart.resize();
});
</script>
</body>
</html>
代码解读与技巧:
echarts.registerMap('GuangDong', guangdongGeoJSON): 这是将我们自定义的GeoJSON数据注册为ECharts可识别地图的关键。'GuangDong'是你给这个地图起的名字,后面在series.map属性中引用。roam: true:这个配置允许用户用鼠标滚轮缩放地图、拖拽平移,交互体验很好。data属性:这里我用了假数据做演示。在实际项目中,你可以通过AJAX再加载一份业务数据(如各城市销售额),然后和地图区域通过name字段进行关联,就能实现数据可视化了。- 使用
fetch或$.getJSON:由于GeoJSON文件可能比较大,建议使用异步加载的方式,避免阻塞页面渲染。现代浏览器都支持fetchAPI,如果你用jQuery,$.getJSON也很方便。
通过这种方式,你就完全摆脱了ECharts内置地图数据的限制,可以使用自己爬取的最新、最符合业务需求的行政区划数据了。无论是做全国大盘,还是深度下钻到某个省、某个市,都可以轻松应对。
6. 常见问题与排坑指南
在实际操作中,你可能会遇到一些“坑”。这里我总结几个最常见的问题和解决方法,希望能帮你节省时间。
问题一:运行脚本时报SSL证书验证错误。
- 现象:在
urlopen时抛出ssl.SSLCertVerificationError。 - 原因:目标服务器的SSL证书可能存在问题,或者你的Python环境证书不完整。
- 解决:我们在代码中已经使用了
ssl._create_unverified_context()来创建一个不验证证书的上下文。请注意,这降低了安全性,仅适用于已知安全的公开API。对于生产环境,建议确保系统证书库更新,或使用requests库并设置正确的证书路径。
问题二:下载到一半网络中断或脚本卡住。
- 现象:脚本卡在某个区域不动,或者报超时错误。
- 原因:网络不稳定,或者服务器对频繁请求做了限流。
- 解决:
- 增加重试机制:可以用一个
for循环包裹请求代码,失败后重试几次。 - 添加延时:在每次循环内,
time.sleep(0.5)或更长,模拟人工操作,避免被封IP。 - 断点续传思路:可以将成功下载的区域代码记录到一个日志文件中。如果脚本中断,重新运行时先读取日志,跳过已下载的区域。这需要你稍微改造一下代码逻辑。
- 增加重试机制:可以用一个
问题三:ECharts地图显示空白或错位。
- 现象:地图注册了,但图表区域一片空白,或者地图元素位置很奇怪。
- 原因:
- GeoJSON坐标系不匹配:ECharts默认使用WGS84坐标系(即GPS常用的经纬度)。确保你爬取的GeoJSON数据也是这个坐标系。阿里云DataV提供的数据通常是符合要求的。
- 注册地图的时机不对:必须确保在
echarts.registerMap执行完成之后,再调用setOption。我们的示例使用了fetch().then()的异步模式,保证了顺序。 - 数据格式错误:检查你加载的GeoJSON文件是否是一个合法的JSON。可以用文本编辑器打开看看,或者用
JSON.parse()测试一下。
- 解决:打开浏览器的开发者工具(F12),查看“控制台”(Console)和“网络”(Network)标签页。通常这里会有详细的错误信息,比如“Invalid GeoJSON format”或404错误,能帮你快速定位问题。
问题四:数据更新后,前后端如何同步?
- 场景:你用脚本更新了本地的
data文件夹,但前端页面引用的还是旧文件。 - 解决:这是一个典型的部署问题。如果前端是静态页面,你需要将新的
data文件夹整体覆盖到服务器上。更优雅的做法是,将数据爬取和更新做成一个后台服务,前端通过API动态请求最新数据。对于小型项目,简单粗暴的覆盖更新往往是最快最有效的。
最后,我想说的是,技术方案没有绝对的好坏,只有适合与否。这套Python自动化爬取GeoJSON的方案,核心优势在于简单、直接、可控。它用最少的依赖完成了从数据获取到应用落地的闭环,特别适合数据分析师、前端开发者或需要快速搭建地图可视化原型的团队。希望这份详细的指南能成为你工具箱里一件称手的兵器,当你下次再需要地图数据时,能够从容地“一键获取”。
更多推荐
所有评论(0)