XML select tags by value of sub tag

Crys

Lt. Commander
Registriert
Apr. 2009
Beiträge
1.634
Hallo miteinander,

ich muss viele xml- (oder genauer gml-) Dateien bearbeiten. Es geht darum, aus einer Datei eine neue zu erstellen, welche nur Tags enthält, welche bestimmte Attribute enthalten.
Die Sprache oder Methode ist egal, es soll so schnell wie möglich funktionieren, da es ca. 1TB an Daten sind. Am liebsten per Linux Shell.

Ein Beispiel:
XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<core:CityModel xmlns:xal=usw>
    <core:cityObjectMember>
        <bldg:Building gml:id="ID123">
            <core:123>
                ...
            <core:123>
            <bldg:function>234</bldg:function>
            <core:789>
                ...
            <core:789>
        </bldg:Building>
        <bldg:Building gml:id="ID789">
            <core:123>
                ...
            <core:123>
            <bldg:function>678</bldg:function>
            <core:789>
                ...
            <core:789>
        </bldg:Building>
    </core:cityObjectMember>
    <core:cityObjectMember>
        ...
    </core:cityObjectMember>
</core:CityModel>

Hier sieht man in Zeile 8den Tag "bldg:function" mit dem Inhalt "234". Immer wenn der Inhalt dieses Tags einen bestimmten Wert enthält soll der komplette vorherige Tag "bldg:Building" kopiert werden, sowie alle Eltern-Element "core:cityObjectMember" und "core:CityModel", aber ohne Kinder. Also Zeile 1 bis 12 und 22 und 26.

"bldg:Building" enthält immer eines oder kein "bldg:function".
"core:cityObjectMember" kann mehrere "bldg:Building" enthalten.
"core:CityModel" gibt es je Datei nur einmal.
Dies alles soll für 1 Mio. Dateien gelooped werden.

Wie mache ich das am geschicktesten?

Vielen Dank.
 
Ich hätte ja intuitiv gleich mal XPath in den Raum geworfen. Anscheinend gibts mittlerweile zig gute Alternativen für die Linux Konsole:
https://stackoverflow.com/questions/15461737/how-to-execute-xpath-one-liners-from-shell

Wenn die XML-Dateien alle gleich sind, könntest Du natürlich die Zeilennummern knallhart mit head und tail rausziehen und einfach in eine neue Datei umleiten.
Quick'n'dirty
Code:
head -12 testxml.xml >neutestxml.xml;sed -n 22p testxml.xml >>neutestxml.xml;tail -n 1 testxml.xml >>neutestxml.xml

Ansonsten ein Perl-Script probieren. Was Du bei der Masse der Daten vermeiden sollest, sind aufwendige Pipe Konstruktionen (erzeugen Forks), auch awk und Co. Am schnellsten sind Bash-Inline Kommandos, wenn sie möglich sind.

Wenn das länger gleichbleibend benötigt wird, würde ich sogar ein kleines C-Programm schreiben, das wäre am schnellsten.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Crys
PHuV schrieb:
Wenn die XML-Dateien alle gleich sind,
Das leider nie. Deshalb muss ich nach den Tags und Values gehen.

PHuV schrieb:
Am schnellsten sind Bash-Inline Kommandos, wenn sie möglich sind.
Ich habe mich auch gleich an xmlstarlet versucht, nur kann ich hier nur Attribut, Value oder Tag ganz exportieren. Aber nicht den Parent-Tag ohne die weiteren Child-Tags (ohne entsprechende Tag-Value).

Mir fehlt ein Schlagwort, wonach ich suchen kann. "XML select tags by value of sub tag" ist nicht so zielführend ...
 
Dann kommst Du leider nicht ums parsen und entsprechendem Auswerten hin. Hier vielleicht mit einer DTD und XLST Transformation rangehen? Aber das wird nicht sehr performant werden.
 
  • Gefällt mir
Reaktionen: Crys
1TB Daten in 1 Mio Dateien? Also jede Datei etwa ein MB groß?

XPath ist schon der richtige Ansatz. Aber es wird umständlich wegen der Namensräume. Da braucht man einen passenden Namespacemanager dazu.

Die Scriptsprache (oder was auch immer) tritt da eher in den Hintergrund. Wichtig ist die Interaktion mit XML - Powershell Core wäre eine Idee, ist aber selbstverständlich nicht Muß und es gibt sicherlich genügend andere Optionen.

Im Sinne des XPath Ausdrucks muß man halt am Ende wieder entsprechend weit zurückgehen:
Code:
 /root:RootNode/otherprefix:SecondLevelNode/leaf:ChildNode[text()="Inhalt"]/..
oder in der Art.

Da muß man ein bißchen basteln. Der erste große Aufwand wird wohl der Namespacemanager, wenn das so dermaßen viele Namespaces sind wie angedeutet.

