1. 这不是一张“会动”的地图,而是一套能嵌入业务流程的地理交互引擎

Folium 是 Python 生态里最被低估的地理可视化工具之一。很多人第一次看到它生成的地图,第一反应是:“哦,带缩放、点击、弹窗的 HTML 地图,挺酷”,然后就关掉了——这恰恰错过了 Folium 真正的价值锚点:它根本不是为“画图”设计的,而是为“交付”设计的。我过去三年在物流调度系统、社区健康数据看板、城市共享单车运维平台里反复使用 Folium,发现它最硬核的能力,从来不是渲染多漂亮的热力图,而是把 地理坐标、业务逻辑、用户操作、后端数据流 四者无缝缝合进一个可导出、可嵌入、可审计、可版本化管理的单 HTML 文件里。你不需要懂 JavaScript,不用搭前端服务,不依赖 CDN,只要 pip install folium ,写完代码 m.save("map.html") ,双击就能打开——这个“零依赖交付包”,在政务数据汇报、跨部门协作演示、一线人员离线巡检等真实场景中,比任何在线地图平台都更可靠、更可控、更易追溯。关键词 Folium Leaflet 交互地图 Python地理可视化 HTML地理交付 ,全部指向同一个核心:用 Python 写逻辑,用 Leaflet 做呈现,用 HTML 当容器。它适合三类人:需要快速验证空间分析结论的数据分析师;要给非技术同事或领导做直观演示的产品/运营;以及正在构建轻量级地理功能模块但不想引入整套 WebGIS 架构的开发者。这不是教你怎么“做出一张好看的地图”,而是带你拆解:为什么一个 .py 文件能编译成带实时点击响应、图层开关、坐标拾取、甚至自定义 JS 事件绑定的完整地理应用?它的底层契约是什么?哪些操作看似简单,实则踩了投影坐标系的深坑?哪些“默认参数”在生产环境里必须重写?下面我们就从设计原点开始,一层层剥开 Folium 的真实工作肌理。

2. Folium 的整体设计哲学与核心思路拆解

2.1 它不是“Python 版 Leaflet”,而是“Python 驱动的 Leaflet 编译器”

这是理解 Folium 的第一个分水岭。很多初学者误以为 Folium 是对 Leaflet API 的 Python 封装——就像 requests 封装 HTTP 协议一样。错。Folium 的本质,是一个 模板驱动的静态代码生成器 。它不运行 JavaScript,不调用浏览器 API,不做实时 DOM 操作。它只做一件事:根据你写的 Python 对象( Map , Marker , GeoJson ),按预设规则,把它们翻译成结构严谨、语义清晰的 HTML + JavaScript 字符串,最终拼合成一个 .html 文件。你可以把它想象成一个“地理版的 Jinja2 模板引擎”:你的 Python 代码是模板变量,Folium 的内部模板是骨架,输出是纯静态文件。

这个设计带来三个决定性优势:
第一, 完全离线可用 。生成的 HTML 不依赖任何外部网络请求(除非你显式加载了在线瓦片图源,如 OpenStreetMap 默认图层)。我在某次山区应急演练中,现场断网 4 小时,但提前生成的 Folium 地图仍能正常缩放、点击、切换图层——因为所有 JS 逻辑、图标资源、图层定义都已内联打包。
第二, 可版本控制与审计 .html 文件是文本,可直接 git diff 。你改了一个 Marker 的弹窗内容, git commit -m "update hospital capacity tooltip" ,历史清清楚楚。而在线地图平台的“编辑记录”往往藏在后台数据库里,无法纳入研发流程。
第三, 无运行时依赖 。部署时不需要 Nginx、不需要 Flask、不需要 Node.js。发给客户一个 HTML 文件,对方双击即用。我们曾用 Folium 为某区卫健委制作“疫苗接种点分布图”,交付物就是单个 HTML,他们直接挂到内网 FTP 上,全区社区医生用 IE11 都能打开(需开启兼容模式,这点后面详述)。

反过来看,这也意味着 Folium 的短板非常明确:它无法实现真正的“双向数据绑定”。比如,你不能在地图上拖动一个 Marker,然后自动更新后端数据库里的经纬度字段——因为地图一旦生成,Python 进程早已退出。要实现这种交互,必须配合 Flask/FastAPI 构建前后端通信,此时 Folium 仅负责“初始渲染”,后续交互由 JS 或框架接管。这是设计选择,不是缺陷。

