traverse 這樣的遞迴 (Recursion) 結構,
正是用來處理「樹狀結構 (Tree Structure)」的最強大武器。
不管是 JSON 設定檔、資料夾裡的檔案架構,
還是程式語言的 AST (抽象語法樹, Abstract Syntax Tree),
在本質上都是一種樹狀結構:它們都有一個「根節點」,
然後往下分岔出無數個「子節點」,子節點又會繼續往下分岔。
這裡我們用一個簡單的例子,
帶你了解如何用遞迴來「走訪 (Traverse)」這些資料,
並且看看這跟 AST 有什麼關聯。
第一步:理解遞迴的核心精神 —— 「俄羅斯娃娃」
遞迴的核心精神就是「自己呼叫自己」。
想像你在玩一個巨大的俄羅斯娃娃,
你的目標是把裡面所有的玩偶都拿出來排好。
你可以把這件事寫成一個規則:
def 打開娃娃(當前的娃娃):
# 1. 終止條件 (Base Case):如果沒有更小的了,就停下來
if 當前的娃娃.是實心的():
紀錄("找到最核心的實心娃娃了!")
return
# 2. 遞迴條件 (Recursive Step):如果裡面還有娃娃
else:
# 先從當前的娃娃肚子裡,把小娃娃「拿出來」
裡面的小娃娃 = 當前的娃娃.取出內容物()
# 然後針對這個小娃娃,再次執行「打開娃娃」的任務(自己呼叫自己)
打開娃娃(裡面的小娃娃) 第二步:簡化的 JSON 遞迴範例
回到程式碼裡的 traverse 結構。
假設我們有一個非常簡單的 JSON
(其實在 Python 裡這就是巢狀字典和列表),
我們目標是「找出所有存在 class_name 的積木」。
data = {
"step1": {
"class_name": "LoginStep",
"description": "User logs in"
},
"step2": [
{
"class_name": "VerificationStep",
"type": "email"
},
"這是一個普通的字串,不用理他"
]
}這個結構裡有字典 (dict) 也有列表 (list)。
我們的遞迴可以這樣寫:
def simple_traverse(node):
# 只要是字典,我們就來檢查
if isinstance(node, dict):
# 1. 如果字典裡有 class_name,我們就抓到目標了!印出來
if "class_name" in node:
print(f"找到目標囉!class_name 叫做: {node['class_name']}")
# 2. 為了確保我們沒有漏掉字典「更深處」的東西,我們把字典裡所有的 value 都拿出來,再走訪一次
for key, value in node.items():
simple_traverse(value) # 遞迴:繼續往下層找
# 如果遇到的是一個列表,那就把列表裡的東西一個一個拿出來走訪
elif isinstance(node, list):
for item in node:
simple_traverse(item) # 遞迴:繼續往下層找
# 開始走訪
simple_traverse(data)輸出結果:

