1. 项目背景与需求解析
在地图应用开发过程中,获取特定类型的城市数据是常见需求。最近我在开发一个出行规划类应用时,需要获取全国已开通地铁的城市列表。这个看似简单的需求,在实际操作中却遇到了不少坑。高德地图作为国内主流地图服务商,其API文档中并没有直接提供"获取开通地铁城市列表"的接口,这就需要我们通过技术手段来实现。
这个需求的核心价值在于:
- 为出行类应用提供城市筛选依据
- 辅助用户判断目的地城市的公共交通发达程度
- 作为数据分析的基础维度(如对比不同城市的地铁发展状况)
2. 技术方案选型
2.1 直接API调用方案分析
首先我查阅了高德地图的官方文档,发现以下几个相关接口:
- 城市搜索接口
- POI搜索接口
- 公交线路查询接口
经过测试,这些接口都无法直接返回"开通地铁的城市列表"。最接近的是公交线路查询接口,但需要已知城市编码才能查询,形成了先有鸡还是先有蛋的问题。
2.2 间接获取方案设计
既然没有直接接口,就需要设计间接获取的方案。经过多次尝试,我总结出以下可行路径:
- 获取全国城市列表:通过高德的"城市编码表"接口获取
- 批量查询地铁线路:对每个城市调用公交线路查询接口
- 结果过滤:根据返回结果判断城市是否开通地铁
这个方案虽然可行,但存在明显的性能问题:需要对全国300多个地级市逐个发起查询请求。
2.3 优化方案实现
为了提高效率,我做了以下优化:
- 多线程并发请求:使用线程池控制并发数
- 缓存机制:将结果缓存到本地,避免重复查询
- 智能重试:对失败请求实现指数退避重试
核心代码结构如下:
python复制import requests
from concurrent.futures import ThreadPoolExecutor
def check_subway(city_code):
url = f"https://restapi.amap.com/v3/bus/line?key=您的key&city={city_code}"
try:
resp = requests.get(url).json()
return bool(resp.get('buslines', []))
except Exception as e:
print(f"查询{city_code}失败: {str(e)}")
return False
def get_all_cities():
# 获取全国城市列表的实现
pass
def main():
cities = get_all_cities()
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(check_subway, cities))
subway_cities = [city for city, has_subway in zip(cities, results) if has_subway]
print("开通地铁的城市:", subway_cities)
3. 关键技术实现细节
3.1 城市编码获取
高德地图使用一套自己的城市编码体系。获取完整城市列表的几种方式:
- 从高德官方文档下载城市编码表(静态文件)
- 调用行政区域查询接口(需要企业级权限)
- 使用第三方维护的城市编码库
我选择了第一种方式,因为:
- 不需要高级权限
- 数据更新频率满足需求(通常半年更新一次)
- 离线可用,不依赖网络请求
3.2 地铁线路识别逻辑
判断一个城市是否有地铁,需要考虑以下特征:
- 公交线路类型字段(buslines.type)
- 线路名称包含"地铁"字样
- 线路站点数量(地铁通常站点较多)
实际处理中发现一些特殊情况:
- 部分城市将地铁归类为"轨道交通"
- 新建线路可能还未同步到高德数据
- 个别城市使用特殊命名(如"城轨")
最终采用的判断逻辑:
python复制def is_subway_line(line_info):
name = line_info.get('name', '')
line_type = line_info.get('type', '')
return ('地铁' in name
or '轨道交通' in name
or line_type == '地铁')
3.3 性能优化实践
在实测中,完整扫描全国城市需要处理以下性能瓶颈:
-
API调用限制:高德免费版有3000次/日的调用限制
- 解决方案:分批执行 + 缓存结果
-
网络延迟:单个请求响应时间约200-500ms
- 解决方案:并发请求(控制在10个线程内)
-
数据解析开销:JSON解析消耗CPU资源
- 解决方案:使用orjson替代标准json库
优化后的性能对比:
| 方案 | 耗时 | 请求次数 |
|---|---|---|
| 单线程 | 约5分钟 | 300+ |
| 多线程(10) | 约30秒 | 300+ |
| 带缓存 | 首次30秒,后续<1秒 | 仅未缓存城市 |
4. 完整实现代码
以下是经过生产验证的完整实现:
python复制import orjson
import requests
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import time
class SubwayCityFetcher:
def __init__(self, api_key):
self.api_key = api_key
self.cache_file = Path('subway_cities.json')
self.cache = self._load_cache()
def _load_cache(self):
if self.cache_file.exists():
with open(self.cache_file, 'rb') as f:
return orjson.loads(f.read())
return {}
def _save_cache(self):
with open(self.cache_file, 'wb') as f:
f.write(orjson.dumps(self.cache))
def get_all_cities(self):
"""从本地文件加载城市列表"""
with open('amap_cities.json') as f:
return orjson.loads(f.read())
def check_subway(self, city):
if city['adcode'] in self.cache:
return self.cache[city['adcode']]
url = f"https://restapi.amap.com/v3/bus/line?key={self.api_key}&city={city['adcode']}"
try:
resp = requests.get(url, timeout=3).json()
has_subway = any(self._is_subway_line(line) for line in resp.get('buslines', []))
self.cache[city['adcode']] = has_subway
return has_subway
except Exception as e:
print(f"查询{city['name']}失败: {str(e)}")
return False
def _is_subway_line(self, line):
name = line.get('name', '').lower()
return ('地铁' in name
or '轨道交通' in name
or line.get('type') == '地铁')
def fetch(self, max_workers=5):
cities = self.get_all_cities()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(self.check_subway, cities))
subway_cities = [
city for city, has_subway in zip(cities, results)
if has_subway
]
self._save_cache()
return subway_cities
5. 常见问题与解决方案
5.1 数据不全问题
现象:某些已知有地铁的城市未被识别
原因排查:
- 城市编码表未更新
- 地铁线路命名不规范
- API数据同步延迟
解决方案:
- 手动维护已知城市白名单
- 增加模糊匹配逻辑
- 定期全量更新缓存
5.2 API限流处理
现象:请求返回"访问已超出每日限额"
应对策略:
- 错峰执行(凌晨执行批量任务)
- 申请更高的QPS配额
- 实现漏桶算法控制请求速率
优化后的请求控制代码:
python复制from ratelimit import limits, sleep_and_retry
class RateLimitedFetcher:
def __init__(self, api_key):
self.api_key = api_key
@sleep_and_retry
@limits(calls=30, period=1) # 控制在30QPS
def make_request(self, url):
return requests.get(url)
5.3 城市边界情况处理
实际运行中遇到的特殊案例:
- 直辖市:北京、上海等需要特殊处理行政区
- 跨城地铁:如广佛地铁涉及两个城市
- 在建线路:部分城市显示有地铁但实际未开通
针对这些情况,我增加了额外的校验规则:
python复制def is_real_subway_city(city):
# 排除仅显示规划线路的城市
if city.get('build_status') == 'planning':
return False
# 处理特殊城市
if city['name'] in ['北京', '上海', '广州', '深圳']:
return True
# 其他通用校验
return has_operating_lines(city)
6. 扩展应用场景
获取到地铁城市列表后,可以进一步开发以下功能:
-
城市地铁发展对比看板:
- 按开通年份统计
- 按线路数量排序
- 按运营里程排名
-
出行规划增强:
python复制def recommend_transport(city): if city in subway_cities: return "推荐乘坐地铁" elif city in bus_cities: return "建议公交出行" else: return "建议打车或自驾" -
商业选址辅助:
- 筛选有地铁的商圈
- 分析地铁站点周边POI分布
- 计算地铁可达性指标
实际项目中,我将这些数据与人口数据、GDP数据结合,构建了城市交通发展指数模型,为商业决策提供了有力支持。