C# 之 Linq to Xml

外來物種發表於2020-12-15

▪ 前言

我相信很多從事 .NET 開發的,在 .NET 3.5 之前操作XML會比較麻煩,但是在此之後出現了 Linq to Xml,而今天的主人公就是 Linq to Xml,廢話不多說,直接進入主題。

一、生成Xml

為了能夠在結構有一定的組織,筆者建議大家新建一個控制檯專案,並且新建一個 CreateXml 類(以下程式碼都屬於該類中)。

並在其中寫入以下屬性:

public static String Path
{
    get
    {
        return String.Format("{0}\\test.xml", Environment.CurrentDirectory);
    }
}

這句程式碼很好理解,就是為了下面我們示例的時候可以將 xml 儲存到當前程式的執行路徑下。

1. 建立簡單的 xml

首先我們先練練手,建立一個簡單的 xml 並儲存到一個檔案中。

/// <summary>
/// 建立簡單的xml並儲存
/// </summary>
public static void CreateElement()
{
    XDocument xdoc = new XDocument(
        // 建立宣告
        new XDeclaration("1.0", "utf-8", "yes"),
        
        // 建立節點
        new XElement("root", 
            new XElement("item", "1"),
            new XElement("item", "2")
        )
    );
    
    // 儲存內容
    xdoc.Save(Path);
}

很多學習過 XML 的人可以從結構就能夠猜測出最終的 xml 的組織,而這也是 linq to xml 的優點之一。

這個函式首先建立一個 xml 文件,並設定該 xml 的版本為 1.0,採用 utf-8 編碼,後面的 yes 表示該xml是獨立的。下面就開始建立每個節點的,首先是 Root節點,然後在 Root 節點中新增兩個 Item 節點。

最終生成的 xml 如下所示:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<root>
  <item>1</item>
  <item>2</item>
</root>

2. 建立註釋

xml 有很多項時,我們就需要利用註釋加以區別,通過 linq to xml 我們一樣可以在其中新增註釋。

比如下面這段程式碼:

/// <summary>
/// 建立註釋
/// </summary>
public static void CreateComment()
{
    XDocument xdoc = new XDocument(
        new XDeclaration("1.0", "utf-8", "yes"),
        new XComment("提示"),
        new XElement("item", "asd")
    );
    
    // 儲存內容
    xdoc.Save(Path);
}

這裡我們直接在版本資訊的後面新增了一條註釋。

最終的結果如下所示:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--提示-->
<item>asd</item>

3. 根據物件建立xml

很多時候我們都會將陣列之類的型別轉換成 xml 以便儲存進永久性儲存介質中,所以下面我們也簡單的舉了一個例子,將陣列轉換成 xml

程式碼如下所示:

/// <summary>
/// 根據物件建立xml並儲存
/// </summary>
public static void CreateElementByObjects()
{
    // 初始化
    var s = Enumerable.Range(1, 10);
    
    // 構建 XML 元素
    XElement xele = new XElement(
        "Root",
        from item in s select new XElement("item", item.ToString())
    );
    
    // 儲存內容
    xele.Save(Path);
}

一開始的程式碼 var s = Enumerable.Radge(1,10) 是從1開始遞增,生成含有10項的陣列,以便後面我們進行新增,有了這個陣列之後,我們通過簡單的 linq 語句將陣列轉換成 xml,新增到Root中。

儲存之後的結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <item>1</item>
  <item>2</item>
  <item>3</item>
  <item>4</item>
  <item>5</item>
  <item>6</item>
  <item>7</item>
  <item>8</item>
  <item>9</item>
  <item>10</item>
</Root>

4. 建立屬性

有時我們不想建立新的子項去儲存資料,而是使用屬性的方式去儲存。理所應當,linq to xml 一樣也支援這個功能,下面我們可以通過簡單的語句去實現它。

/// <summary>
/// 建立屬性
/// </summary>
public static void CreteAttribute()
{
    // 初始化
    XAttribute xa = new XAttribute("V2", "2");
    
    // 構建 XML 元素
    XElement xele = new XElement(
        "Root",
        new XElement("Item", new XAttribute("V1", "1"), xa)
    );
    
    // 儲存內容
    xele.Save(Path);
}