Ganz wichtig, die Präfixe sind nichts als Aliases. Identifikatoren sind nur und ausschließlich die zugeordneten URIs. Entsprechend darf der Namespacemanager andere Präfixes haben, aber muß zwingend die richtigen URIs angeben, damit die Zuordnung tag-zu-Namensraum und weitergehend für die Abfrage via XPath funktioniert.
 
  • Gefällt mir
Reaktionen: Crys, PHuV und andy_m4
Auf welchem Medium sind die Dateien gespeichert?
Welche Anbindung besteht?
Wie viel Arbeitsspeicher und Kerne haben wir zur Verfügung?
Kannst du mal 1-3 der Dateien anhängen, damit man ein wenig damit rumspielen kann?
 
  • Gefällt mir
Reaktionen: Crys
RalphS schrieb:
1TB Daten in 1 Mio Dateien? Also jede Datei etwa ein MB groß?
Ja, es variiert aber sehr stark.

Killkrog schrieb:
Welche Anbindung besteht?
Wie viel Arbeitsspeicher und Kerne haben wir zur Verfügung?
Alles auf internen NVMe, i7 14. Gen. Die Zeit ist aber nicht allzu relevant, es könnte auch ein paar Tage durchrechnen.

Killkrog schrieb:
Kannst du mal 1-3 der Dateien anhängen, damit man ein wenig damit rumspielen kann?
Ein 2MB Beispiel: https://cloud8.eu/s/HpMPQPG4fXFpmd5
 
Nur um sicher zu gehen, dass ich die Vorraussetzungen korrekt begriffen habe...

Angenommen "51009_1611" wäre der gesuchte Wert, entspricht die angehängte Datei dann dem, was du erwartest?

Falls ja krieg ich deine Beispieldatei in ca. 2,88ms gelesen, bearbeitet und wieder auf die Platte geschrieben.

Das wären bei deinen 1M Dateien so um die 50 Minuten.

Hab' jetzt nicht großartig rumoptimiert und das Ergebnis variiert natürlich stark vom Input und der Hardware, aber ich glaub weitere Mühe kann man sich da sparen...
 

Anhänge

  • 20220503065752-0001.xml.txt
    58,1 KB · Aufrufe: 151
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Crys
So sagt er... aber eine Referenzimplementierung bleibt er schuldig. Soll ich wirklich "Bullshit" rufen? :daumen:

Soweit ich die Situation überblicke (und wenn ich unterstelle, daß das nicht einfach händisch rauskopiert wurde) dann ist eine Implementierung in PS Core zwar natürlich möglich, aber deutlich aufwendiger, insbesondere im Hinblick auf die Erstellung der Ziel-Datei (welche ja weiterhin valide sein muß).

Bin auf die Referenzimplementierung gespannt. 👍
 
  • Gefällt mir
Reaktionen: PHuV
Crys schrieb:
Mir fehlt ein Schlagwort, wonach ich suchen kann. "XML select tags by value of sub tag" ist nicht so zielführend ...
Schlagwort ist: (de)serialisieren
XML Dateien sind gespeicherte Objekte. Grundsätzlich läd man ein Objekt aus seiner Datei, arbeitet damit und speichert es ggfs. wieder. Deshalb unterstützen dies auch so viele Sprachen.
Bereits ein
Code:
dataSet.ReadXml("file.xml");
liefert in C# etwas typisiertes. In Java ist es genauso kompliziert.

Transformation ist die Alternative wenn der PC oder Mensch obiges nicht kann.
Code mit Nodes zu schreiben ist für mich hingegen völlig abwegig.
 
Zuletzt bearbeitet:
Nein ist es nicht. Man kann’s dafür verwenden, ja. Aber XML ist zuallererst mal eine hierarchische Datenbank. Nicht mehr. Nicht weniger.

PCs sind auch keine Spielekonsolen. Auch wenn sie hier jeder nur so nutzt.
 
Objekt bzw. Datenbank sind Begriffe aus unterschiedlichen Namensräumen, Programmierung vs. Datenarten. Das Thema hier ist allerdings Programmierung.

Für Nutzer von XPath sind XMLs nur strukturierte, hierarchischen Daten. Nicht mehr.
Für Nutzer von Serialisierung sind sie auch typisiert und relational.
 
Zuletzt bearbeitet:
Sicher dass du XML meinst ? Ein solches Dokument besitzt nämlich genau 1 root element. Immer.
 
das "persons" objekt. Ein array bzw. eine collection ist selbst ja auch ein Objekt.
 
Java:
package com.killkrog.xmlProcessing;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

public class IOTools {
 
    // ===================================================================
    // {[> Attributes
    // ==============
    private static final Transformer transformer;
 
 
 
    // ===================================================================
    // {[> Initializers and Constructors
    // =================================
    static {
    
        try {
        
            TransformerFactory tFactory = TransformerFactory.newInstance();
            tFactory.setAttribute("indent-number", 2);
        
            transformer = tFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
        
        } catch (TransformerConfigurationException e) {
            throw new RuntimeException(e);
        }
    }
 
 
 
