PHP+XML生成家谱树:从模型到渲染的完整实践

发布时间:2026/10/23 20:20:43
PHP+XML生成家谱树:从模型到渲染的完整实践

简介:一份基于PHP(服务端脚本语言)与XML(可扩展标记语言)的家谱树动态生成实例,面向具备PHP基础、了解Web开发流程的初学者,解决如何用后端脚本解析结构化数据并生成可视化家族关系图的问题。项目完整演示了处理链路:PHP脚本接收URL参数后,读取两个HTML文件中的XML数据,借助DOM接口提取家庭成员姓名与亲属关系,并将解析结果转为JSON格式,再转交给Flash组件渲染为可交互的树状图,体现了数据与展示分离的设计思路,也适合用作理解数据格式转换的练手案例。压缩包共五个文件,涵盖PHP逻辑文件、内嵌XML的HTML/HTM文件及一个SWF动画组件,总大小407KB,结构精简,便于直接导入运行和二次修改。已有802人学习下载,适合希望动手实践PHP+XML组合开发、快速理解DOM解析与前端图表呈现核心步骤的读者。

1. 整族关系乱成一团?PHP + XML 生成家谱树的落地起点

之前我接到一个家族信息整理的需求,几十口人的名字、辈分、婚姻关系全压在几张 Excel 表里,想做成能在线浏览的家谱树。我第一反应不是建 MySQL 表,而是去拿 XML。原因很直接:家谱数据量通常不大,但关系嵌套深、带有辈分、配偶、生卒年这些属性,XML 天然能表达层级,PHP 解析 XML 又几乎零成本。这篇笔记把「PHP + XML 生成家谱树」的完整路径拆开,从 XML 建模、递归解析到 HTML 渲染,一步步给出能抄的代码,也会把编码、特殊字符、递归深度这些坑提前摆出来。适合刚接触树形数据的前端开发者,也适合接私活时快速响应需求的后端新手,看完可以直接套到一个家族信息整理的小项目上。

2. 先把家谱定成 XML 模型:嵌套结构、解析选型与三个关键决定

家谱树的第一道坎不是写代码,而是把关系数据放对容器。XML 建模方式直接决定后面解析和渲染的难度,这一章先把「嵌套 XML 和扁平 XML 选哪个」讲透,再谈 PHP 侧的解析方案。

2.1 嵌套 XML 是首选:树长什么样,数据就长什么样

嵌套 XML 的意思是:父亲节点下面直接挂 children 子节点,子节点里再挂孙辈,一层层嵌下去。这种结构最大的好处是直观,肉眼扫一眼 XML,树的拓扑结构就出来了。

下面是我在小型家谱项目里最常用的模板:

<?xml version="1.0" encoding="UTF-8"?> <family name="某氏宗族"> <person id="p001" name="曾祖父" gender="male" birth="1901" death="1980"> <spouse id="p002" name="曾祖母" birth="1905" death="1978"/> <children> <person id="p003" name="祖父" gender="male" birth="1928" death="2001"> <spouse id="p004" name="祖母" birth="1930" death="2015"/> <children> <person id="p005" name="父亲" gender="male" birth="1952"> <spouse id="p006" name="母亲" birth="1956"/> <children> <person id="p007" name="我" gender="male" birth="1985"/> </children> </person> </children> </person> </children> </person> </family>

这段 XML 里,person 节点表示一个人,id 是全局唯一标识;spouse 表示配偶信息,放在 person 内部作为空元素;children 是所有子女的集合容器。每一层<person>嵌套代表一代人,叶子节点就是没有子女的成员。

用嵌套结构的核心收益在解析阶段:递归函数每深入一层,就是往前走了一代,不需要额外的关系索引表。如果项目是静态家谱文件、一次性生成静态页面,这种结构写起来最省力。

2.2 扁平 XML 适合什么场景:动态系统和多人协作才需要

扁平 XML 长这样:

<people> <person id="p001" name="曾祖父" father_id="0" mother_id="0"/> <person id="p002" name="曾祖母" father_id="0" mother_id="0"/> <person id="p003" name="祖父" father_id="p001" mother_id="p002"/> <person id="p005" name="父亲" father_id="p003" mother_id="p004"/> </people>