你看,我們完全不需要知道這個 JSON 到底有幾層,
只要透過 simple_traverse(value) 和 simple_traverse(item),
程式就會像藤蔓一樣自動延伸到底部,
這就是遞迴的魅力!
第三步:這跟 AST (抽象語法樹) 的關係?
是的,處理 AST 百分之百就是用這種遞迴結構!
當 Python (或其他編譯器) 讀取你的程式碼時,
它會把文字轉換成一棵抽象語法樹 (AST)。
例如你寫了一句 a = 1 + 2,
在 AST 裡面可能長這樣 (簡化版):
ast_tree = {
"type": "Assign", # 這是一個指派運算 (Assign)
"targets": ["a"], # 左邊是要被賦值的變數 "a"
"value": { # 右邊要賦的值是一個加法運算 (BinOp)
"type": "BinOp",
"operator": "Add", # 運算子是加號
"left": { "type": "Constant", "value": 1 }, # 左邊的常數 1
"right": { "type": "Constant", "value": 2 } # 右邊的常數 2
}
}這不就跟前面的 JSON 長得一模一樣嗎!
如果開發者想用 AST 工具來檢查
這份程式碼「有沒有用到加法」,
他們寫的程式碼邏輯,
跟寫在 get_path_for_class_v4.py 裡的
traverse 幾乎是同一個模子刻出來的:
def check_addition_ast(node):
# 如果這是一個字典型態的節點
if isinstance(node, dict):
# 如果這個節點是一個 加法運算 (Add)
if node.get("operator") == "Add":
print("警告:這段程式碼使用了加法!")
# 繼續遞迴走訪底下的所有節點 (把左邊、右邊都拆出來檢查)
for key, child_node in node.items():
check_addition_ast(child_node)
check_addition_ast(ast_tree)
# 輸出: 警告:這段程式碼使用了加法!
總結
遞迴 (Recursion) 最適合用來處理「深度未知的主體」,
例如 JSON、資料夾、XML、或是 AST。
get_path_for_class_v4.py 的寫法:
檢查是否為 dict/list -> 做該層該做的事 -> 取出子結點繼續 traverse,
這個骨架其實是所有語法解析器、
甚至爬蟲都在使用的標準設計模式 (Design Pattern)。
等你以後要處理像爬梳 HTML 標籤或是 AST 時,
這個老朋友一定會再跟你見面的!
推薦hahow線上學習python: https://igrape.net/30afN
進階挑戰:不只要找到娃娃,
還要記錄「娃娃在哪裡」(路徑追蹤)
在實務上,我們通常不只滿足於「找到東西」,
我們更想知道「這個東西藏在什麼路徑底下」。
這就像在迷宮中留下麵包屑,
我們可以在遞迴函式中多加上一個代表「目前位置」的參數。
讓我們擴充一下剛才的例子,
這次我們要記錄 class_name 發生的完整路徑。
data = {
"step1": {
"class_name": "LoginStep",
"description": "User logs in"
},
"step2": [
{
"class_name": "VerificationStep",
"type": "email"
},
{"nested_node": {"class_name": "DeepStep"}} # 藏得很深的一層
]
}這次,我們給 traverse 加上一個 current_path 參數,
隨著每次遞迴往下,這個路徑就會變長:
def traverse_with_path(node, current_path="root"):
# 只要是字典,我們就來檢查
if isinstance(node, dict):
# 1. 抓到目標!把目前的「路徑」跟「目標」印出來
if "class_name" in node:
print(f"📍 找到目標!路徑: [{current_path}] -> class_name: {node['class_name']}")
# 2. 繼續往下挖
for key, value in node.items():
# 準備下一層的路徑 (把目前的 key 加上去)
# 例如: root -> root | step1
new_path = f"{current_path} | {key}"
traverse_with_path(value, new_path)
# 如果遇到的是一個列表
elif isinstance(node, list):
for index, item in enumerate(node):
# 列表的路徑我們用 index 來表示
# 例如: root | step2 -> root | step2 | 0
new_path = f"{current_path} | {index}"
traverse_with_path(item, new_path)
# 開始走訪,起點路徑就叫做 "root"
traverse_with_path(data)輸出結果:

💡 路徑追蹤的關鍵技巧
這個寫法有兩個關鍵點:
路徑狀態的傳遞:
我們不是在函式外面宣告一個全域變數來記錄路徑,
而是把 new_path 當作參數傳給下一層。
這樣一來,每一層看見的都是屬於它自己的「歷史軌跡」。
字典用 Key,列表用 Index:
當遇到字典 (dict) 時,我們串接的是字典的 key;
當遇到列表 (list) 時,因為列表沒有 key,
我們改用 index(索引用法:for index, item in enumerate(…))
來表示它的位置。
遞迴不僅是探索未知的利器,
也是繪製地圖的最佳導航的最佳方式。
推薦hahow線上學習python: https://igrape.net/30afN







![Python 讀取 DOCX 圖片關聯:qn+find/findall 與 XPath 的實戰對照 from lxml import etree ; from docx.oxml.ns import qn; lxml.etree._Element.findall( f”.//{ qn(‘a:blip’) }” ) ; .get( qn(“r:embed”) ) #獲取 屬性名 ‘r:embed’ 的 屬性值(如: ‘rId4’) ; lxml.etree._Element.xpath( “//a:blip/@r:embed”, namespaces = NS) #/@r:embed = 獲取 屬性名 ‘r:embed’ 的 屬性值(如: ‘rId4’),使用.findall() 要先.findall()獲取List[_Element],再迴圈_Element.get()獲取屬性值, .xpath() 第一個參數path 使用”//a:blip/@r:embed” ,可直接獲取屬性值(List[str]如: [‘rId4’, ‘rId5’]) ; 如何對docx真實移除圖片瘦身? Python 讀取 DOCX 圖片關聯:qn+find/findall 與 XPath 的實戰對照 from lxml import etree ; from docx.oxml.ns import qn; lxml.etree._Element.findall( f”.//{ qn(‘a:blip’) }” ) ; .get( qn(“r:embed”) ) #獲取 屬性名 ‘r:embed’ 的 屬性值(如: ‘rId4’) ; lxml.etree._Element.xpath( “//a:blip/@r:embed”, namespaces = NS) #/@r:embed = 獲取 屬性名 ‘r:embed’ 的 屬性值(如: ‘rId4’),使用.findall() 要先.findall()獲取List[_Element],再迴圈_Element.get()獲取屬性值, .xpath() 第一個參數path 使用”//a:blip/@r:embed” ,可直接獲取屬性值(List[str]如: [‘rId4’, ‘rId5’]) ; 如何對docx真實移除圖片瘦身?](https://i0.wp.com/savingking.com.tw/wp-content/uploads/2025/11/20251119130848_0_3fbf6b.png?quality=90&zoom=2&ssl=1&resize=350%2C233)


近期留言