ARTICLE DETAIL

资讯详情

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

基于高德API的城市交通拥堵指数历史库构建:数据采集、存储与回补实践

基于高德API的城市交通拥堵指数历史库构建:数据采集、存储与回补实践 简介本资源是一个面向城市交通数据分析、智慧城市研究及GIS开发者的高德地图交通拥堵数据采集与存储项目聚焦解决全国主要城市长期交通拥堵趋势分析缺乏高质量历史数据支撑的问题。项目基于高德地图官方API实现从2019年10月8日起至今的实时与历史拥堵指数自动化采集并完成结构化持久化存储适用于交通规划建模、时序趋势分析、可视化大屏开发等场景。压缩包共10个文件42KB含4个核心Java类含数据拉取、解析与入库逻辑、1个SQL建表脚本定义拥堵指数历史表结构、1个XML配置文件Spring Boot集成配置、1个README.md说明文档及1个docx附赠资源指南整体轻量但功能完整。目前已有44人学习下载读者可直接复用采集框架、快速部署本地数据库、调用现成SQL建模或基于已验证的容错机制与数据清洗逻辑拓展多源交通数据融合能力。 做城市交通分析这行久了你迟早会卡在同一件事上数据不够长。临时要分析一个政策对早高峰拥堵的影响手边只有最近一个月的指数根本说不清趋势想复盘国庆假期前后的拥堵变化又没有历史同期数据可以对比。这个项目要解决的就是把高德地图的交通拥堵指数通过开放API自动抓下来按城市、按时间整理好持久化存进数据库从2019年10月8日这个统一基期开始持续滚动积累最终构建一套全国主要城市的交通拥堵指数历史库。项目本身不复杂但踩坑不少。尤其是“历史数据怎么补”“实时接口怎么算城市指数”“个人版Key够不够用”这几个问题几乎每个做数据采集的人都躲不掉。这篇就把整个项目的设计思路、数据口径、表结构、采集代码、回补方案以及我在线上跑了大半年后遇到的问题全部拆开讲一遍。不管你是刚开始接触高德API还是已经在做交通数据采集这里面的细节都值得参考。1. 项目整体设计与数据口径解析1.1 为什么我要做这个采集项目当时团队需要一个“能追溯到2019年10月8日”的交通指数底库。这个时间点不是随便拍的而是业务方确定的统计基期后续所有同比、环比分析都要以这个日期为锚点。问题是数据不会自己从天上掉下来。市面上能买到的历史交通数据要么粒度太粗要么覆盖城市不全价格还不低。自己采是当时最可控的路径。选高德API的原因有三个。第一高德的开放平台相对成熟交通态势类接口文档清晰返回结构稳定个人开发者就能申请。第二高德有一套完整的城市adcode编码体系城市之间的数据可用同样的口径对齐后续做横向对比很方便。第三高德本身有城市交通分析报告可以作为历史数据回补的官方参考源这对接下来的存量回补很关键。当时的初步方案很直接写一个采集进程定时调用高德交通态势API按城市划分请求区域把返回的道路拥堵等级、速度、道路长度等信息拿回来在本地聚合成城市级拥堵指数再写入MySQL。整个过程用Python实现调度用APScheduler数据库就是最普通的MySQL 8.0。单看架构和大部分API数据采集项目没有本质区别真正的复杂度在后面的数据口径和限流细节上。1.2 “拥堵指数”的数据口径到底怎么定先说个容易误解的地方高德开放API不会直接给你一个“北京市此刻拥堵指数5.2”这种现成数值。它给的是道路级的实时路况比如某个路段当前是畅通、缓行、拥堵还是严重拥堵附带这段路的长度、车速、通行时间等字段。城市级拥堵指数需要你自己聚合。所以做这个项目第一件事就是定口径。我采用的是“拥堵里程占比加权”的思路具体规则如下把一次请求返回的所有道路按拥堵状态分组status1畅通权重按0处理status2缓行权重0.3status3拥堵权重0.6status4严重拥堵权重1.0指数计算公式index_value (缓行里程 * 0.3 拥堵里程 * 0.6 严重拥堵里程 * 1.0) / 总里程 * 10这个公式把结果映射到0到10之间数值越高代表越堵。实际算出来之后可以再根据城市不同做归一化修正但第一版尽量保持简单后面再用同一套口径回算历史数据保证前后一致。有一点必须提醒口径一旦定下来就不要频繁改。如果你今天用“里程加权”明天又换成“道路条数加权”那整个历史库的时间序列就是断裂的后面分析什么都白搭。我在项目里专门用一个配置表存口径版本每次调整都记录生效时间就是为了防止这个问题。1.3 完整数据链路与项目目录设计数据从接口到数据库总共经过四层采集层、清洗层、存储层、监控层。采集层负责按城市和区域发起请求清洗层负责解析JSON、过滤异常数据、计算指数存储层负责去重、入库监控层负责失败重试、报警、任务补偿。项目目录结构我保持了比较轻量的风格没有上大数据框架单机定时任务完全够用traffic-index-collector/ ├── config/ │ ├── cities.json # 城市adcode和边界坐标配置 │ └── settings.py # API Key、数据库连接等配置 ├── collector/ │ ├── amap_client.py # 高德API封装 │ ├── index_calculator.py # 拥堵指数计算逻辑 │ └── city_task.py # 单城市采集任务编排 ├── storage/ │ ├── db.py # 数据库连接池 │ └── traffic_repo.py # 数据写入和去重 ├── scheduler/ │ └── main_scheduler.py # APScheduler任务调度 ├── scripts/ │ └── backfill_historical.py # 历史数据回补脚本 └── logs/ └── collector.logcities.json是最关键的配置文件之一。里面是每个城市的adcode和覆盖建成区的矩形边界。高德交通态势接口支持按矩形范围查询所以一个城市通常需要拆成多个矩形。以北京为例我拆了四个矩形才能覆盖整个五环内区域每块区域一次请求四个请求的结果合并再计算全市指数。2. 核心数据模型与存储设计2.1 数据库选型我的取舍一开始团队有人提议直接用ClickHouse理由是时间序列数据量大分析查询快。但我的想法是先别上太重的组件这个项目的数据量没有想象中那么大MySQL完全扛得住而且团队里大部分人都会写SQL维护成本低。简单算一笔账采集频率5分钟一次一天每个城市288个时间点。100个城市一天下来是28800条记录。一年是1051万条左右。这个量级在MySQL里做好索引和分区查询毫秒级返回完全没问题。只有当你把采集粒度压到1分钟并且覆盖城市超过500个年数据量过亿才需要考虑时序数据库。如果后面真的有高并发查询需求我会选择把历史数据归档到ClickHouseMySQL保留最近一年的热数据。项目上线初期单一MySQL实例最稳妥也最容易排查问题。2.2 表结构设计与DDL数据模型一共三张核心表都很简单第一张是城市信息表city_info存城市的基础元数据包括adcode、城市名、矩形边界、是否启用采集。第二张是交通指数事实表traffic_index这是整个项目的核心每行代表某城市某时刻的拥堵指数。第三张是采集日志表collect_log记录每次任务的执行情况包括本次采集是否成功、耗时、异常信息方便事后排查。city_info的建表语句CREATE TABLE city_info ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, city_name VARCHAR(50) NOT NULL COMMENT 城市名称, adcode VARCHAR(10) NOT NULL COMMENT 高德adcode, city_bounds VARCHAR(1024) NOT NULL COMMENT 矩形边界,多个用分号分隔, is_active TINYINT NOT NULL DEFAULT 1 COMMENT 是否启用采集, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_adcode (adcode) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;traffic_index是数据量最大的表设计时重点考虑写入去重和查询效率。去重逻辑用唯一键city_id collect_time实现同一城市同一时刻重复写入时直接覆盖避免采集任务重试产生重复数据。建表语句CREATE TABLE traffic_index ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, city_id INT UNSIGNED NOT NULL COMMENT 城市id,关联city_info.id, collect_time DATETIME NOT NULL COMMENT 采集时间,精确到分钟, index_value DECIMAL(4,2) NOT NULL COMMENT 拥堵指数, congestion_status TINYINT NOT NULL COMMENT 整体拥堵状态:1畅通2缓行3拥堵4严重拥堵, source VARCHAR(20) NOT NULL DEFAULT amap COMMENT 数据来源, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_city_time (city_id, collect_time), KEY idx_time (collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;collect_log逻辑上独立单独存储避免频繁写入影响业务表性能CREATE TABLE collect_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, task_name VARCHAR(100) NOT NULL COMMENT 任务名称, city_id INT UNSIGNED DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0失败1成功, error_msg VARCHAR(1000) DEFAULT NULL, request_count INT DEFAULT 0 COMMENT 本次任务请求次数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 数据量与分区策略估算前面算过100个城市5分钟粒度一年数据量约1051万条。单表千万级对MySQL来说不算多但如果查询经常按月份过滤最好还是做分区。traffic_index表我按采集时间做了RANGE分区这样删除历史数据直接drop分区查询特定时段自动走分区裁剪效率高很多。分区示例ALTER TABLE traffic_index PARTITION BY RANGE (TO_DAYS(collect_time)) ( PARTITION p2019_10 VALUES LESS THAN (TO_DAYS(2019-11-01)), PARTITION p2019_11 VALUES LESS THAN (TO_DAYS(2019-12-01)), PARTITION p2019_12 VALUES LESS THAN (TO_DAYS(2020-01-01)) );实际操作中我不会手工写每一年的分区而是写一个存储过程每月自动检查下月分区是否存在不存在就创建。存储空间方面一条记录大概200字节左右含索引开销100城市一年约1051万条合计约2GB加上索引和冗余三年历史数据量也就6到8GB。这个体量放在普通ECS数据盘上没有任何压力备份恢复也很快。3. 自动化采集模块的实现3.1 高德API调用方式与限流策略高德交通态势接口的调用方式很标准HTTP GET请求参数包括key、rectangle矩形坐标、level道路等级、extensions是否返回详细字段。请求URL形如https://restapi.amap.com/v3/traffic/status/rectangle关键参数key在高德开放平台申请的API Keyrectangle左下角和右上角坐标格式为左下x,左下y;右上x,右上ylevel道路等级1到66为全部道路extensionsbase或allall会返回更详细的道路信息这里有个隐藏问题个人开发者Key的QPS限制很低默认可能只有每秒几次。而100个城市每个城市拆4个矩形一轮就是400次请求。如果5分钟跑一轮平均QPS只需要1.33看起来很轻松但请求往往是集中在一个时间点发出去的瞬间QPS就冲到几十直接被限流返回403或429。我的解决办法是加了一个简单的限流队列import time import threading class RateLimiter: def __init__(self, max_qps1.0): self.min_interval 1.0 / max_qps self.lock threading.Lock() self.last_request_time 0.0 def wait(self): with self.lock: now time.time() wait_time self.min_interval - (now - self.last_request_time) if wait_time 0: time.sleep(wait_time) self.last_request_time time.time()同时配置了多个Key轮流使用每个Key的QPS限制独立计算等于把可用配额翻倍。3.2 定时调度与任务编排调度用的是APScheduler的BlockingScheduler每5分钟触发一次全量采集任务。选择5分钟而不是1分钟是因为高德路况数据本身有较长时间的缓存更新周期1分钟的采集频率并不会带来更高的数据精度反而白白消耗API配额。5分钟已经能覆盖早晚高峰的变化趋势。调度主逻辑from apscheduler.schedulers.blocking import BlockingScheduler scheduler BlockingScheduler() scheduler.scheduled_job(cron, minute*/5, idcollect_all_cities) def collect_all_cities(): for city in active_cities: try: run_city_task(city) except Exception as e: logger.error(fcity {city[adcode]} collect failed: {e}) notify_alert(city, e)任务编排上有个细节每个城市任务之间不要串行等待。我当时把每个城市任务放进了线程池用ThreadPoolExecutor控制并发数为5整体一轮采集从10分钟压缩到3分钟左右。但并发不要开太大毕竟API限流是硬约束并发再高也会被限流挡回来。3.3 清洗入库与去重逻辑数据清洗是高德采集里容易翻车的一环因为返回的JSON结构有嵌套不同城市返回的道路数量差异很大。北京一次矩形查询可能返回几百条道路而小城市可能只有几十条。清洗流程解析JSON确认status字段是否为1不是则记录错误并返回。提取roads数组过滤掉speed或length为0的异常记录。按拥堵等级累计里程计算指数。过滤非法指数值如负数或大于10直接丢弃。批量写入数据库用INSERT ... ON DUPLICATE KEY UPDATE保证去重。核心入库代码def batch_upsert(cursor, rows): sql INSERT INTO traffic_index (city_id, collect_time, index_value, congestion_status, source) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE index_value VALUES(index_value), congestion_status VALUES(congestion_status) cursor.executemany(sql, rows)这一步去重逻辑非常关键。因为网络超时后任务会重试如果没有唯一键和UPSERT机制同一时刻的数据可能被写两次后续时间序列分析会出现“同一个点出现两个值”的脏数据。3.4 从2019年10月8日起的历史数据回补方案这是项目里最需要说清楚的地方。很多人看到“自2019年10月8日起至今”的标题以为调个API就能把三年前的数据全部拉回来这是不现实的。高德交通态势API是实时接口只能返回当前路况不提供历史上的某个时刻数据。要做历史回补必须换思路。我实际采用的方案是把历史数据分成两段处理第一段是2019年10月8日到项目启动日的存量数据。这部分我有两个来源一是高德官方发布的月度城市交通分析报告PDF或者网页表格把里面的城市拥堵指数手工或半自动整理成CSV再导入库中。二是从合规的数据服务商购买历史指数。前者覆盖城市有限但免费适合先把核心城市补齐后者覆盖面广但要花钱适合补齐全部城市。第二段是项目启动日之后的数据这就是本系统滚动采集的增量数据每天自动累积不需要额外处理。回补脚本的核心逻辑很简单读取CSV按城市和日期写入traffic_index表。但要注意回补的指数口径必须和实时采集保持一致。高德报告里的指数通常基于全天或者高峰时段而实时采集是每5分钟一个点。如果直接把报告的“高峰拥堵指数”写进5分钟粒度表里会出现口径错配。我的做法是回补数据单独存放设置source字段为report查询时和amap数据区分使用。如果你有精力做更细的粒度可以参考我当时的一个变通方法对存量时期用报告中的早晚高峰指数配合当天交通态势的多个采样点做插值还原出近似曲线。但这个方法误差较大只适合做趋势分析不适合做精确计算。4. 常见问题与排查实录4.1 API Key配额与鉴权问题排查项目上线第三天采集突然大面积失败。看日志错误信息是“USER_DAILY_QUERY_COUNT_EXCEED”也就是当日调用量超限。个人开发者Key默认每日调用量是有限额的100城市×4矩形×144次/天57600次请求很容易把配额打满。排查思路很简单先降低采集频率从5分钟改成10分钟再把两个城市的矩形合并减少请求次数最后申请了多个Key轮流使用总容量一下翻了4倍。高德开放平台的后台可以看到每个Key的用量曲线提前发现配额耗尽前就应该切Key。4.2 adcode映射与城市边界问题城市adcode映射是个细节活。高德的adcode是行政区划编码不是普通的城市拼音或车牌号。比如北京市的adcode是110000朝阳区是110105。如果你采集的是“北京市”整体数据要用110000。很多教程里拿的是区域中心的经纬度再根据经纬度反查adcode这样容易取到区级编码。城市边界问题同样值得注意。一个城市的建成区可能横跨多个矩形如果矩形只覆盖了市中心那么算出来的城市指数会系统性偏高因为郊区畅通道路没有进入计算范围。我在cities.json里手动核验了每个城市的边界坐标确保覆盖了主要城区。4.3 数据库写入性能与连接池配置最开始数据库连接是每次写入前新建连接写完后关闭。任务高峰时数据库连接频繁创建销毁导致CPU飙升写入超时。后来换成了DBUtils的PooledDB连接池连接数配置为5到10个写入性能稳定了很多。另一个隐性坑是pymysql的execute和executemany性能差异。同样1000条数据单条execute循环需要几秒executemany批量写只需要几百毫秒。采集任务每一轮都有几万条数据要写批量提交几乎是必须的。4.4 网络抖动、请求超时与任务积压处理5分钟一个周期的调度网络一抖动就可能出乱子。当时遇到的情况是早高峰8点正好是数据最宝贵的时段结果网络波动导致这轮采集拖了20分钟才跑完下一个周期的任务已经触发两个任务重叠执行进一步加重了API限流。解决办法是给调度任务加一个执行锁确保同一时间只有一个全量采集任务在运行。如果上一轮没跑完下一轮自动跳过不叠加。同时每次HTTP请求都设置了10秒超时和3次重试重试之间间隔2秒。这样网络抖动时任务宁可延迟完成也不会堆积成灾。4.5 数据质量自检与异常指数修正写库只是第一步数据质量校验才能保证分析结果可靠。我在每天凌晨加了一个自检任务检查前一天的数据完整率。规则是每个城市每天应该有288条记录如果某城市某天少于200条直接告警。另一类问题是异常值。比如某城市某时刻指数突然从2.0跳到9.8大概率是接口返回异常。我加了一个简单的波动阈值检查如果相邻两个采集点的指数差值超过4就标记该点异常由人工确认是否修正。实际跑下来一年大约能捕获到三四次这类异常全部发生在接口数据结构调整的过渡期。5. 上线后的收益复盘与扩展方向5.1 这个历史库能做什么项目跑通半年后这个库就成了团队的公共基础设施。最直接的应用是日报和周报每天早高峰结束后自动跑一遍SQL输出各主要城市早高峰拥堵指数排名对比前一天和上月的同期数据。以前这个报表要靠手工到处找数据现在一条SQL就出结果。更重要的应用是做政策评估。比如某城市实行尾号限行新规想量化限行前后早高峰拥堵变化直接按城市和时间段拉出数据就能看到变化曲线。没有这套历史库之前这类分析根本无法落地。5.2 后续可以怎么扩展这个项目目前只有高德单一数据源我计划后面接入百度和腾讯的交通指数作为交叉校验。单一地图商的数据有系统性偏差多源数据联合使用能提高指数可信度。另外现有的存储是MySQL等数据积累到一定程度可以导出一份到ClickHouse或者DuckDB用列存引擎跑更复杂的分析。DuckDB特别适合单机分析场景直接把CSV或Parquet文件按年份归档分析时再加载成本极低。如果你也要做类似项目我的建议是先单城市跑通再扩展到全国。别一开始就上100个城市否则限流、边界、数据校验这些问题会同时涌过来根本排查不过来。先用一个城市把链路走通积累几天数据确认无误后再横向扩容。这是我踩了最多坑之后最想说的经验。本文还有配套的精品资源点击获取
返回列表