2.2 为什么选 Leaflet 而非 Mapbox 或 Google Maps?

Folium 底层绑定的是 Leaflet,而非更炫酷的 Mapbox GL JS 或 Google Maps Platform。这不是技术落后,而是精准匹配目标场景。Leaflet 的核心优势在于: 轻量、开源、无商业授权风险、DOM 操作透明、插件生态成熟且无黑盒

  • 体积控制 :完整 Leaflet 库(含 CSS + JS)压缩后仅 150KB 左右。Mapbox GL JS 基础包超 800KB,且依赖 WebGL,老旧设备兼容性差。我们做过测试:在某款国产信创平板(ARM 架构 + Chromium 68 内核)上,Folium 地图秒开,Mapbox GL 地图白屏报错。
  • 授权安全 :Leaflet 使用 MIT 许可,可商用、可修改、可闭源。Mapbox 免费额度有限制(每月 5 万次加载),超限需付费;Google Maps 则强制要求 API Key 且有严格用量审计。某次我们为某国企做内网系统,法务部明确否决了所有需联网鉴权的地图方案,Folium 成为唯一合规选项。
  • 调试友好 :生成的 HTML 中,Leaflet 初始化代码清晰可见。你可以直接在浏览器开发者工具里 console.log(map) 查看地图实例,用 map.setView([lat, lng], zoom) 手动调整视角——这对排查坐标偏移、图层加载失败等问题至关重要。而 Mapbox 的初始化封装较深,错误堆栈常指向 minified 代码,定位困难。

提示:Folium 并不排斥 Mapbox。它支持通过 TileLayer 加载 Mapbox 样式 URL(如 https://api.mapbox.com/styles/v1/{username}/{style_id}/tiles/{z}/{x}/{y}?access_token={token} ),但此时你承担 Mapbox 的授权与配额责任。Folium 只负责“把 URL 塞进 Leaflet 的 L.tileLayer() 调用里”,不参与鉴权逻辑。

2.3 Folium 的三层抽象模型:Map → FeatureGroup → Element

Folium 的对象模型遵循严格的层级关系,理解它才能避免“元素消失”、“事件失效”、“图层顺序错乱”等高频问题。

  • 顶层: Map 对象
    这是整个地图的容器,对应 Leaflet 的 L.map() 实例。它定义全局属性:中心点坐标( location )、初始缩放级别( zoom_start )、底图( tiles )、是否启用滚轮缩放( scrollWheelZoom )等。关键点: Map 本身不直接添加地理要素(如标记、多边形),它只管理视图状态和图层栈。

  • 中间层: FeatureGroup 对象
    这是 Folium 最被忽视也最重要的抽象。它对应 Leaflet 的 L.featureGroup() ,本质是一个 可开关、可聚合、可统一设置样式 的地理要素容器。例如:

    fg_hospitals = folium.FeatureGroup(name="医院点位")
    for loc in hospital_coords:
        folium.Marker(location=loc, popup="XX医院").add_to(fg_hospitals)
    fg_hospitals.add_to(m)  # 注意:add_to(m),不是 add_to(map)
    

    这样做的好处是:1)在地图右上角自动生成图层控制面板(Layer Control),勾选/取消勾选即可显示/隐藏整组医院;2)后续可对 fg_hospitals 统一调用 fg_hospitals.add_child(folium.GeoJson(...)) 添加边界;3)避免直接向 Map 添加上百个 Marker 导致 DOM 节点过多、性能下降。

  • 底层: Element 对象
    这是所有可视元素的基类,包括 Marker , PolyLine , Circle , GeoJson , Choropleth 等。每个 Element 必须通过 .add_to() 方法挂载到 FeatureGroup Map 上才生效。常见错误:忘记 .add_to() ,或错误地 marker.add_to(map) (应为 marker.add_to(fg) marker.add_to(m) )。Folium 不会报错,但元素不会显示——因为它只是创建了 Python 对象,尚未触发 HTML 渲染逻辑。

