使用 LINQ 比较 XML 文件
使用 LINQ 在 XML 文件中进行变更检测。
引言
变更检测算法用于查找两个文档之间的差异,并构建这些差异的 delta 表示。它可用于存档系统、版本控制,或者仅用于您通用应用程序中的一个小需求。我们有两个 XML 文件,我们将生成一个 delta 文件来保存更改。没有 delta 文件的标准。
我一直在研究 XML 文件中的变更检测,然后我对自己说,为什么不使用 LINQ 呢? 本文将向您展示如何使用 LINQ 检测 XML 文件中的删除、插入和移动操作。本文的编写仅用于提供一个想法;它不是一个完整的差异算法。
使用代码
我通过使用这样的 XML 文件来举例说明
<?xml version="1.0" encoding="utf-8" ?>
<employees>
<employee salaried="no">
<name>Test test</name>
<hire_date>7/31/1996</hire_date>
</employee>
<employee salaried="yes">
<name>ZEYNEP PEHLIVAN</name>
<hire_date>1/1/2009</hire_date>
</employee>
</employees>
我将在 employee 标签级别检测更改,但您可以根据您的需要和 XML 文件的结构进行更改。
我们有这些步骤
- 初始化
- 检测变更
在此步骤中,将为每个节点生成一个哈希值,并使用 ID
属性将其添加到每个节点。 POS
属性也被添加。我们需要 ID
和 POS
属性才能在 LINQ 中进行连接。
POS
= 父节点的祖先计数 + "-" + 父节点的右侧兄弟节点的计数 + 节点的祖先计数 + "-" + 节点的右侧兄弟节点的计数。
首先,我们获取所有元素。 BAsicXName
在此示例中为 “employee”。
var source = from o in Version1.DescendantsAndSelf(BasicXName)
where o.Attribute("ID") != null && o.Name == BasicXName
select new
{
ID = o.Attribute("ID").Value,
POS = o.Attribute("POS").Value,
Element = o
};
var version = from o in Version2.DescendantsAndSelf(BasicXName)
where o.Attribute("ID") != null && o.Name == BasicXName
select new
{
ID = o.Attribute("ID").Value,
POS = o.Attribute("POS").Value,
Element = o
};
MOVE(x,y,z)
: 将子节点 x 从节点 y 移动到节点 z。这可以被视为删除和插入操作。这意味着,节点 x 首先被删除,然后被插入作为 z 的子节点。如果一个节点在不改变其父节点的情况下改变其顺序,则可以将其视为移动操作,在有序树中,如下图所示。
如果两个节点具有相同的 ID 但不同的 POS,则该节点被视为从 POS1 移动到 POS2。为了检测此操作,通过过滤相同的 POS 属性,对 ID
属性使用内连接。
var Moved = from o in version
join t in source on o.ID equals t.ID
where o.POS != t.POS
select new
{
IDNEW=o.ID,
IDOLD = t.ID,
REFNEW = o.POS,
REFOLD = t.POS
};
DELETE
: 删除一个节点。 有两种方法可以检测此操作。 一种是两个版本之间的左连接,如下所示
var deleted = from o in source
join block1 in version on o.ID equals block1.ID
into elements
from t in elements.DefaultIfEmpty(null)
where !MovedIDs.Contains(o.ID)
select new
{
Element = o.Element
};
如上所述,移动操作可以被视为删除和插入操作的序列。 这就是为什么在这一步中,移动的元素也被检测为已删除的原因。我们使用 where !MovedIDs.Contains(o.ID)
来删除它们。我们可以用相同的方式检测插入操作,但使用右连接。检测删除操作的第二种方法在“插入”部分中进行了解释。
INSERT
: 插入一个节点。 我们可以通过使用以下方法获取第二个版本中不存在于第一个版本中的所有元素:
var nodenotinsource = (from block2 in version
select new { ID = block2.ID }).Except(from o in source select new { ID = o.ID });
要获取元素节点,我们需要另一个连接,因为 nodenotinsource
仅返回 ID。
var inserted = from o in version
join block1 in nodenotinsource on o.ID equals block1.ID
select new
{
Element = o.Element
};
最后,我们可以删除我们添加的属性;我更喜欢保留 POS,但您也可以将其删除。
DeletedElements.Descendants().Attributes("ID").Remove();
InsertedElements.Descendants().Attributes("ID").Remove();
这是一个具有删除、插入和移动操作的 felta 文件
<?xml version="1.0" encoding="utf-8"?>
<Delta From="Version1.xml" To="Version2.xml">
<Delete>
<employee salaried="yes" POS="0-0-1-3">
<name POS="1-3-2-1">TEST DELETED</name>
<hire_date POS="1-3-2-0">2/6/1998</hire_date>
</employee>
</Delete>
<Insert>
<employee salaried="yes" POS="0-0-1-0">
<name POS="1-0-2-1">ZEYNEP - Inserted</name>
<hire_date POS="1-0-2-0">1/1/2009</hire_date>
</employee>
</Insert>
<Move>
<employee From="0-0-1-2" To="0-0-1-3" />
<employee From="0-0-1-1" To="0-0-1-2" />
<employee From="0-0-1-0" To="0-0-1-1" />
</Move>
</Delta>
POS 值用于移动操作,但您可以使用另一种方法;实际上,对于您所有的 delta 文件结构。
当涉及到复杂性时,它总是随着 LINQ 的连接而呈二次方。所以它是 O(n2)。
LINQDiff.zip:
- Hash.cs: 用于创建和添加
ID
和POS
属性。 - Performance.cs: 我从 MSDN 中获取了这个文件来测试执行时间。我删除了迭代。
- Diff.cs: 用于检测更改。
- 两个 XML 文件,版本 1 和 2。
- Delta 文件
关注点
如果您查看代码,我在同一个函数中检测了所有操作,这实际上不太好。 如果要编写移动、删除、插入函数,则无法将 source
和 version
作为参数传递。 首先,您应该创建一个对象,例如 TagElement
对象,然后您可以使用 LINQ 创建实例。
var source = from o in source.Descendants(Root)
select new TagElement
{
ID = o.Attribute("ID").Value,
POS = o.Attribute("POS").Value,
ELEMENT = o
};
您可以使用 IEnumerable<TagElement> source
将这些结果传递给另一个函数。
结论
我希望它对某人有用,或者将来有用 :)。