每条 person 只声明自己,用 father_id、mother_id 指向上游节点。它的优势是数据表结构完全对应关系数据库,新增成员不需要修改 XML 层级,适合多人协作维护的家谱管理后台。但代价是解析时要先遍历一遍所有人建索引,再根据 father_id 逐个人找子女,逻辑上多一道工夫。

我一般是这么选:只做展示型家谱树,用嵌套 XML;要做在线编辑、按人查关系链的系统,用扁平 XML。这份资源针对的是前一种,后面所有代码都基于嵌套结构展开。

2.3 解析选型:SimpleXML 是小型家谱数据的正解

PHP 处理 XML 的常规武器有三个:SimpleXML、DOMDocument、XMLReader。它们之间的取舍用一个表格就能说清:

对比维度SimpleXMLDOMDocumentXMLReader
内存占用整体载入,适合小文件整体载入,构建完整DOM树流式读取,内存占用极小
上手难度属性/子节点直接用对象访问需要大量 getElementsByTagName手动管理节点状态
代码量最少中等较多
最适合场景几百人以内家谱需要复杂XPath查询且文件不大上万人或超大XML文件

这份项目的典型规模是几十到几百人,XML 文件撑死几百 KB,SimpleXML 完全够用,而且代码写起来最接近「读数据」的自然语义。后文如果遇到极端情况,第六章会补一个大文件的流式方案。

2.4 加载 XML 和解析错误处理:代码要写得能自报故障

SimpleXML 加载 XML 很简单,但失败时返回 false 这个行为很容易被忽略。我在每次接手 XML 时会强制走一遍带错误收集的加载流程:

<?php $file = 'family.xml'; libxml_use_internal_errors(true); $xml = simplexml_load_file($file, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); if ($xml === false) { foreach (libxml_get_errors() as $error) { error_log(sprintf('XML 解析失败 第 %d 行: %s', $error->line, trim($error->message))); } libxml_clear_errors(); exit('XML 格式有误,请先检查 family.xml'); }

这段代码里有三个关键点。第一,libxml_use_internal_errors(true)让 PHP 不直接把解析错误抛成 Warning,而是存到 libxml 的缓存区里,这样程序可以自己决定怎么呈现。第二,LIBXML_NOCDATA会把<![CDATA[...]]>里的内容直接当作普通字符串,避免后面取到的还是一层 SimpleXMLElement 对象。第三,LIBXML_NOBLANKS会过滤纯空白文本节点,否则 XML 里的换行缩进会被解析成无意义的文本节点,干扰后续遍历。

2.5 三个建模关键决定

第一个决定:id 必须显式分配。家谱里同名人极多,尤其「王氏」「李氏」这类重复率高的名字,用 name 当唯一标识就是给自己埋雷。第二个决定:生卒年存成属性而不是子节点,取的时候$node['birth']一句话就拿到。第三个决定:配偶用空元素表示,不把配偶当成家谱树的下级节点,因为配偶不产生新的代数层级,放进 children 会污染树的深度。

这些决定在写递归的时候才会体现出价值,但源头在建模。XML 里偷的懒,后面解析阶段都要加倍还。

3. 递归拆解 XML:把节点变成数组的完整逻辑与内存注意点

XML 模型定了之后,第二步是把 SimpleXML 对象转成 PHP 数组。这一步的核心是一个递归函数,它负责把「一层层的 XML 嵌套」翻译成「一层层的 PHP 数组」。这一章把函数的每一行逻辑拆开讲,再看两个容易出问题的细节。

3.1 为什么树形结构必须走递归

家谱树的每一代节点都带着相同字段:id、name、gender、birth、death,下面可能还挂着 children。这就构成典型的递归结构——「一个人的数据结构里包含若干个人」。如果用循环来做,你得手工维护一个栈来记录当前深度和路径,代码又长又容易漏。递归的写法是:遇到 children 就调用自身,天然匹配这个结构。

伪代码思路是这样:

解析函数(节点): 生成当前成员信息数组 如果节点里有配偶,写入配偶 id 如果节点里有 children: 遍历 children 下的每个 person: 调用解析函数(子节点),结果追加到 children 列表 返回当前成员数组

3.2 核心递归函数:一次调用生成完整子树

直接看实现:

