Golang中如何部分解析XML文件

Golang中如何部分解析XML文件 我需要从一个XML文件中提取几个字段,想知道该如何处理。

首先,一些背景信息: 我需要获取一个libvirt虚拟机的快照信息,这些信息是以XML格式编码的。

我通过以下方式实现:

data, _ := snap.GetXMLDesc(0)

(data是一个字符串变量……是的,libvirt将大约8kb的文件内容返回到一个字符串变量中。)

如果我将这个字符串写入一个XML文件,它包含188行(7.8kb)的字段和属性。实际上,我只需要其中的3个字段。

根据网上的各种资料,我了解到要解析这个XML文件,我需要创建一个数据结构,在其中映射所有的字段和属性,等等。当我只需要这个XML文件中的大约3行数据时,肯定有更好的方法吧?

我想到的一个变通方法是把这个字符串变量转储到一个文件中,然后在文件中使用“grep”来获取我需要的信息,但觉得这种方法不够优雅。一定有一种方法可以只映射文件中我需要的信息,而不是全部吧?

以下是XML文件的一个示例。假设我只想要“parent”、“creation type”和“type arch”字段及其属性,除了将XML转储到文件之外,我没有其他思路。

<description>vmman-generated snapshot</description>
  <state>shutoff</state>
  <parent>
    <name>0.clear</name>
  </parent>
  <creationTime>1667742695</creationTime>
  <memory snapshot='no'/>
  <disks>
    <disk name='vda' snapshot='internal'/>
  </disks>
  <domain type='kvm'>
    <name>alpinedev</name>
    <uuid>055e3a07-533a-41ee-ae94-cbee5ff404f0</uuid>
    <metadata>
      <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
        <libosinfo:os id="http://alpinelinux.org/alpinelinux/3.16"/>
      </libosinfo:libosinfo>
    </metadata>
    <memory unit='KiB'>786432</memory>
    <currentMemory unit='KiB'>786432</currentMemory>
    <vcpu placement='static'>1</vcpu>
    <os>
      <type arch='x86_64' machine='pc-q35-7.0'>hvm</type>
      <boot dev='hd'/>
    </os>

更多关于Golang中如何部分解析XML文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

我明白我之前哪里做错了……谢谢你!

更多关于Golang中如何部分解析XML文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个使用你的数据的示例 - Go Playground - The Go 编程语言

您可以考虑使用XML的“sax”或“流式”解析。

或者也可以使用XPath。

如果您选择的库实现良好,内存消耗将相对较小,加减一些在查找文件时通常会产生、可被垃圾回收的临时对象。

您可以只定义一个结构体,并指定要从数据中读取的标签:Go Playground - The Go Programming Language

比XPath更简单,谢谢。我没想到可以在结构体和XML文档之间进行“部分映射”。我本想在Playground上测试一下,结果被@nobbz的解决方案吸引住了。

谢谢,这个方法有效。

哦……我想我不知怎么地错过了XPath?它和我之前用过的一个Python3库非常相似,那是一个之前(可能是一个移植版/分支版)。这是我今天找到的最接近的解决方案了。

我本来打算采用我在原帖中提到的那种变通方法,整个过程都捏着鼻子进行 吐舌表情

谢谢。我不知道为什么我在搜索时错过了XPath!不过没关系,我现在有解决方案了。

好的,我本以为它可行,但实际上并非如此(提醒自己:编译通过不等于能运行)。我不确定这是因为我对Go的了解尚浅,还是因为我对XML文档的记忆已经生疏了。以下是我需要解析的XML的编辑版本:

<domainsnapshot>
  <name>1.hello</name>
  <description>vmman-generated snapshot</description>
  <state>shutoff</state>
  <parent>
    <name>0.clear</name>
  </parent>
  <creationTime>1667742695</creationTime>
</domainsnapshot>

现在,我需要获取快照名称、父级名称(如果存在)以及创建时间。

我构建了以下结构体:

type ParentElement struct {
	XMLName xml.Name `xml:"parent"`
	Name    string   `xml:"parentname"`
}
type SnapshotXMLstruct struct {
	SnapshotName string        `xml:"snapname"`
	Creationdate uint64        `xml:"creationdate"`
	ParentName   ParentElement `xml:"parent",omitempty`
}

我为ParentName设置了omitempty,因为XML文档中可能缺少这个标签。 我用于检索XML并将所需的3个字段追加到我自己的结构体中的代码如下:

	var snapXMLdata SnapshotXMLstruct
	var snaps []SnapshotXMLstruct
	<snip>
	snapshots, _ := domain.ListAllSnapshots(0)

	for _, snap := range snapshots {
		data, _ := snap.GetXMLDesc(0)
		err := xml.Unmarshal([]byte(data), &snapXMLdata)
		if err != nil {
			fmt.Println("Error: ", err)
			os.Exit(0)
		}
		snaps = append(snaps, snapXMLdata)
	}

