资讯动态

百度地图瓦片下载工具:离线底图批量抓取与避坑指南

发布时间:2026/10/21 14:47:34 来源:尧图企业网站定制
简介这是一款面向离线地图开发者与GIS应用人员的百度地图瓦片下载工具用于批量获取指定区域、指定层级的地图瓦片解决无网络环境下地图数据来源问题。压缩包共3个文件约5.05MB包含可执行jar主程序、properties配置文件和txt使用说明分别承担下载逻辑、参数配置与操作指引结构精简、开箱即用。工具支持百度坐标系转换、经纬度或行政区域范围选择、瓦片级别设定与批量下载并可将零散瓦片整合为完整离线地图适用于离线导航、地图应用开发、地理数据分析、教学科研及应急救援等场景。目前已有5544人学习下载读者可借此掌握瓦片组织原理与坐标转换思路快速搭建本地地图数据源降低开发与部署成本。1. 地图瓦片下载工具从“手动一张张存”到“离线底图自由”做 GIS 可视化、内网大屏或者离线地图应用的人迟早会撞上一堵墙底图从哪来。在线调用当然方便可一旦部署环境没有外网、或者要求毫秒级响应不能等网络请求就必须把地图瓦片提前落到本地磁盘。所谓瓦片就是把地图按缩放级别切成一张张 256×256 或 512×512 的小图片按z/x/y的目录结构存放。标题里的“百度地图瓦片下载工具”本质就是解决“怎么把这些切片批量、可控、不重复地抓下来并组织成标准目录”这件事。它适合三类人做离线地图的 GIS 工程师、给内网系统配底图的后端、以及需要固定底图做视觉比对或标注的算法同学。工具本身不复杂但坐标系、并发、去重、断点续传这几个点能把人折腾到怀疑人生。2. 瓦片下载的底层逻辑URL 规则、坐标系与并发模型2.1 百度瓦片的 URL 拼接规则与坐标系陷阱百度地图的瓦片服务走的是自己的协议和常见的 Web 墨卡托EPSG:3857不是一套东西。它的瓦片 URL 大致长这样https://maponline{0-3}.bdimg.com/starpic/?qtsatepcux{x};y{y};z{z};v009fm46其中{0-3}是负载均衡子域x、y、z分别是列号、行号、层级。这里第一个大坑就是坐标系百度瓦片的x/y是基于 BD-09 的墨卡托投影原点在经纬度 (0,0) 对应的投影位置而且 y 轴方向和标准 TMS 相反。如果你拿标准 XYZ 的公式去算百度瓦片的编号下载下来的图会整体偏移几百米到几公里越到高层越明显。常见做法是先把 WGS-84 经纬度转成 BD-09再套百度的瓦片编号公式。转换分两步WGS-84 → GCJ-02 → BD-09。网上有成熟的转换函数但要注意精度损失一般民用场景够用测绘级需求得另找权威参数。import math # WGS-84 转 GCJ-02简化版民用精度足够 def wgs84_to_gcj02(lng, lat): a 6378245.0 ee 0.00669342162296594323 dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lng dlng, lat dlat def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret这段代码里_transform_lat和_transform_lng是偏移量计算wgs84_to_gcj02把偏移叠加回原坐标。参数a是长半轴ee是第一偏心率平方都是 CGCS2000/WGS-84 椭球的标准值。实际用的时候如果你拿到的原始坐标已经是 GCJ-02比如从某些国内地图 SDK 直接取的就跳过第一步只做 GCJ-02 → BD-09。2.2 瓦片编号计算别拿 XYZ 公式硬套BD-09 坐标算百度瓦片编号的公式和标准 XYZ 不一样。百度用的是中心点加偏移的算法核心是先把经纬度转成墨卡托米制坐标再除以瓦片分辨率。def bd09_to_tile(lng, lat, zoom): # BD-09 墨卡托投影 x lng * 20037508.34 / 180.0 y math.log(math.tan((90.0 lat) * math.pi / 360.0)) / (math.pi / 180.0) y y * 20037508.34 / 180.0 # 百度瓦片分辨率第 18 级为 0.5 米/像素左右这里用通用公式 res 2 ** (18 - zoom) tile_x int((x 20037508.34) / (256 * res)) tile_y int((20037508.34 - y) / (256 * res)) return tile_x, tile_y注意res这个分辨率因子不同层级的实际分辨率百度没有完全公开常见做法是用 18 级作为基准反推。tile_x和tile_y算出来后还要根据百度自己的编号规则做一次偏移否则下载的图会错位。这个偏移量在不同资料里说法不一稳妥的办法是拿一个已知地点的瓦片手动比对校准一次后固定下来。2.3 并发下载线程池不是越大越好瓦片数量动辄几万到几十万张单线程下载等到天荒地老。但并发也不是无脑开大。百度瓦片服务对单 IP 有隐性的频率限制开 200 个线程猛冲大概率触发限流返回 403 或者空图片。我一般用concurrent.futures.ThreadPoolExecutor线程数控制在 8 到 16 之间配合每个请求之间的随机延迟。import os import random import time import requests from concurrent.futures import ThreadPoolExecutor, as_completed HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://map.baidu.com/ } def download_tile(tile_x, tile_y, zoom, save_dir, session): url fhttps://maponline{random.randint(0,3)}.bdimg.com/starpic/?qtsatepcux{tile_x};y{tile_y};z{zoom};v009fm46 path os.path.join(save_dir, str(zoom), str(tile_x), f{tile_y}.png) if os.path.exists(path): return skip os.makedirs(os.path.dirname(path), exist_okTrue) try: resp session.get(url, headersHEADERS, timeout10) if resp.status_code 200 and len(resp.content) 100: with open(path, wb) as f: f.write(resp.content) return ok return fail except Exception: return error finally: time.sleep(random.uniform(0.05, 0.2)) # 随机延迟降低被封风险 def batch_download(tile_list, save_dir, max_workers12): session requests.Session() stats {ok: 0, skip: 0, fail: 0, error: 0} with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(download_tile, tx, ty, z, save_dir, session) for tx, ty, z in tile_list] for future in as_completed(futures): stats[future.result()] 1 return statsmax_workers12是经验值实测在多数网络环境下能跑满带宽又不触发限流。time.sleep放在finally里保证每个请求后都歇一下random.uniform(0.05, 0.2)的区间是试出来的太短没用太长效率掉得厉害。session复用 TCP 连接比每次新建连接快不少。len(resp.content) 100这个判断是为了过滤掉百度返回的占位空图那些图通常只有几十字节。3. 从零跑通一个瓦片下载器目录规划、断点续传与校验3.1 目录结构设计z/x/y 还是 z/y/x瓦片存盘目录结构直接影响后续能不能被 Leaflet、OpenLayers、Mapbox 这些库直接读取。标准做法是{z}/{x}/{y}.png这也是绝大多数前端地图库默认的模板。tiles/ ├── 10/ │ ├── 123/ │ │ ├── 456.png │ │ └── 457.png │ └── 124/ │ └── 456.png ├── 11/ │ └── ...但百度瓦片的 y 轴方向和 TMS 相反如果你打算把下载的瓦片直接喂给标准 XYZ 的库存盘时得做一次 y 翻转y_tms 2^z - 1 - y_xyz。这个翻转放在下载阶段做比后期批量重命名省事得多。def flip_y(tile_y, zoom): return (2 ** zoom) - 1 - tile_y2 ** zoom是该层级的总行数减一减tile_y就得到 TMS 编号。注意这个翻转只对标准 XYZ 库有意义如果你用的就是百度自家的 JS API那不用翻保持原样。3.2 断点续传靠文件存在性判断就够了瓦片下载最怕跑到一半断了重新跑又怕重复下载。最简单的断点续传就是下载前检查目标文件是否存在存在就跳过。上面download_tile里的if os.path.exists(path): return skip就是干这个的。但这里有个细节如果上次下载到一半文件写了一半就断了文件存在但内容是坏的。稳妥做法是下载到临时文件写完再重命名。def download_tile_safe(tile_x, tile_y, zoom, save_dir, session): url fhttps://maponline{random.randint(0,3)}.bdimg.com/starpic/?qtsatepcux{tile_x};y{tile_y};z{zoom};v009fm46 final_path os.path.join(save_dir, str(zoom), str(tile_x), f{tile_y}.png) tmp_path final_path .tmp if os.path.exists(final_path): return skip os.makedirs(os.path.dirname(final_path), exist_okTrue) try: resp session.get(url, headersHEADERS, timeout10) if resp.status_code 200 and len(resp.content) 100: with open(tmp_path, wb) as f: f.write(resp.content) os.replace(tmp_path, final_path) # 原子操作避免半截文件 return ok return fail except Exception: if os.path.exists(tmp_path): os.remove(tmp_path) return error finally: time.sleep(random.uniform(0.05, 0.2))os.replace在大多数文件系统上是原子操作要么旧文件被替换要么新文件完整写入不会出现半截状态。tmp文件在异常时主动清理避免残留一堆垃圾。3.3 下载后的完整性校验别等前端报错才发现图是空的批量下载完最好跑一遍校验。校验分两层文件大小和图片可解码性。文件大小低于阈值比如 200 字节的基本是空图或错误页用 PIL 打开一次能过滤掉损坏文件。from PIL import Image import os def validate_tiles(root_dir, min_size200): bad [] for dirpath, _, filenames in os.walk(root_dir): for name in filenames: if not name.endswith(.png): continue fp os.path.join(dirpath, name) if os.path.getsize(fp) min_size: bad.append((fp, too_small)) continue try: with Image.open(fp) as img: img.verify() except Exception as e: bad.append((fp, str(e))) return badimg.verify()只检查文件头不真正解码像素速度快。如果连文件头都过不了说明下载时内容就是错的。min_size200是经验值正常瓦片至少几 KB200 字节以下基本可以判定异常。4. 避坑与排查那些让我重跑整晚的坑4.1 坑一下载的瓦片整体偏移前端叠加后对不上现象瓦片能下载目录结构也对但叠到地图上整体偏移几百米放大后偏移更明显。原因坐标系没转直接拿 WGS-84 的经纬度套了百度瓦片编号公式。百度瓦片基于 BD-09和 WGS-84 差着两次非线性偏移。解决老老实实做 WGS-84 → GCJ-02 → BD-09 两步转换转换后再算瓦片编号。如果原始数据已经是 GCJ-02跳过第一步。校准方法找一个地标建筑手动算出它的瓦片编号下载后和在线地图比对确认对齐。4.2 坑二跑了几万张后突然全部返回 403现象前几千张正常后面开始大量 403换 IP 后恢复过一会又 403。原因并发太高或请求间隔太短触发了服务端的频率限制。百度瓦片服务对单 IP 的 QPS 有隐性阈值具体数值没公开但实测超过 20 QPS 就危险。解决把max_workers降到 8 到 12每个请求后加random.uniform(0.05, 0.2)的延迟。如果还是被封把延迟下限提到 0.3 秒或者分批下载每批之间歇几分钟。别用固定延迟固定间隔的请求模式更容易被识别。4.3 坑三断点续传后部分瓦片是半截图现象中断后重跑跳过已存在的文件但前端加载时部分瓦片显示不全或花屏。原因上次中断时文件写了一半os.path.exists判断为存在直接跳过坏文件被保留。解决用临时文件加os.replace的原子写入方案。如果已经产生了坏文件跑一遍validate_tiles把异常文件删掉重下。校验时min_size别设太低200 字节是底线正常瓦片至少 1KB 以上。4.4 坑四高层级瓦片数量爆炸磁盘写满现象下载到 17、18 级时瓦片数量指数增长磁盘很快满了。原因每升一级瓦片数量翻四倍。一个城市 18 级的瓦片可能上百万张单张按 10KB 算就是 10GB 起。解决下载前先算总量。公式是4^z乘以覆盖区域占比。如果只是局部区域用边界框算出x、y的范围只下范围内的瓦片。别全量下没必要。另外如果只是做离线底图16 级通常够用再高就是浪费。4.5 坑五下载的瓦片在 Leaflet 里不显示现象目录结构、文件内容都正常但 Leaflet 加载后一片空白。原因y 轴方向没翻转。百度瓦片是 TMS 方向Leaflet 默认是 XYZ 方向两者 y 轴相反。解决存盘时做y_tms 2^z - 1 - y_xyz翻转或者在 Leaflet 的TileLayer里设置tms: true。推荐前者因为翻转一次后面通用不用每个前端都改配置。5. 进阶技巧用边界框精准控制下载范围与层级5.1 从经纬度边界框算出瓦片编号范围全量下载是新手最容易犯的错。实际项目里你往往只需要某个城市、某个园区、甚至某条路两侧的瓦片。用边界框bbox算出瓦片编号范围能把下载量砍掉九成以上。def bbox_to_tile_range(min_lng, min_lat, max_lng, max_lat, zoom): # 先转 BD-09 min_lng_bd, min_lat_bd wgs84_to_bd09(min_lng, min_lat) max_lng_bd, max_lat_bd wgs84_to_bd09(max_lng, max_lat) # 算四个角的瓦片编号 x_min, y_max bd09_to_tile(min_lng_bd, min_lat_bd, zoom) x_max, y_min bd09_to_tile(max_lng_bd, max_lat_bd, zoom) return x_min, x_max, y_min, y_max def generate_tile_list(x_min, x_max, y_min, y_max, zoom): tiles [] for x in range(x_min, x_max 1): for y in range(y_min, y_max 1): tiles.append((x, y, zoom)) return tileswgs84_to_bd09是前面两步转换的封装。注意y_max和y_min的对应关系纬度越小y 越大所以min_lat对应y_maxmax_lat对应y_min。这个容易搞反搞反了范围就是空的。5.2 层级选择不是越高越好层级选择取决于用途。做全国概览5 到 8 级够用做城市级离线地图12 到 15 级做园区级精细底图16 到 18 级。每升一级瓦片数量翻四倍磁盘和下载时间都指数增长。层级适用场景单城市瓦片量级磁盘占用约5-8全国/省概览几百到几千几十 MB9-12城市级底图几万到十几万几百 MB 到 1 GB13-15区县级精细底图几十万1-5 GB16-18园区/街道级上百万5-20 GB这张表是经验值具体数字取决于城市面积和边界框大小。下载前先用generate_tile_list算一下总数心里有底再跑。5.3 一个我常用的下载前检查习惯跑批量下载前我习惯先下 10 张样本手动打开看三件事图片内容对不对、坐标系有没有偏、文件大小是否正常。这三件事确认了再跑全量能省掉整晚重跑的血泪教训。def sample_check(tile_list, save_dir, n10): import random samples random.sample(tile_list, min(n, len(tile_list))) stats batch_download(samples, save_dir, max_workers4) print(f样本下载结果: {stats}) # 手动打开 save_dir 下的图片确认内容 return statsrandom.sample从全量列表里随机抽避免只抽到边缘瓦片。max_workers4是样本阶段慢一点无所谓别触发限流。下载完手动打开几张图确认是地图内容而不是错误页或空图。这个习惯帮我拦下过好几次坐标系配错的问题比跑完几万张再发现要划算得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