我們依然可以看到熟悉的語法,這裡我們利用了 XAttribute 去建立一個屬性,並新增到 XElement 中。

最終的結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <Item V1="1" V2="2" />
</Root>

5. 建立名稱空間

對於一些企業級的 xml 格式,會非常的嚴格。特別是在同一個 xml 中可能會出現重複的項,但是我們又想區分開來,這個時候我們可以利用名稱空間將他們分開(跟C#中的名稱空間類似。)。

下面是建立名稱空間的示例:

/// <summary>
/// 建立名稱空間
/// </summary>
public static void CreateNamespace()
{
    // 構建 XML 元素
    XElement xele = new XElement("{http://www.xamarin-cn.com}Root",
        new XElement("Item", "1"),
        new XElement("{http://www.baidu.com}Item", 2)
    );
        
    // 儲存內容
    xele.Save(Path);
}

結果如下所示:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns="http://www.xamarin-cn.com">
  <Item xmlns="">1</Item>
  <Item xmlns="http://www.baidu.com">2</Item>
</Root>

從這個結果中我們可以看到對應的屬性中有了xmlns屬性,並且值就是我們賦給它的名稱空間。

二、查詢並修改Xml

Linq to xml 不僅僅是建立 xml 簡單,在查詢,編輯和刪除方面一樣是非常方便的,下面我們就會介紹這些。

首先我們建立一個 QueryXml 類,並在其中寫入如下的屬性:

public static String Path
{
    get
    {
        return String.Format("{0}\\test1.xml", Environment.CurrentDirectory);
    }
}

同時在該路徑下新建一個 test1.xml 檔案,並在其中寫入如下內容:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <Item v1="1" v2="2">Item1</Item>
  <Item v1="1" v2="2">Item2</Item>
</Root>

下面我們就可以正式開始了。

1. 通過檔案讀取xml

既然我們要對 xml 查詢就需要讀取對應的 xml 檔案,當然後面會介紹其他的方式。

程式碼如下:

/// <summary>
/// 通過檔案讀取xml
/// </summary>
public static void QueryElementByFile()
{
    XElement xele = XElement.Load(Path);
    XElement xeleItem = xele.Element("Item");
    
    Console.Write(xeleItem.Value.Trim());
    Console.ReadKey();
}

我們可以利用 XElement 的靜態方法 Load 讀取指定路徑下的 xml 檔案,這裡我們不僅讀取了該 xml 檔案,同時還獲取的該 xml 的第一個 Item 的值並輸出。

所以我們可以看到如下的結果:

Item1

2. 在指定節點前後新增新節點

上面我們僅僅只是讀取xml以及簡單的查詢,下面我們不僅僅查詢並且還要在該節點前後插入新的節點。

程式碼如下:

/// <summary>
/// 在指定節點前後新增新節點
/// </summary>
public static void AddToElementAfterAndBefore()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Elements("Item") where ele.Value.Equals("Item2") select ele).SingleOrDefault();
    
    if( item != null ){
        XElement nele = new XElement("NItem", "NItem");
        XElement nele2 = new XElement("BItem", "BItem");
        
        item.AddAfterSelf(nele);
        item.AddBeforeSelf(nele2);
        
        xele.Save(Path);
    }
}

xele.Elements("Item") 表示獲取 xele 元素的下級節點中名稱為 Item 的節點

我們簡單的分析一下上面的程式碼,首先我們利用 Linq 從中查詢 Item 的值為 Item2 的節點,然後獲取其中第一個節點,然後通過 AddAfterSelfAddBeforeSelf 在該節點的後面和前面分別新增新的節點。

新增完之後的xml結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <Item v1="1" v2="2">Item1</Item>
  <BItem>BItem</BItem>
  <Item v1="1" v2="2">Item2</Item>
  <NItem>NItem</NItem>
</Root>

3. 新增屬性到節點中

我們已經可以動態的新增節點,但是建立的時候不僅僅可以建立節點,並且還能建立屬性,下面我們可以通過 SetAttributeValue 去新增新的屬性或者修改現有屬性。

程式碼如下:

