在C#开发里,接口对接或者配置文件解析常会遇到层级很深的XML。如果直接按节点循环,代码既难写也难维护。把XML扁平化成DataTable,相当于把树拉平为行和列,下游用SQL或Excel处理就轻松很多。下面先看一张示意。

所谓扁平化,并不是简单把节点名当列名,而是要决定哪些路径作为一列、哪些重复节点生成多行。比如一个订单XML里,订单头信息是一条,订单明细是多个,那就应该以明细为行,把头部字段横向拼到每一行上。
基础方案:用XmlDocument递归读取
最直观的做法是用XmlDocument加载XML,再写递归函数收集所有叶子节点的路径。每遇到一个叶子节点,就往当前行写入对应列的值。如果某个路径第一次出现,就给DataTable加一列。
这种方法的优点是逻辑清楚,不依赖第三方库;缺点是对大规模XML性能一般,而且属性与文本节点需要区分处理。下面示例展示了一个简化版本,假设我们只处理元素文本和属性。
using System;
using System.Data;
using System.Xml;
public class XmlFlattener
{
private DataTable _dt = new DataTable();
private void EnsureColumn(string name)
{
// 列名重复时加后缀,避免异常
if (!_dt.Columns.Contains(name))
{
_dt.Columns.Add(name);
}
}
private void Walk(XmlNode node, string path, DataRow row)
{
if (node.Attributes != null)
{
foreach (XmlAttribute attr in node.Attributes)
{
string col = path + "@" + attr.Name;
EnsureColumn(col);
row[col] = attr.Value;
}
}
if (node.HasChildNodes)
{
foreach (XmlNode child in node.ChildNodes)
{
if (child.NodeType == XmlNodeType.Element)
{
Walk(child, path + child.Name + ".", row);
}
else if (child.NodeType == XmlNodeType.Text)
{
EnsureColumn(path.TrimEnd('.'));
row[path.TrimEnd('.')] = child.Value;
}
}
}
}
public DataTable Convert(string xml)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
DataRow row = _dt.NewRow();
Walk(doc.DocumentElement, "", row);
_dt.Rows.Add(row);
return _dt;
}
}
属性与文本的区分
上面的代码里,属性用@符号前缀,文本直接放在去掉末尾点的路径上。这样在表里就能同时看到节点属性和内容,不会互相覆盖。实际项目中,很多人忽略属性,导致数据缺失。
如果XML里同一个父节点下有多个同名子元素,基础递归会把它们当作同一列反复赋值,只保留最后一个。要解决这个问题,需要把重复元素转成多行,见下一节。
处理重复节点生成多行
当某个路径下出现多个相同元素,比如多个<item>,合理的扁平化是每一个item一行。我们可以约定:遇到多个子元素且名称相同,就为每个元素新建一行,并把已收集的上级字段复制过去。
实现时可以在递归中判断同级同名节点数量,大于一就触发分行逻辑。下面代码演示了如何把订单明细展开。
using System;
using System.Data;
using System.Xml;
using System.Collections.Generic;
public class OrderXmlToTable
{
public DataTable Convert(string xml)
{
DataTable dt = new DataTable();
dt.Columns.Add("OrderId");
dt.Columns.Add("Customer");
dt.Columns.Add("ItemName");
dt.Columns.Add("Qty");
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
XmlNode order = doc.SelectSingleNode("/Order");
string orderId = order.Attributes["id"].Value;
string customer = order.SelectSingleNode("Customer").InnerText;
foreach (XmlNode item in order.SelectNodes("Item"))
{
DataRow row = dt.NewRow();
row["OrderId"] = orderId;
row["Customer"] = customer;
row["ItemName"] = item.SelectSingleNode("Name").InnerText;
row["Qty"] = item.SelectSingleNode("Qty").InnerText;
dt.Rows.Add(row);
}
return dt;
}
}
动态列与固定列的取舍
固定列写法简单,适合结构已知;如果XML结构经常变,还是要用第一节的动态EnsureColumn方式,只是要在遇到重复元素时改为分行而不是覆盖。两者结合,才能兼顾灵活与准确。
要注意,动态列可能生产出几十列,界面展示前最好过滤掉全为空值的列,减轻用户阅读负担。
使用LINQ to XML简化代码
除了XmlDocument,XDocument配合LINQ查询写起来更短。特别在.NET Core之后,XDocument内存占用更友好。我们可以用Descendants直接拿特定节点。
下面示例把任意层级的error节点提取成一行一个错误,适合日志型XML。代码虽短,但只适合路径明确的场景。
using System;
using System.Data;
using System.Linq;
using System.Xml.Linq;
public class LinqFlatten
{
public DataTable ErrorsToTable(string xml)
{
XDocument x = XDocument.Parse(xml);
DataTable dt = new DataTable();
dt.Columns.Add("Code");
dt.Columns.Add("Msg");
foreach (var e in x.Descendants("error"))
{
DataRow r = dt.NewRow();
r["Code"] = e.Attribute("code")?.Value;
r["Msg"] = e.Value;
dt.Rows.Add(r);
}
return dt;
}
}
性能与异常注意
LINQ写法可读性高,但Descendants会遍历整棵树,超大数据文件建议用XmlReader逐节点读,边读边写DataTable,避免全量载入内存。
另外,XML里如果包含命名空间,SelectSingleNode和Descendants都要带上命名空间管理器,否则查不到节点,这是常见坑。
总结实践建议
复杂XML转DataTable,先弄清业务上谁该是一行,再决定递归还是定点查询。结构固定用XDocument最省事,结构多变用XmlDocument动态列更稳。遇到重复节点务必分行,属性与文本都要保留,空白列可后置清理。
只要把握住路径即列、重复即行这两个原则,绝大多数扁平化需求都能在百行代码内解决,不必引入重量级的映射框架。
C#XML_to_DataTableXML_flatten修改时间:2026-08-02 20:24:30