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抓取, 历史数据, 数据抓取, 数据提取, 数据格式转换, 数据科学, 数据解析, 文档结构分析, 无后门, 气象数据, 爬虫, 环境监测, 空气质量指数, 空气质量监测站, 解码工具, 资源验证, 逆向分析, 逆向工具