/// <summary>
/// 新增屬性到節點中
/// </summary>
public static void AddAttributeToEle()
{
    XElement xele = XElement.Parse(@"<?xml version='1.0' encoding='utf-8'?><Root><!--前面的註釋-->
<Item v1='1' v2='2'>Item1</Item><!--後面的註釋--><Item v1='1' v2='2' v3='3'>Item2</Item></Root>");

    var item = (from ele in xele.Elements("Item") where ele.Value.Equals("Item2") select ele).SingleOrDefault();
    item.SetAttributeValue("v3", "3");
    
    xele.Save(Path);
}

我們可以明顯的看出,這裡我們已經不是使用 XElement.Load 去讀取 xml 檔案,而是通過直接讀取 xml 字串。接著我們還是跟上面一樣去查詢,然後通過 SetAttributeValue 新增了新的屬性,並儲存。

Xml內容如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--前面的註釋-->
  <Item v1="1" v2="2">Item1</Item>
  <!--後面的註釋-->
  <Item v1="1" v2="2" v3="3">Item2</Item>
</Root>

我們可以看到第二個 Item 中多了一個 v3=”3” 新的屬性。

4. 新增註釋到指定節點前後

這裡的語法基本跟新增節點到指定節點前後是相似的,只是讀取 xml 的方式不同。

程式碼如下:

/// <summary>
/// 新增註釋到節點前後
/// </summary>
public static void AddCommentToAfterAndBefore()
{
    TextReader tr = new StringReader(@"<?xml version='1.0' encoding='utf-8'?><Root><!--前面的註釋-->
<Item v1='1' v2='2'>Item1</Item><!--後面的註釋--><Item v1='1' v2='2' v3='3'>Item2</Item></Root>");

    XElement xele = XElement.Load(tr);
    var item = (from ele in xele.Elements("Item") where ele.Value.Equals("Item1") select ele).FirstOrDefault();
    
    if( item != null ){
        XComment xcom = new XComment("後面的註釋");
        XComment xcoma = new XComment("前面的註釋");
        
        item.AddAfterSelf(xcom);
        item.AddBeforeSelf(xcoma);
    }
    
    tr.Close();
    xele.Save(Path);
}

上面我使用 StringReaderTextReader 讀取 xml 字串並使用 XElement.Load 讀取該物件,然後就是在新建節點的時候新建的是註釋節點,最後利用一樣的語法新增到指定節點前後。

最終結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--前面的註釋-->
  <!--前面的註釋-->
  <Item v1="1" v2="2">Item1</Item>
  <!--後面的註釋-->
  <!--後面的註釋-->
  <Item v1="1" v2="2" v3="3">Item2</Item>
</Root>

5. 替換指定節點

修改節點的值通過 SetValue 即可做到,但是有時涉及到子節點,而我們想一次性全部替換掉,那麼我們就需要使用 ReplaceWith

程式碼如下:

/// <summary>
/// 替換指定節點
/// </summary>
public static void ReplaceElement()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Elements("Item")
                where ele.Value.Equals("Item2")
                select ele).FirstOrDefault();
    if (item != null)
    {
        item.ReplaceWith(new XElement("Item", "Item3"));
    }
    xele.Save(Path);
}

這裡的重點在於 ReplaceWith 方法,呼叫該方法會發生兩個操作。首先是刪除該節點,然後在該節點的位置上將我們的節點插入完成替換。

最後的xml結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--前面的註釋-->
  <!--前面的註釋-->
  <Item v1="1" v2="2">Item1</Item>
  <!--後面的註釋-->
  <!--後面的註釋-->
  <Item>Item3</Item>
</Root>

這樣我們很輕易的就替換了整個節點。

6. 刪除指定屬性

前面我們介紹了建立、修改和新增屬性,但是還沒有介紹如何刪除指定的屬性,下面我們就通過一個簡單的例項來演示。

程式碼如下:

/// <summary>
/// 刪除指定屬性
/// </summary>
public static void RemoveAttribute()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Elements("Item")
                where ele.Value.Equals("Item1")
                select ele).FirstOrDefault().Attribute("v1");
    if (item != null)
    {
        item.Remove();
    }
    xele.Save(Path);
}