<?php function parseFamily(SimpleXMLElement $node): array { $item = [ 'id' => (string) $node['id'], 'name' => (string) $node['name'], 'gender' => (string) $node['gender'], 'birth' => (string) $node['birth'], 'death' => (string) $node['death'], 'spouse_ids' => [], 'children' => [], ]; if (isset($node->spouse) && $node->spouse['id']) { $item['spouse_ids'][] = (string) $node->spouse['id']; } if (isset($node->children)) { foreach ($node->children->person as $childNode) { $item['children'][] = parseFamily($childNode); } } return $item; } $xml = simplexml_load_file('family.xml'); $tree = parseFamily($xml->person);

这段代码的关键在三个地方。

第一,所有属性值都用(string)强转。SimpleXML 属性取出来并不是纯字符串,而是一个 SimpleXMLElement 对象,如果不强转,后面 json_encode 时会得到一串怪异的对象,而不是干净的数组。

第二,isset($node->spouse)这一句负责判断该节点有没有配偶。用 isset 而不是直接访问,是因为没有配偶的节点直接访问会抛警告,isset 则不会。

第三,$node->children->person的意思是:在当前节点下找到名为children的子节点,再遍历它下面所有名为person的子元素。这里用到了 SimpleXML 的属性式访问,->children访问的是元素名而不是children()方法,这是一个容易踩的坑,第五章专门讲。

3.3 spouse_ids 的设计:为什么用数组

有人会问,一个人只能有一个配偶,为什么 spouse_ids 要用数组?因为「一个配偶」是常态,但「多段婚姻」在真实家谱里并不罕见。尤其整理老族谱时,前妻后妻、继室续弦都很正常。

如果一开始就把配偶设计成单值'spouse_id' => 'p002',后面遇到多配偶就要重构整个 XML 和解析函数。改成数组后,XML 那边也只需在 person 节点下多放几个 spouse 元素:

<person id="p005" name="父亲" gender="male" birth="1952"> <spouse id="p006" name="母亲" birth="1956"/> <spouse id="p008" name="继母" birth="1960"/> <children> <person id="p007" name="我" gender="male" birth="1985"/> </children> </person>

解析函数会自动把两个配偶的 id 都收进spouse_ids。渲染层再根据 id 去查配偶姓名,动态拼接展示,逻辑完全不用改。

3.4 递归的出口和边界条件

递归函数最常见的翻车点是忘记写出口。这个函数里出口有两个:一是当前 person 节点没有 children,直接返回 $item;二是 children 下面没有 person 元素,foreach 空循环,自然结束。

我给你补一段防御性代码,在处理深层嵌套时可以限制最大代数,防止有人把死循环数据喂进来:

<?php function parseFamilySafe(SimpleXMLElement $node, int $depth = 0, int $maxDepth = 20): array { if ($depth > $maxDepth) { error_log("节点 {$node['id']} 超出最大深度 {$maxDepth},已截断"); return [ 'id' => (string) $node['id'], 'name' => (string) $node['name'], 'gender' => (string) $node['gender'], 'children' => [], ]; } $item = [ 'id' => (string) $node['id'], 'name' => (string) $node['name'], 'gender' => (string) $node['gender'], 'birth' => (string) $node['birth'], 'death' => (string) $node['death'], 'spouse_ids' => [], 'children' => [], ]; if (isset($node->children)) { foreach ($node->children->person as $childNode) { $item['children'][] = parseFamilySafe($childNode, $depth + 1, $maxDepth); } } return $item; }

$depth 参数每递归一层加一,超过 $maxDepth 就不继续往下解析,而是直接返回一个截断节点并记录错误日志。真实家谱能到 15 代以上已经很夸张了,20 层上限足够用。

3.5 解析完成后先打印一眼,再写渲染

我每次写完递归函数,第一步不是写 HTML,而是先裸打印看结构:

header('Content-Type: text/plain; charset=UTF-8'); print_r($tree);

这一步能马上暴露三个问题:字段是不是字符串、children 是不是真的嵌套进去了、配偶 id 有没有正确收集。当年我第一次写解析函数,漏了(string)强转,print_r 出来全是一堆 SimpleXML 对象,一眼就发现问题在哪。后面所有 XML 解析任务,我都保留这个习惯。

4. 渲染成网页不靠前端框架:PHP 输出嵌套列表的三板斧

数据结构搭好以后,渲染就是顺水推舟的事。家谱树的视觉表达核心是「层级缩进 + 连线」,用 HTML 的嵌套 ul/li 天然就是树的骨架,CSS 加几条线就是成型的家谱图。这一章直接给完整可跑的渲染代码。

