资讯动态

地图瓦片下载工具详解:从金字塔原理到离线地图实践

发布时间:2026/10/15 22:16:19 来源:尧图企业网站定制
简介这是一款面向地图开发、GIS分析与离线制图场景的瓦片下载工具核心执行程序为AZMap.exe版本4.9可帮助用户从Google Maps、OpenStreetMap等在线服务批量获取指定层级与区域的瓦片数据适用于需要离线地图浏览、地理数据备份或本地地图服务搭建的工程师与爱好者。压缩包共841个文件容量47.15MB主要包含大量JavaScript前端脚本、prj坐标投影定义、PNG/GIF图像素材、DLL动态库、CSV配置表以及azdb/db数据库文件同时附带了IIS启动、缓存清理等批处理脚本与配置文件目录结构围绕任务索引、省市区域示例和地图服务辅助模块展开可按需调用。已有1210人学习下载对于需要定制化高分辨率地图数据且网络条件有限的用户这套工具能省去逐张下载的繁琐流程直接生成结构化瓦片库并可结合脚本完成本地发布与更新为后续GIS分析和地图应用提供基础数据支撑。1. 地图瓦片下载工具离线地图的最后一公里做项目实施最常遇到的尴尬是设备都摆好了网络却不给面子。内网机房、地下停车场、野外作业点这些地方在线地图基本指望不上地图瓦片下载工具要解决的正是这件事把线上地图按金字塔结构切成的小图片成批拉到本地离线环境里照样能渲染、能拼接、能交付。这类工具适合 GIS 项目交付、外业底图准备、大屏离线展示的从业者。很多人以为它只是批量下载脚本实际上背后是投影坐标、瓦片编号、频率限制和存储选型这一串问题。下面按“切片原理 → 下载脚本 → 参数调优 → 踩坑记录 → 进阶校验”的顺序把一条能直接落地的做法讲透。2. 瓦片金字塔与 z/x/y 坐标先看懂地图瓦片是怎么编号的2.1 Web Mercator 投影与金字塔模型为什么每放大一级瓦片数量翻四倍在线地图的底图不是一张“大图”而是成千上万张 256×256 的小图片专业说法叫瓦片。之所以要切碎是因为一张覆盖全球的完整地图在任意缩放级别下都大得离谱传输和内存都扛不住切成瓦片后客户端只需要请求视野里的几张加载速度才有保障。瓦片组织的底层是 Web Mercator 投影对应坐标系 EPSG:3857。它把地球近似展开成一个正方形在缩放级别 z 上全球被均分成 2^z × 2^z 个格子z0 只有 1 张瓦片覆盖全球z1 变成 4 张z2 是 16 张。每放大一级全球瓦片数量翻四倍这就是常说的金字塔模型。理解这个模型对下载工具有一个直接影响任务量是按级别指数级膨胀的。同样一个城市范围从 z14 下到 z18瓦片张数相差约 4^(18-14)256 倍。规划下载范围时先算一笔账再动手能省掉大量等待时间这是这类工具里最重要的预算意识。另一个和投影强相关的边界是纬度。Web Mercator 的有效纬度范围是 ±85.051129°超过这个区域在投影上不存在所以大部分瓦片服务在高纬度地区要么返回空白要么直接 404。写下载脚本时最好把这个范围硬编码进去做裁剪后面避坑章节会展开。2.2 XYZ 与 TMS 两种编号约定y 轴翻转是怎么坑人的有了金字塔模型还需要一套给每张瓦片编号的方式。最常见的编号格式是 z/x/y 三个数字z 是缩放级别x 是列号y 是行号。问题是 y 的起点到底在哪一行行业内存在两种约定这是瓦片下载工具最容易翻车的地方之一。XYZ 约定以左上角为原点y 向下递增左上角瓦片是 (z, 0, 0)。绝大多数在线地图服务、浏览器端地图库都采用这个约定URL 模板通常写成 /{z}/{x}/{y}.png。TMS 约定则以左下角为原点y 向上递增常见于 GIS 桌面软件和部分瓦片规范。两者的换算关系只有一条y_xyz 2^z - 1 - y_tms反过来也一样。对比项XYZ 约定TMS 约定坐标原点左上角左下角y 递增方向向下向上常见场景在线瓦片、前端渲染GIS 软件、MBTiles 文件换算关系y_xyz 2^z - 1 - y_tmsy_tms 2^z - 1 - y_xyz下载器里最稳的做法是把约定作为参数暴露出来默认用 XYZ对接 GIS 工具或写 MBTiles 时显式做翻转。不要靠“感觉哪张不对”去猜等拼图时发现上下颠倒再回头改坐标代价远大于一开始就确认规范。2.3 瓦片服务 URL 的常见规律拼接型与带签名型写下载脚本之前先摸清目标瓦片服务的 URL 规律。绝大多数服务是拼接型域名 固定前缀 z/x/y 扩展名例如 https://tiles.example.com/{z}/{x}/{y}.png。有些服务为了负载均衡会把请求分布到多个子域比如 a.example.com、b.example.com逻辑上只是把同一个模板换成不同 host脚本里轮询即可。还有一类带访问控制的服务URL 后面会挂 token、skey 之类的参数有的参数会随时间过期。对付这类服务我会先抓包看一次完整请求确认参数有效期后再写脚本如果参数每次请求都动态变化那就属于加密型服务不建议对这类接口做逆向优先找官方离线包或企业接口。这类接口本质上是个黑匣子投入产出比太低。无论哪种类型写代码前都先手工验证一次curl -I https://tiles.example.com/15/27121/12485.png返回 HTTP 200 且 Content-Type 是 image/png模板可用如果返回 403 或 text/html说明需要带请求头或参数先解决鉴权再动手。很多下载器跑半天全是 HTML 垃圾文件根因就是跳过了这一步。提示模板验证通过后再批量跑这一步能拦下九成“下了一堆 HTML”的事故。3. 从零实现一个瓦片下载器Python 代码与三个核心函数3.1 经纬度转瓦片行列号两条公式解决坐标换算下载的第一步是把用户给的经纬度范围换算成瓦片坐标。这里只依赖 Python 标准库不需要额外安装第三方包公式是 Web Mercator 的标准换算。import math def lonlat_to_tile(lon, lat, z): 经纬度转瓦片行列号XYZ 约定输入 WGS84 经纬度 n 2 ** z lat_rad math.radians(lat) # x 方向是线性比例经度均匀分布 x int((lon 180.0) / 360.0 * n) # y 方向要经过墨卡托投影反变换asinh(tan) 是标准写法 y int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) return x, y这套公式里x 方向是简单线性比例因为 Web Mercator 的经度分布是均匀的y 方向必须经投影反算直接用纬度比例会随着级别增高明显错位。注意 2 ** z 在 Python 里是大整数运算z 超过 20 后数字很大但结果仍是正常整数不用担心溢出。如果只想验证某个点落在哪张瓦片跑一遍这个函数打印结果再对照在线地图的开发者工具能很快验证公式是否正确。到位后建议把 z 的合法范围也校验一下一般服务端是 0 到 19 或 0 到 22超过范围直接拒绝省得后面白请求。3.2 下载主循环并发、重试、落盘与任务拆分换算完坐标下一步是下载。我一般把单张瓦片的下载封装成一个独立函数负责重试、落盘和跳过已存在文件这样配合线程池时逻辑最清晰。import os import time import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed TILE_URL https://tiles.example.com/{z}/{x}/{y}.png TILE_ROOT ./tiles HEADERS {User-Agent: map-tile-tool/1.0} def download_one(z, x, y, retry3): 下载单张瓦片失败重试支持断点续传 path os.path.join(TILE_ROOT, str(z), str(x), f{y}.png) if os.path.exists(path) and os.path.getsize(path) 0: return True, path # 已有非空文件直接跳过 url TILE_URL.format(zz, xx, yy) os.makedirs(os.path.dirname(path), exist_okTrue) for attempt in range(retry): try: req urllib.request.Request(url, headersHEADERS) with urllib.request.urlopen(req, timeout10) as resp: data resp.read() if len(data) 100: # 过小的响应体大概率是异常占位页按失败处理 time.sleep(2 * (attempt 1)) continue with open(path, wb) as f: f.write(data) return True, path except Exception: # 指数退避2s / 4s / 6s避免重试风暴 time.sleep(2 * (attempt 1)) return False, path这段代码里有几个值得留意的参数。timeout10 同时约束连接和读取是防“卡死”的关键retry 上限设为 3配合指数退避既给网络抖动留了余量又不会对服务端形成重试风暴。小于 100 字节的响应体按失败处理是因为很多风控页面和错误占位图体积都很小这个阈值可以根据实际瓦片大小调整。HEADERS 里至少带一个 User-Agent。很多瓦片服务对默认的 Python 请求头是直接拒绝的伪装成常规客户端能显著降低 403 概率。如果需要更严格的模拟还可以补 Referer 字段指向瓦片服务对应的页面地址。3.3 按范围批量生成任务从经纬度框到 z/x/y 列表单张下载有了剩下的是把经纬度范围展开成任务列表。这里有两个细节必须处理级别是区间、范围要向外取整。def build_tasks(min_lon, min_lat, max_lon, max_lat, z_list): 把经纬度外包框展开成 (z, x, y) 任务列表 tasks [] for z in z_list: # 西北角与东南角分别换算得到覆盖整个框的行列范围 x_min, y_top lonlat_to_tile(min_lon, max_lat, z) x_max, y_bottom lonlat_to_tile(max_lon, min_lat, z) # int() 截断可能让边界反序先修正再展开 if x_min x_max: x_min, x_max x_max, x_min if y_top y_bottom: y_top, y_bottom y_bottom, y_top for x in range(x_min, x_max 1): for y in range(y_top, y_bottom 1): tasks.append((z, x, y)) return tasks用户给的经纬度框往往不会正好落在瓦片边界上int() 截断会让四周边界缺一行或一列所以要用“西北角 → 东南角”对角换算后再取整能保证整个框被完整覆盖。y 方向注意这里用的是 y_top 和 y_bottom 组成的区间别在 range() 里写反。最后串起来执行def run(min_lon, min_lat, max_lon, max_lat, z_min, z_max, workers4): tasks build_tasks(min_lon, min_lat, max_lon, max_lat, range(z_min, z_max 1)) ok fail 0 with ThreadPoolExecutor(max_workersworkers) as pool: future_map {pool.submit(download_one, z, x, y): (z, x, y) for z, x, y in tasks} for future in as_completed(future_map): success, _ future.result() if success: ok 1 else: fail 1 print(f完成 {ok} 张失败 {fail} 张)线程池的 max_workers 就是并发数这里先留成参数具体怎么调放到下一章讲。as_completed 的好处是任务完成一个处理一个不会因为某个慢任务阻塞整体进度。选 threading 而不是 asyncio是因为瓦片下载是典型的 IO 密集任务线程池足够用代码更直白出问题时也好排查。另外补一个边界提示如果下载范围跨过了东西经 180 度例如覆盖从 170°E 到 -170°W 的区域上面的简单取整会出错需要在 build_tasks 里拆成两个框分别算。这种需求实际项目里不多但遇到时别慌拆两半是最简单的解法。多个下载范围叠加时任务列表里可能出现重复坐标跑之前顺手去重能省掉无谓请求。4. 参数怎么设并发数、超时、存储格式与坐标系对齐写下载器时功能跑通只算完成一半另一半是参数调优。这一章把并发、超时、存储和坐标系四个关键选型讲清楚全是交付前必须确认的细节。4.1 并发数与限速公共瓦片服务的安全下载节奏下载器写出来后第一件事不是跑全量而是先用一个小范围试并发。并发数是最影响成败的参数太小速度慢太大容易被服务端封 IP。我的经验值是公共瓦片服务上线程数不超过 4内网自建服务可以放到 8 甚至 16。参数常用取值说明并发线程数2~8公共服务建议 ≤4自建服务可放开单请求超时10 秒连接与读取都设防卡死失败重试3 次指数退避 2s/4s/6sUser-Agent带版本号的工具标识比默认 UA 通过率高请求间隔0~0.5 秒高并发时等效于限速跑全量之前建议先估算任务总量把每个级别的“列数 × 行数”求和再乘以单张瓦片的平均大小经验值 10~30KB。以一片 5 公里的城区为例z17 大约几千张瓦片总量几十 MB单线程半小时左右如果任务量上了十万张就要考虑分批次、错峰下载。这个预判能帮你决定是一次跑完还是分段执行。还要注意一个容易被忽视的问题合法性。下载前确认目标瓦片服务的条款是否允许批量抓取优先使用开放数据源或服务商提供的离线包接口下载尺度控制在“自己项目需要的范围”。这是做 GIS 交付的基本操守也是对公共服务器最基本的尊重。4.2 目录存储还是 MBTiles 单文件两种交付形态怎么选瓦片下载后的存储形态直接决定后续怎么用。最朴素的是按目录存tiles/{z}/{x}/{y}.png好处是直观、支持增量更新配合任意静态文件服务器就能用缺点是文件数量巨大拷贝到移动设备时小文件传输很慢动辄几万个小文件来回拷是件折磨人的事。另一种常见形态是合成为 MBTiles 单文件。MBTiles 本质是 SQLite 数据库一张表存瓦片数据一张表存元信息交付时一个文件直接拷走很多移动端离线地图应用直接认这个格式。写库时有一个关键细节MBTiles 规范内部使用 TMS 行号也就是 y 轴自底向上写入前必须做翻转否则拿到的图是上下颠倒的。import sqlite3 def write_mbtiles(tiles, db_path): 把 (z, x, y_xyz, data) 列表写入 MBTiles注意 y 翻转 conn sqlite3.connect(db_path) conn.execute(CREATE TABLE IF NOT EXISTS tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB)) conn.execute(CREATE TABLE IF NOT EXISTS metadata (name TEXT, value TEXT)) # 元信息至少写格式和范围很多离线应用靠它识别文件 conn.execute(INSERT INTO metadata VALUES (format, png)) conn.execute(INSERT INTO metadata VALUES (bounds, -180,-85,180,85)) conn.execute(BEGIN) # 批量提交单条 commit 会慢到怀疑人生 for z, x, y_xyz, data in tiles: row (2 ** z) - 1 - y_xyz # XYZ 行号转 TMS 行号 conn.execute(INSERT INTO tiles VALUES (?,?,?,?), (z, x, row, data)) conn.execute(COMMIT) conn.close()这个函数最值得记住的是 row (2 ** z) - 1 - y_xyz 这一行。它把 XYZ 约定的行号翻转为 TMS 行号很多现成工具拼接结果颠倒往往就是在这步偷懒了。批量插入用 BEGIN/COMMIT 包起来几千张瓦片也是秒级逐条提交会慢一个数量级。我自己的习惯是开发调试阶段用目录存储方便抽查瓦片内容到了出交付包阶段再转 MBTiles因为单文件不容易丢、拷贝快、第三方工具兼容性好。两个形态之间用上面这段代码来回转换成本很低不用一开始就纠结。注意如果只是临时给前端预览目录存储就够了别为了“正规”强行转 MBTiles多一层转换就多一层出错机会。4.3 坐标系对齐3857、4326 与国内偏移坐标下载瓦片本质是搬图片但搬回来能不能和你的业务数据叠上取决于坐标系是否对齐。绝大多数在线瓦片是 EPSG:3857也就是 Web Mercator 投影坐标系而你本地的手绘矢量、测量成果往往是 WGS84 经纬度EPSG:4326或者平面坐标。图片本身没有坐标信息是“瓦片行列号 缩放级别”隐含了空间位置所以换算时用的投影必须和切片时一致混用 3857 和 4326 的公式会直接错位。一个容易踩的坑是偏移坐标。国内部分地图服务的瓦片采用带偏移的坐标体系比如 GCJ-02这类瓦片叠加 WGS84 数据时会有几百米的系统偏差表现为道路对不上、地物整体平移。这既不是下载工具的问题也不是瓦片坏了纯粹是坐标基准不一致。解决的办法是下载前确认数据源的坐标系尽量选择和业务数据同源的底图服务。另外自建瓦片服务还有个隐藏参数切片的起算原点和行列号是否从 0 开始。少数自建服务行列号从 1 开始或者原点设在左下角这会让下载器任务整体偏移一位。对接前问一句服务方用的什么切片方案比下载后发现错位再返工省事得多。验证坐标系是否对齐最土也最有效的办法是目检下载某个路口周边瓦片在 GIS 软件里叠加该路口的矢量点看偏差在不在可接受范围。这个方法虽然朴素但能一票否决大多数坐标问题。5. 避坑指南瓦片下载工具常见的 5 个翻车现场5.1 下载到一半开始大量 403被服务端风控拦了现象前面几百张瓦片正常下载之后开始成片返回 403速度明显下降重试也没有用。原因公共瓦片服务基本都有频率控制按 IP 维度统计请求量短时间内的并发过高会被临时封锁另外缺少 User-Agent 或 Referer 的请求会被 CDN 层直接拒绝这类 403 从第一张就会开始。解决把并发降到 2~4请求间隔控制在 0.1~0.5 秒补上 User-Agent 和 Referer 请求头重试带上指数退避。脚本里要把失败坐标落到日志里方便定位是不是集中在某个区域。如果降速后依然 403说明 IP 已被拉黑换网络或联系服务商申请开放接口不要用更高并发硬刚只会把封锁期拖得更长。5.2 拼出来的图上下颠倒XYZ 和 TMS 混用了现象瓦片文件都下载成功了数量也对但拼成的大图上下颠倒或者某一行整体错位。原因下载时用的编号约定和目标瓦片实际的约定不一致。例如服务端是 TMS脚本按 XYZ 生成任务两者相差一个 y 翻转等于把所有行号替换到了错误位置。解决下载前先看服务文档确认约定不看文档就用 2.3 节的方法抓两张瓦片人工比对在脚本里把约定抽象成参数凡写 MBTiles 或对接 GIS 工具时一律显式用 y_xyz 2^z - 1 - y_tms 换算。这个坑我踩过一次后就再也不敢凭感觉猜了。5.3 高纬度区域瓦片缺失或空白投影边界问题现象下载范围包含高纬度地区比如北纬 70° 以上这些区域的瓦片返回 404 或内容是空白失败列表里一大片集中在一块区域。原因Web Mercator 有效纬度上限是 ±85.051129°超出部分没有投影定义很多服务商干脆不切这些区域的瓦片或者只在特定级别有数据。这不是网络问题也不是下载器 bug。解决在 build_tasks 之前先把输入的纬度范围裁剪到 ±85.051129°超出部分直接忽略并提示用户。如果业务确实需要极地数据要换用专门覆盖极区的数据源普通在线瓦片解决不了。5.4 任务卡死不动超时与重试配置不当现象下载进程一直停在某张瓦片上内存不涨、日志不动CtrlC 才能退出重跑后又卡在同一位置附近。原因单个请求没有设置读取超时服务器收包后不响应连接挂在那里占着线程池的槽位或者某个网段被限速请求全部进入长时间等待。解决urlopen 必须设 timeout一般 10 秒足够重试次数给上限不要无限循环线程池里用 as_completed 配合 future.result(timeout...) 做兜底超时任务按失败处理并记录坐标。另外每次重跑前先清掉残留的半截文件避免脏数据影响断点续传判断。5.5 拼接出现白缝和黑边透明瓦片与 404 占位现象大图拼完后海洋、空白区域出现有规律的白色方块或黑色边框看起来像网格。原因空白区域的瓦片服务返回 404脚本没做占位处理拼图时该位置留空另一部分瓦片带透明通道叠加到深色底图上时透明区域呈黑色。解决下载时对非 200 状态统一写一张纯色占位瓦片或生成一个缺失清单文件拼图时先铺浅色底图再把瓦片按 RGBA 粘贴最后统一转 RGB。透明边问题在大图拼接时几乎必然出现提前在底图阶段处理比事后逐块修补省事得多。6. 进阶技巧完整性校验、增量更新与一键拼大图6.1 用数量与文件尺寸双重校验下载完整性下载完成不等于任务完成先跑一遍校验再收工是我的固定动作。第一层是数量校验统计每个级别实际文件数和 build_tasks 算出的理论数对比第二层是内容校验抽几张瓦片看 PNG 文件头是否正确避免全是风控页面存下来的 HTML。这种土办法有时候比日志还靠谱。# 统计各级瓦片数量和任务清单比对 find ./tiles -name *.png | wc -l # 抽查文件头是否为合法 PNG head -c 8 ./tiles/15/27121/12485.png | xxd数量对不上就说明有缺失或重复拿缺失清单重新跑增量更新即可文件头不是 PNG 魔数 \x89PNG 的基本可以断定是被风控页面或错误页污染了清掉重下。这个小习惯帮我拦下了好几次翻车事故。6.2 增量更新只补缺失瓦片省时间也省带宽地图数据是持续变化的离线包交付后常常需要更新某个区域。这时候不要整包重下用 download_one 里“已存在且非空就跳过”的逻辑把新旧任务清单做个差集只下载缺失的瓦片即可。实际操作就是把 tasks 过滤一遍def missing_tasks(tasks): 过滤出本地缺失或为空的瓦片任务 result [] for z, x, y in tasks: path os.path.join(TILE_ROOT, str(z), str(x), f{y}.png) if not (os.path.exists(path) and os.path.getsize(path) 0): result.append((z, x, y)) return result这样一轮增量更新通常只需要拉几十到几百张瓦片比全量重下快两个数量级。需要注意的细节是判断条件必须包含“非空”因为上一次失败可能留下 0 字节的半截文件只判断存在会漏掉它们。6.3 把散瓦片拼成一张大图选对插值方式才能不留边有些交付场景需要把某个区域拼成单张大图导出核心思路是建一块空白画布按行列号把瓦片逐张贴上去最后统一导出。from PIL import Image def merge_tiles(z, x0, x1, y0, y1, outmerged.png): 把指定行列范围的瓦片拼接成一张大图 w, h (x1 - x0 1) * 256, (y1 - y0 1) * 256 canvas Image.new(RGB, (w, h), #f0f0f0) # 浅灰底兜底透明区域 for x in range(x0, x1 1): for y in range(y0, y1 1): try: tile Image.open(f./tiles/{z}/{x}/{y}.png).convert(RGBA) except FileNotFoundError: continue # 缺失瓦片留灰底肉眼可辨 canvas.paste(tile, ((x - x0) * 256, (y - y0) * 256)) canvas.save(out) print(f输出 {out}尺寸 {w}x{h})拼接时有两个容易忽略的参数一是底色要浅色而不是黑色透明瓦片拼黑色底会变成黑块二是如果后续需要缩放必须用最近邻插值而不是默认的双线性否则瓦片边缘会出现一圈发白的过渡带。这类工具做出来的大图如果出现网格线九成是插值方式没改。早年我交付某离线地图项目时图省事跳过了校验环节结果交付包里某个区域整片缺失现场演示时地图开天窗被问得下不来台。从那以后我养成的习惯是每个下载任务跑完先过数量校验再抽三张瓦片人工看一眼最后才放进交付包。这个流程加起来不到两分钟却能拦住绝大多数低级事故值得每次执行。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