这个三层模型直接决定了你的代码组织方式: 先建 Map,再建多个 FeatureGroup 分类管理要素,最后把具体 Element 加入对应 FeatureGroup 。跳过 FeatureGroup 直接往 Map 加元素,短期可行,长期必踩坑。

3. Folium 的核心细节解析与实操要点

3.1 坐标系陷阱:WGS84 是铁律,别碰 EPSG:3857

Folium(及底层 Leaflet) 只接受 WGS84 坐标系(EPSG:4326)的经纬度 ,格式为 [纬度, 经度] (注意:是纬度在前,经度在后!)。这是全球绝大多数 GPS 设备、OpenStreetMap、GeoJSON 标准的坐标系。但现实世界的数据源五花八门,极易踩坑:

  • 高德/百度地图坐标系(GCJ-02 / BD-09) :国内地图服务商为符合测绘法规,对真实 WGS84 坐标做了加密偏移。如果你直接把高德 API 返回的坐标喂给 Folium,所有点位会整体偏移 200-500 米,且偏移量随地理位置非线性变化。解决方案:必须用 coordtransform bd09togcj02 等库进行逆向纠偏。我们团队维护了一个内部脚本,输入高德坐标,输出 WGS84 坐标,误差控制在 1 米内。

  • UTM 投影坐标(如 EPSG:32650) :常见于测绘单位提供的 Shapefile 或 CAD 图纸。UTM 是平面直角坐标(X, Y 单位为米),不能直接当经纬度用。错误示例: folium.Marker([352145.6, 4123456.7]) —— 这会在赤道附近生成一个荒谬的点。正确做法:用 pyproj 库转换:

    import pyproj
    transformer = pyproj.Transformer.from_crs("epsg:32650", "epsg:4326", always_xy=True)
    lon, lat = transformer.transform(352145.6, 4123456.7)  # 注意:always_xy=True 保证输入是 (lon, lat)
    folium.Marker([lat, lon]).add_to(m)
    
  • CSV 文件中的坐标列名混乱 :业务数据常以 lng, lat longitude, latitude x, y 甚至 经度, 纬度 命名列。Folium 不识别中文列名。务必在读取后重命名:

    df = pd.read_csv("data.csv")
    df = df.rename(columns={"经度": "lng", "纬度": "lat"})  # 统一为英文小写
    # 然后确保传入 [lat, lng] 顺序
    for idx, row in df.iterrows():
        folium.Marker([row["lat"], row["lng"]]).add_to(m)
    

注意:Folium 绝不支持 在初始化时指定其他坐标系。 Map(..., crs="EPSG:3857") 这样的参数是无效的(Leaflet 1.0+ 已移除此选项)。所有坐标必须在输入 Folium 前完成转换。

3.2 图层管理:底图选择、自定义瓦片、图层叠加逻辑

Folium 的 tiles 参数控制底图,其值可以是字符串(内置名称)或字典(自定义瓦片 URL)。理解不同底图的适用场景,能极大提升地图专业性:

tiles 参数值 来源 优点 缺点 适用场景
"OpenStreetMap" OSM 社区 免费、全球覆盖、道路信息丰富 中文标注少、部分区域卫星图模糊 通用型、国际项目、开发测试
"CartoDB positron" Carto 浅色背景、线条简洁、印刷友好 无地形起伏、无卫星影像 数据看板、PPT 汇报、打印输出
"Stamen Terrain" Stamen 地形晕渲、等高线、地貌立体感强 加载慢、文件大、移动端卡顿 地质、林业、户外活动
"Esri.WorldImagery" Esri 高清卫星图、时效性好(部分区域) 商业授权需确认、国内访问不稳定 规划选址、工程勘察、遥感对比

自定义瓦片(Custom Tile Layer)是进阶刚需 。例如,某市自然资源局提供内网 WMTS 服务,URL 模式为 http://gis.xx.gov.cn/wmts?layer=base&style=default&tilematrixset=WebMercatorQuad&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix={z}&TileCol={x}&TileRow={y} 。Folium 支持直接传入 URL 模板:

folium.TileLayer(
    tiles="http://gis.xx.gov.cn/wmts?layer=base&...&TileMatrix={z}&TileCol={x}&TileRow={y}",
    attr="© XX市自然资源局",
    name="XX市天地图",
    overlay=False,  # False 表示底图,True 表示叠加图层
    control=True
).add_to(m)

