3. 轨迹数据解析:GPX文件结构解析、Python解析GPX文件、提取轨迹点与时间戳、轨迹数据清洗
好,咱们进入实战环节。上一章我们把地图底图搞定了,现在该处理真正的轨迹数据了。你想想看,不管是跑步、骑行还是开车,记录下来的数据大多以GPX格式存在。这玩意儿长什么样?怎么把它读进Python里?读进来之后怎么清洗?这一章我带你一步步搞定。
3.1 GPX文件结构解析
GPX,全称GPS Exchange Format,说白了就是一种基于XML的文本格式。我最早接触它的时候,还以为是某种加密格式,打开一看——嗯,就是纯文本,结构非常清晰。
一个标准的GPX文件长这样:
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Garmin">
<trk>
<name>下午骑行</name>
<trkseg>
<trkpt lat="39.9042" lon="116.4074">
<ele>52.3</ele>
<time>2024-01-15T14:30:00Z</time>
</trkpt>
<trkpt lat="39.9045" lon="116.4078">
<ele>52.5</ele>
<time>2024-01-15T14:30:05Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
核心结构就三层:
- gpx:根节点,包含版本号和创建者信息
- trk:一条轨迹,可以包含名称、描述等元数据
- trkseg:轨迹段,里面是一个个轨迹点
- trkpt:单个轨迹点,包含经纬度、海拔、时间戳
我在项目中遇到过一个问题:有些设备导出的GPX文件,<ele>和<time>标签可能缺失。嗯,这很正常,不是所有GPS芯片都记录海拔。所以解析的时候一定要做容错处理。
关键点:GPX的核心是<trkpt>节点,它的lat和lon属性是必须的,其他都是可选。
3.2 Python解析GPX文件
Python解析GPX,我推荐两种方式。第一种是用现成的库gpxpy,省心省力。第二种是自己手写XML解析,适合定制化需求。
方式一:使用gpxpy库
我个人习惯用这个库,因为它把轨迹段、点的提取都封装好了。安装很简单:
pip install gpxpy
解析代码:
import gpxpy
with open('track.gpx', 'r', encoding='utf-8') as f:
gpx = gpxpy.parse(f)
for track in gpx.tracks:
print(f"轨迹名称: {track.name}")
for segment in track.segments:
for point in segment.points:
print(f"纬度: {point.latitude}, 经度: {point.longitude}")
print(f"海拔: {point.elevation}, 时间: {point.time}")
你看,几行代码就把所有轨迹点读出来了。不过要注意,gpxpy返回的时间是datetime对象,可以直接用。
方式二:手写XML解析
有时候你需要更精细的控制,比如只提取某些字段。这时候用xml.etree.ElementTree更灵活:
import xml.etree.ElementTree as ET
tree = ET.parse('track.gpx')
root = tree.getroot()
# GPX的命名空间
ns = {'gpx': 'http://www.topografix.com/GPX/1/1'}
points = []
for trkpt in root.findall('.//gpx:trkpt', ns):
lat = float(trkpt.get('lat'))
lon = float(trkpt.get('lon'))
ele = trkpt.find('gpx:ele', ns)
time = trkpt.find('gpx:time', ns)
point = {
'lat': lat,
'lon': lon,
'ele': float(ele.text) if ele is not None else None,
'time': time.text if time is not None else None
}
points.append(point)
小技巧:手写解析时,记得处理命名空间。GPX 1.1的命名空间是http://www.topografix.com/GPX/1/1,别写错了。
3.3 提取轨迹点与时间戳
提取轨迹点不难,但时间戳的处理是个坑。我曾经遇到过一个设备,它记录的时间是本地时间,没有带时区信息。结果对齐到地图上,轨迹全偏了。
正确的做法是:
- 统一将时间转换为UTC
- 如果原始数据没有时区,默认当作UTC处理
- 存储时使用
datetime对象,方便后续计算
示例代码:
from datetime import datetime, timezone
def parse_time(time_str):
"""解析GPX时间字符串,统一转为UTC"""
if time_str is None:
return None
try:
# 尝试解析带时区的时间
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
return dt.astimezone(timezone.utc)
except:
# 如果没有时区,当作UTC处理
return datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
注意:时间戳是轨迹对齐的关键。如果时间不准,后面的地图匹配、速度计算全都会出错。我建议你在解析完成后,先检查一下时间序列是否单调递增。
3.4 轨迹数据清洗
数据读进来了,但别急着用。真实世界的GPS数据,脏得很。我总结了几种常见问题:
| 问题类型 | 表现 | 处理方法 |
|---|---|---|
| 重复点 | 连续两个点经纬度完全一样 | 删除重复点,只保留第一个 |
| 漂移点 | 某个点突然偏离很远 | 计算相邻点距离,超过阈值则剔除 |
| 时间跳跃 | 两个点的时间间隔异常大 | 检查时间差,超过阈值则分段处理 |
| 海拔异常 | 海拔值突然变化几百米 | 使用滑动窗口平滑或剔除 |
清洗代码示例:
import math
def clean_track(points, max_speed=50.0):
"""
清洗轨迹点
:param points: 轨迹点列表,每个点包含lat, lon, time
:param max_speed: 最大允许速度(米/秒)
"""
cleaned = []
prev = None
for point in points:
if prev is None:
cleaned.append(point)
prev = point
continue
# 计算距离(简化版,实际用haversine公式)
dist = haversine(prev['lat'], prev['lon'], point['lat'], point['lon'])
time_diff = (point['time'] - prev['time']).total_seconds()
if time_diff > 0:
speed = dist / time_diff
if speed > max_speed:
# 漂移点,跳过
continue
# 检查重复点
if dist < 0.5: # 小于0.5米视为重复
continue
cleaned.append(point)
prev = point
return cleaned
避坑指南:我曾经处理过一份骑行数据,里面有个点经纬度完全正确,但时间戳比前后点晚了整整一天。这种点用速度过滤是查不出来的,必须检查时间序列的连续性。我的做法是:计算每个点与前后点的时间差,如果某个点的时间差同时大于前后两个时间差,就标记为异常。
3.5 本章知识体系
下面这张图帮你梳理本章的核心逻辑:
嗯,这一章的内容就这些。GPX解析看起来简单,但实际项目中坑不少。我建议你拿到数据后,先打印几个点看看结构,再决定用哪种解析方式。清洗这一步千万别省,否则后面地图匹配的时候,你会被漂移点折磨到怀疑人生。