4.1 递归渲染函数:从数组到 HTML

数组的结构和 XML 是同构的,所以渲染同样用递归。我把渲染函数单独拆出来,方便你嵌套到任何页面里:

<?php function renderTree(array $node, string $search = ''): string { $hit = false; if ($search !== '' && mb_strpos($node['name'], $search) !== false) { $hit = true; } $html = '<li' . ($hit ? ' class="hit"' : '') . '>'; $html .= '<span class="person">' . htmlspecialchars($node['name'], ENT_QUOTES, 'UTF-8') . '</span>'; if (!empty($node['death'])) { $html .= '<span class="lifespan">' . htmlspecialchars($node['birth'] . '-' . $node['death'], ENT_QUOTES, 'UTF-8') . '</span>'; } if (!empty($node['children'])) { $html .= '<ul>'; foreach ($node['children'] as $child) { $html .= renderTree($child, $search); } $html .= '</ul>'; } $html .= '</li>'; return $html; } $htmlTree = '<ul class="tree">' . renderTree($tree, $_GET['q'] ?? '') . '</ul>';

这个函数里,$hit 变量用来标记当前成员是否匹配搜索词,匹配就加class="hit",方便 CSS 高亮。渲染时用 htmlspecialchars 包住姓名和生卒年,防止 XML 里的特殊字符在 HTML 里变成注入点。递归出口同样是「没有 children」时直接闭合 li。

4.2 CSS 连线:伪元素画竖线和横线的思路

树形结构最常见的样式问题是「线怎么画」。用真实边框图片太笨,用 SVG 又过重。CSS 伪元素是最轻的方案:

