为何contextily自动缩放模式下无法获取部分瓦片?
问题描述
使用contextily为GeoPandas地图添加OSM图层时,设置zoom="auto"会随机触发404错误,报错对应瓦片URL不存在。
最小复现代码
import pandas as pd import geopandas as gpd import contextily as ctx df = pd.DataFrame({"key": ["A"], "lon": [3.6], "lat": [43.4]}) point = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs=4326).to_crs(crs="ESRI:102014") circle = point.geometry.buffer(3000) axe = point.plot(color="red") circle.plot(alpha=0.35, color="red", ax=axe) ctx.add_basemap(axe, zoom="auto", crs=point.crs)
报错信息
--------------------------------------------------------------------------- HTTPError Traceback (most recent call last) File ~/.local/lib/python3.10/site-packages/contextily/tile.py:396, in _retryer(tile_url, wait, max_retries) 395 request = requests.get(tile_url, headers={"user-agent": USER_AGENT}) --> 396 request.raise_for_status() 397 except requests.HTTPError: File /usr/local/lib/python3.10/dist-packages/requests/models.py:1021, in Response.raise_for_status(self) 1020 if http_error_msg: --> 1021 raise HTTPError(http_error_msg, response=self) HTTPError: 404 Client Error: Not Found for url: https://stamen-tiles-a.a.ssl.fastly.net/terrain/14/8353/5993.png During handling of the above exception, another exception occurred: HTTPError Traceback (most recent call last) Cell In [9], line 3 1 axe = point.plot(color="red") 2 circle.plot(alpha=0.35, color="red", ax=axe) ----> 3 ctx.add_basemap(axe, zoom="auto", crs=point.crs) File ~/.local/lib/python3.10/site-packages/contextily/plotting.py:121, in add_basemap(ax, zoom, source, interpolation, attribution, attribution_size, reset_extent, crs, resampling, **extra_imshow_args) 117 left, right, bottom, top = _reproj_bb( 118 left, right, bottom, top, crs, {"init": "epsg:3857"} 119 ) 120 # Download image --> 121 image, extent = bounds2img( 122 left, bottom, right, top, zoom=zoom, source=source, ll=False 123 ) 124 # Warping 125 if crs is not None: File ~/.local/lib/python3.10/site-packages/contextily/tile.py:222, in bounds2img(w, s, e, n, zoom, source, ll, wait, max_retries) 220 x, y, z = t.x, t.y, t.z 221 tile_url = provider.build_url(x=x, y=y, z=z) --> 222 image = _fetch_tile(tile_url, wait, max_retries) 223 tiles.append(t) 224 arrays.append(image) File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:594, in MemorizedFunc.__call__(self, *args, **kwargs) 593 def __call__(self, *args, **kwargs): --> 594 return self._cached_call(args, kwargs)[0] File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:537, in MemorizedFunc._cached_call(self, args, kwargs, shelving) 534 must_call = True 536 if must_call: --> 537 out, metadata = self.call(*args, **kwargs) 538 if self.mmap_mode is not None: 539 # Memmap the output at the first call to be consistent with 540 # later calls 541 if self._verbose: File /usr/local/lib/python3.10/dist-packages/joblib/memory.py:779, in MemorizedFunc.call(self, *args, **kwargs) 777 if self._verbose > 0: 778 print(format_call(self.func, args, kwargs)) --> 779 output = self.func(*args, **kwargs) 780 self.store_backend.dump_item( 781 [func_id, args_id], output, verbose=self._verbose) 783 duration = time.time() - start_time File ~/.local/lib/python3.10/site-packages/contextily/tile.py:252, in _fetch_tile(tile_url, wait, max_retries) 250 @memory.cache 251 def _fetch_tile(tile_url, wait, max_retries): --> 252 request = _retryer(tile_url, wait, max_retries) 253 with io.BytesIO(request.content) as image_stream: 254 image = Image.open(image_stream).convert("RGBA") File ~/.local/lib/python3.10/site-packages/contextily/tile.py:399, in _retryer(tile_url, wait, max_retries) 397 except requests.HTTPError: 398 if request.status_code == 404: --> 399 raise requests.HTTPError( 400 "Tile URL resulted in a 404 error. " 401 "Double-check your tile url:\n{}".format(tile_url) 402 ) 403 elif request.status_code == 104: 404 if max_retries > 0: HTTPError: Tile URL resulted in a 404 error. Double-check your tile url: https://stamen-tiles-a.a.ssl.fastly.net/terrain/14/8353/5993.png
现象总结
- 仅在
zoom="auto"时触发,手动调低缩放级(如设为13)可正常运行 - 复杂几何图形的点位不会触发该问题
解决方案
1. 切换瓦片提供商
问题本质是默认的Stamen Terrain瓦片在部分高缩放级别下没有预渲染对应瓦片(比如示例中的14级)。可以切换到覆盖更全的瓦片源,比如OpenStreetMap的标准瓦片:
# 替换add_basemap的source参数 ctx.add_basemap(axe, zoom="auto", crs=point.crs, source=ctx.providers.OpenStreetMap.Mapnik)
其他可选的稳定提供商还有:
ctx.providers.CartoDB.Positron(浅色简洁风格)ctx.providers.Stamen.Toner(黑白风格,瓦片覆盖率高)
2. 限制自动缩放的最大级别
如果坚持使用Stamen Terrain,可以手动设置自动缩放的最大允许级别,避免请求不存在的高分辨率瓦片:
# 先计算自动缩放级别,再取与最大级别中的较小值 from contextily.tile import calculate_zoom # 获取当前轴的边界 bounds = axe.get_extent() # 计算自动缩放级别 auto_zoom = calculate_zoom(bounds[0], bounds[2], bounds[1], bounds[3], crs=point.crs) # 设置最大允许级别为13 final_zoom = min(auto_zoom, 13) ctx.add_basemap(axe, zoom=final_zoom, crs=point.crs)
3. 启用重试与降级机制
可以自定义瓦片获取逻辑,当请求高级别瓦片404时自动降级到低一级别:
import requests def safe_add_basemap(ax, crs, max_zoom=18, source=ctx.providers.Stamen.Terrain): bounds = ax.get_extent() zoom = calculate_zoom(bounds[0], bounds[2], bounds[1], bounds[3], crs=crs) # 从计算出的级别往下尝试 for z in range(zoom, max(0, zoom-5), -1): try: ctx.add_basemap(ax, zoom=z, crs=crs, source=source) return except requests.HTTPError as e: if "404" not in str(e): raise # 如果所有级别都失败,用最低级别 ctx.add_basemap(ax, zoom=5, crs=crs, source=source) # 使用自定义函数替换原add_basemap safe_add_basemap(axe, crs=point.crs)
问题根源说明
- 瓦片提供商限制:部分瓦片源(如Stamen Terrain)不会为所有区域渲染最高级别的瓦片,尤其是人口稀少或地形简单的区域,这类区域的高分辨率瓦片不存在,自然返回404。
- contextily自动缩放逻辑:
zoom="auto"是根据当前地图的边界范围计算所需的最高分辨率级别,不会检查该级别下的瓦片是否实际存在,因此会出现请求不存在瓦片的情况。
内容的提问来源于stack exchange,提问作者jlandercy
相关产品推荐
相关产品推荐

