evoleinik/aqi-liberator

GitHub: evoleinik/aqi-liberator

逆向解码 aqicn.org 专有的增量编码协议,将其十余年历史空气质量监测数据导出为标准 CSV/JSON 格式,让数据真正自由流通。

Stars: 3 | Forks: 0

# aqi-liberator 解码 [aqicn.org](https://aqicn.org) 历史空气质量数据(采用其专有编码)。获取超过 10 年的每日 AQI 数据,输出为纯 CSV 或 JSON 格式。 ## 为什么选择它 aqicn.org 拥有世界上最好的历史 AQI 数据库——包含数千个监测站,数据可追溯至 2012 年。但是它没有下载按钮。数据通过 Server-Sent Events 以自定义的类二进制编码提供,在客户端通过 JavaScript 渲染,且从未公开原始数值。 此工具破解了该编码,并为您提供 CSV 或 JSON 格式的数据。将其通过管道传递给 pandas、DuckDB、Excel 或任何您喜欢的工具。 ## 安装 ``` # 使用 uv(推荐) uv tool install aqi-liberator # 或 pip pip install aqi-liberator # 或直接运行 uvx aqi-liberator fetch 5775 --pol pm25 ``` ## 快速开始 ``` # 获取清迈 10 年的每日 PM2.5 数据(站点 5775) aqi-liberator fetch 5775 # 比较两个城市,仅限四月 aqi-liberator compare 5775 5774 --month 04 # 输出 JSON 以通过管道传递给 jq aqi-liberator fetch 5775 --json --pol pm25 | jq '.[0]' # 保存原始数据用于离线分析 aqi-liberator fetch 5775 --save aqi-liberator decode 5775.sse --pol pm25,pm10 # 按名称查找站点 ID aqi-liberator stations --search "bangkok" # 按坐标查找最近站点(适用于没有命名站点的城市) aqi-liberator stations --near 12.57,99.95 ``` ## 查找监测站 ID aqicn.org 上的每个监测站都有一个数字 ID。您可以通过以下方式找到它: 1. 访问 `https://aqicn.org/city/YOUR-CITY/` 并检查 URL 2. 使用 `aqi-liberator stations --search "city"` 3. 查看历史页面:`https://aqicn.org/historical/` —— 将鼠标悬停在监测站上 一些著名的监测站: | ID | 城市 | |----|------| | 5775 | 泰国清迈 | | 5774 | 泰国罗勇 | | 5773 | 泰国曼谷 | | 1827 | 泰国普吉岛 | | 1826 | 泰国素叻他尼 | | 1849 | 泰国春武里 | ## 命令 ### `fetch` — 下载并解码 ``` aqi-liberator fetch STATION_ID [STATION_ID ...] --pol pm25,pm10 # filter pollutants (default: all) --month 04 # filter to month --from-date 2024-01-01 --to-date 2024-12-31 --json # JSON instead of CSV --save # also save raw SSE file --timeout 30 # HTTP timeout in seconds ``` 输出 (CSV): ``` date,station_id,station_name,pollutant,value 2024-04-01,5775,Chiang Mai,pm25,164.0 2024-04-02,5775,Chiang Mai,pm25,227.0 ``` ### `decode` — 解码本地 SSE 文件 ``` aqi-liberator decode FILE [FILE ...] --raw # decode a single encoded string from stdin --json / --pol / --month / --from-date / --to-date (same as fetch) ``` ### `compare` — 并排比较 ``` aqi-liberator compare STATION_ID STATION_ID [...] --pol pm25 # single pollutant (default: pm25) --json / --month / --from-date / --to-date (same as fetch) ``` 输出 (CSV,宽格式): ``` date,Chiang Mai,Rayong 2025-04-01,154.0,56.0 2025-04-02,153.0,59.0 ``` ### `stations` — 查找监测站 ID ``` aqi-liberator stations --search "city name" # search by name aqi-liberator stations --near 12.57,99.95 # search by coordinates --json ``` `--near` 标志对于没有命名监测站的城市非常有用——它会按距离返回最近的被监测站点。 ### `usage` — 遥测 ``` aqi-liberator usage [--json] ``` ## 管道示例 ``` # 按年份统计四月 PM2.5 平均值 aqi-liberator fetch 5775 --pol pm25 --month 04 \ | awk -F, 'NR>1{y=substr($1,1,4); s[y]+=$5; n[y]++} END{for(y in s) print y, s[y]/n[y]}' # 加载到 DuckDB aqi-liberator fetch 5775 5774 --pol pm25 \ | duckdb -c "SELECT station_name, avg(value) FROM read_csv('/dev/stdin') GROUP BY 1" # 与 jq 并排使用 aqi-liberator compare 5775 5774 --json --month 04 --from-date 2025-04-01 \ | jq '.[] | select(."Chiang Mai" > 150)' ``` ## 编码格式 本节记录了 aqicn.org 使用的专有编码,该编码从其 `historic-full.js` 逆向工程得出。 ### 数据源 历史数据以 Server-Sent Events 的形式提供,来源为: ``` https://att.waqi.info/api/attsse/{station_id}/yd.json ``` 响应是一个 SSE 事件流: ``` event: debug data: "Fetching 2026-P3" event: data data: {"msg":{"st":492312,"dh":24,"ps":{"pm25":"1!104eZXJg!-34lMP"},"time":{"span":["2026-03-29T00:00:00Z","2026-03-29T00:00:00Z"]},"meta":{"si":{"city":{"name":"Chiang Mai","idx":5775}}}}} ``` 每个 `event: data` 消息包含一个时间块(通常为一个月或一个季度),具有以下内容: | 字段 | 描述 | |-------|-------------| | `msg.st` | 自 Unix 纪元以来的开始时间(以小时为单位) | | `msg.dh` | 每个数据点的小时数(24 = 每天) | | `msg.ps` | 污染物系列——键为污染物名称,值为编码字符串 | | `msg.time.span` | 此块覆盖的日期范围 | | `msg.meta.si.city` | 监测站元数据 | ### 增量编码 每个污染物值都是一个类似于 `"1!104eZXJg!-34lMP"` 的字符串。 第一个字符是格式版本: - `1` = 日数据(每个 `dh` 小时一个值) - `2` = 月/周聚合(不同的时间索引) 其余部分是一个紧凑的增量编码序列。解码器维护: - `n` — 时间槽索引(从 0 开始) - `r` — 运行值(累积增量) - `o` — 待处理重复计数 - `scale` — 值乘数(默认为 1) 每个输出点为:在时间 `epoch = (n * dh + st) * 3600 秒` 时的 `value = r * scale` #### 字符表 | 字符 | 代码 | 动作 | |------|------|--------| | `A`-`Z` | 65-90 | 发出增量 = code - 65 (A=0, B=1, ..., Z=25) | | `a`-`z` | 97-122 | 发出增量 = -(code - 97) - 1 (a=-1, b=-2, ..., z=-26) | | `0`-`9` | 48-57 | 累积重复计数:`o = 10*o + digit` | | `!` | 33 | 从后面的有符号整数发出增量 | | `\|` | 124 | 跳过槽:n 推进后面的数字减 1 | | `$` | 36 | 跳过 1 个槽 | | `%` | 37 | 跳过 2 个槽 | | `'` | 39 | 跳过 3 个槽 | | `/` | 47 | 从后面的数字设置比例因子 | | `*` | 42 | 设置比例 = 1/后面的数字(仅在位置 0) | 当在字母或 `!` 之前累积了重复计数 `o` 时,发出操作将执行 `o` 次而不是一次。 #### 完整示例 编码:`!104eZXJg` ``` !104 → delta=104, emit: n=1, r=104 → value=104 e → delta=-5, emit: n=2, r=99 → value=99 Z → delta=25, emit: n=3, r=124 → value=124 X → delta=23, emit: n=4, r=147 → value=147 J → delta=9, emit: n=5, r=156 → value=156 g → delta=-7, emit: n=6, r=149 → value=149 ``` 结果:6 个每日值:`[104, 99, 124, 147, 156, 149]` #### 时间重建 对于 `st=492312` 的日数据 (`dh=24`): - 索引 `n` 处的点具有时间戳:自纪元以来的 `(n * 24 + 492312) * 3600` 秒 - 在 Python 中:`datetime.utcfromtimestamp((n * 24 + 492312) * 3600)` ### 可用污染物 每个监测站可能包含以下任意组合: | 键 | 污染物 | |-----|-----------| | `pm25` | PM2.5 (AQI) | | `pm10` | PM10 (AQI) | | `o3` | 臭氧 (AQI) | | `no2` | 二氧化氮 (AQI) | | `so2` | 二氧化硫 (AQI) | | `co` | 一氧化碳 (AQI) | 数值采用 US EPA AQI 标度 (0-500),而非原始浓度。 ## AX 合规性 此工具遵循 [Agent Experience 原则](https://evoleinik.com/posts/vx-launch/): - 结构化输出:每个命令都支持 `--json`,返回裸数组 - stdout = 数据,stderr = 诊断信息 - 无交互式提示 - 确定性退出代码:0=正常,1=用户错误,2=网络错误,3=解码错误 - 所有网络操作均支持 `--timeout` - 空结果指引(在 stderr 中提供包含具体标志的提示) - 使用情况遥测:`aqi-liberator usage` ## 许可证 MIT
标签:AQICN, AQI数据, BeEF, DuckDB, ESC4, JSON导出, OSINT, pandas, PM2.5, Python, Server-Sent Events, SSE, URL抓取, 历史数据, 数据抓取, 数据提取, 数据格式转换, 数据科学, 数据解析, 文档结构分析, 无后门, 气象数据, 爬虫, 环境监测, 空气质量指数, 空气质量监测站, 解码工具, 资源验证, 逆向分析, 逆向工具