export + exporters:圖 → 檔案

graph.json / graph.html / Obsidian vault / SVG / callflow HTML 是怎麼生成的
檔案:export.py · exporters/html.py · tree_html.py · callflow_html.py

大方向

export.py 是 pipeline 的最後一站,把 NetworkX 圖變成各種檔案。最有名的是三個:graph.json(給查詢與其他工具)、GRAPH_REPORT.md(report.py 生成)、graph.html(互動視覺化)。本站 實作案例 的互動圖就是這個 exporter 的產物。


1. export.py — 主輸出

to_json(G, communities, output_path, *, force, …) → bool export.py:232寫出 graph.json——但先做「拒絕縮小」的防護。

to_json 是 graphify 最重視資料安全的地方:

  1. Shrink guard(issue #479 的核心):如果既有 graph.json 存在且新圖節點數更少,拒絕覆寫。因為「新圖比較小」通常代表前一次 session 的 chunk 檔遺失、或 fuzzy dedup 誤併。讀不到既有檔(corrupt)時也 fail-safe——寧可不寫,也不讓局部重建覆蓋掉好的圖。
  2. 寫入節點中繼:每個節點補上 communitycommunity_namenorm_label(去掉重音符號的小寫 label,供檢索)。
  3. 邊方向還原:無向 NetworkX 儲存可能把 calls 等有向邊的端點順序搞反,build 時在 _src/_tgt 藏了真實端點,這裡還原(issue #563)。
  4. 超邊防護:圖沒有 hyperedges key 但磁碟上的舊檔有——印出清楚警告(issue #2485)。
  5. Atomic writewrite_json_atomic() 原子寫入——中途當機不會截斷好的 graph.json。
教學重點:「寫一個 JSON」這個平凡動作,這裡的工程密度非常高——shrink guard、方向還原、超邊防護、原子寫。資料安全是這個函數的第一要務。
235 if not force and existing_path.exists():
275 if new_n < existing_n: # 新圖更小 → 拒絕
287 return False
296 node["community"] = cid
309 true_src = link.pop("_src", None) # 還原真實方向
348 write_json_atomic(output_path, data, indent=2) # 原子寫入
to_obsidian(G, …) → Path export.py:513把圖輸出成 Obsidian vault(每社群一篇 + wikilinks)。

段落式說明:--obsidian flag 的實作。為每個社群產生一篇 _COMMUNITY_<name>.md,節點之間用 Obsidian wikilink([[…]])互連——這樣你在 Obsidian 的 graph view 直接看到整個知識圖譜。檔名用 _obsidian_safe_stem 安全化、_dedup_node_filenames 避免同名檔衝突、frontmatter 用 _yaml_str 防注入(與 security 對應)。

to_cypher · to_graphml · to_svg · to_canvas export.py 各處其他輸出格式。

段落式說明:

  • to_cypher(G, path) — 產生 cypher.txt(Neo4j / FalkorDB 匯入),_cypher_escape / _cypher_label 處理 Cypher 字串與 label 安全。
  • to_graphml(G, path) — 輸出 GraphML(Gephi / yEd)。
  • to_svg(G, path) — SVG 圖(Notion / GitHub 內嵌)。
  • to_canvas(G, …) — Obsidian Canvas 輸出。
  • backup_if_protected(out_dir) — 目標目錄受保護時先備份。

2. exporters/html.py — 互動圖 graph.html

to_html(nodes, edges, communities, output_path, …) → Path exporters/html.py:325產出單一檔案的互動圖:內嵌節點/邊/圖例 JSON + vis-network JS。

這就是本站 實作案例 互動圖的生成器。輸出是單一自含 HTML

  • 資料內嵌:節點(含顏色、大小、community)、邊、圖例各自序列化成 JS 常量(RAW_NODES / RAW_EDGES / LEGEND)寫進 <script>
  • 視覺化:用 vis-network(unpkg CDN)以 forceAtlas2Based 物理引擎畫力導向圖。
  • 側欄:搜尋、節點資訊(type / community / source / degree / neighbors)、社群圖例(可勾選隱藏)。
  • 超邊疊加_hyperedge_script 在 canvas afterDrawing 時畫半透明群組區域。
  • HTML-escape:節點 label 都經過 esc() 才進 innerHTML——這呼應 SECURITY.md 的 XSS 緩解。
教學重點:這個 exporter 是「怎麼把 NetworkX 圖變成可互動 web 應用」的完整範例——資料序列化、渲染、UI、安全 escape,全部塞進一個 <details> 都能理解的單一檔案。本站的 graph-zh.html 就是對它的產物做 UI 字串中文化。
_html_styles · _html_script · _viz_node_limit · _hyperedge_script exporters/html.py 各處HTML 的零件。

段落式說明:

  • _html_styles() — 回傳內嵌 CSS 字串(深色主題、側欄、圖例)。
  • _html_script(nodes_json, edges_json, legend_json) — 組出全部渲染 JS(dataset 建立、network 選項、搜尋、點選、圖例互動)。
  • _viz_node_limit() — 節點上限防護(超過就不生成 HTML,避免瀏覽器開不動)。
  • _hyperedge_script(hyperedges_json) — 超邊的 canvas 疊加渲染。

3. tree_html.py — 檔案樹圖

build_tree · emit_html · write_tree_html tree_html.py把目錄結構畫成互動樹狀圖。

段落式說明:build_tree(paths) 把檔案路徑清單組成一棵樹(_common_root 找出共同根、_make_truncation_leaf 處理過多子節點),emit_html / write_tree_html 渲染成自含 HTML。這讓瀏覽者先看到專案結構,再進圖。


4. callflow_html.py — 架構/呼叫流圖

load_graph · normalize_node · write_callflow_html … callflow_html.py從 graphify-out 產生 Mermaid 架構圖與呼叫流 HTML。

段落式說明:graphify export callflow-html 的實作。讀取圖資料(load_graph),把節點/邊正規化成統一形狀(normalize_node / normalize_edge),最後渲染成含 Mermaid 圖的 HTML。裝了 hook 時,每次 git commit 都會自動重新生成。

看完這頁你應該能說出:graph.json 為什麼拒絕「更小的覆寫」、graph.html 是怎麼做到單檔自含的、Obsidian vault 的 wikilink 從哪來。接著讀 ops 維運與安全 看 cache/dedup/serve 等服務模組。

① 進階真實情境 Worked Example:把一張圖輸出成四種格式、餵給四種消費者

前面是「看 shrink guard 的原理」;這次是跨工具的分發場景:同一份知識圖譜,要同時給「網頁瀏覽者、Obsidian 使用者、Neo4j 分析師、CI 報告」四種角色。

  1. 網頁瀏覽者 → graph.htmlgraphify export(或預設產出)給互動圖;確認節點數低於 _viz_node_limit,太大就分流。
  2. Obsidian 使用者 → vaultgraphify export --obsidian——每社群一篇 _COMMUNITY_<name>.md,節點間用 [[wikilink]] 互連;檔名安全化(_obsidian_safe_stem)避免跳字與同名衝突,frontmatter 走 _yaml_str 防注入。
  3. Neo4j/FalkorDB 分析師 → cypher.txtgraphify export --neo4jto_cypher 的產物,讓圖論分析直接在資料庫跑(Cypher 字串與 label 都有 _cypher_escape 保護)。
  4. CI/文件 → SVG + GraphML--svg 給 Notion/GitHub 內嵌,--graphml 給 Gephi/yEd 做深度視覺化。
  5. 對齊與版本化:所有輸出都來自同一張圖;built_at_commit 記在 graph.json,確保四份輸出都標明「由哪個 commit 產生」,不會有人拿過期圖做決策。
為什麼選這條路徑:export 的存在價值就是「一次建圖、多格式分發」——四種消費者共用同一份資料,卻各自用最順手的工具。路徑的關鍵是「單一來源 + 標註新鮮度」,否則四份輸出各自演進就會開始打架。

② 深入原理擴充:shrink guard、原子寫、與「圖的資料安全」

export 頁最被低估的是一堆「寫檔防護」——它們是「圖被自己搞壞」的最後防線:

大家以為建圖正確、但其實有誤的案例:以為「export 只是把圖序列化,不會改資料」。其實 export 階段會變造資料:節點補上 communitycommunity_namenorm_label(小寫去重音,供檢索);邊補 _src/_tgt 還原方向;還會因 shrink guard 拒絕寫入。若你拿「export 後的 graph.json」直接當建圖輸入重跑,會看到多餘屬性、甚至格式不相容——export 的產物是「給消費者讀的」,不是「給 pipeline 吃的」。想重跑要用原始 extraction/chunk 檔。

③ 診斷式疑難排解表

症狀可能原因解決方案
to_json 回傳 False、檔案沒更新shrink guard 擋下(新圖更小)或既有檔損壞讀不到檢查上一輪 chunk 檔是否遺失;確認後再 --force;不要盲推 --force
graph.json 半截/格式壞掉舊版沒走原子寫,或外部工具覆寫重新 export;確認 write_json_atomic 生效;serve 會印復原訊息
Obsidian vault 出現斷鏈 wikilink節點 label 含不安全的檔名字元(/、空格、跳字)確認 _obsidian_safe_stem_dedup_node_filenames 有走;重跑 export
互動圖無法生成節點數超過 _viz_node_limit(瀏覽器開不動)縮小語料、用社群篩選、或改用 SVG/GraphML
Neo4j 匯入報語法錯誤label 含特殊字元、Cypher 字串沒 escape確認 _cypher_escape/_cypher_label 有走;檢查產物 cypher.txt 內容

④ 進階挑戰題

  1. shrink guard 用「節點數」當判據。請設計一個反例:新圖節點數一樣甚至更多、但內容其實變差(例如語意邊大量遺失)——guard 會漏接。要怎麼改判據才能抓到這種「等量退化」?
  2. 「export 產物不適合當 pipeline 輸入」。如果要設計一個 round-trip(export → 重import → 原圖),你需要在 export 時保留哪些額外資訊,才能讓 round-trip 得到 identical graph?
  3. graph.html 是單一自含檔(1.8MB)。請評估「多檔 split(js/css 分開)」與「單檔 self-contained」在部署、快取、離線使用的取捨,並說明 graphify 為何選後者。