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}")

这里有几个我踩过坑后总结的细节

  1. 使用os.makedirs()代替os.mkdir()mkdir只能创建单级目录,如果data文件夹不存在,它会报错。而makedirs会递归创建所有需要的父目录,更省心。
  2. os.path.join()构建路径:直接用字符串拼接路径(如"data/" + level + "/")在Windows和Linux系统上可能因为斜杠方向不同而出问题。os.path.join()是跨平台的正确姿势。
  3. ensure_ascii=False:这是关键!如果不设置这个参数,json.dump会把所有非ASCII字符(比如中文)转义成\uXXXX的形式,文件内容会变得难以阅读。设置为False后,中文就能原样保存了。
  4. 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做地图时,如果想在某个城市的中心点显示一个标记或者数值,直接从这个文件里查坐标就行了,无需再去计算几何中心。

给新手的几个实战建议

  1. 首次运行先测试:你可以先修改主循环,用list(index_data.items())[:5]只遍历前5个区域,测试一下脚本是否能正常工作,避免一开始就发起大量请求。
  2. 处理网络问题:如果网络环境不稳定,可以考虑在请求之间增加短暂的延时,比如time.sleep(0.5),以减轻对目标服务器的压力,也降低自己被屏蔽的风险。
  3. 数据更新策略:行政区划数据不会天天变。你可以每月或每季度运行一次这个脚本,更新本地数据。可以把脚本设置为定时任务(如Linux的cron或Windows的任务计划程序),实现完全自动化更新。
  4. 备用数据源:正如原始资料提到的,如果这个源的数据不够全面(例如某些特别新的行政区划调整尚未收录),国家地理信息公共服务平台是一个更权威的官方来源。不过,其数据获取方式可能更复杂,通常需要注册、申请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文件可能比较大,建议使用异步加载的方式,避免阻塞页面渲染。现代浏览器都支持fetch API,如果你用jQuery,$.getJSON也很方便。

通过这种方式,你就完全摆脱了ECharts内置地图数据的限制,可以使用自己爬取的最新、最符合业务需求的行政区划数据了。无论是做全国大盘,还是深度下钻到某个省、某个市,都可以轻松应对。

6. 常见问题与排坑指南

在实际操作中,你可能会遇到一些“坑”。这里我总结几个最常见的问题和解决方法,希望能帮你节省时间。

问题一:运行脚本时报SSL证书验证错误。

  • 现象:在urlopen时抛出ssl.SSLCertVerificationError
  • 原因:目标服务器的SSL证书可能存在问题,或者你的Python环境证书不完整。
  • 解决:我们在代码中已经使用了ssl._create_unverified_context()来创建一个不验证证书的上下文。请注意,这降低了安全性,仅适用于已知安全的公开API。对于生产环境,建议确保系统证书库更新,或使用requests库并设置正确的证书路径。

问题二:下载到一半网络中断或脚本卡住。

  • 现象:脚本卡在某个区域不动,或者报超时错误。
  • 原因:网络不稳定,或者服务器对频繁请求做了限流。
  • 解决
    1. 增加重试机制:可以用一个for循环包裹请求代码,失败后重试几次。
    2. 添加延时:在每次循环内,time.sleep(0.5)或更长,模拟人工操作,避免被封IP。
    3. 断点续传思路:可以将成功下载的区域代码记录到一个日志文件中。如果脚本中断,重新运行时先读取日志,跳过已下载的区域。这需要你稍微改造一下代码逻辑。

问题三:ECharts地图显示空白或错位。

  • 现象:地图注册了,但图表区域一片空白,或者地图元素位置很奇怪。
  • 原因
    1. GeoJSON坐标系不匹配:ECharts默认使用WGS84坐标系(即GPS常用的经纬度)。确保你爬取的GeoJSON数据也是这个坐标系。阿里云DataV提供的数据通常是符合要求的。
    2. 注册地图的时机不对:必须确保在echarts.registerMap执行完成之后,再调用setOption。我们的示例使用了fetch().then()的异步模式,保证了顺序。
    3. 数据格式错误:检查你加载的GeoJSON文件是否是一个合法的JSON。可以用文本编辑器打开看看,或者用JSON.parse()测试一下。
  • 解决:打开浏览器的开发者工具(F12),查看“控制台”(Console)和“网络”(Network)标签页。通常这里会有详细的错误信息,比如“Invalid GeoJSON format”或404错误,能帮你快速定位问题。

问题四:数据更新后,前后端如何同步?

  • 场景:你用脚本更新了本地的data文件夹,但前端页面引用的还是旧文件。
  • 解决:这是一个典型的部署问题。如果前端是静态页面,你需要将新的data文件夹整体覆盖到服务器上。更优雅的做法是,将数据爬取和更新做成一个后台服务,前端通过API动态请求最新数据。对于小型项目,简单粗暴的覆盖更新往往是最快最有效的。

最后,我想说的是,技术方案没有绝对的好坏,只有适合与否。这套Python自动化爬取GeoJSON的方案,核心优势在于简单、直接、可控。它用最少的依赖完成了从数据获取到应用落地的闭环,特别适合数据分析师、前端开发者或需要快速搭建地图可视化原型的团队。希望这份详细的指南能成为你工具箱里一件称手的兵器,当你下次再需要地图数据时,能够从容地“一键获取”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