基于 Overpass API 的城市电网基础设施与 POI 提取与可视化

在当今地理信息系统(GIS)与位置智能迅速发展的背景下,OpenStreetMap(OSM)作为全球最大的开源地图数据项目,已成为学术研究、商业应用和社区制图的重要数据来源。然而,面对 OSM 庞大而复杂的全球数据集,如何高效、精准地提取所需信息成为使用者面临的关键挑战。为此,Overpass API 应运而生------它是一个专为查询 OpenStreetMap 数据而设计的高性能只读接口,允许用户通过简洁而灵活的查询语言(如 Overpass QL 或 XML 格式),按地理范围、要素类型、标签属性等条件动态获取节点(nodes)、路径(ways)和关系(relations)。

Overpass API 是一个功能强大且高效的只读查询接口,专为从 OpenStreetMap(OSM)数据库中提取地理空间数据而设计。与直接下载完整的 OSM 数据文件相比,Overpass API 允许用户按需获取特定区域、特定类型或满足特定条件的地理要素(如道路、建筑、兴趣点等),从而显著减少数据处理负担并提升开发效率。

Overpass API 官方文档:Overpass API

这里通俗易懂的解释一下:

OpenStreetMap(OSM)就像一个超级大的露天菜市场,里面什么都有------白菜、萝卜、土豆、辣椒、鱼、肉、豆腐......应有尽有,但全都混在一起堆着。如果你想做一道西红柿炒蛋,总不能把整个市场的菜都搬回家再挑吧?那太费时费力了。这时候,Overpass API 就像是一个贴心的菜市场导购员。你只要告诉他:"我只要新鲜的西红柿和鸡蛋,最好在东边摊位,不要烂的",他就会立刻跑遍市场,精准地帮你挑出符合要求的西红柿和鸡蛋,打包好递给你,其他无关的东西一概不拿;

Overpass API 是 OpenStreetMap(OSM)的一个专门查询接口。

  • OpenStreetMap 本身是一张开源的地图数据库,里面存了全世界的道路、建筑、河流、公交站点等所有地理要素。

  • 但是 OSM 数据体量非常大,通常不会直接把它整个爬下来。

  • Overpass API 就是一个为 OSM 设计的查询服务,它允许用专门的语法向服务器发请求,只取出需要的那一部分数据。

为了深入理解 Overpass API 的实际应用,我们将以获取特定基础设施数据为例进行实践------例如电力网络(电网)相关要素。事实上,已有研究者和开源社区成员利用 Overpass API 成功提取并可视化了全球范围内的电网数据,相关成果已在公开平台上展示(感兴趣的读者可自行查阅)。接下来,我们就以此为切入点,通过具体查询示例,逐步掌握如何借助 Overpass API 精准检索 OpenStreetMap 中的专题地理信息;

电力网络地图:开放基础设施地图

那我们理解原理之后,我们在构建电网信息数据库时,我们需要关注一系列与电力基础设施密切相关的地理要素。首先,输电塔(power=tower)作为高大的金属结构,用于支撑高压输电线路,通常位于野外或城市边缘,以点的形式存在。其次,电线杆(power=pole)是较矮的杆状结构,多用于城市或乡村的中低压配电线路,同样以点的形式表示。变电站(power=substation)是变换电压等级、分配电力的关键设施,可以以面或者点的形式存在于数据中。发电厂(power=plant)包括火电厂、水电站、风电场、光伏电站等多种类型,根据其规模和详细程度,可以以点或面的形式记录。变压器(power=transformer)用于电压转换,常见于配电网络末端,如小区电箱旁或杆上,以点的形式存在。最后,电力线(power=line 或 power=minor_line)连接了塔、杆、变电站和用户,分为高压/中压输电线路和低压配电线路两种,以线的形式表现。

ok,我们明确需要哪些标签之后,我们就可以开始获取数据了;

完整代码#运行环境Python 3.11

python 复制代码
import os
import requests
import geopandas as gpd
from shapely.geometry import Point, LineString, Polygon

city_name = "上海市"  #  改成你想要的城市,如 "北京市", "广州市", "成都市"

OVERPASS_URL = "https://overpass-api.de/api/interpreter"

