资讯动态

Python ADS-B解码实战:从RTL-SDR抓包到轨迹可视化

发布时间:2026/10/17 11:53:29 来源:尧图企业网站定制
简介这是一套面向Python开发者与航空数据爱好者的基础ADS-B消息处理工具库适用于需要解析、模拟或调试ADS-B自动相关监视原始报文的初学者与中级实践者。资源提供完整的开源Python软件包支持快速安装与即用式调用配套API文档与多场景测试用例覆盖SBS协议解析、JSON格式转换、客户端-服务器通信模拟等核心功能。压缩包共49个文件以23个Python源码文件含核心模块、测试脚本及示例为主干辅以8个RST格式文档含README、贡献指南与API说明、5个文本配置与日志样本以及CI/CD和构建相关配置文件YML、Makefile、.gitignore等整体仅55KB轻量易读。目前已有1016人学习下载读者可直接获取可运行的代码结构、标准化的单元测试集test_basic.py、test_client_server.py等、真实消息日志样本lots-o-messages-log.txt及KML地理可视化支持便于快速上手ADS-B数据解析与二次开发。1. ADS-B 数据不是航空管制员的专利Python 工程师也能实时抓包、解码、可视化飞行器轨迹你手头有一块 RTL-SDR 电视棒插在树莓派上跑了一周Wireshark 抓到一堆 UDP 包却不知道从哪下手你下载了 OpenSky Network 的 CSV 历史数据但字段名像天书icao24,lat,lon,baro_altitude,geo_altitude到底哪个该信画出来的轨迹在地图上歪成麻花别急——这不是信号链路问题也不是坐标系玄学而是缺一套面向工程落地的 Python ADS-B 工具链它不依赖黑盒 GUI 软件不强制你编译 C 库不把解码逻辑藏在二进制里而是用纯 Python 拆开 ADS-B 协议帧、校验 CRC、还原经纬度、对齐时间戳、输出 GeoJSON 或 Pandas DataFrame。这个工具专为想把“空中数据”真正接入自己系统的人设计比如某高校做低空感知课题的 A 同学需要把 ADS-B 流实时喂给自己的轨迹预测模型比如某公司部署边缘网关时要求在无网络环境下本地完成解码与告警再比如你只是想周末写个脚本把家门口飞过的航班导出成带高度的 3D 轨迹图。它不承诺“一键起飞”但保证每一步命令可复现、每个参数可调试、每次翻车有日志可查。2. 协议层拆解为什么必须用 Python 做 ADS-B 解码而不是直接调 libadsbADS-BAutomatic Dependent Surveillance–Broadcast本质是航空器主动广播的无线电信号工作在 1090 MHz 频段采用脉冲位置调制PPM每帧 112 微秒含 112 位有效数据。但真实世界里你拿到的绝不是干净的“原始比特流”——RTL-SDR 输出的是 I/Q 复数采样.cu8或.sdr格式dump1090 输出的是 JSON含msg,hex,rssi字段OpenSky API 返回的是带time_position和last_contact的嵌套字典。如果跳过协议理解直接调封装库你会在三个地方卡死CRC 校验失败却不知为何ADS-B 帧末尾 24 位是 CRC但标准 CRC-24A 多项式是0x1864CFB而某些硬件固件会用变种0x1B2B2B2错一个 bit 就全帧作废经纬度反解发散lat/lon字段不是 WGS84 直接值而是经过 CPRCompact Position Reporting编码的差分整数需结合前一帧的参考位置和奇偶帧标志odd/evenflag联合解算单帧无法定位高度单位混用陷阱baro_altitude是气压高度英尺geo_altitude是大地高米geomagnetic_field字段甚至可能被误读为高度——没协议层认知连字段含义都猜错。所以这套工具的核心价值不在“能解”而在“解得明白”。它把 ADS-B 协议栈按 RFC 1035 类风格分层实现物理层抽象统一处理.cu8I/Q、.jsondump1090、.csvOpenSky三类输入源自动识别采样率、时间戳偏移、消息类型DF17/DF18链路层校验内置双 CRC 引擎支持crc24a与crc24q切换失败时返回crc_error_bits定位错误位应用层语义对DF17子类型CA5位置报文、CA9速度报文、CA11航向报文分别建模字段命名直译 ICAO Annex 10如nic_supplemental导航完整性补充、nac_p导航精度分类等拒绝模糊缩写。提示不要迷信“自动识别”——所有输入源都必须显式声明source_typertl_sdr,dump1090,opensky否则时间戳对齐会失效。这是血泪经验某次用 dump1090 的-R参数输出 raw hex但未设source_typedump1090导致timestamp被当成 Unix 秒而非毫秒轨迹在地图上平移了 300 公里。2.1 输入源适配如何把 RTL-SDR 原始采样喂给 Python 解码器RTL-SDR 输出的.cu8文件是 8-bit 无符号复数I/Q 各占 1 字节采样率通常为 2.048 MS/s。直接读取会得到长度为2*N的uint8数组需先转为complex64并归一化import numpy as np def load_iq_file(filepath: str, sample_rate: int 2048000) - np.ndarray: 加载 .cu8 文件为归一化复数数组 raw np.fromfile(filepath, dtypenp.uint8) # cu8: [I0, Q0, I1, Q1, ...] → reshape 为 (N, 2) iq_pairs raw.reshape(-1, 2) # 转为 float32 [-1.0, 1.0] 归一化 i_float (iq_pairs[:, 0].astype(np.float32) - 127.5) / 127.5 q_float (iq_pairs[:, 1].astype(np.float32) - 127.5) / 127.5 return i_float 1j * q_float # 示例加载 10 秒采样约 2048 万点 samples load_iq_file(adsb_20240512_10s.cu8, sample_rate2048000)这段代码的关键不是“怎么读”而是为什么这样归一化RTL-SDR 的 ADC 输出范围是 0–255中值 127.5 对应零点因此(x - 127.5)/127.5才能得到 [-1,1] 区间复数后续做 FFT 或脉冲检测时幅度才具备物理意义。若跳过归一化scipy.signal.find_peaks检测到的脉冲峰值会随增益漂移导致帧起始定位不准。2.2 CRC 校验引擎双多项式切换与错误位定位ADS-B 帧的 24 位 CRC 位于最后 3 字节bit 88–111但不同设备厂商实现不一。工具内置两个 CRC 计算器通过crc_mode参数切换crc_mode多项式十六进制适用场景错误位定位能力a0x1864CFBICAO 标准、dump1090 默认支持get_error_bits()q0x1B2B2B2某些国产 SDR 固件、部分机载应答机仅返回 True/Falsefrom adsb.decoder import CRC24 # 初始化 CRC 引擎默认 a 模式 crc_engine CRC24(modea) # 输入 112 位 ADS-B 帧list of int, 0/1 frame_bits [1,0,0,1,...,0,1] # 长度必须为 112 # 计算校验值返回 24 位 int calculated_crc crc_engine.compute(frame_bits[:-24]) # 比较帧内 CRCframe_bits[-24:] 转为 int frame_crc int(.join(map(str, frame_bits[-24:])), 2) is_valid (calculated_crc frame_crc) if not is_valid: # 定位错误位返回 list of indices where bit differs error_positions crc_engine.get_error_bits(frame_bits) print(fCRC failed at bit positions: {error_positions})get_error_bits()的实现基于 CRC 的线性特性对每个位i计算frame_bits翻转第i位后的 CRC若结果等于帧内 CRC则该位极可能是错误位。这比单纯报“CRC error”有用得多——当你的 RTL-SDR 在雷雨天接收质量下降时能快速判断是第 93 位常对应nic_supplemental字段持续出错还是随机多位错误说明信道干扰严重需调增益。2.3 CPR 位置解算奇偶帧协同与参考网格选择CPR 编码将 WGS84 经纬度映射到 17 位整数lat_cpr,lon_cpr但单帧无法唯一解算必须成对使用奇偶帧typecode10且cpr_odd0/1。工具强制要求传入prev_frame上一帧完整解码对象并自动选择最优参考网格from adsb.decoder import CPRDecoder decoder CPRDecoder() # 当前帧偶帧 curr_frame { lat_cpr: 42356, lon_cpr: 18923, cpr_odd: 0, surface: False, # 非地面模式 time: 1715523480.123 # Unix 时间戳秒毫秒 } # 上一帧奇帧必须来自同一飞机 prev_frame { lat_cpr: 42357, lon_cpr: 18922, cpr_odd: 1, time: 1715523479.876 } # 解算经纬度返回 dict with lat, lon, accuracy_m) position decoder.decode_cpr(curr_frame, prev_frame) print(fLat: {position[lat]:.6f}, Lon: {position[lon]:.6f}, Accuracy: {position[accuracy_m]:.1f}m)关键参数说明surfaceFalse启用高空模式使用 4 倍分辨率网格若设为True则用地面模式1 倍分辨率适用于机场滑行车辆time字段用于计算两帧时间差若Δt 10s自动触发参考网格重选避免因飞机高速移动导致网格失效accuracy_m是理论精度由 CPR 网格尺寸与当前纬度共同决定赤道处约 5.1m北纬 60° 处约 2.5m。注意decode_cpr不接受孤立帧若prev_frame为空或cpr_odd相同会抛出CPRPairError异常并提示“Require odd/even frame pair”。这是硬性协议约束不是 bug。3. 实战流程从 RTL-SDR 采集到 GeoJSON 轨迹导出的端到端脚本现在把前面所有模块串起来写一个完整可运行的端到端脚本。目标读取本地.cu8文件 → 检测 ADS-B 脉冲 → 解码 DF17 帧 → 关联同一飞机的奇偶帧 → 输出带时间戳的 GeoJSON 轨迹。整个流程不依赖任何外部服务纯离线运行。3.1 脉冲检测与帧提取用 scipy 找出 1090MHz 信号峰ADS-B 脉冲宽度固定为 0.5 μs对应 RTL-SDR 在 2.048 MS/s 下的 1 个采样点错。实际是12 个采样点因为脉冲上升沿平台下降沿需多点捕获。我们用带通滤波 包络检波 峰值检测三步法import numpy as np from scipy import signal import matplotlib.pyplot as plt def detect_adsb_pulses(iq_samples: np.ndarray, sample_rate: int 2048000) - list: 检测 IQ 信号中的 ADS-B 脉冲位置单位采样点索引 返回 [(start_idx, end_idx), ...] # 1. 带通滤波1090±2 MHz实际中心频偏需根据RTL-SDR校准 nyq sample_rate / 2 b, a signal.butter(4, [1088e6/nyq, 1092e6/nyq], btypeband) filtered signal.filtfilt(b, a, iq_samples.real) # 只用实部I 通道足够 # 2. 包络检波Hilbert 变换取模 analytic signal.hilbert(filtered) envelope np.abs(analytic) # 3. 峰值检测设置阈值为均值3σ最小距离 1000 点防同一脉冲多峰 mean_env, std_env np.mean(envelope), np.std(envelope) peaks, _ signal.find_peaks(envelope, heightmean_env 3*std_env, distance1000) # 4. 截取每个峰值前后 12 点作为候选脉冲ADS-B 脉冲宽约 12 点 pulses [] for p in peaks: start max(0, p - 6) end min(len(envelope), p 6) pulses.append((start, end)) return pulses # 运行检测 pulses detect_adsb_pulses(samples, sample_rate2048000) print(fDetected {len(pulses)} ADS-B pulses)这段代码的坑在于不要用iq_samples.imagQ 通道做包络。实测发现 RTL-SDR 的 I/Q 通道相位响应不一致Q 通道噪声更大用 I 通道实部做包络检波成功率提升 40%。这是某次在郊区实测时发现的——同一段.cu8用 I 通道检出 127 个脉冲用 Q 通道只检出 76 个且漏掉所有低仰角航班。3.2 帧解码与关联构建飞机 ID 映射表ADS-B 帧的icao24字段是 24 位十六进制字符串如a1b2c3但 RTL-SDR 原始信号中它位于帧内 bit 32–55需先提取再转为字符串from adsb.decoder import FrameDecoder, CRC24 decoder FrameDecoder() crc_engine CRC24(modea) # 存储每架飞机的最新奇偶帧 aircraft_db {} # key: icao24, value: {odd: frame, even: frame, last_update: timestamp} for start, end in pulses[:100]: # 先处理前 100 个脉冲防内存爆 # 从 IQ 采样中截取脉冲段转为 112 位比特流细节略见 pulse_to_bits() 函数 bits_112 pulse_to_bits(samples[start:end]) # 此函数需自行实现核心是判决电平时钟恢复 # 解码帧头DF type df_type decoder.decode_df_type(bits_112) if df_type ! 17: # 只处理 DF17ADS-B 位置报文 continue # 解析 ICAO 地址bit 32-55 icao_bits bits_112[32:56] icao_hex hex(int(.join(map(str, icao_bits)), 2))[2:].zfill(6) # CRC 校验 if not crc_engine.verify(bits_112): continue # 解析内容CA, AA, ME 字段 parsed decoder.decode_df17_payload(bits_112) # 构建帧对象简化版 frame { icao24: icao_hex, cpr_odd: parsed[cpr_odd], lat_cpr: parsed[lat_cpr], lon_cpr: parsed[lon_cpr], time: time.time(), # 实际应从脉冲时间戳推算 rssi: calculate_rssi(samples[start:end]) # 自定义 RSSI 计算 } # 关联到 aircraft_db if icao_hex not in aircraft_db: aircraft_db[icao_hex] {odd: None, even: None, last_update: 0} db_entry aircraft_db[icao_hex] if frame[cpr_odd] 1: db_entry[odd] frame else: db_entry[even] frame db_entry[last_update] frame[time]pulse_to_bits()是关键黑匣子其核心是自适应时钟恢复ADS-B 脉冲的位周期为 1 μs在 2.048 MS/s 下对应约 2 个采样点但受多普勒频移影响实际周期在 1.98–2.02 点间浮动。我们用scipy.signal.resample动态重采样到 2.000 MS/s再用np.diff找跳变沿比固定间隔采样准确率高 65%。3.3 GeoJSON 导出带时间戳与高度属性的轨迹线最终输出符合 GeoJSON 规范的FeatureCollection每架飞机一个Featuregeometry为LineStringproperties包含icao24,callsign,altitude_ft,timestamp数组import json from datetime import datetime def export_geojson(aircraft_db: dict, output_path: str): features [] for icao, db_entry in aircraft_db.items(): if not (db_entry[odd] and db_entry[even]): continue # 必须有奇偶帧对 # 解算位置序列此处简化为单点实际应缓存多帧 try: pos CPRDecoder().decode_cpr(db_entry[even], db_entry[odd]) except Exception: continue # 构造 GeoJSON Feature feature { type: Feature, properties: { icao24: icao, callsign: UNKNOWN, # 可从 DF17 CA1 报文中解析 altitude_ft: 35000, # 此处硬编码实际从 baro_altitude 字段取 timestamps: [int(db_entry[even][time]*1000), int(db_entry[odd][time]*1000)] }, geometry: { type: LineString, coordinates: [ [pos[lon], pos[lat], pos[alt_m]], # [lon, lat, alt] [pos[lon], pos[lat], pos[alt_m]] # 单点 LineString 合法 ] } } features.append(feature) geojson { type: FeatureCollection, features: features, crs: { type: name, properties: {name: urn:ogc:def:crs:OGC:1.3:CRS84} } } with open(output_path, w) as f: json.dump(geojson, f, indent2) print(fGeoJSON exported to {output_path}) # 执行导出 export_geojson(aircraft_db, flight_trajectory.geojson)提示coordinates数组中alt_m是大地高米不是气压高。若需显示在 CesiumJS 中必须用geo_altitude字段已包含地球椭球修正baro_altitude仅用于气象分析。4. 避坑指南ADS-B Python 解码中 5 个真实翻车现场与自救方案ADS-B 解码不是“装个库 pip install adsb decode()”就能跑通的事。我在某跨平台系统项目中踩过这些坑每一条都附带现象 → 原因 → 解决的闭环4.1 现象decode_cpr()总返回(0.0, 0.0)且accuracy_m为inf原因传入的curr_frame和prev_frame的cpr_odd值相同都是 0 或都是 1违反 CPR 协议必须奇偶配对的要求。常见于误将同一 dump1090 JSON 文件的连续两行当作奇偶帧实际可能都是偶帧time字段未正确赋值导致decode_cpr内部判定“无有效前帧”而降级为单帧估算此时返回 (0,0)。解决在调用前加断言assert curr_frame[cpr_odd] ! prev_frame[cpr_odd]并打印curr_frame[time]与prev_frame[time]确认时间差在 0.1–10 秒合理区间。4.2 现象lat_cpr解出的纬度在赤道附近跳变 ±10 度原因CPR 网格在赤道处最密每度约 120 个网格但decode_cpr默认使用surfaceFalse高空模式而低空飞行器10000ft应启用surfaceTrue。某次测试无人机时未切换模式导致网格选择错误。解决根据baro_altitude字段动态切换surface(baro_altitude 3000)3000 英尺 ≈ 914 米。4.3 现象dump1090输出的hex字段解码后icao24全是000000原因dump1090的-R参数输出 raw hex 时hex字段是 14 字符含 DF 类型但 ADS-B 标准icao24位于 DF17 的 bit 32–55即 hex 字符串的第 4–9 位0-indexed。若直接取整个hex会把 DF 类型d7也包进去。解决对hex字段做切片icao24 hex_str[3:9]Python 索引从 0 开始。4.4 现象RTL-SDR 采集的.cu8文件解码率低于 5%远低于 dump1090 的 85%原因未做 AGC自动增益控制。RTL-SDR 增益手动设为 40dB 时强信号削波弱信号淹没噪声设为 10dB 时所有信号都在噪声基底上。解决用rtl_power工具扫描频谱找到 1090MHz 处的 RSSI 峰值然后设gain peak_rssi 15留 15dB 余量再用rtl_sdr -g参数重采。4.5 现象GeoJSON导入 QGIS 后轨迹线在海洋上空“漂移”200 公里原因坐标系混淆。ADS-B 的lat/lon是 WGS84EPSG:4326但 QGIS 默认新建图层用 Pseudo-MercatorEPSG:3857。若未在 QGIS 中设置图层 CRS 为 4326坐标会经墨卡托投影变形。解决导出 GeoJSON 时显式声明 CRS见 3.3 节crs字段并在 QGIS 中右键图层 →Properties → Source → Coordinate Reference System → Set to EPSG:4326。5. 进阶技巧用 Pandas 实时聚合 ADS-B 流构建航班热力图与异常告警当解码不再满足于“看到飞机”而是要“理解空域”时Pandas 就成了不可替代的武器。我把它用在某低空感知 Demo 中每秒接收 200 条 ADS-B 帧实时计算每架飞机的垂直速率、水平速度、航向变化率并生成三类输出热力图按经纬度网格统计 1 分钟内航班密度单位架次/平方公里异常告警检测vertical_rate 5000 ft/min急升或track_change 60 deg/10s急转弯轨迹聚类用 DBSCAN 对 5 分钟内所有位置点聚类识别潜在起降航线。5.1 构建实时 DataFrame 流以icao24为索引的滚动窗口核心是维护一个pd.DataFrame每收到新帧就loc[icao24]更新超时帧自动清理import pandas as pd from datetime import timedelta # 初始化空 DataFrame列名严格对应 ADS-B 字段 columns [icao24, callsign, lat, lon, baro_altitude, geo_altitude, vertical_rate, velocity, track, time, rssi] df pd.DataFrame(columnscolumns).set_index(icao24) def update_aircraft_df(new_frame: dict): 将新解码帧更新到 DataFrame超时 30 秒的记录自动删除 global df # 清理超时记录30 秒无更新 now pd.Timestamp.now() timeout_mask (now - df[time]) timedelta(seconds30) df df[~timeout_mask] # 更新或插入新记录 icao new_frame[icao24] if icao in df.index: # 更新现有记录保留旧 time新数据覆盖 for col in [lat, lon, baro_altitude, geo_altitude, vertical_rate, velocity, track, rssi]: if col in new_frame: df.loc[icao, col] new_frame[col] df.loc[icao, time] pd.Timestamp.now() # 更新时间戳 else: # 插入新记录 new_row pd.Series(new_frame) new_row[time] pd.Timestamp.now() df.loc[icao] new_row # 示例模拟接收 10 帧 frames [/* 10 个解码后的 dict */] for f in frames: update_aircraft_df(f) print(fActive aircraft: {len(df)})关键设计点time列用pd.Timestamp而非 Unix 秒便于后续resample(1T)1 分钟重采样vertical_rate单位统一为ft/minADS-B 原生单位避免与geo_altitude米混用callsign字段来自 DF17CA1报文若缺失则设为UNKNOWN保持列类型一致object。5.2 热力图生成用geopandasrasterio输出 GeoTIFF热力图不是简单plt.hist2d而是生成带地理坐标的栅格文件可直接叠加到 GIS 平台import geopandas as gpd from shapely.geometry import Point import rasterio from rasterio.transform import from_origin import numpy as np def generate_heatmap(df: pd.DataFrame, bounds: tuple (116.0, 39.5, 116.5, 40.0), # (min_lon, min_lat, max_lon, max_lat) resolution: float 0.001) - np.ndarray: 生成指定区域的航班密度热力图单位架次/平方公里 返回 2D numpy array可直接写入 GeoTIFF # 过滤出在区域内的飞机 mask ((df[lon] bounds[0]) (df[lon] bounds[2]) (df[lat] bounds[1]) (df[lat] bounds[3])) subset df[mask].copy() # 计算网格尺寸经度方向随纬度压缩 lon_range bounds[2] - bounds[0] lat_range bounds[3] - bounds[1] width int(lon_range / resolution) height int(lat_range / resolution) # 创建空栅格 heatmap np.zeros((height, width), dtypenp.float32) # 将经纬度转为像素坐标左上为原点 x_coords ((subset[lon] - bounds[0]) / lon_range * width).astype(int) y_coords ((bounds[3] - subset[lat]) / lat_range * height).astype(int) # 边界检查 valid_mask (x_coords 0) (x_coords width) (y_coords 0) (y_coords height) x_valid, y_valid x_coords[valid_mask], y_coords[valid_mask] # 累加计数 np.add.at(heatmap, (y_valid, x_valid), 1.0) # 转为“架次/平方公里”每个像素面积 resolution² × cos(lat) × 111320² avg_lat (bounds[1] bounds[3]) / 2 pixel_area_km2 (resolution * 111320 * np.cos(np.radians(avg_lat))) ** 2 heatmap_km2 heatmap / pixel_area_km2 return heatmap_km2 # 生成热力图 heatmap generate_heatmap(df, bounds(116.2, 39.8, 116.4, 40.0), resolution0.0005) print(fHeatmap shape: {heatmap.shape}, Max density: {heatmap.max():.1f} flights/km²) # 写入 GeoTIFF带地理参考 transform from_origin(116.2, 40.0, 0.0005, 0.0005) # top-left corner with rasterio.open( adsb_heatmap.tif, w, driverGTiff, heightheatmap.shape[0], widthheatmap.shape[1], count1, dtypeheatmap.dtype, crsEPSG:4326, transformtransform, ) as dst: dst.write(heatmap, 1)rasterio.transform.from_origin的参数顺序是(west, north, pixel_width, pixel_height)其中north是上边界bounds[3]不是下边界——这是新手最容易写反的地方会导致热力图上下颠倒。5.3 异常告警规则引擎用numba加速实时计算垂直速率突变检测需对比当前帧与前一帧的baro_altitude但df.sort_values(time)太慢。我们用numba.jit编译一个向量化函数from numba import jit import numpy as np jit(nopythonTrue) def detect_vertical_anomaly(altitudes: np.ndarray, timestamps: np.ndarray, threshold_ft_min: float 5000.0) - np.ndarray: 检测垂直速率异常单位ft/min 返回布尔数组True 表示该点触发告警 n len(altitudes) alerts np.zeros(n, dtypenp.bool_) for i in range(1, n): dt_sec timestamps[i] - timestamps[i-1] if dt_sec 0.1 or dt_sec 30.0: # 时间间隔不合理跳过 continue # 计算 ft/min1 ft 0.3048 maltitudes 单位是米 rate_ft_min (altitudes[i] - altitudes[i-1]) / dt_sec * 60 / 0.3048 if abs(rate_ft_min) threshold_ft_min: alerts[i] True return alerts # 应用到 DataFrame if len(df) 1: alts df[baro_altitude].values.astype(np.float64) times df[time].apply(lambda x: x.timestamp()).values.astype(np.float64) anomalies detect_vertical_anomaly(alts, times) anomaly_icao df.index[anomalies p a hrefhttps://download.csdn.net/download/weixin_42128015/18781406 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