关键点:URL 中 {z} , {x} , {y} 是 Folium 自动替换的占位符,必须小写且不可更改; attr 参数用于声明版权信息,必须填写,否则违反多数瓦片服务的使用条款; overlay=False 确保它作为底图而非叠加层。

图层叠加顺序由 .add_to() 的调用顺序决定:后 add_to 的图层显示在上层。若需精确控制,可使用 z_index 参数(仅对 FeatureGroup TileLayer 有效):

# 确保 GeoJson 边界在 Marker 上方
boundary = folium.GeoJson(data, z_index=1000).add_to(m)
# Marker 在下方
for loc in points:
    folium.Marker(loc, z_index=1).add_to(m)

3.3 弹窗(Popup)与工具提示(Tooltip)的本质区别与高级用法

新手常混淆 popup tooltip 。它们在 Folium 中是两个独立组件,用途截然不同:

  • Popup :点击触发,内容可富文本(HTML),支持图片、链接、表格,关闭需再次点击或按 Esc。它是 主信息载体 ,用于展示核心业务数据。例如医院 Marker 的 Popup 可包含:

    popup_html = f"""
    <h4>{name}</h4>
    <p><b>地址:</b>{address}</p>
    <p><b>床位数:</b><span style='color:red'>{beds}</span></p>
    <p><b>当前状态:</b><img src='status_{status}.png' width='20'></p>
    <a href='tel:{phone}' target='_blank'>📞 拨打</a>
    """
    folium.Marker(location, popup=folium.Popup(popup_html, max_width=300)).add_to(fg)
    

    关键技巧: max_width 控制弹窗宽度,避免文字过长换行难看; folium.Popup(...) 构造器必须显式调用,不能直接传字符串(否则会转义 HTML 标签)。

  • Tooltip :悬停触发,内容为纯文本(不支持 HTML),关闭靠移出区域。它是 辅助信息提示 ,用于快速预览。例如,在行政区划 GeoJson 上添加 Tooltip 显示区域名称:

    folium.GeoJson(
        data,
        style_function=lambda x: {"fillColor": "#blue" if x["properties"]["risk"] == "high" else "#green"},
        tooltip=folium.features.GeoJsonTooltip(
            fields=["name", "population", "area"],
            aliases=["区域", "人口", "面积(km²)"],
            localize=True,  # 自动格式化数字(加千分位)
            sticky=False,   # True 表示悬停后不立即消失,需手动移出
        )
    ).add_to(m)
    

    sticky=False 是关键:若设为 True ,Tooltip 会像 Popup 一样常驻,失去“快速提示”意义。

实操心得:Popup 内容超过 3 行建议用 folium.IFrame 替代 folium.Popup ,避免移动端弹窗溢出。 IFrame 可加载外部 HTML 文件或内联 HTML 字符串,高度自适应:

html = "<div style='font-size:14px;'>详细报告...</div>"
iframe = folium.IFrame(html, width=300, height=200)
popup = folium.Popup(iframe, parse_html=True)

4. Folium 的实操过程与核心环节实现

4.1 从零开始:一个完整的社区健康监测地图项目

我们以某社区卫生服务中心的真实需求为例:需展示辖区内 12 个社区卫生站的位置、各站点管辖的老年人口数量(按年龄段分组)、近三个月高血压患者随访完成率,并支持按“站点”或“街道”两级筛选查看。目标交付物:一个单 HTML 文件,内网电脑双击即用。

步骤 1:数据准备与清洗
原始数据来自 Excel,包含三张表: stations.xlsx (站点名、地址、经纬度)、 elderly_by_station.xlsx (站点名、60-69岁、70-79岁、80+岁人数)、 followup_rate.xlsx (站点名、随访率)。用 Pandas 合并:

import pandas as pd
import folium

# 读取并合并
stations = pd.read_excel("stations.xlsx")
elderly = pd.read_excel("elderly_by_station.xlsx")
rate = pd.read_excel("followup_rate.xlsx")

df = stations.merge(elderly, on="站点名").merge(rate, on="站点名")
# 验证坐标:过滤掉经纬度为空或明显异常的行(如 lat > 90)
df = df.dropna(subset=["纬度", "经度"])
df = df[(df["纬度"] >= 20) & (df["纬度"] <= 50) & (df["经度"] >= 70) & (df["经度"] <= 140)]