query = f"""
[out:json][timeout:500];
area["name"="{city_name}"]->.search_area;
(
  node(area.search_area)["power"="tower"];
  node(area.search_area)["power"="pole"];
  way(area.search_area)["power"="substation"];
  node(area.search_area)["power"="plant"];
  way(area.search_area)["power"="plant"];
  node(area.search_area)["power"="transformer"];
  node(area.search_area)["amenity"="transformer"];
  way(area.search_area)["power"="line"];
);
out geom;
"""

print(f"正在请求 Overpass API 获取 {city_name} 电网全要素数据...")
response = requests.post(OVERPASS_URL, data={"data": query}, timeout=500)
response.raise_for_status()
elements = response.json().get("elements", [])
print(f"成功获取 {len(elements)} 个电力相关要素")

# 动态生成输出目录
output_dir = f"{city_name.rstrip('市')}电网"
os.makedirs(output_dir, exist_ok=True)

features = {
    "tower": [], "pole": [], "substation_polygon": [],
    "plant_point": [], "plant_polygon": [],
    "transformer": [], "line": []
}


def close_ring(coords):
    if coords and coords[0] != coords[-1]:
        coords.append(coords[0])
    return coords


for el in elements:
    tags = el.get("tags", {})

    # 输电线路
    if el["type"] == "way" and tags.get("power") == "line":
        if len(el.get("geometry", [])) >= 2:
            try:
                coords = [(pt["lon"], pt["lat"]) for pt in el["geometry"]]
                features["line"].append({"geometry": LineString(coords), "osm_id": el["id"], **tags})
            except Exception:
                pass
        continue

    # 点状要素
    if el["type"] == "node":
        geom = Point(el["lon"], el["lat"])
        power = tags.get("power")
        if power == "tower":
            features["tower"].append({"geometry": geom, "osm_id": el["id"], **tags})
        elif power == "pole":
            features["pole"].append({"geometry": geom, "osm_id": el["id"], **tags})
        elif power in ("plant", "generator"):
            features["plant_point"].append({"geometry": geom, "osm_id": el["id"], **tags})
        elif power == "transformer" or tags.get("amenity") == "transformer":
            features["transformer"].append({"geometry": geom, "osm_id": el["id"], **tags})

    # 面状变电站
    elif el["type"] == "way" and tags.get("power") == "substation":
        geom_data = el.get("geometry")
        if geom_data and len(geom_data) >= 3:
            try:
                coords = close_ring([(pt["lon"], pt["lat"]) for pt in geom_data])
                poly = Polygon(coords)
                if poly.is_valid and not poly.is_empty:
                    features["substation_polygon"].append({"geometry": poly, "osm_id": el["id"], **tags})
            except Exception:
                pass

    # 面状电厂
    elif el["type"] == "way" and tags.get("power") == "plant":
        geom_data = el.get("geometry")
        if geom_data and len(geom_data) >= 3:
            try:
                coords = close_ring([(pt["lon"], pt["lat"]) for pt in geom_data])
                poly = Polygon(coords)
                if poly.is_valid and not poly.is_empty:
                    features["plant_polygon"].append({"geometry": poly, "osm_id": el["id"], **tags})
            except Exception:
                pass

# 保存图层
crs = "EPSG:4326"
layer_files = {
    "tower": f"{city_name.rstrip('市')}_power_tower.shp",
    "pole": f"{city_name.rstrip('市')}_power_pole.shp",
    "substation_polygon": f"{city_name.rstrip('市')}_substation.shp",
    "plant_point": f"{city_name.rstrip('市')}_plant_point.shp",
    "plant_polygon": f"{city_name.rstrip('市')}_plant_polygon.shp",
    "transformer": f"{city_name.rstrip('市')}_transformer.shp",
    "line": f"{city_name.rstrip('市')}_power_line.shp"
}

for key, filename in layer_files.items():
    feat_list = features[key]
    if feat_list:
        gdf = gpd.GeoDataFrame(feat_list, crs=crs)
        gdf.to_file(os.path.join(output_dir, filename), encoding="utf-8")
        print(f"已保存 {len(feat_list)} 个 {key} → {filename}")
    else:
        print(f"无 {key} 数据,跳过保存 {filename}")

print(f"\n所有数据已成功导出至:{os.path.abspath(output_dir)}")

