3. AIS原始报文解析与航迹字段提取
AIS航迹数据的源头是一串串以“!”或“$”开头的ASCII文本,即NMEA 0183语句。无论后续做轨迹可视化、船舶行为分析还是碰撞风险识别,第一步都必须把这些“看起来像乱码”的报文还原为可计算的经纬度、航速、航向和时间戳。本节围绕语句结构、报文类型解码、字段含义与校验四个层面展开,目标是让读者能够独立完成从原始报文到航迹字段的提取。
NMEA 0183语句结构:先拆封装,再谈内容
AIS常用的封装语句有四种:!AIVDM(其他船发来的AIS数据)、!AIVDO(本船自身数据)、!AIVDM的多片段形式以及基站转发形式。一条典型语句如下:
!AIVDM,1,1,,A,15Muq?P0000G?t@E>4p@0?vN0<0u,0*5C
按逗号切分后各字段含义为:语句标识AIVDM;片段总数1;当前片段序号1;多片段消息ID(此处为空);AIS信道A;有效载荷15Muq?P...;填充位数0;校验和5C。其中有效载荷是6位ASCII编码的二进制串,填充位数表示末尾补了几个无效比特,校验和是“$”与“*”之间所有字符的异或值。解析前必须先做校验和验证,再按填充位截断,否则后续位偏移会整体错位。
报文类型1/2/3/5/18/19/24的解码方法
AIS报文类型由有效载荷的前6位(bit 0–5)给出。类型1/2/3为A类船舶的动态信息,结构相同,仅消息类型值不同;类型5为A类船舶的静态与航次信息;类型18为B类船舶动态信息;类型19为B类船舶静态与动态合并信息;类型24为B类船舶静态信息(分A、B两部分)。解码的核心是“按位取值”:把6位ASCII载荷还原为二进制串后,依据各类型的位定义表逐字段截取。
以类型1为例,关键字段的位偏移为:消息类型bit 0–5;MMSI bit 8–37;航行状态bit 38–41;转向率bit 42–49;对地航速bit 50–59;位置精度bit 60;经度bit 61–88;纬度bit 89–115;对地航向bit 116–127;时间戳bit 128–136。经度和纬度均为1/10000分的有符号整数,需除以600000转换为度;航速单位为0.1节,除以10;航向单位为0.1度,除以10。类型18的字段顺序与类型1不同,例如航速位于bit 46–55,经度位于bit 57–84,纬度位于bit 85–112,解码时必须使用对应类型的位表,不能混用。
类型5的载荷较长(通常为2个片段),包含船名、呼号、船型、尺寸、目的地和ETA等静态字段。船名位于bit 112–231,共20个6位字符,需按AIS 6位字符表映射为可读文本;尺寸字段分为船首到参考点、船尾到参考点、左舷和右舷四个值,单位均为米。类型24分为Part A(船名)和Part B(船型、尺寸、呼号),解析时需根据Part Number字段区分,并将同一MMSI的两部分合并。
动态信息与静态信息字段含义
动态信息描述船舶“此刻在哪里、怎么走”,更新频率高(A类船在航行时约2–10秒一次),是航迹重建的主体。核心字段包括:MMSI(海上移动通信业务标识,9位数字,前三位为MID国家码)、经度、纬度、对地航速(SOG)、对地航向(COG)、真航向(HDG)、航行状态(如“在航”“锚泊”“受限操纵”)、转向率(ROT)和时间戳。需要特别注意“不可用”约定:经度181°、纬度91°、航速1023、航向3600、时间戳60均表示该字段无效,解析后应置为空值而非直接参与计算。
静态信息描述船舶“是谁、长什么样”,更新频率低(通常6分钟一次),包括船名、呼号、IMO编号、船型、尺寸、目的地和ETA。静态字段用于给航迹点“贴标签”,例如按船型筛选货船与渔船,或按尺寸估算船舶占位。类型19将静态与动态合并在一条报文中,适合B类船舶;类型24则把静态信息拆成两部分,解析时需按MMSI缓存合并,否则会出现“有船名无尺寸”的残缺记录。
解析实战与字段校验
下面给出一段Python示例,演示从单条类型1报文到航迹字段的完整提取过程(示例参数,非实测):
import binascii
def sixbit_to_bin(payload):
bits = ""
for ch in payload:
v = ord(ch) - 48
if v > 40:
v -= 8
bits += format(v, "06b")
return bits
def decode_type1(payload, fill):
bits = sixbit_to_bin(payload)[:len(sixbit_to_bin(payload)) - fill]
mmsi = int(bits[8:38], 2)
sog = int(bits[50:60], 2) / 10.0
lon = int(bits[61:89], 2)
lat = int(bits[89:116], 2)
if lon & (1 << 27): lon -= (1 << 28)
if lat & (1 << 26): lat -= (1 << 27)
lon /= 600000.0
lat /= 600000.0
cog = int(bits[116:128], 2) / 10.0
ts = int(bits[128:137], 2)
return {"mmsi": mmsi, "sog": sog, "lon": lon, "lat": lat, "cog": cog, "ts": ts}
print(decode_type1("15Muq?P0000G?t@E>4p@0?vN0<0u", 0))
校验环节建议至少做四类检查:其一,校验和比对,不一致的语句直接丢弃;其二,范围检查,经度超出±180、纬度超出±90即判为异常;其三,不可用值检查,航速1023、航向3600、时间戳60应转为空值;其四,连续性检查,同一MMSI相邻两点的距离与时间差若推算出不合理速度(例如超过60节),应标记为跳点。经过这四步,写入航迹表的每条记录才具备可用性。
完成上述解析与校验后,每条航迹记录应至少包含:MMSI、UTC时间、经度、纬度、SOG、COG、船名、船型、数据来源类型。这张结构化的航迹表,才是后续轨迹聚类、航速分布统计和异常行为识别可以直接使用的输入。