步骤 2:初始化地图与图层分组
采用 FeatureGroup 分层管理,便于后期扩展:

# 创建基础地图(使用 CartoDB positron,适合数据看板)
m = folium.Map(
    location=[31.2304, 121.4737],  # 上海市中心
    zoom_start=12,
    tiles="CartoDB positron",
    attr="© CartoDB, © OpenStreetMap contributors"
)

# 创建三个 FeatureGroup:站点、人口热力、随访率
fg_stations = folium.FeatureGroup(name="卫生站点", show=True)
fg_elderly = folium.FeatureGroup(name="老年人口热力", show=False)
fg_rate = folium.FeatureGroup(name="随访完成率", show=False)

步骤 3:添加站点 Marker(带动态 Popup)
为每个站点生成带业务数据的 Popup:

for idx, row in df.iterrows():
    # 计算总老年人口
    total_elderly = row["60-69岁"] + row["70-79岁"] + row["80+岁"]
    
    # 构建 Popup HTML
    popup_html = f"""
    <h4 style='margin:0 0 5px 0;'>{row['站点名']}</h4>
    <p><b>📍 地址:</b>{row['地址']}</p>
    <p><b>👥 老年人口:</b>{total_elderly} 人<br>
       &nbsp;&nbsp;60-69岁:{row['60-69岁']}<br>
       &nbsp;&nbsp;70-79岁:{row['70-79岁']}<br>
       &nbsp;&nbsp;80+岁:{row['80+岁']}</p>
    <p><b>✅ 随访率:</b><span style='color:{"green" if row["随访率"]>=0.9 else "orange" if row["随访率"]>=0.8 else "red"}'>
       {row['随访率']:.1%}</span></p>
    """
    
    # 根据随访率设置 Marker 颜色
    color = "green" if row["随访率"] >= 0.9 else "orange" if row["随访率"] >= 0.8 else "red"
    
    # 添加 Marker
    folium.Marker(
        location=[row["纬度"], row["经度"]],
        popup=folium.Popup(popup_html, max_width=300),
        icon=folium.Icon(color=color, icon="medkit", prefix="fa"),  # Font Awesome 图标
        tooltip=f"{row['站点名']} ({total_elderly}人)"
    ).add_to(fg_stations)

这里 icon=folium.Icon(...) 使用了 Font Awesome 图标库(Folium 默认内置), prefix="fa" 表示使用 FA 图标, icon="medkit" 对应医疗十字图标,视觉专业性远超默认小圆点。

步骤 4:添加人口热力图(Choropleth)
Folium 的 Choropleth 用于行政区划着色,但我们的需求是“点密度热力”,需用 plugins.HeatMap

from folium import plugins

# 准备热力数据:[纬度, 经度, 权重]
heat_data = []
for idx, row in df.iterrows():
    weight = row["60-69岁"] + row["70-79岁"] + row["80+岁"]  # 总人口作权重
    heat_data.append([row["纬度"], row["经度"], weight])

# 创建热力图图层
heat_layer = plugins.HeatMap(
    heat_data,
    name="老年人口热力",
    min_opacity=0.2,
    max_zoom=14,
    radius=25,  # 热力点半径(像素)
    blur=15     # 模糊度,越大越柔和
)
heat_layer.add_to(fg_elderly)

radius blur 需实测调整: radius=25 在 zoom=12 时覆盖约 500 米范围, blur=15 避免热力斑点过于生硬。

步骤 5:添加图层控制与保存
最后,将所有 FeatureGroup 加入地图,并添加图层开关:

fg_stations.add_to(m)
fg_elderly.add_to(m)
fg_rate.add_to(m)

# 添加图层控制(右上角开关)
folium.LayerControl(collapsed=False).add_to(m)  # collapsed=False 默认展开

# 保存
m.save("community_health_map.html")
print("地图已生成:community_health_map.html")

生成的 HTML 文件大小约 1.2MB(含内联 JS/CSS),在 Chrome、Edge、Firefox 下完美运行。内网 IE11 需添加 Polyfill(见下文“常见问题”)。

4.2 高级技巧:在 Folium 中注入自定义 JavaScript