我们这里筛选了包括,输电塔(power=tower)、电线杆(power=pole)、变电站(power=substation)、发电厂(power=plant)、变压器(power=transformer)、电力线(power=line 或 power=minor_line)这些标签数据,输出为shp图层数据;

放大细节我们可以看到,电线杆、电力线、变电站这些细节,我们再简单看一下上海电力网络的分布特征;

上海电网设施分布呈现出高度密集、结构复杂且覆盖全面 的特点,从整体布局来看,电网设施主要集中在中心城区及周边重点发展区域,如浦东新区、徐汇区、静安区、黄浦区和长宁区等,这些区域由于人口密度高、商业活动频繁,对电力供应的稳定性与容量要求极高。图中可见,高压输电线路(通常为220kV及以上)呈网状交织,贯穿全市东西南北 ,尤其在市中心区域形成多个环网结构,确保供电可靠性。其中,黄浦江两岸的电网布局尤为密集,尤其是浦东陆家嘴金融区和浦西外滩区域,均布设有多个变电站和配电节点,支撑着城市核心功能区的用电需求。

在郊区,如嘉定、青浦、松江、奉贤和金山等区,电网设施相对稀疏但仍在不断扩展,以适应新城建设和产业发展的需要。例如,虹桥枢纽、张江高科技园区、临港新片区等重要功能区均配置了大型变电站和智能电网设施 ,保障重点产业和交通枢纽的电力供应。此外,图中黑色点状标记代表各类变电站或配电所,它们沿道路、河流和轨道交通线分布,形成了"主干---分支---末端"三级供电网络。值得注意的是,崇明岛虽地理位置偏远,但仍通过跨海电缆接入主网,实现了全岛通电,体现了电网建设的统筹规划与技术实力;

这里有一个小tips,如果需要修改城市名称的话,可能直接在city_name进行修改;

在成功获取并结构化存储了目标城市的电网基础设施(包括输电塔、电线杆、变电站、电厂及线路)之后,为进一步拓展对城市空间结构的理解,接下来我们将转向更广泛的城市兴趣点(POI)数据获取。通过 Overpass API 查询 OpenStreetMap 中的各类 POI 信息(如学校、医院、商场、交通枢纽等),旨在刻画城市的功能布局、人口活动热点与公共服务分布,为后续多维度的城市空间分析奠定基础。

这里,我们聚焦于城市商业活力的一个典型切面------连锁咖啡品牌的门店分布情况。以星巴克(Starbucks) 和 瑞幸咖啡(Luckin Coffee) 为代表,这两者分别代表了高端精品咖啡与本土数字化快咖啡的不同商业模式,在中国城市中形成了广泛而差异化的空间布局。通过 Overpass API 从 OpenStreetMap 中提取这些品牌的 POI 数据;

完整代码#运行环境Python 3.11

python 复制代码
# -*- coding: utf-8 -*-
import os
import re
import requests
import geopandas as gpd
from shapely.geometry import Point

def fetch_poi(city, pattern):
    # 用第一个名称作为文件名
    main_name = pattern.split('|')[0]
    filename = re.sub(r'[^\w\u4e00-\u9fa5]', '_', main_name) + ".shp"
    output_path = os.path.join("poi数据集", filename)

    query = f"""
    [out:json][timeout:180];
    area["name"="{city}"]["boundary"="administrative"]->.search_area;
    (
      node["name"~"{pattern}",i](area.search_area);
      node["brand"~"{pattern}",i](area.search_area);
    );
    out center;
    """

    try:
        print(f"查询 {main_name}...")
        resp = requests.post("https://overpass-api.de/api/interpreter", data={"data": query}, timeout=180)
        resp.raise_for_status()
        elements = [e for e in resp.json().get("elements", []) if e["type"] == "node"]

        if not elements:
            print(f"无结果: {main_name}")
            return

        # 去重并构建要素
        seen, features = set(), []
        for e in elements:
            if e["id"] in seen: continue
            seen.add(e["id"])
            tags = e.get("tags", {})
            features.append({
                "name": tags.get("name", main_name),
                "brand": tags.get("brand", ""),
                "address": tags.get("addr:full") or tags.get("address", ""),
                "osm_id": e["id"],
                "geometry": Point(e["lon"], e["lat"])
            })

        # 保存
        os.makedirs("poi数据集", exist_ok=True)
        gdf = gpd.GeoDataFrame(features, crs="EPSG:4326")
        gdf.to_file(output_path, encoding="utf-8")
        print(f"保存 {len(features)} 个点 → {filename}")

    except Exception as e:
        print(f"{main_name} 出错: {e}")


