Python实现天地图API调用缓存

引言

因为个人开发者API调用的次数限制原因,加载相同瓦片时仍然会耗费API次数,通过JS的拦截器重定向到web服务器,由服务器进行缓存,进而实现2次调用时(命中缓存)时,走缓存瓦片,进而节省API次数。

JS拦截器

class UniversalRequestInterceptor {
    constructor() {
        this.interceptors = [];
        this.init();
    }
    use(callback) {
        if (typeof callback === 'function') {
            this.interceptors.push(callback);
        }
    }


    triggerInterceptors(requestInfo) {
        const info = {
            ...requestInfo
        };
        for (const callback of this.interceptors) {
            try {
                const result = callback(info);
                if (result === false) {
                    console.log(`[拦截阻止] 类型: ${info.type}, 地址: ${info.url}`);
                    return false;
                }
                if (info.url !== requestInfo.url) {
                    console.log(`[请求篡改] 原地址: ${requestInfo.url} → 新地址: ${info.url}`);
                }
            } catch (e) {
                console.error(`[拦截回调异常]`, e);
            }
        }
        console.log(`[拦截通过] 类型: ${info.type}, 地址: ${info.url}`);
        return true;
    }

    init() {
        this.interceptImage();
    }


    interceptImage() {
        const originalCreateElement = document.createElement;
        const interceptor = this;
        const originalSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src') ||
            Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'src');

        document.createElement = function (tagName) {
            const elem = originalCreateElement.call(this, tagName);
            if (tagName.toLowerCase() === 'img') {
                // 给动态 img 绑定相同的拦截逻辑
                Object.defineProperty(elem, 'src', {
                    set: function (url) {
                        const urlStr = url.toString().trim();
                        if (!urlStr) {
                            originalSrcDesc.set?.call(elem, urlStr);
                            return;
                        }

                        const requestInfo = {
                            type: 'image',
                            url: urlStr,
                            method: 'GET',
                            params: this.parseParams(urlStr)
                        };

                        requestInfo.url = "/getGeoImage?url=" + encodeURIComponent(requestInfo.url);
                        const allow = interceptor.triggerInterceptors(requestInfo);
                        if (allow) {
                            originalSrcDesc.set?.call(elem, requestInfo.url);
                        } else {
                            elem.onerror?.call(elem, new Error(`动态 Image 请求被拦截: ${urlStr}`));
                        }
                    }.bind(elem),
                    get: function () {
                        return originalSrcDesc.get?.call(elem) || '';
                    },
                    configurable: true,
                    enumerable: true
                });

                // 动态 img 的 setAttribute 处理
                const originalSetAttr = elem.setAttribute;
                elem.setAttribute = function (name, value) {
                    if (name.toLowerCase() === 'src') {
                        elem.src = value;
                    } else {
                        originalSetAttr.call(this, name, value);
                    }
                };

                // 解析参数辅助方法
                elem.parseParams = function (url) {
                    const params = {};
                    const query = url.split('?')[1];
                    if (query) {
                        query.split('&').forEach(item => {
                            const [key, value] = item.split('=');
                            if (key) params[decodeURIComponent(key)] = decodeURIComponent(value || '');
                        });
                    }
                    return params;
                };
            }
            return elem;
        };
    }


    // 通用:解析 URL 参数
    parseParams(url) {
        const params = {};
        const query = url.split('?')[1];
        if (query) {
            query.split('&').forEach(item => {
                const [key, value] = item.split('=');
                if (key) params[decodeURIComponent(key)] = decodeURIComponent(value || '');
            });
        }
        return params;
    }
}

const requestInterceptor = new UniversalRequestInterceptor();

requestInterceptor.use((requestInfo) => {
    console.log('[拦截回调]', requestInfo);
    if(requestInfo.type === 'image'){
        return true;
    }});

console.log('请求拦截器初始化完成');

其中代码由AI生成,部分代码进行改造,实际上他的调用并非XHR,因此这里走了些弯路。

其中requestInfo.url = "/getGeoImage?url=" + encodeURIComponent(requestInfo.url);是把请求地址改成Host,并且加上真正调用的地址,如http://t7.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk=xxxxxxxxxx&TILECOL=aaaa&TILEROW=bbbb&TILEMATRIX=13

服务端实现

这里以FastAPI实现

@router.get("/getGeoImage", summary="getGeoImage")
async def getGeoImage(request: Request):
    try:
        trust_url = request.query_params['url']
        params_dict = parse_qs(trust_url)
        layer = params_dict['LAYER'][0]
        tilecol = params_dict['TILECOL'][0]
        tilerow = params_dict['TILEROW'][0]
        tilematrix = params_dict['TILEMATRIX'][0]
        cache_file = f"""{layer}_{tilecol}_{tilerow}_{tilematrix}"""
        cache_path = os.path.join(sysconfig.get_root_directory(),'static','geo_data',cache_file)
        headers = dict(request.headers)
        del headers['host']
        del headers['referer']
        if os.path.exists(cache_path):
            print("cache")
            return FileResponse(cache_path, media_type='image/png')
        else:
            image = requests.get(trust_url, headers=headers)
            with open(cache_path,'wb') as f:
                f.write(image.content)
            return FileResponse(cache_path, media_type='image/png')
    except Exception as e:
        import traceback
        traceback.print_exc()
        return re.Response(code=500, msg='error', data=None)

其中图层、经纬度、级别作为关键数据,进行提取当作文件名,当命中缓存时,走缓存,否则下载到本地并返回。
image.png

结语

这里仅提供缓存思路,如果地图更新,需要手动更新缓存,因此不会保证地图数据是最新的,使用者应遵循用户协议。

评论 0

还没有评论,来说点什么吧。

提交后需要审核才会公开。