OONI 測量資料結構導覽¶
ASN 觀測資料擷取與分析 說明了如何擷取 OONI 公開資料,取得資料之後會遇到下一個問題:一筆測量有二十多個頂層欄位,test_keys 裡還有二十幾個,該看哪一個。
以下用兩筆台灣的真實測量對照,說明一筆網路連線測試(web_connectivity)的組成,以及每個欄位對應到上游 ooni/spec 的哪份規格。看懂之後,你可以自己判斷一筆測量說了什麼,也能決定分析程式該取哪些欄位。
範例資料來源
兩筆都是 2026-08-04 由台灣的 OONI Probe 產生的公開資料,可在 OONI Explorer 查到原始內容。
| 正常通過 | 判定異常 | |
|---|---|---|
| 測量對象 | http://presidentlee.tw/ |
https://ntc.party/ |
measurement_uid |
20260804085935.603513_TW_webconnectivity_4a5fd27dec0b32f6 |
20260804084548.416537_TW_webconnectivity_f4e7b0ab3d0251bf |
一筆測量的三層結構¶
不用一開始就記住所有欄位。一筆測量可以拆成三層來讀:
- 外殼:誰在哪裡、用什麼軟體、什麼時候測的。所有測項共用同一套欄位,規格是 df-000-base。
- 判定:測量得出的結論。欄位隨測項而異,全部收在
test_keys底下,web_connectivity的定義在 ts-017。 - 證據:支撐結論的原始紀錄,包含每一次 DNS 查詢、TCP 連線、TLS 握手與 HTTP 請求,以及對照組的同類紀錄。同樣放在
test_keys底下,各自對應一份df-開頭的規格。
判定層告訴你結論,證據層讓你驗證結論。兩者分開讀,各欄位的角色就清楚了。
自己取一筆來看¶
邊讀邊對照手上的樣本會快很多。不必先架好環境,用公開 API 就能取得單筆資料。先列出符合條件的測量:
curl -s "https://api.ooni.io/api/v1/measurements?probe_cc=TW&test_name=web_connectivity&limit=5" \
| python3 -m json.tool | head -40
回應中的 measurement_uid 可以換取完整內容:
curl -s "https://api.ooni.io/api/v1/raw_measurement?measurement_uid=<measurement_uid>" \
| python3 -m json.tool | head -60
原始回應是未斷行的長字串,直接輸出到終端機不易閱讀,上面兩段都接了 python3 -m json.tool 排版。加上 anomaly=true 可以只列出被判定異常的測量,適合用來找對照範例。把 probe_cc 換成所在地區、probe_asn 換成自己的 ASN,就能看到本地網路上的觀測紀錄。
需要批次處理大量資料時,改走 AWS S3 公開資料集會更有效率,作法見 ASN 觀測資料擷取與分析。
外殼:誰在哪裡測的¶
外殼層的欄位在所有測項中都一樣,實務上最常用到的如下:
| 欄位 | 正常通過那筆 | 判定異常那筆 | 說明 |
|---|---|---|---|
probe_cc |
TW |
TW |
測量發生的國家代碼 |
probe_asn |
AS3462 |
AS3462 |
執行測量的網路所屬 ASN |
probe_network_name |
Chunghwa Telecom Co., Ltd. |
Chunghwa Telecom Co., Ltd. |
該 ASN 的組織名稱 |
resolver_asn |
AS3462 |
AS13335 |
測量時實際使用的 DNS 解析器所屬 ASN |
resolver_network_name |
Chunghwa Telecom Co., Ltd. |
Cloudflare Inc |
解析器的組織名稱 |
input |
http://presidentlee.tw/ |
https://ntc.party/ |
測量對象的網址 |
test_name |
web_connectivity |
web_connectivity |
測項名稱 |
software_name |
ooniprobe-cli |
ooniprobe-desktop-unattended |
產生資料的 Probe 種類 |
software_version |
3.29.1 |
3.26.0 |
Probe 版本 |
measurement_start_time |
2026-08-04 08:59:30 |
2026-08-04 08:45:45 |
測量開始時間(UTC) |
report_id |
20260804T062033Z_webconnectivity_TW_3462_n4_uEH5rGoD07cN2oYQ |
20260804T084446Z_webconnectivity_TW_3462_n4_dFfWCDrwouM0TsT2 |
同一次執行產生的多筆測量共用此值 |
probe_asn 與 resolver_asn 可能不同,上表就是實例。兩筆都在中華電信的網路上執行,但其中一筆的使用者把 DNS 指向 Cloudflare。做 ASN 分析時兩者要分開看,混用會讓「哪家電信商的網路上看到什麼」失準。
probe_ip 永遠是 127.0.0.1
OONI 刻意不收集測量者的真實 IP,probe_ip 固定寫入本機位址。想追測量來源只能仰賴 probe_asn 加時間,同一條隱私邊界在 OONI Run v2 操作說明 也提醒協助者留意。
判定:測量得出的結論¶
判定欄位全部收在 test_keys 底下。讀之前要先知道一件事:web_connectivity 的判定建立在雙邊對照上。Probe 測完之後,OONI 架設在外部網路的測量伺服器(test helper)會對同一個網址再測一次,兩邊結果的差異才是判定的依據。test helper 那一側的紀錄收在 test_keys.control,下表的「對照組」指的就是它。
判定結果與內容比對共八個欄位。把兩筆並排,差異一眼可見:
| 欄位 | 正常通過 | 判定異常 | 說明 |
|---|---|---|---|
blocking |
false |
"dns" |
判定的干預類型,可能值為 dns、tcp_ip、http-failure、http-diff,未觀測到干預時為布林值 false |
accessible |
true |
false |
是否取得了合理的回應 |
dns_consistency |
"consistent" |
"inconsistent" |
Probe 的 DNS 結果與對照組是否一致 |
title_match |
true |
null |
網頁標題是否與對照組相符 |
headers_match |
true |
null |
回應標頭是否相符 |
status_code_match |
true |
null |
HTTP 狀態碼是否相符 |
body_length_match |
true |
null |
回應內容長度是否相近 |
body_proportion |
1 |
0 |
內容長度與對照組的比值 |
各階段的失敗原因另有三個欄位,判讀時要一起看:
| 欄位 | 記錄什麼 |
|---|---|
dns_experiment_failure |
DNS 階段的失敗原因,判定異常那筆為 "dns_nxdomain_error" |
http_experiment_failure |
HTTP 階段的失敗原因,例如 "generic_timeout_error" |
control_failure |
對照組本身是否失敗。有值時雙邊比對的前提不成立,判定結果不可信 |
blocking 的四種值分別對應不同階段的異常:dns 是解析結果與對照組不一致、tcp_ip 是封包送不到目標位址、http-failure 是連線建立後 HTTP 階段失敗、http-diff 是取得的內容與對照組不同(常見於封鎖告示頁)。四種手法在 什麼是 OONI 有概念層的介紹。
兩個容易誤判的型別問題
blocking 未觀測到干預時是布林值 false,有干預時是字串。拿它做統計前要先統一處理,否則 false 與 "dns" 會被算成兩類不同的東西。
四種值的命名本身不一致,tcp_ip 用底線,http-failure 與 http-diff 用連字號。上游如此定義,照抄即可,不要自行統一。
四個 *_match 欄位在異常那筆全是 null,原因是 DNS 階段就失敗,連線沒有建立,後續沒有東西可以比對。看到一整排 null 時,回頭找測量流程中第一個有值的 failure 欄位,那裡才是問題發生的地方。
異常不等於封鎖
blocking 有值只代表該筆測量的結果與對照組不一致,判斷是否真的存在網路干預需要更多佐證。以判定異常那筆為例,Probe 對 ntc.party 的 A 與 AAAA 查詢都回報 dns_nxdomain_error(網域不存在),但撰稿時從多個公開 DNS 解析器查詢,該網域可解析到 IPv6 位址。單筆測量無法區分網路干預、解析器當下的暫時狀態,以及網域本身的設定變動。
要下封鎖結論,需要跨時間、跨 ASN、跨解析器的多筆測量交叉比對。判定機制的完整拆解與常見誤判來源見 OONI 怎麼判定一個網站被封鎖,資料集層級的品質控制則可參考 OONI 如何分辨壞掉的量測資料。
證據:支撐結論的原始紀錄¶
test_keys 底下另有幾個欄位,記錄測量過程中每一次網路操作。每個欄位各有一份規格:
| 欄位 | 內容 | 規格 |
|---|---|---|
queries |
每一次 DNS 查詢的問題、回應與失敗原因 | df-002-dnst |
tcp_connect |
每一次 TCP 連線嘗試的目標與結果 | df-005-tcpconnect |
tls_handshakes |
每一次 TLS 握手的參數、憑證與結果 | df-006-tlshandshake |
requests |
每一次 HTTP 請求與回應的完整內容 | df-001-httpt |
network_events |
連線過程的時序事件,用於分析延遲與中斷點 | df-008-netevents |
control |
對照組的同類紀錄,結構與 Probe 側對應,判定欄位全部由它與 Probe 端的差異算出 | ts-017 |
各欄位的 failure 字串 |
所有失敗原因的統一命名,例如 dns_nxdomain_error、generic_timeout_error、ssl_unknown_authority |
df-007-errors |
把兩筆的 queries 攤開對照,就能看到判定的依據:
{
"hostname": "presidentlee.tw",
"query_type": "A",
"failure": null,
"answers": [{"answer_type": "A", "ipv4": "43.254.17.201"}]
}
{
"hostname": "ntc.party",
"query_type": "A",
"failure": "dns_nxdomain_error",
"answers": []
}
判定層的 dns_consistency 是結論,queries 與 control 是它的依據。想確認一筆測量的判定是否合理,往證據層查驗即可。
版本與相容性¶
讀 spec 之前先確認版本,能避免不少困惑:
data_format_version目前是0.2.0。外殼層的欄位定義穩定,df-000-base 可以直接對照。web_connectivity實際流通的test_version是0.4.3。ts-017 的規格內容已更新為描述 v0.5 演算法,但生產環境仍以 v0.4 為主。spec 明訂新版演算法必須使用不同的 test_keys 欄位,v0.4 的欄位定義保持相容,因此上表的欄位讀法對兩個版本都適用。x_開頭的欄位不在 spec 內。實際資料中會看到x_dns_runtime、x_status、x_th_runtime等欄位,屬於實作端的實驗性擴充,隨版本增減,分析程式不應依賴它們。
上游 spec 的 master 分支自 2025-06 起沒有新的合併,但 issue 與 PR 討論持續進行(進行中的提案包含 ICMP 資料格式與 DPI 分片測項)。spec 現階段適合當作穩定參考,引用時建議連回上游原文,避免自行複製規格內容而在上游更新後失準。
延伸閱讀¶
看懂欄位之後,接著看判定欄位怎麼算出來,以及為什麼有值不等於封鎖。
上游規格的完整目錄在 ooni/spec,其中 data-formats 收錄資料格式、nettests 收錄各測項的演算法定義。