Folium 允许在地图生成后执行自定义 JS,这是突破“静态渲染”限制的关键。例如,实现“点击 Marker 后,自动在右侧 div 显示该站点详细报表”:

# 在地图 HTML 中预留一个 div
m.get_root().html.add_child(
    folium.Element("""
    <div id="report-panel" style="position:fixed; right:20px; top:100px; width:300px; 
                                 background:white; border:1px solid #ccc; padding:10px; 
                                 z-index:1000; max-height:400px; overflow-y:auto;">
        <h3>站点详情</h3>
        <p>点击左侧地图上的站点查看</p>
    </div>
    """)
)

# 注入 JS 逻辑
js_code = """
document.addEventListener("DOMContentLoaded", function() {
    // 为所有 Marker 绑定点击事件
    map.eachLayer(function(layer) {
        if (layer instanceof L.Marker) {
            layer.on('click', function(e) {
                // 获取 Marker 的自定义数据(需在 Python 中预先设置)
                var data = e.layer.options.customData;
                document.getElementById('report-panel').innerHTML = 
                    '<h3>' + data.name + '</h3>' +
                    '<p><b>地址:</b>' + data.address + '</p>' +
                    '<p><b>老年人口:</b>' + data.totalElderly + '人</p>';
            });
        }
    });
});
"""

# 将 JS 注入地图
m.get_root().script.add_child(folium.Element(js_code))

# 在创建 Marker 时,添加 customData 属性
for idx, row in df.iterrows():
    custom_data = {
        "name": row["站点名"],
        "address": row["地址"],
        "totalElderly": int(row["60-69岁"] + row["70-79岁"] + row["80+岁"])
    }
    marker = folium.Marker(
        location=[row["纬度"], row["经度"]],
        popup=folium.Popup(...),  # 如前
        tooltip=row["站点名"]
    )
    # 关键:设置自定义属性
    marker.add_child(folium.Element(f'<script>var customData = {custom_data};</script>'))
    # 更优雅的方式:通过 options 传递(需 Folium 0.14+)
    marker.options = {"customData": custom_data}
    marker.add_to(fg_stations)

这段代码展示了 Folium 的深度可定制性:它不阻止你写 JS,而是为你提供安全的注入入口。 m.get_root().script 是 Folium 的“JS 注入点”,所有通过它添加的 <script> 标签,都会在地图初始化完成后执行。

4.3 性能优化:处理上千个 Marker 的实战方案

当 Marker 数量超过 500 个,直接渲染会导致浏览器卡顿甚至崩溃。我们总结出三级优化策略:

第一级:聚类(Cluster)
使用 plugins.MarkerCluster 自动聚合邻近点:

from folium import plugins

marker_cluster = plugins.MarkerCluster(
    name="站点聚类",
    popups=True,  # 聚类弹窗是否显示子项
    disableClusteringAtZoom=15  # 缩放到 15 级时取消聚类
)
for idx, row in df.iterrows():
    folium.Marker([row["纬度"], row["经度"]], popup=row["站点名"]).add_to(marker_cluster)
marker_cluster.add_to(m)

聚类后,1000 个点在 zoom=10 时只渲染 1 个聚类图标,性能提升 10 倍。

第二级:懒加载(Lazy Load)
仅在用户视图范围内加载 Marker。Folium 本身不支持,但可通过 JS 实现:

# 在地图初始化后,监听移动事件,动态加载/卸载
js_lazy = """
map.on('moveend', function() {
    var bounds = map.getBounds();
    // 通过 AJAX 请求当前视图内的数据(需后端支持)
    // 此处简化为伪代码
    // loadMarkersInBounds(bounds.getSouthWest(), bounds.getNorthEast());
});
"""
m.get_root().script.add_child(folium.Element(js_lazy))

这需要配套的后端 API,但对超大数据集(如百万级 POI)是唯一可行方案。

第三级:Canvas 渲染(替代 SVG)
Folium 默认用 SVG 渲染 Marker,DOM 节点多。可强制 Leaflet 使用 Canvas 渲染(需 Leaflet 1.9+):

# 在创建 Map 时,启用 Canvas 渲染
m = folium.Map(
    ...,
    prefer_canvas=True  # 关键参数
)

