ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

地图瓦片下载工具从入门到避坑:XYZ规则、坐标系与并发控制

地图瓦片下载工具从入门到避坑:XYZ规则、坐标系与并发控制 简介这是一款面向 GIS 使用者与地图开发者的地图瓦片下载工具可批量从在线地图服务抓取指定区域、指定层级的瓦片数据解决离线浏览、地理数据分析和本地地图服务搭建中的底图获取问题。压缩包共 841 个文件、约 47.15MB除核心程序 AZMap 外还包含 js/css/html 等前端资源、prj/wkt/csv 坐标与投影定义、png 瓦片样例、dll 依赖库以及多项批处理与配置文件便于直接运行和二次调整。目前已有 1209 人学习/下载说明该工具在实际项目中具备一定参考价值。通过内置的部署与清理脚本用户可以快速在本机搭建瓦片服务目录借助行政区划、投影文件等辅助数据也能更方便地完成区域选择与坐标对齐适合户外地图制作、GIS 数据预处理及网络地图备份等场景。1. 地图瓦片下载工具离线GIS里最缺的那块拼图做应急测绘或者内网交付时最常遇到的不是算法不会写而是手里没有底图。系统部署在没有外网的机房在线地图服务调不了项目方又希望界面里有清晰的道路和影像做参照。过去我处理这类需求第一反应是找现成数据包但现成数据要么覆盖范围对不上要么坐标系不统一来回折腾一整天。后来换成自己动手验证地图瓦片下载工具后很多问题才真正解开把需要的范围按瓦片规则批量存到本地再拼成一张标准底图或直接发布离线地图服务。这篇内容主要面向GIS开发、测绘工程师和做内网系统的运维核心是讲清楚瓦片下载不该在哪个环节省事以及参数到底怎么调才能稳定落地。2. 地图瓦片下载的核心规则XYZ编号、缩放级别与坐标系2.1 XYZ、TMS与WMTS一套编号规则决定文件怎么落地瓦片下载工具本质上做的工作很单一把一个矩形地理范围按缩放级别切成若干小方块每个方块对应一张图片文件名即坐标。理解这一点之前先要接受一个基本设全球地图在Web Mercator投影EPSG:3857下是一个可被幂等分割的正方形。第0级是整张世界地图256×256像素第1级切成2×2第2级是4×4依此类推。任意一级的瓦片总数是2的z次方的平方。最常接触的三套命名规则差异很小但混用时会直接导致拼接错位规则坐标轴方向常见来源典型URL或文件名XYZGoogle/OSM约定x向右y向下OSM、高德、大量在线地图z/x/y.png如 15/27132/12535.pngTMSTile Map Servicex向右y向上GeoServer自带的瓦片缓存、部分开源地图z/x/y.png 但y值需换算WMTS以TileMatrix、TileCol、TileRow表示天地图、国家基础地理信息中心TileMatrix15TileCol27132TileRow12535看起来只是y轴方向相反但实际坑远比想象中多。高德瓦片遵循XYZ但它的y轴与OSM一致向下增长而TMS的y是从南向北增长。若写下载工具时不做转换在北方区域下到的图会竖向错位拼出来像被撕开的纸条。另一个常见问题是国内在线瓦片服务普遍有加密坐标系偏移高德采用GCJ-02坐标天地图省级节点甚至直接用不同的经纬度基准。所以下载前先确认两件事这个数据源的瓦片编号按什么坐标系计算你自己手里的范围坐标在哪个坐标系下。两者不一致时请先转换坐标再算瓦片编号否则后面所有拼接和配准都会偏移。验证一个瓦片地址是否可用的最小命令直接用curl检查响应头即可curl -I https://webrd0${rand}.is.autonavi.com/appmaptile?x27132y12535z15langzh_cnsize1scl1style8逻辑说明-I参数表示只请求响应头不放行正文适合快速判断该瓦片是否存在以及返回状态码是否为200。URL中的x、y、z是瓦片编号参数实际使用时需要替换成自己计算出来的值。若返回200说明该服务允许访问如果返回403或418说明源站有防盗链或限流策略后续下载器必须带User-Agent与Referer请求头。2.2 从经纬度到瓦片号三个必会的换算公式下载的第一步不是写下载器而是把地理范围换算成瓦片编号。这个换算在任何语言里都依赖同一组公式。以Python为例这是最常用的一段转换函数import math def lon_lat_to_tile(lon, lat, zoom): 将经纬度坐标转换为XYZ瓦片编号 lat_rad math.radians(lat) n 2 ** zoom x_tile int((lon 180.0) / 360.0 * n) y_tile int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) return x_tile, y_tile逻辑说明经度方向是线性映射全球经度范围从-180到180对应瓦片范围从0到2^z纬度方向不是线性的因为Web Mercator投影在纬度越高处拉伸越严重所以公式里使用了反双曲正切函数来还原投影关系。x_tile与y_tile最终结果向下取整这是标准做法取整方向错误会出现相邻瓦片重叠或漏下。参数说明lon和lat是十进制度数zoom是缩放级别0到18或更高。不同级别下全球瓦片数量差异极大下载前先估算清楚缩放级别典型用途全球瓦片数量约10省级概览104万14城市级2.68亿16街区级42.9亿18建筑级687亿所以实际下载必须按经纬度范围裁剪而不是按全球级别去拉。省市级范围用14到16级单个项目区域用16到18级足够再往上单张瓦片体积虽小但数量指数级增长磁盘与网络开销都扛不住。还有一个小技巧日常工地上用的范围框不是一个精确的经纬度矩形而是多边形。常见做法是先取外包矩形计算瓦片范围下载后裁剪掉多余边缘瓦片或者下载时直接判断瓦片中心点是否落在多边形内这个判断放下载器里做。3. 最小可用方案用Python请求循环把瓦片落到本地3.1 最小下载器requests循环与目录结构理解瓦片编号规则后写一个最小可用的下载器就水到渠成。先从单线程串行版本开始这能让你看清每一步做了什么出问题也容易定位。以下代码是实际项目里最简形态的骨架import os import time import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } def download_tile(base_url, z, x, y, save_dir): 下载单张瓦片并保存到本地目录 # 按XYZ规则组织本地目录结构z/x/y.png file_path os.path.join(save_dir, str(z), str(x), f{y}.png) if os.path.exists(file_path) and os.path.getsize(file_path) 0: return True url base_url.format(zz, xx, yy) resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: os.makedirs(os.path.dirname(file_path), exist_okTrue) with open(file_path, wb) as f: f.write(resp.content) return True return False def download_area(start_x, end_x, start_y, end_y, zoom, base_url, save_dir): 遍历指定x/y范围内的所有瓦片 for x in range(start_x, end_x 1): for y in range(start_y, end_y 1): ok download_tile(base_url, zoom, x, y, save_dir) if not ok: print(ffailed: z{zoom}, x{x}, y{y}) time.sleep(0.05)逻辑说明download_tile函数先检查本地文件是否已存在且非空存在则跳过下载这是后续断点续传的基础。URL通过format方法动态拼接base_url里必须有{x}、{y}、{z}占位符。save_dir下的目录结构直接按z/x/y来组织这样QGIS读取时不需要额外映射文件。参数说明time.sleep(0.05)表示每下载一张等待50毫秒这是对大多数在线瓦片服务最保守的礼貌性请求间隔。20张每秒的速度对单线程足够如果不加这个延时部分服务会直接封禁IP。timeout10是请求超时时间超过10秒仍未响应则放弃防止网络抖动导致整个循环卡死。这个版本虽然简陋但胜在逻辑透明所有失败都会打印具体坐标方便排查。实际使用时国内数据源需要额外设置Referer通常会伪装成浏览器请求headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://www.amap.com/ }3.2 计算下载范围先框范围再算瓦片数很多新手拿到下载工具就开始写循环却不知道要下多少张。正确流程是在在线地图上先获取目标区域的外包矩形四角坐标再用程序算出对应瓦片范围。下面这段代码把矩形范围转换为起始和终止瓦片编号def calc_tile_range(lon_min, lat_min, lon_max, lat_max, zoom): 计算经纬度矩形范围对应的瓦片编号范围 x_min, y_max lon_lat_to_tile(lon_min, lat_max, zoom) x_max, y_min lon_lat_to_tile(lon_max, lat_min, zoom) return x_min, x_max, y_min, y_max逻辑说明这里需要注意的是经纬度与瓦片坐标的对应关系。经度最小值和纬度最大值组合得到左上角瓦片经度最大值和纬度最小值组合得到右下角瓦片。如果直接拿lat_min对应y_tile会得到错误的范围边界。返回的y_min与y_max是标准XYZ方向的值供download_area使用。参数说明lon_min、lat_min、lon_max、lat_max分别是范围矩形的左下角经度、左下角纬度、右上角经度、右上角纬度。一个值得养成的习惯是下载前先算好瓦片总数对比自己磁盘剩余空间。方法很简单tile_count (x_max - x_min 1) * (y_max - y_min 1) estimated_size tile_count * 15 / 1024 / 1024 # 按平均15KB每张估算单位MB不同数据源单张瓦片大小差异很大矢量底图约5KB到20KB卫星影像约30KB到上百KB。按15KB估算会偏小按50KB估算更稳妥。我见过项目里因没做估算下载到一半磁盘写满导致已下载的瓦片也因文件系统异常而损坏。这种问题影响的不只是下载进度还会污染已完成的缓存文件。4. 实用配菜批量下载的并发控制、断点续传与数据源适配4.1 并发下载用线程池让速度与安全兼顾串行下载在几百张瓦片的场景下还能接受一旦范围扩大到上万张20张每秒的速度就让人坐不住。常见的做法是引入Python的concurrent.futures.ThreadPoolExecutor把瓦片坐标分发给多个线程同时下载。但并发数绝不是越大越好在线瓦片服务基本都有请求频率限制过高的并发会导致大量403响应反而比串行还慢。from concurrent.futures import ThreadPoolExecutor, as_completed def concurrency_download(task_list, workers4, max_retry3): 使用线程池并发下载瓦片带失败重试 def worker(task): z, x, y task for attempt in range(max_retry): try: return download_tile(base_url, z, x, y, save_dir), task except Exception: time.sleep(0.5 * (attempt 1)) # 退避等待 return False, task with ThreadPoolExecutor(max_workersworkers) as executor: futures [executor.submit(worker, task) for task in task_list] for future in as_completed(futures): ok, task future.result() if not ok: failed_tasks.append(task)逻辑说明worker函数是单个下载任务的包装器内部尝试最多max_retry次每次失败后等待时间按0.5秒、1秒、1.5秒递增这是最常见的指数退避策略。主线程用as_completed遍历已完成的任务失败的任务坐标被记录到failed_tasks列表便于后续统一重试。参数说明workers设为4是经验值对高德、天地图这类免费瓦片服务4个并发基本不会触发限流如果用的是商业服务或有授权的瓦片源可以逐步调高到8或16但要观察失败率。max_retry3表示单张瓦片最多重试3次超过则放弃。time.sleep的退避间隔不宜太短0.5秒起步比较稳妥。需要注意的是线程池里的异常处理。requests.get可能抛出的异常类型很多比如ConnectionError、Timeout这些都需要捕获。如果只捕获了Exception但没在函数内做类型区分可能会遗漏DNS解析错误导致的异常。我在实际工地上习惯把异常捕获拆开处理except requests.exceptions.Timeout: # 超时重试 except requests.exceptions.ConnectionError: # 连接被重置说明并发太高需要降低workers4.2 断点续传与失败清单给下载器加一副后悔药瓦片下载耗时长中途网络抖动、限流静默、服务器强制断开连接都是家常便饭。如果因为中途失败就从头开始不仅浪费时间还可能被源站判定为恶意请求。断点续传的核心逻辑只有一条下载前先检查文件是否存在且大小不为0。前面download_tile里已经做了这层检查这里再补两个关键细节。第一某些瓦片服务在限流时返回的不是图片而是HTML错误页虽然状态码是200但文件内容完全不可用。因此判断文件是否有效不能只看存在性还要检查文件头是否是PNG或JPEG格式def is_valid_image(file_path): 检查文件是否为有效的PNG或JPEG try: with open(file_path, rb) as f: header f.read(4) return header[:3] b\x89PNG or header[:2] b\xff\xd8 except Exception: return False逻辑说明PNG文件的前4个字节固定为0x89 0x50 0x4E 0x47即PNG的魔数JPEG文件前2字节为0xFF 0xD8。检查完成后在download_tile里把这个判断加进去即可避免把无效文件当作已下载成果。建议额外维护一个失败清单文件missing.txt把每次下载失败的瓦片坐标追加记录。下载结束后单独对missing.txt里的坐标进行第二轮补下而不是全量重跑。这个过程非常符合实际交付节奏第一轮先把大头跑完失败的小批量用更低并发、更慢的间隔去磨基本能够补齐。with open(missing.txt, a) as f: f.write(f{z}/{x}/{y}\n)5. 避坑高德与天地图瓦片的坐标偏移、限速和边界问题5.1 高德瓦片坐标偏移GCJ-02与WGS-84的偏差这是所有下载工具最容易翻车的地方没有之一。之前在某个项目中我用WGS-84坐标范围计算瓦片编号去下载高德影像瓦片然后在QGIS里叠加WGS-84路网数据结果偏移了大约300米。细查才发现高德地图整体使用GCJ-02坐标系它的瓦片网格是加密后的坐标系下切割的经纬度必须先转换为GCJ-02坐标系下的值再去计算瓦片编号。如果需要在高德瓦片上叠加WGS-84矢量数据有两种方案方案一推荐是把矢量数据转换到GCJ-02坐标系再叠加到瓦片底图上。QGIS里可以通过自定义CRS使用公开的坐标转换参数来近似实现。操作路径是设置 → 自定义坐标参考系统定义proj4字符串例如用常见七参数或四参数方案的近似写法。不过不同地区偏移量不同这种做法属于“能用但不严谨”。更可靠的办法是用专业转换库做坐标转换后再叠加。方案二数据采集阶段规避是直接选择WGS-84的瓦片源例如OSM标准瓦片或一些明确标注WGS-84的影像服务这样本地数据叠加时不需要处理坐标偏移。但如果你必须用高德瓦片作为底图来源那么接受GCJ-02网格、把其他数据转过来比把瓦片转回WGS-84要轻松得多。实践中的手感是先下载一张瓦片用本地图形工具打开再用QGIS加载同一范围的高德在线地图做对比。如果完全重合说明坐标系处理正确如果差200米到500米基本可以确定是坐标系没对齐。不要在这个环节省时间坐标对齐错位会让整个交付成果变成废品。5.2 限速、封IP与越界下载“翻车”的三个高发原因瓦片服务都有访问策略只是有些显式、有些隐式。现象通常是下载刚开始正常几百张之后请求全部返回403或418甚至IP被临时封禁。原因多数是并发过高或请求频率超过阈值。更隐蔽的一种是返回200但响应体是HTML错误页这种最坑因为工具无法通过状态码察觉。应对策略分为三层第一层严格遵守请求间隔和并发上限。串行下载间隔50毫秒并发下载workers4且每个线程内间隔200毫秒到500毫秒。不同服务商阈值不同高德对普通区域瓦片的容忍度高于天地图省级节点后者经常需要更保守的间隔。第二层带全浏览器请求头。User-Agent和Referer缺一不可部分服务还会校验Cookie。更稳妥的做法是先用浏览器开发者工具抓一次正常瓦片请求的完整请求头全部复制到下载器里。第三层设置总下载量的熔断机制。下载器维护一个连续失败的计数器如果连续失败超过20次自动暂停60秒再继续让服务端的限流窗口过去。这个机制能显著降低被封概率。5.3 天地图省级瓦片token参数与节点差异天地图瓦片分为主站与省级节点山东省天地图等省级节点的瓦片地址与主站不同且部分服务需要token认证。下载器里必须把token参数作为独立配置项开放出来而不是写死在URL里。实测中省级天地图并发能力比主站弱workers2已经是安全上限间隔需要放大到500毫秒以上。另外天地图瓦片服务支持多种投影格式常见的有Web Mercator和经纬度直投EPSG:4326。同一范围在这两种投影下计算出的瓦片编号完全不同下载前先确认你需要的成果坐标系。若最终用于Leaflet或OpenLayers网页地图选Web Mercator若用于ArcGIS传统工程或需要与WGS-84经纬度直接配准的数据选EPSG:4326模式。常见的怀疑是数据源是否允许自行下载这里不展开法律争议但真实工程中多数在线瓦片服务条款限制大数据量离线抓取商用项目建议购买官方离线数据授权。个人学习与小范围应急使用时保守限速、不做全网抓取也是从业者的基本自觉。6. 进阶下载结果的验证方法与增量更新技巧瓦片下载完成不代表工作结束。加载到QGIS之前先用gdalbuildvrt做一个快速拼接这是验证下载成果是否完整的最快方式gdalbuildvrt output.vrt /path/to/tiles/{z}/{x}/{y}.png逻辑说明gdalbuildvrt不会把瓦片真正合并成大文件而是生成一个虚拟栅格索引文件QGIS直接加载vrt就能看到整个范围的完整底图。检查时重点看瓦片之间有没有细缝或重叠以及底图与矢量数据是否对齐。如果拼接出的影像呈锯齿状说明部分瓦片缺失需要根据vrt中的空洞信息定位缺瓦范围重新补下。增量更新方面日常维护时不必每次全量重新下载。我通常的做法是保留上一次下载的目录新数据下载到临时目录比对两张同名瓦片的文件大小或ETag有变化才覆盖。如果瓦片服务支持ETag响应头下载器可以直接通过判断ETag是否变化来决定是否重下这样能把增量更新成本降到极低。但多数普通瓦片服务不返回ETag所以退一步用文件大小比对。最后说一个个人习惯下载器跑完后我都会在根目录写一个metadata.json记录下载时间、源服务商URL模板、瓦片范围、缩放级别、坐标系和线程数配置。这个文件不占空间但几个月后你要复现同一个范围时价值极大——不用重新试参数直接照做。希望这些细节能帮你在瓦片数据获取这条路上少走弯路。本文还有配套的精品资源点击获取
返回列表