data变量非空,所以问题不在于没有获取到有效的XML文档。 ……然而,snapXMLdata是空的,并且err == nil

我想我还没能理解这个简单的概念 :-/

在Go中部分解析XML文件可以使用xml.Decoder的Token API,这样无需定义完整的结构体。以下是针对你需求的解决方案:

package main

import (
    "encoding/xml"
    "fmt"
    "strings"
)

func main() {
    xmlData := `<?xml version="1.0"?>
<snapshot>
<description>vmman-generated snapshot</description>
  <state>shutoff</state>
  <parent>
    <name>0.clear</name>
  </parent>
  <creationTime>1667742695</creationTime>
  <memory snapshot='no'/>
  <disks>
    <disk name='vda' snapshot='internal'/>
  </disks>
  <domain type='kvm'>
    <name>alpinedev</name>
    <uuid>055e3a07-533a-41ee-ae94-cbee5ff404f0</uuid>
    <metadata>
      <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
        <libosinfo:os id="http://alpinelinux.org/alpinelinux/3.16"/>
      </libosinfo:libosinfo>
    </metadata>
    <memory unit='KiB'>786432</memory>
    <currentMemory unit='KiB'>786432</currentMemory>
    <vcpu placement='static'>1</vcpu>
    <os>
      <type arch='x86_64' machine='pc-q35-7.0'>hvm</type>
      <boot dev='hd'/>
    </os>
  </domain>
</snapshot>`

    result := parsePartialXML(xmlData)
    fmt.Printf("Parent: %s\n", result.Parent)
    fmt.Printf("Creation Time: %s\n", result.CreationTime)
    fmt.Printf("Architecture: %s\n", result.Arch)
}

type SnapshotInfo struct {
    Parent       string
    CreationTime string
    Arch         string
}

func parsePartialXML(xmlStr string) SnapshotInfo {
    decoder := xml.NewDecoder(strings.NewReader(xmlStr))
    var info SnapshotInfo
    var inParent, inType bool
    
    for {
        token, err := decoder.Token()
        if err != nil {
            break
        }

        switch se := token.(type) {
        case xml.StartElement:
            switch se.Name.Local {
            case "parent":
                inParent = true
            case "creationTime":
                var creationTime string
                if err := decoder.DecodeElement(&creationTime, &se); err == nil {
                    info.CreationTime = creationTime
                }
            case "type":
                inType = true
                for _, attr := range se.Attr {
                    if attr.Name.Local == "arch" {
                        info.Arch = attr.Value
                    }
                }
            }
        case xml.EndElement:
            if se.Name.Local == "parent" {
                inParent = false
            } else if se.Name.Local == "type" {
                inType = false
            }
        case xml.CharData:
            if inParent && string(se) != "\n" && string(se) != "  " {
                info.Parent = string(se)
            }
        }
    }
    
    return info
}

如果你需要更精确地获取parent的name字段,可以使用这个版本:

func parsePartialXML(xmlStr string) SnapshotInfo {
    decoder := xml.NewDecoder(strings.NewReader(xmlStr))
    var info SnapshotInfo
    var inParent, inParentName bool
    
    for {
        token, err := decoder.Token()
        if err != nil {
            break
        }

        switch se := token.(type) {
        case xml.StartElement:
            switch se.Name.Local {
            case "parent":
                inParent = true
            case "name":
                if inParent {
                    inParentName = true
                }
            case "creationTime":
                var creationTime string
                if err := decoder.DecodeElement(&creationTime, &se); err == nil {
                    info.CreationTime = creationTime
                }
            case "type":
                for _, attr := range se.Attr {
                    if attr.Name.Local == "arch" {
                        info.Arch = attr.Value
                    }
                }
            }
        case xml.EndElement:
            if se.Name.Local == "parent" {
                inParent = false
            } else if se.Name.Local == "name" && inParent {
                inParentName = false
            }
        case xml.CharData:
            if inParentName {
                info.Parent = string(se)
            }
        }
    }
    
    return info
}

对于你的libvirt场景,直接使用返回的字符串:

func GetSnapshotInfo() (SnapshotInfo, error) {
    data, err := snap.GetXMLDesc(0)
    if err != nil {
        return SnapshotInfo{}, err
    }
    
    return parsePartialXML(data), nil
}

这种方法只解析你需要的字段,不会处理整个XML结构,内存效率更高。

回到顶部