简介这份工具包基于高德地图API实现批量地理位置距离计算主要面向物流行业与需要处理大量地址数据的Java开发者。功能覆盖CSV批量导入导出、距离矩阵计算、多线程并发处理与结果可视化展示可有效缩短成百上千个坐标点的处理时间辅助配送路线优化与网点覆盖分析。压缩包共16个文件、约695KB内含4个XML配置、3个Java源码、3个Class编译文件、2个Iml工程文件以及docx、txt、md、jar等其中docx与txt提供详细使用说明README帮助快速理解项目结构。已有178人学习下载适合具备一定Java基础的用户。通过阅读源码与文档可以掌握高德地图API的地理编码和距离计算调用方式学习多线程任务拆分、CSV数据读写及结果可视化的具体实现为物流调度等真实场景提供可直接复用的参考方案。1. 物流场景的批量距离计算为什么高德API能把这个月的工作压成几分钟物流调度的日常里最磨人的往往不是路线规划本身而是先把几十个网点两两之间的距离算出来。一个同城配送项目里50个网点需要算2450对距离人工在高德地图网页上一个个点至少半天500个网点就是12万多对基本不可能手动完成。而这个基于高德地图API批量地理位置距离计算的高效工具输入CSV、调用距离矩阵接口、多线程并发处理几分钟就能把距离矩阵跑完再叠加可视化展示辅助决策。它服务三类人物流调度要算网点覆盖半径运费结算要核对里程数据分析要拿距离矩阵做聚类分群。不需要GIS背景Python能跑就行。下面按从选型到落地的顺序把每一步的参数、代码和坑都讲清楚。2. 高德API选型与配额设计距离矩阵API为什么是批量算距的主力2.1 高德API家族里怎么选地理编码、路径规划与距离矩阵的边界高德Web服务API里和距离计算相关的有三个很多人上来就搞混。第一个是路径规划API它只接受一个起点和一个终点返回一条具体路线的驾车/骑行/步行方案带红绿灯等待、收费预估等信息。它的优点是精度高缺点是单次只能算一对批量场景下要循环调几千次既慢又费配额。第二个是地理编码API它把「北京市朝阳区望京东路4号」这样的文字地址转成经纬度坐标本身不计算距离但批量地址输入的第一步就是它。第三个才是主角——距离矩阵API它接受多个起点和多个终点一次请求返回一个二维距离数组正好对应「批量地理位置距离计算」这个标题。选型结论很直接只要目标是算多对多距离就选距离矩阵API不要用路径规划API硬拼。距离矩阵API底层走的也是高德路网数据返回的驾车距离和路径规划是同源的只是它把一堆起点终点打包成矩阵计算结果返回。如果你拿到的经纬度本身就是网点的精确坐标连地理编码都不用调直接喂给距离矩阵API就行。只有CSV里存的是文字地址时才需要先走一次地理编码。2.2 Key申请与配额个人开发者和企业认证的差距在哪里高德开放平台的Key分类型做这种服务端批量计算必须在控制台创建一个「Web服务」类型的Key不能用Web端JS API的Key两者权限域完全不同。创建后要把对应服务打开一般默认会开通全部Web服务API但如果你看到某个接口报错说服务未开通第一件事就是回控制台确认这个Key的「API权限」勾选了距离测量相关服务。配额这块是高德所有场景里最容易被低估的。个人开发者Key的日配额通常不大而且不同账号、不同认证等级差别明显企业认证或付费后配额会提升。这个数字不要凭记忆拍脑袋直接看控制台「配额管理」页。另外高德提供一个配额查询接口每跑一批任务前查一次余量比等请求失败再排查要省事得多curl https://restapi.amap.com/v3/account/status?key你的Web服务Key返回值里能看到当日的配额总额、已用量和剩余量。我一般会在脚本启动时先调这个接口做一次「配额体检」如果剩余量低于本次任务预估的请求数就直接中止免得跑到一半弹一堆配额超限错误。2.3 距离矩阵API的请求参数与返回结构一次调用能拿到多少对距离距离矩阵API的正式端点是/v3/distance核心参数如下表参数含义示例与说明keyWeb服务Key控制台获取注意类型别选错origins起点坐标列表多个坐标用竖线 destinations终点坐标列表格式同上单次最多100个type距离类型0是直线距离1是驾车路径距离物流默认用1单次请求最多100个起点乘以100个终点也就是一次能拿回1万对距离。这个「100×100」的上限是整个工具设计的锚点如果网点数是500要拆成5×5共25次请求如果网点数是2000就要拆成20×20共400次请求。返回结果是results数组每一项里带origin_index、dest_index、distance和duration其中distance单位是米。origin_index和dest_index是0起编号正好和origins、destinations列表里的位置一一对应解析时不要自己重新排序。3. 从CSV到可计算的矩阵地址清洗、坐标缓存与请求拆分3.1 CSV输入格式设计location_id、坐标与地址列怎么排CSV的字段规划直接决定后面脚本写起来顺不顺手。我建议最小字段是五列location_id、location_name、address、lng、lat。location_id是内部主键不要用中文名做关联键同一个网点在Excel里可能出现「朝阳分仓」和「朝阳分仓库」两种写法但ID是稳定的。address填文字地址lng和lat是经纬度。一个常见的坑是如果你的数据源是其他系统导出的坐标列可能缺失或混着坐标字符串所以代码里要做空值判断。下面这段代码负责读取CSV并构建坐标列表它的输入是原始CSV输出是一个标准坐标数组和一个缓存字典import csv import os def load_locations(csv_path): locations [] with open(csv_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: loc_id row[location_id].strip() name row[location_name].strip() address row[address].strip() lng row.get(lng, ).strip() lat row.get(lat, ).strip() if lng and lat: try: coord (float(lng), float(lat)) except ValueError: coord None else: coord None locations.append({ id: loc_id, name: name, address: address, coord: coord }) return locations这个函数有两个细节值得注意。第一encodingutf-8-sig是为了兼容Excel导出的带BOM的CSV不写这个参数第一列的列名很可能变成\ufefflocation_id后续所有按列名的取值都会静默失败。第二坐标列在CSV里可能是空字符串、None或非法数值这里统一捕获ValueError并把coord置为None后面再走地理编码补全比在循环外部做一堆if not coord判断要干净。3.2 地址清洗与坐标缓存同样的地址不调第二次API批量地址输入的第二道工序是地址清洗和地理编码。清洗的常见规则包括去首尾空格、把全角空格和常见干扰字符归一化、给短地址补城市前缀。例如「上地十街10号」这种缺省城市的地址如果不补前缀高德地理编码返回的坐标可能落在全国任意一个同名地点这是批量地址输入最典型的翻车点。地理编码接口的city参数就是干这个的要么传网点所在城市要么先通过地址库补齐城市再调用。接下来是坐标缓存策略。地理编码按次计费一套50个地址的CSV可能只有两三个地址没有坐标需要补但如果这套CSV每周跑一次每次都重复调地理编码就太浪费了。缓存只需要一个字典加一个JSON文件import json import requests GEOCODE_URL https://restapi.amap.com/v3/geocode/geo def geocode_address(address, city, key): params {address: address, city: city, key: key} resp requests.get(GEOCODE_URL, paramsparams, timeout5) data resp.json() if data[status] 1 and data[geocodes]: location data[geocodes][0][location] lng, lat location.split(,) return float(lng), float(lat) return None def load_cache(cache_pathgeo_cache.json): if os.path.exists(cache_path): with open(cache_path, r, encodingutf-8) as f: return json.load(f) return {} def save_cache(cache, cache_pathgeo_cache.json): with open(cache_path, w, encodingutf-8) as f: json.dump(cache, f, ensure_asciiFalse, indent2)使用方式是把city | address作为缓存的key命中就直接读坐标没命中才调API并把结果写回缓存。注意缓存文件不要和CSV放在同一个目录避免Excel误打开JSON文件导致编码损坏。一个更稳妥的做法是SQLite但JSON在几十万量级以下完全够用物流网点很少有超过这个规模的。3.3 把大矩阵拆成100×100的请求块组合逻辑与去重拿到所有网点的坐标列表后就面临矩阵拆分问题。前面提到距离矩阵API单次最多100个起点、100个终点因此任意规模的矩阵都要按100切片做笛卡尔组合。假设网点数n250切片数是ceil(250/100)3分别命名为A0、A1、A2那请求组合就是A0×A0、A0×A1、A0×A2以此类推共9个请求返回9万对距离而实际需要的距离对是250×2506.25万说明这里存在多余的请求——因为A0×A1和A1×A0在对称矩阵里是重复的。有没有办法省掉一半请求当然有。如果用户只是要两两距离做聚类或覆盖分析只需要上三角矩阵也就是只发i j的切片组合。但如果你做的是运费结算起点和终点有方向性例如从仓库A到客户B和从客户B到仓库A可能因为单行线、限行产生不同距离那就要保留完整矩阵。我在工具里加了一个symmetric开关默认开生成请求时只保留i j的组合能省将近一半的API调用量。下面是拆请求的代码def build_distance_requests(coords, chunk_size100, symmetricTrue): chunks [coords[i:ichunk_size] for i in range(0, len(coords), chunk_size)] requests [] for ci, origin_chunk in enumerate(chunks): start ci if symmetric else 0 for cj, dest_chunk in enumerate(chunks[start:], startstart): requests.append({ origins: origin_chunk, destinations: dest_chunk, origin_offset: ci * chunk_size, dest_offset: cj * chunk_size, }) return requests拆出来的每个请求里都带origin_offset和dest_offset作用是后续把返回的距离填入大矩阵时能把小块坐标映射回全局坐标索引。这个偏移量如果不记录解析响应时会对不上号尤其是切片大小不是整除数的时候最后一个块长度不足100偏移量容易算错。4. 多线程并发调用线程池骨架、限流退避与参数取舍4.1 串行调用到底慢在哪先算一笔账再决定并发距离矩阵API单次请求最多返回1万对距离听着很高效但服务端计算量并不小尤其是type1驾车距离要在路网上做大量路径搜索。一个实际经验值单次100×100的请求响应时间通常在1到3秒之间网络波动时能到5秒以上。假设网点数2000按完整矩阵计算需要400次请求串行循环跑完大约需要400乘以2秒等于800秒也就是13分钟以上如果其中夹杂几次超时重试半小时很正常。多线程的价值就是把这400次请求分散到多个并发连接上。常见的做法是用ThreadPoolExecutor控制并发数而不是无脑开线程。高德对单个Key有QPS限制并发数不是越大越好。我一般建议先设8个线程跑小样本观察失败率和响应时间再往上调高德控制台配额页会明确写出当前Key的QPS上限理论上线程数可以等于QPS上限但我习惯留20%余量比如QPS限制10就开到8。4.2 基于ThreadPoolExecutor的并发代码最小可跑骨架下面的骨架是这类工具的核心循环它接收上一步生成的请求列表并发调用距离矩阵API并把结果合并到大矩阵中from concurrent.futures import ThreadPoolExecutor, as_completed import requests import time DISTANCE_URL https://restapi.amap.com/v3/distance def fetch_distance(origin_chunk, dest_chunk, key, type1): origins |.join(f{lng:.6f},{lat:.6f} for lng, lat in origin_chunk) destinations |.join(f{lng:.6f},{lat:.6f} for lng, lat in dest_chunk) params {key: key, origins: origins, destinations: destinations, type: type} resp requests.get(DISTANCE_URL, paramsparams, timeout15) data resp.json() if data[status] ! 1: raise RuntimeError(fAPI error: {data[infocode]} {data[info]}) return data[results] def run_concurrent(req_list, key, max_workers8): matrix {} with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {} for req in req_list: fut pool.submit( fetch_distance, req[origins], req[destinations], key ) future_map[fut] req for fut in as_completed(future_map): req future_map[fut] results fut.result() for item in results: o_idx req[origin_offset] item[origin_index] d_idx req[dest_offset] item[dest_index] if o_idx d_idx: matrix[(o_idx, d_idx)] 0.0 else: matrix[(o_idx, d_idx)] item[distance] return matrix代码逻辑分三步先把坐标切片格式化成高德要求的lng,lat|lng,lat字符串然后提交到线程池最后解析结果时用origin_offset和dest_offset把块内编号还原成全局编号。注意as_completed返回的future顺序和提交顺序无关一定要通过future_map找回对应的请求信息否则偏移量会串位。参数上timeout15是给网络请求留的余量1万对距离的响应超过15秒就放弃重试坐标格式化成6位小数这是高德坐标精度的下限格式化成{lng:.6f}能避免浮点数过长或科学计数法污染URL。4.3 限流与重试三个参数把失败率压下去多线程并发后的第一个异常往往是限流。现象是任务开始后前几十个请求正常突然出现大量status ! 1的响应伴随infocode为10044或10019等配额相关错误。高德的限流是按秒计算的当你开的线程数超过Key的QPS上限超出的请求会被直接拒绝。解决思路有三个参数可以调max_workers、重试间隔、重试上限。def fetch_distance_with_retry(origin_chunk, dest_chunk, key, retries3): for attempt in range(retries): try: return fetch_distance(origin_chunk, dest_chunk, key) except RuntimeError as e: if 10044 in str(e) or 10019 in str(e): wait 2 ** attempt time.sleep(wait) continue raise raise RuntimeError(quota exceeded after retries)重试间隔用指数退避第一次失败等2秒第二次等4秒第三次等8秒。这个策略的合理性在于限流通常是短时的等待几秒钟后配额窗口会释放。注意不要把retries设成10以上指数增长到16秒、32秒会让整个任务卡在尾部反而丧失并发优势。实测经验是并发8线程、重试3次在限流边缘能把失败率压到0.1%左右。5. 避坑现场五个让批量距离计算翻车的细节5.1 返回10001 INVALID_USER_KEYkey本身对服务没开现象请求返回infocode10001提示INVALID_USER_KEY但Key是刚从控制台复制的复制过程没有多余空格。原因大概率不是Key错了而是这个Key没有开通「Web服务API」对应的服务或者你复制的是「Web端(JS API)」的Key两种Key不通用。解决去控制台检查Key类型确认是Web服务然后在「API权限」里确认距离测量/路径规划服务对该Key可见重新生成一次Key再试。这个小问题在团队协作时特别常见A同学拿B同学临时给的Key调试但B的Key没开服务权限。5.2 type0的直线距离和物流里程差太远现象两个实际驾车里程约18公里的网点type0返回9.5公里差了近一半。原因type0算的是球面直线距离只考虑经纬度差完全不理会路网、河流、山体。物流运费结算或配送时效评估必须用type1只有做网点覆盖半径的初筛时type0才有参考价值。这个参数一旦写错整张距离矩阵全部作废而且因为数字是“合理”的不容易被发现。我的习惯是把type定义成常量DISTANCE_TYPE 1并在CSV导出文件名里标注type1杜绝误用。5.3 并发一上去就成片失败QPS配额与退避策略现象max_workers20时前10个请求正常第11个开始连续失败错误集中在10044和10019。原因单Key的QPS上限被瞬间打满多线程完全踩在限流阈值上。解决不要盲目加线程。先去控制台看QPS上限把max_workers设为上限的80%代码里必须带指数退避重试。这里有一个深层坑即使QPS上限是10ThreadPoolExecutor的线程数是10也不能保证10个请求不会同时落在一个秒级窗口内因为线程调度有延迟。所以并发数要留余量跑小样本观察失败率再放大是唯一靠谱的做法。5.4 CSV坐标在Excel里变成科学计数法现象CSV文件里lng116.481028用Excel打开后显示成1.16481E08再保存一次CSV坐标精度彻底丢失后续计算全部偏到另一个城市。原因Excel把纯数字列自动识别为科学计数法并且保存时截断精度。解决第一CSV里不要存纯坐标列改成lng列前加前缀例如lng_str列存116.481028并带上不可见的字符前缀这会影响后续解析更干净的办法是导出xlsx格式用openpyxl把坐标列的单元格格式设为文本from openpyxl import Workbook wb Workbook() ws wb.active ws.append([location_id, lng, lat]) ws[B1].number_format ws[C1].number_format 坐标都是经纬度字符串用文本格式写进xlsxExcel打开后不会变形。坐标的精度保留6位小数就够6位对应约0.1米的精度超过这个位数对物流距离没有意义反而增加字符串长度。5.5 矩阵对角线自带几公里误差现象用build_distance_requests生成对称矩阵后location_id相同的点自己到自己返回的距离不是0而是350米甚至更远。原因地理编码对同一地址两次调用的坐标可能有毫秒级抖动或者地址匹配到路网不同位置距离矩阵API对同一点做路径搜索也会因为起点吸附位置不同产生非零距离。解决不管API返回什么解析结果时一律把o_idx d_idx的距离强制置为0.0。代码里我在run_concurrent中已做了这个处理这是距离矩阵场景的通用约定。6. 结果验证与增量更新把工具调到生产可用的最后一步6.1 先拿三对已知距离当基准并发跑通后不要直接信整张矩阵。从网点列表里挑三个已知短、中、长距离的地对例如5公里以内的同城对、30公里的城郊对、100公里的跨城对拿手机导航App的推荐路线里程做对照。注意距离矩阵API是静态路网计算不走实时路况和导航显示的里程通常有5%到10%的偏差这可以接受但如果偏差超过20%基本就是type设错了或者坐标源有问题。这个基准对照应该写成一个独立函数每次修改代码后先跑这个小样本再跑全量比事后翻矩阵更省时间。基准脚本的核心很简单把三对已知坐标单独喂给fetch_distance打印结果和导航里程做对比。成本就是三次API调用但换来的是对整批计算可信度的确认。6.2 SQLite缓存让第二次跑几乎零成本物流网点的坐标一般每周甚至每月才变一次距离矩阵更是长期稳定。第二次跑相同CSV时如果重新调API纯属浪费配额。常见做法是在本地SQLite里建两张表places存地址和坐标distances存距离结果CREATE TABLE places ( loc_id TEXT PRIMARY KEY, loc_name TEXT, address TEXT, lng REAL, lat REAL, updated_at TEXT ); CREATE TABLE distances ( origin_id TEXT, dest_id TEXT, distance_m REAL, duration_s REAL, PRIMARY KEY (origin_id, dest_id) );每次跑任务前先查询CSV里的location_id在places里的坐标是否和之前一致坐标没变的网点对直接复用distances里的距离只有新增或坐标变化的地址才进API计算。实际操作中一次增量更新通常只需要补几个新网点的距离API调用量从几百次降到几十次。我自己的习惯是第一版跑通后不要急着扩并发而是把样本缩到20个网点把每一步的返回码都打印出来看一眼确认没有配额告警再放大规模。这套流程执行过两三次后面基本不会再踩限流的坑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?