我們首先查詢出指定的節點,然後指定某個屬性,最後呼叫 XAttribute 的 Remove 方法既可。

結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--前面的註釋-->
  <!--前面的註釋-->
  <Item v2="2">Item1</Item>
  <!--後面的註釋-->
  <!--後面的註釋-->
  <Item>Item3</Item>
</Root>

7. 刪除指定節點

既然上面已經可以刪除屬性,自然也少不了刪除屬性。

程式碼如下所示:

/// <summary>
/// 刪除指定節點
/// </summary>
public static void RemoveElement()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Elements("Item")
                where ele.Value.Equals("Item1")
                select ele).FirstOrDefault();
    if (item != null)
    {
        item.Remove();
    }
    xele.Save(Path);
}

依然是呼叫同樣的方法。

結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--前面的註釋-->
  <!--前面的註釋-->
  <!--後面的註釋-->
  <!--後面的註釋-->
  <Item>Item3</Item>
</Root>

三、按節點關係查詢

上面的查詢都是通過相關的條件進行查詢,但是我們有時僅僅只需要通過之間的關係即可,這樣反而可以避免很多的程式碼,當然稍加探索可以發現其實 XElement 都提供給我們了。

我們依然要新建一個 StructureXml 類,並在其中新建一個屬性。

如下所示:

public static String Path
{
    get
    {
        return String.Format("{0}\\test2.xml", Environment.CurrentDirectory);
    }
}

同時在該資料夾下新建一個 test2.xml 並寫入如下內容:

<?xml version="1.0" encoding="utf-8" ?>
<Root>
  <Item>
    <SubItem1>1</SubItem1>
    <SubItem>
      <Child>sss</Child>
    </SubItem>
    <SubItem2>2</SubItem2>
  </Item>
</Root>

1. 顯示指定節點的所有父節點

通過上面的 xml 檔案,我們清晰的看出 xml 是具有結構性的,彼此之間都存在關係,而現在我們需要顯示某個節點的父級節點的名稱。

程式碼如下所示:

/// <summary>
/// 顯示指定節點的所有父節點
/// </summary>
public static void ShowAllParentEle()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Descendants("Child") select ele).FirstOrDefault();
                
    if( item != null ){
        foreach( var sub in item.Ancestors() ){
            Console.WriteLine(sub.Name);
        }
        
        Console.WriteLine("----------------");
        
        foreach( var sub in item.AncestorsAndSelf() ){
            Console.WriteLine(sub.Name);
        }
        
        Console.ReadKey();
    }
}

其中我們通過 Descendants 獲取最底的節點,然後使用 Ancestors 獲取所有的父級節點,而 AncestorsAndSelf 則表示包含本身。

最終結果如下所示:

SubItem
Item
Root
----------------
Child
SubItem
Item
Root

我們從上面的結果中可以看出,分割線前顯示的是不包含本身的,而下面是包含本身的。

2. 顯示指定節點的所有子節點

我們不僅僅可以輸出一個節點的所有父級節點,同樣也可以輸出一個節點的所有子節點。

程式碼如下所示:

/// <summary>
/// 顯示指定節點的所有子節點
/// </summary>
public static void ShowAllChildEle()
{
    XElement xele = XElement.Load(Path);
    
    foreach( var sub in xele.Descendants() ){
        Console.WriteLine(sub.Name);
    }
    
    Console.WriteLine("-----------------");
    
    foreach( var sub in xele.DescendantsAndSelf() ){
        Console.WriteLine(sub.Name);
    }
    
    Console.ReadKey();
}

這裡我們依然是分成輸出子級節點以及包含自己的。

結果如下所示:

Item
SubItem1
SubItem
Child
SubItem2
-----------------
Root
Item
SubItem1
SubItem
Child
SubItem2

3. 顯示同級節點之前的節點

既然有了父子關係,當然也少不了同級關係,首先我們先顯示同級節點之前的節點。

程式碼如下所示:

/// <summary>
/// 顯示同級節點之前的節點
/// </summary>
public static void ShowPrevEle()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Descendants("SubItem") select ele).FirstOrDefault();
    
    if( item != null ){
        foreach( var sub in item.ElementsBeforeSelf() ){
            Console.WriteLine(sub.Name);
        }
    }
    
    Console.ReadKey();
}