.tree, .tree ul { list-style: none; margin: 0; padding-left: 1.6em; } .tree li { position: relative; padding: 0.35em 0; } .tree li::before { content: ""; position: absolute; left: -1.2em; top: 1.1em; width: 1em; height: 100%; border-left: 1px solid #b0a99c; border-bottom: 1px solid #b0a99c; } .tree li:last-child::before { height: 1.1em; } .tree .lifespan { color: #888; font-size: 0.85em; margin-left: 0.5em; } .tree .hit { background: #fff3bf; outline: 1px solid #f0b429; }

原理不复杂:每个 li 的 ::before 画一条从左上角向下 100% 的竖线,再靠 border-bottom 画一条横线,整体看起来就是「父节点右侧伸出一条横线,连到子女的竖线」。:last-child 截断竖线是因为最后一个节点下面不该再有连线。这套结构在老式浏览器里的兼容性也不错,不需要 flex 或 grid。

4.3 配偶姓名怎么补:建一个 id => name 映射表

渲染函数里只有成员自己的姓名,配偶只存了 id。要显示配偶姓名,需要提前在整棵树里过一遍,收集 id 到姓名的对应关系:

<?php function collectPeople(array $node, array &$map): void { $map[$node['id']] = $node['name']; foreach ($node['children'] as $child) { collectPeople($child, $map); } } $peopleMap = []; collectPeople($tree, $peopleMap);

然后渲染函数里加一段配偶输出:

if (!empty($node['spouse_ids'])) { foreach ($node['spouse_ids'] as $spouseId) { if (isset($peopleMap[$spouseId])) { $html .= '<span class="spouse">配 ' . htmlspecialchars($peopleMap[$spouseId], ENT_QUOTES, 'UTF-8') . '</span>'; } } }

这里有个现实问题:配偶节点在嵌套 XML 里是空元素,不会作为 person 出现在解析结果里,所以不会进入 $peopleMap。我实际处理时,会在解析阶段额外做一次 XML 全局遍历,把所有<spouse>属性也收进 map:

<?php foreach ($xml->xpath('//person') as $p) { $map[(string) $p['id']] = (string) $p['name']; } foreach ($xml->xpath('//spouse') as $sp) { $map[(string) $sp['id']] = (string) $sp['name']; }

xpath 在这里比递归遍历简单得多,尤其适合配偶这种散落在各层的节点。

4.4 完整页面骨架:一个 PHP 文件跑通整棵树

把上面所有片段拼成一个完整页面流程:

<?php $xml = simplexml_load_file('family.xml'); $tree = parseFamily($xml->person); $peopleMap = []; foreach ($xml->xpath('//person') as $p) { $peopleMap[(string) $p['id']] = (string) $p['name']; } foreach ($xml->xpath('//spouse') as $sp) { $peopleMap[(string) $sp['id']] = (string) $sp['name']; } $search = $_GET['q'] ?? ''; ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>家谱树</title> <style> /* 上一节的 CSS 放这里 */ </style> </head> <body> <form method="get"> <input type="text" name="q" value="<?= htmlspecialchars($search, ENT_QUOTES, 'UTF-8') ?>" placeholder="输入姓名搜索"> <button type="submit">查找</button> </form> <ul class="tree"> <?= renderTree($tree, $search) ?> </ul> </body> </html>

这个文件就能直接跑了。搜索逻辑是在渲染时给匹配的 li 加高亮类,用户输入什么,renderTree 的第二个参数就传什么。注意渲染函数继承了 PHP 的字符串拼接方式,别在模板里用短标签写复杂逻辑。

4.5 渲染层性能边界:多少人以内可以直接输出

SimpleXML 整体载入 + 递归渲染,几百人以内是完全无感的。我曾经拿一个 800 多人、5 代的家谱数据测试,整个渲染过程在 50 毫秒级。但如果超过 3000 人,页面 HTML 可能达到几百 KB,这时候就该考虑分页查看,或者按代数折叠展示,默认只展开前 3 代,其余用按钮展开。折叠逻辑可以在 renderTree 里加一个 $depth 参数,超过阈值就不输出 children。

5. 避坑手册:编码、特殊字符与 children 这个名字的陷阱

这个项目里真正让我花时间排查的不是算法,而是几个看起来很小但破坏力很强的细节。每一条都来自实际踩过的坑,按「现象 → 原因 → 解决」记下来。

5.1 中文全部变成乱码,页面显示一堆问号

现象:浏览器打开页面后,所有中文姓名、地名全部显示成「???」或者乱码方块。

原因:最常见的是 XML 文件本身是 GBK 或 GB2312 编码保存的,但 XML 声明写的是encoding="UTF-8"。SimpleXML 按 UTF-8 去读 GBK 的字节流,中文自然全部解析失败。另一个常见原因是 PHP 脚本文件带了 BOM,BOM 字符会混进输出流,导致浏览器识别编码出错。

解决:强制统一为 UTF-8 无 BOM。XML 文件用编辑器另存为 UTF-8 编码,PHP 文件也保持 UTF-8,并在输出 HTML 时加<meta charset="UTF-8">。如果 XML 已经乱码,可以用文本编辑器打开转码,或者用 PHP 读取后转码再解析。

5.2 成员名字里带 & 符号,XML 直接解析失败

现象:simplexml_load_file 返回 false,错误日志里显示Entity 'xxx' not defined。

原因:XML 规范里,&开头的字符序列会被当成实体引用。名字里有「AT&T」或者「张 A&B」时,& 后面对应的内容不是已定义的实体,解析器直接报错。<和>同理,会把 XML 标签结构搞乱。

解决:生成 XML 文件时,对所有成员姓名做 XML 转义。PHP 有一个非常合适的函数:

<?php $name = '张 A&B <公司>'; $safeName = htmlspecialchars($name, ENT_XML1 | ENT_COMPAT, 'UTF-8');

ENT_XML1 是专门为 XML 场景设计的转义模式,会转义&、<、>,而且不会产生&nbsp;这类非 XML 实体。如果是手写 XML,记住一条规则:姓名里出现&一律写成&amp;。

5.3$node->children和$node->children()是完全不同的东西

现象:解析代码里写foreach ($node->children() as $child),结果把出生年份、性别这种文本节点也当成 person 处理了。

原因:SimpleXML 的对象里,children()是方法,返回当前节点的所有子元素;但如果我们用的 XML 结构里确实有一个名为children的元素,那么$node->children(不加括号)访问的是这个元素。两者语法上只差一对括号,语义天差地别。很多人写嵌套结构时顺手写children(),拿到的是根节点下所有子元素的集合,自然不是预期结果。

解决:两种方案任选。要么在代码里始终用$xml->children->person这种属性式访问,明确拿到children元素再取 person;要么把 XML 里的集合节点改名为<childs>,彻底避开方法名冲突。我后来统一改用<childs>,代码里写$node->childs->person,大脑不用再区分加不加括号了。

<person id="p003" name="祖父" gender="male"> <spouse id="p004" name="祖母"/> <childs> <person id="p005" name="父亲" gender="male"/> </childs> </person>

对应解析代码:

if (isset($node->childs)) { foreach ($node->childs->person as $childNode) { $item['children'][] = parseFamily($childNode); } }

5.4 递归层级太深,PHP 直接报 Maximum function nesting level

现象:页面白屏,日志里出现Maximum function nesting level of 256 reached。

原因:开发环境里 Xdebug 默认限制函数嵌套层数,一般是 256 层。家谱数据如果超过 10 代,配合渲染函数每层调用两三个函数,很容易在调试模式撞上这个上限。生产环境没有 Xdebug 通常不会报这个错,但会表现为内存占用异常飙升。

解决:如果只是开发环境限制,可以在解析文件入口手动提高限制:

<?php ini_set('xdebug.max_nesting_level', 1000);

更保险的做法是给递归函数加深度参数,超过设定代数直接截断,不去无限递归。另外注意,递归时临时变量会被层层保留,解析几千人的家谱前先算一下 PHP 的内存限制memory_limit,不够就临时调大。

5.5 两个同名的「张伟」被程序当成了同一个人

现象:搜索某个常见姓名时,页面把所有同名的人都合并成一个节点,关系链全乱。

原因:我在 2.5 里强调过 id 必须显式分配,但很多人拿到现成数据后偷懒用 name 作为数组键名。两个同名的人使用同一个键,后面的数据直接覆盖前面的,渲染时就出现了「同一人出现在两个位置」的怪象。

解决:严格遵守 id 唯一。如果没有现成 id,用循环自动生成也行:

<?php $id = 'p' . str_pad(++$counter, 3, '0', STR_PAD_LEFT);

更严谨的做法是在数据准备阶段做一次重复检查,把所有 name 相同的成员列出来逐个人工确认,再分配 id 写进 XML。做族谱整理时这个步骤省不了,别指望代码能自动区分两个同名的人。

6. 进阶:XMLReader 流式解析、搜索定位与 JSON 导出

前面整套方案适合几百人规模。如果手里是一份几万人的电子族谱,再用 SimpleXML 一次性载入,内存会吃不消。这时候把加载方式换成 XMLReader,代码改动不大,内存表现却完全不一样。

XMLReader 是 PHP 内置的流式解析器,它不会把整个 XML 塞进内存,而是像流水线一样从头遍历到尾部,每经过一个节点触发一次判断:

<?php $reader = new XMLReader(); $reader->open('family.xml'); while ($reader->read()) { if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'person') { $name = $reader->getAttribute('name'); $id = $reader->getAttribute('id'); // 按需处理,比如只收集少量字段,或写入临时文件 } } $reader->close();

这段代码只读取 person 元素的 name 和 id 属性,不构建任何父子关系,适合做全量扫描、统计人数、生成索引这类前置任务。XMLReader 的代价是拿不到树的整体结构,需要自己维护栈来重建层级,所以它跟 SimpleXML 的分工是:大文件做预处理用 XMLReader,渲染时仍然走解析好的数组。

搜索定位功能在上一章的渲染函数里已经内置了,通过$_GET['q']传入搜索词,命中成员自动加class="hit"高亮。如果你想做得更顺手,可以在搜索时默认展开到目标节点所在层级,实现方式是给 renderTree 传一个目标 id,当节点链路上出现该 id 时就强制输出 children。

最后是 JSON 导出。家谱树数据解析成纯数组后,JSON 只需要一行代码:

<?php header('Content-Type: application/json; charset=UTF-8'); echo json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

JSON_UNESCAPED_UNICODE 保证中文不变成\uXXXX的转义序列,JSON_PRETTY_PRINT 让输出可读。导出的 JSON 可以直接喂给 Vue、React 这类前端框架做交互式家谱图,后端代码完全不用改。

做了几个家谱树相关项目之后,我养成了一个固定习惯:每次开工先检查 XML 文件编码,再确认每个节点都有唯一 id,然后才写解析代码。这两个动作看起来不起眼,却能挡住后续 80% 的返工。希望这篇笔记帮你在 PHP + XML 生成家谱树的路上少走几步弯路。

本文还有配套的精品资源,点击获取