if __name__ == "__main__":
    CITY = "上海市"
    BRANDS = [
        "星巴克|Starbucks",
        "瑞幸咖啡|Luckin"

    ]

    for brand in BRANDS:
        fetch_poi(CITY, brand)

    print(f"\n完成!所有数据已保存至:{os.path.abspath('poi数据集')}")

我们这里筛选了包括 "星巴克|Starbucks","瑞幸咖啡|Luckin",有需要可以改成其他poi,城市名称也可以一并修改(osm的poi数据集不一定是特别全,但是作为参考还是没问题的);

在 Overpass 中:["name"~"星巴克"] ,表示只要字段中包含"星巴克:就匹配,所以这里写了 "星巴克|Starbucks",意思兼容中文名和英文名,同时如果还有其他别名并可以继续加"|"进行分格,脚本运行结束,这些poi数据会输出为shp图层数据保存脚本目录下的"poi数据集";

接下来,我们进行看图说话:

星巴克和瑞幸咖啡等连锁咖啡品牌在上海的门店分布呈现出高度集中在中心城区,并形成"核心---外围"梯度格局的特点。市中心区域如黄浦、静安、徐汇、长宁、虹口等地 ,特别是南京路步行街、淮海中路、人民广场、陆家嘴金融区、徐家汇商圈、中山公园商圈等商业活动最旺盛的地方 ,这些品牌的门店密度极高,部分地段甚至出现了"百米内多店并存"的现象。这表明了在高人流量、高消费能力的地区,对咖啡饮品的需求也相应较高。

这些咖啡品牌门店的布局强烈依赖交通枢纽与商业中心绝大多数门店位于地铁出入口500米范围内 ,尤其是在1、2、7、8、9、10、13、16号线等高流量线路沿线 。例如,人民广场站、徐家汇站、龙阳路站、虹桥火车站等枢纽周边聚集了大量的门店,满足了通勤族和游客的即时消费需求。同时,来福士广场、正大广场、国金中心、环球金融中心、万象城、太古里、K11、第一八佰伴等高端商场内部或周边,以及甲级写字楼集中区(如陆家嘴金融贸易区、张江科学城、漕河泾开发区) 也是这些品牌的重要承载地,体现了对高收入群体和商务客群的精准覆盖。

整体来看,上海的星巴克和瑞幸咖啡门店分布呈现出了圈层式扩散与网络化连接特征 ,以市中心为核心向外层层扩展,形成"核心圈→次级圈→边缘圈"的层级结构。越靠近城市中心,门店密度越高;越远离中心,密度递减 。同时,门店沿主干道路和轨道交通线呈链状分布,形成了"交通走廊+消费节点"的网络体系,极大提升了可达性与服务半径。这种布局模式不仅符合现代都市消费者的行为习惯,也为品牌提供了更加广泛的市场覆盖和服务能力。

文章仅用于分享个人学习成果与个人存档之用,分享知识,如有侵权,请联系作者进行删除。所有信息均基于作者的个人理解和经验,不代表任何官方立场或权威解读。

相关推荐
q***23571 小时前
python的sql解析库-sqlparse
数据库·python·sql
18你磊哥2 小时前
Django WEB 简单项目创建与结构讲解
前端·python·django·sqlite
月殇_木言2 小时前
Python期末复习
开发语言·python
BBB努力学习程序设计4 小时前
Python面向对象编程:从代码搬运工到架构师
python·pycharm
rising start4 小时前
五、python正则表达式
python·正则表达式
BBB努力学习程序设计5 小时前
Python错误处理艺术:从崩溃到优雅恢复的蜕变
python·pycharm
我叫黑大帅5 小时前
什么叫可迭代对象?为什么要用它?
前端·后端·python
Dillon Dong5 小时前
Django + uWSGI 部署至 Ubuntu 完整指南
python·ubuntu·django
k***82515 小时前
python爬虫——爬取全年天气数据并做可视化分析
开发语言·爬虫·python