    // ===================================================================
    // {[> Public Static Methods
    // =========================
    public static Document read(File f) {
    
        Document doc;
    
        try {
            doc = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder().parse(f);
        } catch (IOException | ParserConfigurationException | SAXException e) {
            throw new RuntimeException(e);
        }
    
        removeInvalidTextNodes(doc.getFirstChild());
    
        return doc;
    }
 
    public static void write(Document document, File destination) {
    
        try {
        
            transformer.transform(
                    new DOMSource(document),
                    new StreamResult(new OutputStreamWriter(new FileOutputStream(destination), StandardCharsets.UTF_8))
            );
        
        } catch (FileNotFoundException | TransformerException e) {
            throw new RuntimeException(e);
        }
    }
 
 
 
    // ===================================================================
    // {[> Private Static Methods
    // ==========================
    private static boolean containsChildOfType(Node parent, short type) {
    
        NodeList nodeList = parent.getChildNodes();
    
        for (int i = 0; i < nodeList.getLength(); i++) {
        
            if (nodeList.item(i).getNodeType() == type) {
                return true;
            }
        }
    
        return false;
    }
 
    private static void removeInvalidTextNodes(Node node) {
    
        NodeList nodeList = node.getChildNodes();
        Node parent;
    
        for (int i = nodeList.getLength() - 1; i >= 0; i--) {
        
            removeInvalidTextNodes(nodeList.item(i));
        
            if (nodeList.item(i).getNodeType() == Node.TEXT_NODE) {
            
                parent = nodeList.item(i).getParentNode();
            
                if ((parent != null) && containsChildOfType(parent, Node.ELEMENT_NODE)) {
                    parent.removeChild(nodeList.item(i));
                }
            }
        }
    }
}

Java:
package com.killkrog.xmlProcessing;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class ProcessingJob implements Runnable {
 
    // ===================================================================
    // {[> Attributes
    // ==============
    private final File file;
 
 
 
    // ===================================================================
    // {[> Initializers and Constructors
    // =================================
    public ProcessingJob(File file) {
        this.file = file;
    }
 
 
 
    // ===================================================================
    // {[> Private Static Methods
    // ==========================
    private static List<Node> children(Node node) {
    
        List<Node> children = new ArrayList<>();
    
        NodeList nl = node.getChildNodes();
    
        for (int i = 0; i < nl.getLength(); i++) {
            children.add(nl.item(i));
        }
    
        return children;
    }
 
    private static void removeBuildingsWithoutValidFunction(Node cityObjectMember) {
    
        boolean containsValidFunction;
    
        for (Node building : children(cityObjectMember)) {
        
            if (building.getNodeName().equals("bldg:Building")) {
            
                containsValidFunction = false;
            
                for (Node function : children(building)) {
                
                    if (function.getNodeName().equals("bldg:function")) {
                    
                        if (function.getTextContent().equals(XMLProcessor.VALID_FUNCTION_VALUE)) {
                            containsValidFunction = true;
                        }
                    
                        break;
                    }
                }
            
                if (!containsValidFunction) {
                    cityObjectMember.removeChild(building);
                }
            }
        }
    }
 
 
 
    // ===================================================================
    // {[> Public Methods
    // ==================
    public void run() {
    
        Document document = IOTools.read(file);
    
        Node cityModel = document.getFirstChild();
    
        for (Node cityObjectMember : children(cityModel)) {
        
            if (!cityObjectMember.getNodeName().equals("core:cityObjectMember")) {
                continue;
            }
        
            removeBuildingsWithoutValidFunction(cityObjectMember);
        
            if (cityObjectMember.getChildNodes().getLength() == 0) {
                cityModel.removeChild(cityObjectMember);
            }
        }
    
        IOTools.write(document, new File(XMLProcessor.OUTPUT_DIRECTORY, file.getName()));
    }
}

Java:
package com.killkrog.xmlProcessing;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


public class XMLProcessor {
 
    // ===================================================================
    // {[> Attributes
    // ==============
    public static final File OUTPUT_DIRECTORY = new File("D:/! XML Processing/output");
    public static final File INPUT_DIRECTORY = new File("D:/! XML Processing/input");
    public static final String VALID_FUNCTION_VALUE = "51009_1611";
 
 
 
    // ===================================================================
    // {[> Public Static Methods
    // =========================
    public static void main(String[] args) {
    
        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    
        for (File file : INPUT_DIRECTORY.listFiles()) {
            pool.execute(new ProcessingJob(file));
        }
    
        pool.shutdown();
    
        try {
            pool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Explizite Validierung der geschriebenen Datei wird nicht gemacht. Da aber die DOM Manipulation sehr nachvollziehbar aussieht, würde ich auch eher "per Hand" validieren, falls überhaupt benötigt.

Damit meine ich, in der DOM Struktur schauen, ob ein Dokument erzeugt wurde, welches kein einziges cityObjectMember mehr enthält, und entsprechend eine Warnung ausgeben. Irgendwie sowas. Falls das überhaupt wichtig ist für dich. Musst selber wissen.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: PHuV, RalphS und Crys
Zurück
Oben