這裡我們看到我們通過 ElementsBeforeSelf 獲取該節點之前的同級節點,當然我們還可以傳入引數作為限制條件。這裡我們通過查詢獲取了 SubItem 這個節點,並顯示該節點之前的同級節點。

最終結果如下:

SubItem1

4. 顯示同級節點後面的節點

作為上面的補充。

程式碼如下所示:

/// <summary>
/// 顯示同級節點後面的節點
/// </summary>
public static void ShowNextEle()
{
    XElement xele = XElement.Load(Path);
    var item = (from ele in xele.Descendants("SubItem") select ele).FirstOrDefault();
    
    if( item != null ){
        foreach( var sub in item.ElementsAfterSelf() ){
            Console.WriteLine(sub.Name);
        }
    }
    
    Console.ReadKey();
}

最終結果如下所示:

SubItem2

四、監聽xml事件

你可能會疑惑xml為什麼還要監聽,其實這樣是有意義的,比如你要根據某個節點的值作為依賴,那麼你就要監聽這個節點,如果這個節點發生改變的時候,你才可以及時的作出反應。

但是 xml 的事件監聽有一個特點,跟瀏覽器中的 DOM 事件類似,監聽父節點同樣也可以監聽的到它的子節點的事件。下面我們通過一個簡單的例項來說明。

例項程式碼如下:

public static class EventXml
{
    public static void BindChangeing()
    {
        XElement xele = new XElement("Root");
        
        xele.Changed += xele_Changed;
        xele.Changing += xele_Changing;
        xele.Add(new XElement("Item", "123"));
        
        var item = xele.Element("Item");
        item.ReplaceWith(new XElement("Item", "2"));
        
        item = xele.Element("Item");
        item.Remove();
        
        Console.ReadKey();
    }

    static void xele_Changed(object sender, XObjectChangeEventArgs e)
    {
        XElement ele = sender as XElement;
        Console.WriteLine(String.Format("已完成 {0}-{1}", ele.Name, e.ObjectChange));
    }

    static void xele_Changing(object sender, XObjectChangeEventArgs e)
    {
        XElement ele = sender as XElement;
        Console.WriteLine(String.Format("正在進行中 {0}-{1}", ele.Name, e.ObjectChange));
    }
}

其中的關鍵就是Changing和Changed事件,其次就是在事件中判斷事件的來源。

最終結果如下所示:

正在進行中 Item-Add
已完成 Item-Add
正在進行中 Item-Remove
已完成 Item-Remove
正在進行中 Item-Add
已完成 Item-Add
正在進行中 Item-Remove
已完成 Item-Remove

五、處理xml流

在實際的商業化的開發中,xml不可能僅僅儲存這麼點資料。有可能儲存著非常多的資料。但是我們還是按照以往的方式,就會將xml全部讀取進記憶體。

這樣會佔據很多記憶體,影響系統的效能,針對這種情況我們需要使用流的方式去處理xml,因為流會按照我們的順序讀取部分xml進記憶體,並不會將所

有xml都讀取進記憶體。

Xml檔案內容如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<Root>
  <SubItem>1</SubItem>
  <SubItem>1</SubItem>
  <SubItem>1</SubItem>
  <Item>A</Item>
  <SubItem>1</SubItem>
  <Item>B</Item>
</Root>

程式碼如下所示:

public static class ReadXmlStream
    {
        public static String Path
        {
            get
            {
                String path = String.Format("{0}\\test3.xml", Environment.CurrentDirectory);
                return path;
            }
        }

        /// <summary>
        /// 流式處理XML
        /// </summary>
        public static void ReadXml()
        {
            XmlReader reader = XmlReader.Create(Path);
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element && reader.Name.Equals("Item"))
                {
                    XElement ele = XElement.ReadFrom(reader) as XElement;
                    Console.WriteLine(ele.Value.Trim());
                }
            }
            Console.ReadKey();
        }
}

這裡我們通過XmlReader的Create靜態方法開啟xml檔案,並通過Read一個節點的進行讀取,並判斷該節點的型別。

最終結果如下:

A
B

相關文章