Benutzer mit den meisten Antworten
Wie parse ich vernünftig XML Node für Node?

Frage
-
Hallo,
so langsam zweifel ich an meinen Programmier"künsten"! ;) Ich habe folgendes Problem (ich nutze VS2017Community mit VB):
ich habe eine XML-Datei erstellt, die folgendermaßen aussieht:
<?xml version="1.0" encoding="utf-8" ?> <Klassen> <Klasse id="1"> <Name>Dieb</Name> <Attribute> <Mut>1W8+3</Mut> <Klugheit>1W6+8</Klugheit> <Intuition>1W8+7</Intuition> <Charisma>1W8+5</Charisma> <Fingerfertigkeit>1W8+9</Fingerfertigkeit> <Geschicklichkeit>1W8+8</Geschicklichkeit> <Konstitution>2W4+4</Konstitution> <Körperkraft>1W10+5</Körperkraft> </Attribute> </Klasse> <Klasse id="2"> <Name>Händler</Name> <Attribute> <Mut>1W8+2</Mut> <Klugheit>1W8+8</Klugheit> <Intuition>1W10+7</Intuition> <Charisma>1W10+5</Charisma> <Fingerfertigkeit>1W6+6</Fingerfertigkeit> <Geschicklichkeit>1W4+8</Geschicklichkeit> <Konstitution>2W4+4</Konstitution> <Körperkraft>1W8+6</Körperkraft> </Attribute> </Klasse> </Klassen>
Nun möchte ich die einzelnen Werte abrufen und zwar nur für eine "Klasse" (sagen wir "Dieb"). Ich habe jetzt in den MSDN-Hilfen alles Mögliche zu XML (und auch LINQ) gelesen, aber ich komme nicht wirklich weiter.
Zunächst dachte ich, dass ich mit XElement 1 Unterknoten von Klassen zurück kriegen würde, aber das war wohl ein Fehler; es liefert ja ALLE einzelnen "Klasse"-Elemente innerhalb <Klassen>.
Nun die Gretchenfrage, die ich mir immer noch nicht beantworten kann:
wie kann ich alle Werte der einzelnen Nodes aus einer bestimmten Klasse auslesen und entsprechenden Variablen zuweisen?
Also quasi: Var_Mut = Wert von <Mut> der Klasse mit der id=1
Ich hoffe, ich habe mich hinreichend verständlich ausgedrückt. ;)
Mittlerweile weiß ich (wieder), warum ich das Programmieren nicht mehr beruflich mache... ;)
Danke jedenfalls für jeden hilfreichen Hinweis.
Gruß
WarFred
Antworten
-
Hi Fred,
in VB.NET gibt es eine sehr gute Unterstützung für XML. Es reicht, den XML-Stream (Datei o.ä.) in ein XElement einzulesen. Da wird im Hintergrund der Parser ausgeführt, der im XElement die Struktur der XML-Daten abgelegt. Den Umweg über XDocument brauchst Du nicht zu machen. Mit LinQ to XML kannst Du dann direkt auf die Knoten zugreifen. Diese Unterstützung ist in VB.NET viel eleganter als in C#.NET. Deine Methode Fuelle sieht nach der Umstellung auf XElement und LinQ so aus:Public Function Fuelle(id As Integer) As XElement Return (From xe1 In xe0.Descendants Where CType(xe1.@id, Integer) = id).FirstOrDefault End Function
Deine Codeschnispel habe ich mal als Demo erstellt. Ich habe lediglich den Dateinamen geändert und zur Erzeugung des Pfades die Path-Klasse genutzt.
Imports System.IO Public Class Form1 Private WithEvents BtnStart As New Button With {.Text = "Start"} Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Controls.AddRange(New Control() {BtnStart}) End Sub Private Sub BtnStart_Click(sender As Object, e As EventArgs) Handles BtnStart.Click Dim Var_Klasse As XElement Dim Var_Test As New Test() Dim Var_Node As XElement Dim id = 3 Var_Node = Var_Test.Fuelle(id) Var_Klasse = Var_Node End Sub Public Class Test Private xe0 As XElement Public Sub New() Dim Pfad As String = My.Application.Info.DirectoryPath xe0 = XElement.Load(Path.Combine(Pfad, "Form45XMLFile1.xml")) End Sub Public Function Fuelle(id As Integer) As XElement Return (From xe1 In xe0.Descendants Where CType(xe1.@id, Integer) = id).FirstOrDefault End Function End Class End Class
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Montag, 16. Juli 2018 07:22
- Als Antwort markiert WarFred Dienstag, 17. Juli 2018 09:00
-
Hi Michael,
mit dem XElement ist ganz leicht, Knoten für Knoten zu verarbeiten. Wenn mit JSon gearbeitet wird, dann kann man direkt aus dem JSon eigene Objekte erstellen. Eine direkte Umwandlung in .NET-Objekte mit eigener Struktur ist aus dem XElement ist nicht so einfach möglich wie aus JSon. Die Syntax in VB.NET in LinQtoXML ist aber sehr elegant, was das problemlose Extrahieren von Untermengen ermöglicht (einfacher als in C#.NET). Aus diesem Grund ist es wichtig, ob im Projekt mit Objekten eigener Struktur (Klassen) oder nur mit Inhalten aus dem XML zu arbeiten ist. Auch ist wichtig, ob Änderungen der Werte dann in XML zurückzuschreiben sind. Wenn es wenige Klassen sind, dann kann sich der Aufwand lohnen, selbst etwas zu programmieren, z.B. so:Imports System.IO Public Class Form45 Private WithEvents BtnStart As New Button With {.Text = "Start", .Dock = DockStyle.Top} Private lb As New ListBox With {.Dock = DockStyle.Fill} Private Sub Form45_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Controls.AddRange(New Control() {lb, BtnStart}) End Sub Private Sub BtnStart_Click(sender As Object, e As EventArgs) Handles BtnStart.Click Dim Var_Test As New Test() lb.DataSource = Var_Test.Fuelle End Sub Public Class Test Private xe0 As XElement Public Sub New() Dim Pfad As String = My.Application.Info.DirectoryPath xe0 = XElement.Load(Path.Combine(Pfad, "Form45XMLFile1.xml")) End Sub Public Function Fuelle() As List(Of Klasse) Return (From xe1 In xe0...<Klasse> Select New Klasse(xe1)).ToList End Function End Class Public Class Klasse Public Sub New(xe2 As XElement) Name = xe2...<Name>.Value Mut = xe2...<Attribute>.<Mut>.Value Klugheit = xe2...<Attribute>.<Klugheit>.Value ' usw. End Sub Public Property Name As String Public Property Mut As String Public Property Klugheit As String ' usw. ''' <summary> ''' Objekt als XML-String zurückgeben ''' </summary> ''' <returns></returns> Public Function SaveKlasse() As String Dim xe4 As New XElement("Klasse") xe4.Add(New XElement("Name") With {.Value = Name}) Dim xe5 As New XElement("Attribute") xe4.Add(xe5) xe5.Add(New XElement("Mut") With {.Value = Mut}) xe5.Add(New XElement("Klugheit") With {.Value = Klugheit}) ' usw. Return xe4.ToString End Function ''' <summary> ''' Für Anzeige ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Return $"Name: {Name}, Mut: {Mut}, Klugheit: {Klugheit}" End Function End Class End Class
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Als Antwort markiert WarFred Montag, 16. Juli 2018 08:39
Alle Antworten
-
Hi,
warum nutzt Du nicht XML Serialisierung/Deserialisierung? Damit ist das doch erheblich einfacher zu machen.
Hier mal zwei Methoden, mit denen Du aus einem beliebigen serialisierbaren Object einen XML String und umgekehrt aus dem XML String ein passendes Objekt erzeugen kannst.
Public Shared Function SerializeObject( Of T )( ByVal DataObject As T ) As String Dim Result As String Dim Serializer As New XmlSerializer( GetType( T ), String.Empty ) Dim MemoryStream As New MemoryStream() Dim TextWriter As New XmlTextWriter( MemoryStream, Encoding.UTF8 ) TextWriter.Indentation = 4 TextWriter.IndentChar = " "c TextWriter.Formatting = Formatting.Indented Dim XmlNamespace As New XmlSerializerNamespaces() XmlNamespace.Add( String.Empty, String.Empty ) Serializer.Serialize( TextWriter, DataObject, XmlNamespace ) Result = Encoding.UTF8.GetString( MemoryStream.ToArray() ) TextWriter.Close() MemoryStream.Close() MemoryStream.Dispose() Return Result End Function Public Shared Function DeserializeString( Of T )( ByVal XmlContent As String ) As T Dim Result As T Dim Serializer As New XmlSerializer( GetType( T ) ) Dim StringReader As New StringReader( XmlContent ) Result = DirectCast( Serializer.DeSerialize( StringReader ), T ) StringReader.Close() StringReader.Dispose() Return Result End Function
Da musst dann nur noch deine Klassenstruktur erstellen, die dem Aufbau der XML Datei entspricht.
Über das Hilfsprogramm xsd.exe kannst Du aus der .xsd (oder auch einer .xml) die zugehörigen Codedateien für die Klassen erstellen lassen.
http://msdn.microsoft.com/de-de/library/x6c1kb0s.aspx
Über entsprechende Kommandozeilenparameter lassen sich einige Optionen bzgl. der Generierung des Codes einstellen.
Gruß, Stefan
Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport -
Hallo Stefan,
erst einmal vielen Dank für deine schnelle Antwort.
Zu deiner ersten Frage, warum ich das nicht nutze: weil ich davon noch nix gehört habe und auch dein Coding liest sich für mich wie Hebräisch ;)
Ich behalte es mal im Hinterkopf, weil ich denke, dass es da doch eine "einfachere" Möglichkeit geben muss. Ich kann doch nicht der Erste sein, der dieses Problem hat. Insofern warte ich mal ab, ob noch jemand eine andere Idee hat.
-
Hallo WarFred,
Stefan hat schon recht, es ist die einfachste und beste Möglichkeit aus dem XML wieder ein Objekt zu machen und erst dann damit zu arbeiten.
Du könntest aber auch mit JSON arbeiten, ist um einiges einfacher. Hier findest Du auch ein Beispiel Link
Gruß Thomas
13 Millionen Schweine landen jährlich im Müll
Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings -
Das Ganze kommt mir aber so ein bisschen "durch die Brust ins Auge" vor. Da muss es doch was einfacheres geben.
So in etwa: Node isolieren (also aus einem XElement den richtigen Node heraussuchen), und dann die einzelnen "Unter"Nodes immer mit NextNode durchgehen. Sowas soll nicht einfach gehen? Ich kann es nicht glauben.
Trotzdem auch Danke für deine Einschätzung Thomas.
-
So, den ersten Teil meiner Aufgabe, das "Extrahieren" der entsprechenden Nodes habe ich mittlerweile hinbekommen:
Imports System.Xml Public Class Test Private Cls_Klassen As New XDocument Public Sub New() Dim Pfad As String = My.Application.Info.DirectoryPath & "\XML\" Cls_Klassen = XDocument.Load(Pfad & "Klassen.xml") End Sub Public Function Fuelle(id As Integer) As XNode For Each node As XNode In Cls_Klassen.DescendantNodes If node.NodeType = XmlNodeType.Element Then For Each att In DirectCast(node, XElement).Attributes If att.Name = "id" Then If att.Value = id.ToString Then Return node Exit Function End If End If Next End If Next Return Nothing End Function End Class
Aufgerufen wird das Ganze mit:
Private Sub BtnStart_Click(sender As Object, e As EventArgs) Handles BtnStart.Click Dim Var_Klasse As XElement Dim Var_Test As New Test() Dim Var_Node As XNode Var_Node = Var_Test.Fuelle(id) Var_Klasse = Var_Node End Sub
Anschließend habe ich in Var_Klasse nur noch die entsprechende "Klasse" aus dem XML, also nur noch:
<Klasse id="3"> <Name>Kämpfer</Name> <Attribute> <Mut>1W6+8</Mut> <Klugheit>1W6+7</Klugheit> <Intuition>1W6+5</Intuition> <Charisma>1W8+5</Charisma> <Fingerfertigkeit>1W8+6</Fingerfertigkeit> <Geschicklichkeit>1W8+7</Geschicklichkeit> <Konstitution>2W6+6</Konstitution> <Körperkraft>2W4+9</Körperkraft> </Attribute> </Klasse>
Das Ganze mag bis dahin vielleicht nicht elegant gelöst sein, aber es erfüllt seinen Zweck ohne dass ich mich extra in JASON oder Serialization einarbeiten müsste. Ich hoffe, den Rest kriege ich jetzt auch alleine hin.
Bei diesem Forum habe ich manchmal das Gefühl, dass unglaubliche Experten hier Tipps geben (und nein, ich meine das nicht sarkastisch oder ironisch, sondern tatsächlich!), die aber das Gefühl für die einfachen Lösungen verloren haben. Dies ist meine zweite Anfrage hier und zum zweiten Mal gebe ich mir selbst die Antwort.
[EDIT] Sobald ich das eigentliche Parsen auch noch hinbekommen habe, poste ich es hier auch noch.
- Bearbeitet WarFred Sonntag, 15. Juli 2018 19:29
-
Hi,
woher kommt deine XML-Datei? Wenn die Daten lediglich aus deinem Programm kommen, kann ich nicht verstehen, was du gegen Serialisierung hast. Ist auch meiner Meinung nach das einfachste.
Wenn du es "händisch" machen möchtest, kannst du auch LinqToXml (oder hier) nehmen, das finde ich persönlich ziemlich gut.
Gruß
Freiberufler im Bereich Softwareentwicklung Von der PLC und Robotik zu VB.NET & C#, vorrangig WPF und UWP
-
Die Frage ist ganz einfach erklärt: weil ich null Ahnung von Serialization habe und mich da erst einarbeiten müsste (das VB-Buch, das ich mir extra gekauft habe, damit ich hin und wieder mal was nachschlagen kann, hat noch nicht einmal Serialisierung im Index!).
Und dein Beispiel LinqToXml ist im Grunde genau das, was ich ja auch hinbekommen habe, allerdings in C# und nicht in VB und es löst ja noch nicht das Problem des "Durchparsens".
[EDIT] Achso: ja, die XML-Datei ist von mir und dient mir als Datenspeicher. Sie ist ja nur ein ganz kleiner Teil von etlichen XML-Dateien, die ich für mein Programm benötige.- Bearbeitet WarFred Sonntag, 15. Juli 2018 20:51
-
Am ende macht das aber nun mal viel mehr Arbeit als das De-Serialisieren, vor allem bei JSON. Wir wollen dir natürlich Mehrarbeit ersparen. Hier mal eine Demo mit JSON. Für die Demo brachst Du noch das Nuget Paket Newtonsoft.Json
Imports System Module Program Sub Main(args As String()) Dim list As List(Of Klassen) = New List(Of Klassen) From { New Klassen() With { .Id = 1, .Name = "Dieb", .Attribute = New Attribute() With { .Mut = "1W8+3", .Klugheit = "1W6+8", .Intuition = "1W8+7", .Charisma = "1W8+5", .Fingerfertigkeit = "1W8+9", .Geschicklichkeit = "1W8+8", .Konstitution = "2W4+4", .Körperkraft = "1W10+5" } }, New Klassen() With { .Id = 1, .Name = "Händler", .Attribute = New Attribute() With { .Mut = "1W8+2", .Klugheit = "1W8+8", .Intuition = "1W10+7", .Charisma = "1W10+5", .Fingerfertigkeit = "1W6+6", .Geschicklichkeit = "1W4+8", .Konstitution = "2W4+4", .Körperkraft = "1W8+6" } } } 'Objekt zu text Dim json = Newtonsoft.Json.JsonConvert.SerializeObject(list) 'Text zu Objekt Dim list2 = Newtonsoft.Json.JsonConvert.DeserializeObject(Of List(Of Klassen))(json) End Sub Public Class Klassen Public Property Id As Integer Public Property Name As String Public Property Attribute As Attribute End Class Public Class Attribute Public Property Mut As String Public Property Klugheit As String Public Property Intuition As String Public Property Charisma As String Public Property Fingerfertigkeit As String Public Property Geschicklichkeit As String Public Property Konstitution As String Public Property Körperkraft As String End Class End Module
Ein Json string unterscheidet sich etwas von XML, er sieht in diesem fall so aus
[ { "Id":1, "Name":"Dieb", "Attribute": { "Mut":"1W8+3", "Klugheit":"1W6+8", "Intuition":"1W8+7", "Charisma":"1W8+5", "Fingerfertigkeit":"1W8+9", "Geschicklichkeit":"1W8+8", "Konstitution":"2W4+4", "Körperkraft":"1W10+5" } }, { "Id":1, "Name":"Händler", "Attribute": { "Mut":"1W8+2", "Klugheit":"1W8+8", "Intuition":"1W10+7", "Charisma":"1W10+5", "Fingerfertigkeit":"1W6+6", "Geschicklichkeit":"1W4+8", "Konstitution":"2W4+4", "Körperkraft":"1W8+6" } } ]
Gruß Thomas
13 Millionen Schweine landen jährlich im Müll
Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings -
Hi Fred,
in VB.NET gibt es eine sehr gute Unterstützung für XML. Es reicht, den XML-Stream (Datei o.ä.) in ein XElement einzulesen. Da wird im Hintergrund der Parser ausgeführt, der im XElement die Struktur der XML-Daten abgelegt. Den Umweg über XDocument brauchst Du nicht zu machen. Mit LinQ to XML kannst Du dann direkt auf die Knoten zugreifen. Diese Unterstützung ist in VB.NET viel eleganter als in C#.NET. Deine Methode Fuelle sieht nach der Umstellung auf XElement und LinQ so aus:Public Function Fuelle(id As Integer) As XElement Return (From xe1 In xe0.Descendants Where CType(xe1.@id, Integer) = id).FirstOrDefault End Function
Deine Codeschnispel habe ich mal als Demo erstellt. Ich habe lediglich den Dateinamen geändert und zur Erzeugung des Pfades die Path-Klasse genutzt.
Imports System.IO Public Class Form1 Private WithEvents BtnStart As New Button With {.Text = "Start"} Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Controls.AddRange(New Control() {BtnStart}) End Sub Private Sub BtnStart_Click(sender As Object, e As EventArgs) Handles BtnStart.Click Dim Var_Klasse As XElement Dim Var_Test As New Test() Dim Var_Node As XElement Dim id = 3 Var_Node = Var_Test.Fuelle(id) Var_Klasse = Var_Node End Sub Public Class Test Private xe0 As XElement Public Sub New() Dim Pfad As String = My.Application.Info.DirectoryPath xe0 = XElement.Load(Path.Combine(Pfad, "Form45XMLFile1.xml")) End Sub Public Function Fuelle(id As Integer) As XElement Return (From xe1 In xe0.Descendants Where CType(xe1.@id, Integer) = id).FirstOrDefault End Function End Class End Class
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Montag, 16. Juli 2018 07:22
- Als Antwort markiert WarFred Dienstag, 17. Juli 2018 09:00
-
Super Lösungen! Danke dafür!
Jetzt besteht nur noch das "eigentliche" Problem, nämlich das XElement Node für Node zu parsen und Variablen zuzuweisen.
@Thomas: danke für deinen Ansatz, ich werde ihn mir mal beizeiten ansehen. Er hat nur eine gravierende Schwachstelle: es ist nicht das, was ich machen möchte! Ich kenne das aus Foren, wo dann einfach gesagt wird "aber in Programmiersprache XY (<> der Programmiersprache in der man programmiert!) geht das aber viel einfacher!". Ja, das mag sein, aber wenn ich etwas lernen will - und das ist u.a. mein Anspruch an dieses Projekt, dann will ich es auch so machen, wie es von mir vorgesehen war. Die Ansätze von Peter und Florian zeigen, dass es zumindest möglich zu seien scheint. ;)
Das soll jetzt keine Manöverkritik oder Überheblichkeit sein, ich möchte nur versuchen XML-Verarbeitung in VB.NET zu verstehen. JASON und Serialisierung kriegen vielleicht auch irgendwann mal ihre Zeit. ;)
Gruß
Michael
-
Hi Michael,
mit dem XElement ist ganz leicht, Knoten für Knoten zu verarbeiten. Wenn mit JSon gearbeitet wird, dann kann man direkt aus dem JSon eigene Objekte erstellen. Eine direkte Umwandlung in .NET-Objekte mit eigener Struktur ist aus dem XElement ist nicht so einfach möglich wie aus JSon. Die Syntax in VB.NET in LinQtoXML ist aber sehr elegant, was das problemlose Extrahieren von Untermengen ermöglicht (einfacher als in C#.NET). Aus diesem Grund ist es wichtig, ob im Projekt mit Objekten eigener Struktur (Klassen) oder nur mit Inhalten aus dem XML zu arbeiten ist. Auch ist wichtig, ob Änderungen der Werte dann in XML zurückzuschreiben sind. Wenn es wenige Klassen sind, dann kann sich der Aufwand lohnen, selbst etwas zu programmieren, z.B. so:Imports System.IO Public Class Form45 Private WithEvents BtnStart As New Button With {.Text = "Start", .Dock = DockStyle.Top} Private lb As New ListBox With {.Dock = DockStyle.Fill} Private Sub Form45_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Controls.AddRange(New Control() {lb, BtnStart}) End Sub Private Sub BtnStart_Click(sender As Object, e As EventArgs) Handles BtnStart.Click Dim Var_Test As New Test() lb.DataSource = Var_Test.Fuelle End Sub Public Class Test Private xe0 As XElement Public Sub New() Dim Pfad As String = My.Application.Info.DirectoryPath xe0 = XElement.Load(Path.Combine(Pfad, "Form45XMLFile1.xml")) End Sub Public Function Fuelle() As List(Of Klasse) Return (From xe1 In xe0...<Klasse> Select New Klasse(xe1)).ToList End Function End Class Public Class Klasse Public Sub New(xe2 As XElement) Name = xe2...<Name>.Value Mut = xe2...<Attribute>.<Mut>.Value Klugheit = xe2...<Attribute>.<Klugheit>.Value ' usw. End Sub Public Property Name As String Public Property Mut As String Public Property Klugheit As String ' usw. ''' <summary> ''' Objekt als XML-String zurückgeben ''' </summary> ''' <returns></returns> Public Function SaveKlasse() As String Dim xe4 As New XElement("Klasse") xe4.Add(New XElement("Name") With {.Value = Name}) Dim xe5 As New XElement("Attribute") xe4.Add(xe5) xe5.Add(New XElement("Mut") With {.Value = Mut}) xe5.Add(New XElement("Klugheit") With {.Value = Klugheit}) ' usw. Return xe4.ToString End Function ''' <summary> ''' Für Anzeige ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Return $"Name: {Name}, Mut: {Mut}, Klugheit: {Klugheit}" End Function End Class End Class
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Als Antwort markiert WarFred Montag, 16. Juli 2018 08:39
-
Hi Michael,
natürlich funktioniert mein Beispiel. Du kannst es selbst testen. Einfach den gesamten Code in den Codeteil eines leeren Windows Forms kopieren und Klassenname anpassen (aus Form45 z.B. Form1 machen), die XML-Datei mit ins Projekt nehmen (Eigenschaft Copy to Output: copy if newer) und Name der Datei im Code anpassen.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Montag, 16. Juli 2018 11:21