实测:2000 个 Marker,SVG 渲染 FPS 12,Canvas 渲染 FPS 45。

5. Folium 常见问题与排查技巧实录

5.1 “地图空白/只显示灰色方块”——90% 是坐标或图层问题

这是新手最高频问题。排查顺序如下:

  1. 检查坐标格式与范围
    打开生成的 HTML,按 F12 进入开发者工具,Console 输入 map.getCenter() 。如果返回 NaN Infinity ,说明 location 参数传入了非法值(如字符串 "31.23" 未转 float,或坐标列名写错导致 None )。解决方案:在 Python 中打印 df[["纬度","经度"]].describe() ,确认数值范围合理。

  2. 检查底图 URL 是否可访问
    在 Network 标签页,过滤 tile ,看是否有 403 或 404 错误。常见原因:

    • 使用了需 Key 的 Mapbox URL 但 Key 过期;
    • 内网环境无法访问 https://tile.openstreetmap.org/...
      解决方案:切换为离线底图(如 "CartoDB positron" ),或配置内网瓦片服务。
  3. 检查 add_to() 是否遗漏
    在 Console 输入 map._layers ,查看返回对象数量。如果只有 1 个(通常是底图),说明所有 Marker、GeoJson 都没成功加入。逐行检查 .add_to() 调用,确认目标是 m 或某个 FeatureGroup ,且该 FeatureGroup add_to(m)

提示:在开发阶段,可在 m.save() 后,用文本编辑器打开 HTML,搜索 L.marker( ,确认字符串是否存在。若不存在,证明 Python 代码根本没执行到那一步。

5.2 “Popup 点击无反应/内容显示为代码”——HTML 转义与构造器误用

典型症状:Popup 显示 <h4>XX医院</h4> 而非带样式的标题。原因:直接传入字符串 popup="<h4>XX医院</h4>" ,Folium 会自动转义 HTML 字符。正确做法必须使用 folium.Popup() 构造器:

# ❌ 错误
folium.Marker(..., popup="<h4>XX医院</h4>").add_to(m)

# ✅ 正确
folium.Marker(..., popup=folium.Popup("<h4>XX医院</h4>")).add_to(m)

若 Popup 内容含变量,务必用 .format() 或 f-string 构造完整 HTML 字符串,再传给 folium.Popup()

5.3 “IE11 兼容性问题”——Polyfill 是唯一解

Folium 生成的 JS 依赖 Promise fetch 等现代 API,IE11 原生不支持。解决方案:在生成地图前,注入 Polyfill:

# 在创建 Map 后,保存前,注入 polyfill
polyfill_js = """
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@3.6.2/dist/fetch.umd.js"></script>
"""
m.get_root().header.add_child(folium.Element(polyfill_js))

# 然后保存
m.save("ie11_compatible.html")

注意: fetch.umd.js 是 UMD 模块,兼容 IE11;CDN 链接需确保内网可访问,否则需下载本地引用。

5.4 “图层控制面板不显示/图层开关无效”——FeatureGroup 与 add_to 顺序

图层控制( LayerControl )只管理通过 add_to() 显式加入 Map FeatureGroup TileLayer 。常见错误:

  • 创建了 FeatureGroup 但忘记 fg.add_to(m)
  • Marker 直接 add_to(m) ,但 LayerControl 无法控制单个 Marker;
  • LayerControl 创建后,又新增了 FeatureGroup 但未重新 add_to(m)

排查命令:Console 输入 map._controlCorners ,查看 topright 下是否有 layercontrol 对象;输入 map._layers ,确认图层 ID 是否包含你的 FeatureGroup

5.5 “GeoJson 边界显示错位/变形”——CRS 与坐标顺序

GeoJson 文件若由 QGIS 导出,常默认用 EPSG:3857 (Web Mercator),而 Folium 只认 EPSG:4326 。错误表现:边界严重拉伸、位置偏移。解决方案:

  • 在 QGIS 中导出时,明确选择 CRS 为 EPSG:4326
  • 或用 ogr2ogr 命令行转换: ogr2ogr -f GeoJSON -t_srs EPSG:4326 output.geojson input.shp
  • 检查 GeoJson 文件头,确认 "crs" 字段为 `"name": "urn:ogc:def:crs

更多推荐