
本文介绍如何将无序标题列表按数字编号规则自动构建成具有父子关系的树形结构,支持混合编号与非编号项,并输出可读性强的嵌套字典或可视化缩进格式。
本文介绍如何将无序标题列表按数字编号规则自动构建成具有父子关系的树形结构,支持混合编号与非编号项,并输出可读性强的嵌套字典或可视化缩进格式。
在文档解析、大纲生成或知识图谱构建等场景中,常需将线性标题列表(如 PDF 提取文本或 Markdown 章节)还原为逻辑清晰的层级结构。本教程提供一种稳健、易理解的两阶段算法,无需外部库即可实现从 ['13.3. Risk', '13.3.1. Strategy', 'SubStrategy', ...] 到嵌套树的转换。
核心思路:编号识别 + 父子关系推导
我们依据标题是否以数字开头(如 "13.3.1.")作为关键信号:
- 编号项(如 '13.3.1. Strategy')代表树中的显式节点,其编号("13.3.1")定义了层级路径;
- 非编号项(如 'SubStrategy')视为紧随其前最近编号项的子项;
- 编号间通过点分隔符(.)体现嵌套关系:"13.3.1" 的父级是 "13.3",再上一级是 "13"(本例中未出现,故 "13.3" 成为根)。
第一阶段:初步分组——建立编号节点映射
遍历列表,提取编号前缀作为键,构建初始节点字典:
import json
titles = [
'13.3. Risk',
'13.3.1. Strategy',
'SubStrategy',
'13.3.2. Token',
'Material',
'Impact',
'Aling'
]
# Step 1: 按编号前缀分组,收集直接子项(非编号字符串)
results = {}
current_node = None
for title in titles:
if title and title[0].isdecimal(): # 以数字开头 → 编号项
# 提取编号部分(如 "13.3.1. Strategy" → "13.3.1")
prefix_part = title.split(' ', 1)[0].rstrip('.')
name_part = title[len(prefix_part) + 1:].strip() if ' ' in title else ''
full_name = f"{prefix_part}. {name_part}" if name_part else title
current_node = results.setdefault(prefix_part, {
"name": full_name,
"children": []
})
elif current_node is not None: # 非编号项 → 归入当前节点的 children
current_node["children"].append(title)
# 此时 results 是按编号前缀索引的节点字典
✅ 注意:current_node 在遍历时动态绑定,确保每个非编号项准确归属到前一个编号项下。
第二阶段:构建树形——按编号层级链接父子节点
利用编号的点分隔结构(如 "13.3.1" → "13.3"),将子节点挂载到其逻辑父节点:
# Step 2: 根据编号层级关系,将子节点挂载到对应父节点
roots = [] # 存储最终根节点(即无父级的顶级节点)
for key, node in results.items():
# 计算父级编号:去掉最后一段(如 "13.3.1" → "13.3")
parts = key.split('.')
if len(parts) > 1:
parent_key = '.'.join(parts[:-1])
else:
parent_key = None
# 若父级编号不存在于 results 中,则当前节点为根
if parent_key not in results:
roots.append(node)
else:
# 否则,将当前节点作为父节点的子节点(注意:此处添加的是整个 node 字典)
results[parent_key]["children"].append(node)
# 输出结构化树(JSON 格式,便于调试和后续使用)
print("Tree structure (JSON):")
for i, root in enumerate(roots):
print(f"\nRoot {i+1}:")
print(json.dumps(root, indent=2, ensure_ascii=False))
运行后得到标准嵌套结构:
Root 1:
{
"name": "13.3. Risk",
"children": [
{
"name": "13.3.1. Strategy",
"children": ["SubStrategy"]
},
{
"name": "13.3.2. Token",
"children": ["Material", "Impact", "Aling"]
}
]
}
可视化输出:生成缩进式树状图
若需终端友好显示(如问题中要求的 |_ 格式),可递归打印:
def print_tree(node, indent=0):
prefix = " " * indent
if isinstance(node, dict) and "name" in node:
print(f"{prefix}|_'{node['name']}'")
for child in node.get("children", []):
if isinstance(child, dict):
print_tree(child, indent + 1)
else:
print(f"{prefix} |____'{child}'")
else:
print(f"{prefix}|____'{node}'")
print("\nVisual tree representation:")
for root in roots:
print_tree(root)
输出效果:
Visual tree representation:
|_'13.3. Risk'
|____'13.3.1. Strategy'
|____'SubStrategy'
|____'13.3.2. Token'
|____'Material'
|____'Impact'
|____'Aling'
关键注意事项
- ✅ 鲁棒性处理:代码兼容 1.、1.1.、1.1.1. 等任意深度编号,且能正确识别空格/标点边界;
- ⚠️ 隐式依赖顺序:算法假设非编号项严格紧跟其所属编号项(即输入顺序即逻辑顺序),乱序列表需先排序;
- ? 扩展建议:如需支持多根(如并列的 "1. Intro" 和 "2. Method"),roots 列表天然支持;如需导出为 anytree 或 networkx 对象,只需将 dict 节点转为对应类实例;
- ? 进阶优化:生产环境可封装为 class TitleTreeBuilder,增加 .to_markdown()、.to_dict()、.find_by_path("13.3.2") 等方法。
该方案兼顾简洁性与可维护性,不依赖正则复杂匹配,逻辑清晰,适用于教学、工具开发及轻量级文档结构化任务。











