2008-10-02 15 views
3

Ich habe viele XML-Dateien und ich möchte einen Bericht von ihnen generieren. Der Bericht sollte Informationen wie:Programm zum Analysieren einer Menge von XML

root 100% 
a*1 90% 
b*1 80% 
    c*5 40% 

dass alle Dokumente ein Wurzelelement haben, 90% haben eine ein Element in der Wurzel, 80% haben eine b Element in der Wurzel, 40% haben 5 c Elemente in b.

Wenn zum Beispiel einige Dokumente haben 4 c Elemente, etwa 5 und etwa 6, sollte es so etwas sagen wie:

c*4.3 4 6 40% 

was bedeutet, dass 40% haben zwischen 4 und 6 c es Elemente, und der Durchschnitt ist 4,3.

Ich suche nach freier Software, wenn es nicht existiert Ich werde es schreiben. Ich wollte es tun, aber ich überlegte, es zu überprüfen. Ich bin vielleicht nicht der Erste, der analysieren muss und einen strukturellen Überblick über Tausende von XML-Dateien erhält.

Antwort

11

Hier ist eine XSLT 2.0-Methode.

Unter der Annahme, dass eine Sequenz von Dokumentknoten enthält, die Sie scannen möchten, möchten Sie für jedes Element, das in den Dokumenten angezeigt wird, eine Zeile erstellen. Sie können <xsl:for-each-group> verwenden zu tun, dass:

<xsl:for-each-group select="$docs//*" group-by="name()"> 
    <xsl:sort select="current-group-key()" /> 
    <xsl:variable name="name" as="xs:string" select="current-grouping-key()" /> 
    <xsl:value-of select="$name" /> 
    ... 
</xsl:for-each-group> 

Dann wollen Sie die Statistiken zu den Unterlagen für dieses Element herausfinden. Zuerst finden die Dokumente in ihnen ein Element dieses Namens haben:

<xsl:variable name="docs-with" as="document-node()+" 
    select="$docs[//*[name() = $name]" /> 

Zweitens müssen Sie eine Sequenz von der Anzahl der Elemente dieses Namens in jedem der Dokumente:

<xsl:variable name="elem-counts" as="xs:integer+" 
    select="$docs-with/count(//*[name() = $name])" /> 

Und jetzt Sie können die Berechnungen durchführen. Durchschnitt, Minimum und Maximum können mit den Funktionen avg(), min() und max() berechnet werden. Der Prozentsatz ist einfach die Anzahl der Dokumente, die das Element enthalten, dividiert durch die Gesamtzahl der formatierten Dokumente.

Putting, dass zusammen:

<xsl:for-each-group select="$docs//*" group-by="name()"> 
    <xsl:sort select="current-group-key()" /> 
    <xsl:variable name="name" as="xs:string" select="current-grouping-key()" /> 
    <xsl:variable name="docs-with" as="document-node()+" 
    select="$docs[//*[name() = $name]" /> 
    <xsl:variable name="elem-counts" as="xs:integer+" 
    select="$docs-with/count(//*[name() = $name])" /> 
    <xsl:value-of select="$name" /> 
    <xsl:text>* </xsl:text> 
    <xsl:value-of select="format-number(avg($elem-counts), '#,##0.0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number(min($elem-counts), '#,##0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number(max($elem-counts), '#,##0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number((count($docs-with) div count($docs)) * 100, '#0')" /> 
    <xsl:text>%</xsl:text> 
    <xsl:text>&#xA;</xsl:text> 
</xsl:for-each-group> 

Was habe ich hier noch nicht fertig ist zerklüftet die Linien entsprechend der Tiefe des Elements. Ich habe gerade die Elemente alphabetisch sortiert, um Ihnen Statistiken zu geben. Dafür gibt es zwei Gründe: Erstens ist es wesentlich schwieriger (wie zu aufwendig, hier zu schreiben), die Elementstatistiken in einer Art Struktur darzustellen, die ihre Darstellung in den Dokumenten widerspiegelt, nicht zuletzt, weil verschiedene Dokumente unterschiedliche Strukturen haben können. Zweitens kann in vielen Markup-Sprachen die genaue Struktur der Dokumente nicht bekannt sein (weil beispielsweise Abschnitte innerhalb von Abschnitten in beliebiger Tiefe verschachtelt werden können).

Ich hoffe, es ist dennoch nützlich.

UPDATE:

die XSLT-Wrapper benötigen und einige Anweisungen für XSLT ausgeführt wird? OK. Holen Sie zuerst Ihre Hände auf Saxon 9B.

Sie müssen alle Dateien, die Sie analysieren möchten, in ein Verzeichnis einfügen. Mit Saxon können Sie auf alle Dateien in diesem Verzeichnis (oder seinen Unterverzeichnissen) zugreifen, indem Sie eine Sammlung mit einer special URI syntax verwenden. Es lohnt sich, sich diese Syntax anzuschauen, wenn Sie rekursiv recherchieren oder die Dateien, die Sie sehen, nach ihrem Dateinamen filtern möchten.

Nun ist die vollständige XSLT:

<xsl:stylesheet version="2.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    exclude-result-prefixes="xs"> 

<xsl:param name="dir" as="xs:string" 
    select="'file:///path/to/default/directory?select=*.xml'" /> 

<xsl:output method="text" /> 

<xsl:variable name="docs" as="document-node()*" 
    select="collection($dir)" /> 

<xsl:template name="main"> 
    <xsl:for-each-group select="$docs//*" group-by="name()"> 
    <xsl:sort select="current-group-key()" /> 
    <xsl:variable name="name" as="xs:string" select="current-grouping-key()" /> 
    <xsl:variable name="docs-with" as="document-node()+" 
     select="$docs[//*[name() = $name]" /> 
    <xsl:variable name="elem-counts" as="xs:integer+" 
     select="$docs-with/count(//*[name() = $name])" /> 
    <xsl:value-of select="$name" /> 
    <xsl:text>* </xsl:text> 
    <xsl:value-of select="format-number(avg($elem-counts), '#,##0.0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number(min($elem-counts), '#,##0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number(max($elem-counts), '#,##0')" /> 
    <xsl:text> </xsl:text> 
    <xsl:value-of select="format-number((count($docs-with) div count($docs)) * 100, '#0')" /> 
    <xsl:text>%</xsl:text> 
    <xsl:text>&#xA;</xsl:text> 
    </xsl:for-each-group> 
</xsl:template> 

</xsl:stylesheet> 

Und es laufen Sie würde so etwas tun:

 
> java -jar path/to/saxon.jar -it:main -o:report.txt dir=file:///path/to/your/directory?select=*.xml 

Dies sagt Saxon den Prozess mit der Vorlage main Namen zu starten, setzen die dir Parameter auf file:///path/to/your/directory?select=*.xml und senden Sie den Ausgang an report.txt.

+0

Jeni, ich bin ein großer Fan von dir, da deine Seite mir bei vielen Herausforderungen mit XPath geholfen hat. Ich bin froh, dass Sie bei uns hier bei StackOverflow sind. –

+0

Habe dein Update gesehen - jetzt bekomme ich es :) –

0

[Community posten, hier: kein Karma beteiligt;)]
Ich schlage vor, ein code-challenge hier:

Parsen alle xml in xmlfiles.com/examples finden und versuchen, mit der folgenden Ausgabe zu kommen :

Analyzing plant_catalog.xml: 
Analyzing note.xml: 
Analyzing portfolio.xml: 
Analyzing note_ex_dtd.xml: 
Analyzing home.xml: 
Analyzing simple.xml: 
Analyzing cd_catalog.xml: 
Analyzing portfolio_xsl.xml: 
Analyzing note_in_dtd.xml: 
Statistical Elements Analysis of 9 xml documents with 34 elements 
CATALOG*2 22% 
    CD*26 50% 
    ARTIST*26 100% 
    COMPANY*26 100% 
    COUNTRY*26 100% 
    PRICE*26 100% 
    TITLE*26 100% 
    YEAR*26 100% 
    PLANT*36 50% 
    AVAILABILITY*36 100% 
    BOTANICAL*36 100% 
    COMMON*36 100% 
    LIGHT*36 100% 
    PRICE*36 100% 
    ZONE*36 100% 
breakfast-menu*1 11% 
    food*5 100% 
    calories*5 100% 
    description*5 100% 
    name*5 100% 
    price*5 100% 
note*3 33% 
    body*1 100% 
    from*1 100% 
    heading*1 100% 
    to*1 100% 
page*1 11% 
    para*1 100% 
    title*1 100% 
portfolio*2 22% 
    stock*2 100% 
    name*2 100% 
    price*2 100% 
    symbol*2 100% 
+0

Diese Challenge berücksichtigt nicht den zweiten Teil der ursprünglichen Frage und verfolgt, wie oft ein Tag erscheint. – tye

+0

Ich bin mir nicht sicher, ob ich folge: Die Anzahl des Auftretens eines Tags ist gleich nach dem '*'. Wenn dieses Element in verschiedenen XML-Dateien mit einer anderen Kardinalität angezeigt würde, würde es als "minimale ... maximale durchschnittliche" Kardinalität angezeigt. Da dies in den xmlcodes-Beispieldateien nicht der Fall ist, haben Sie nur '* x'. – VonC

0

Hier ist eine mögliche Lösung in rubin diese code-challenge ...
Da es mein erster Rubin progra ist Ich bin mir sicher, dass es ziemlich schrecklich codiert ist, aber es kann zumindest die Frage von J. Pablo Fernandez beantworten.

Kopieren-fügen Sie es in eine '.rb-Datei ein und rufen Ruby darauf auf. Wenn Sie eine Internetverbindung haben, wird es funktionieren;)

require "rexml/document" 
require "net/http" 
require "iconv" 
include REXML 
class NodeAnalyzer 
    @@fullPathToFilesToSubNodesNamesToCardinalities = Hash.new() 
    @@fullPathsToFiles = Hash.new() #list of files in which a fullPath node is detected 
    @@fullPaths = Array.new # all fullpaths sorted alphabetically 
    attr_reader :name, :father, :subNodesAnalyzers, :indent, :file, :subNodesNamesToCardinalities 
    def initialize(aName="", aFather=nil, aFile="") 
     @name = aName; @father = aFather; @subNodesAnalyzers = []; @file = aFile 
    @subNodesNamesToCardinalities = Hash.new(0) 
    if aFather && !aFather.name.empty? then @indent = " " else @indent = "" end 
    if aFather 
     @indent = @father.indent + self.indent 
     @father.subNodesAnalyzers << self 
     @father.updateSubNodesNamesToCardinalities(@name) 
    end 
    end 
    @@nodesRootAnalyzer = NodeAnalyzer.new 
    def NodeAnalyzer.nodesRootAnalyzer 
    return @@nodesRootAnalyzer 
    end 
    def updateSubNodesNamesToCardinalities(aSubNodeName) 
    aSubNodeCardinality = @subNodesNamesToCardinalities[aSubNodeName] 
    @subNodesNamesToCardinalities[aSubNodeName] = aSubNodeCardinality + 1 
    end 
    def NodeAnalyzer.recordNode(aNodeAnalyzer) 
    if aNodeAnalyzer.fullNodePath.empty? == false 
     if @@fullPaths.include?(aNodeAnalyzer.fullNodePath) == false then @@fullPaths << aNodeAnalyzer.fullNodePath end 
     # record a full path in regard to its xml file (records it only one for a given xlm file) 
     someFiles = @@fullPathsToFiles[aNodeAnalyzer.fullNodePath] 
     if someFiles == nil 
     someFiles = Array.new(); @@fullPathsToFiles[aNodeAnalyzer.fullNodePath] = someFiles; 
     end 
     if !someFiles.include?(aNodeAnalyzer.file) then someFiles << aNodeAnalyzer.file end 
    end 
    #record cardinalties of sub nodes for a given xml file 
    someFilesToSubNodesNamesToCardinalities = @@fullPathToFilesToSubNodesNamesToCardinalities[aNodeAnalyzer.fullNodePath] 
    if someFilesToSubNodesNamesToCardinalities == nil 
     someFilesToSubNodesNamesToCardinalities = Hash.new(); @@fullPathToFilesToSubNodesNamesToCardinalities[aNodeAnalyzer.fullNodePath] = someFilesToSubNodesNamesToCardinalities ; 
    end 
    someSubNodesNamesToCardinalities = someFilesToSubNodesNamesToCardinalities[aNodeAnalyzer.file] 
    if someSubNodesNamesToCardinalities == nil 
     someSubNodesNamesToCardinalities = Hash.new(0); someFilesToSubNodesNamesToCardinalities[aNodeAnalyzer.file] = someSubNodesNamesToCardinalities 
     someSubNodesNamesToCardinalities.update(aNodeAnalyzer.subNodesNamesToCardinalities) 
    else 
     aNodeAnalyzer.subNodesNamesToCardinalities.each() do |aSubNodeName, aCardinality| 
     someSubNodesNamesToCardinalities[aSubNodeName] = someSubNodesNamesToCardinalities[aSubNodeName] + aCardinality 
     end 
    end 
    #puts "someSubNodesNamesToCardinalities for #{aNodeAnalyzer.fullNodePath}: #{someSubNodesNamesToCardinalities}" 
    end 
    def file 
    #if @file.empty? then @father.file else return @file end 
    if @file.empty? then if @father != nil then return @father.file else return '' end else return @file end 
    end 
    def fullNodePath 
    if @father == nil then return '' elsif @father.name.empty? then return @name else return @father.fullNodePath+"/"[email protected] end 
    end 
    def to_s 
    s = "" 
    if @name.empty? == false 
     s = "#{@indent}#{self.fullNodePath} [#{self.file}]\n" 
    end 
    @subNodesAnalyzers.each() do |aSubNodeAnalyzer| 
     s = s + aSubNodeAnalyzer.to_s 
    end 
    return s 
    end 
    def NodeAnalyzer.displayStats(aFullPath="") 
    s = ""; 
    if aFullPath.empty? then s = "Statistical Elements Analysis of #{@@nodesRootAnalyzer.subNodesAnalyzers.length} xml documents with #{@@fullPaths.length} elements\n" end 
    someFullPaths = @@fullPaths.sort 
    someFullPaths.each do |aFullPath| 
     s = s + getIndentedNameFromFullPath(aFullPath) + "*" 
     nbFilesWithThatFullPath = getNbFilesWithThatFullPath(aFullPath); 
     aParentFullPath = getParentFullPath(aFullPath) 
     nbFilesWithParentFullPath = getNbFilesWithThatFullPath(aParentFullPath); 
     aNameFromFullPath = getNameFromFullPath(aFullPath) 
     someFilesToSubNodesNamesToCardinalities = @@fullPathToFilesToSubNodesNamesToCardinalities[aParentFullPath] 
     someCardinalities = Array.new() 
     someFilesToSubNodesNamesToCardinalities.each() do |aFile, someSubNodesNamesToCardinalities| 
     aCardinality = someSubNodesNamesToCardinalities[aNameFromFullPath] 
     if aCardinality > 0 && someCardinalities.include?(aCardinality) == false then someCardinalities << aCardinality end 
     end 
     if someCardinalities.length == 1 
     s = s + someCardinalities.to_s + " " 
     else 
     anAvg = someCardinalities.inject(0) {|sum,value| Float(sum) + Float(value) }/Float(someCardinalities.length) 
     s = s + sprintf('%.1f', anAvg) + " " + someCardinalities.min.to_s + "..." + someCardinalities.max.to_s + " " 
     end 
     s = s + sprintf('%d', Float(nbFilesWithThatFullPath)/Float(nbFilesWithParentFullPath) * 100) + '%' 
     s = s + "\n" 
    end 
    return s 
    end 
    def NodeAnalyzer.getNameFromFullPath(aFullPath) 
    if aFullPath.include?("/") == false then return aFullPath end 
    aNameFromFullPath = aFullPath.dup 
    aNameFromFullPath[/^(?:[^\/]+\/)+/] = "" 
    return aNameFromFullPath 
    end 
    def NodeAnalyzer.getIndentedNameFromFullPath(aFullPath) 
    if aFullPath.include?("/") == false then return aFullPath end 
    anIndentedNameFromFullPath = aFullPath.dup 
    anIndentedNameFromFullPath = anIndentedNameFromFullPath.gsub(/[^\/]+\//, " ") 
    return anIndentedNameFromFullPath 
    end 
    def NodeAnalyzer.getParentFullPath(aFullPath) 
    if aFullPath.include?("/") == false then return "" end 
    aParentFullPath = aFullPath.dup 
    aParentFullPath[/\/[^\/]+$/] = "" 
    return aParentFullPath 
    end 
    def NodeAnalyzer.getNbFilesWithThatFullPath(aFullPath) 
    if aFullPath.empty? 
     return @@nodesRootAnalyzer.subNodesAnalyzers.length 
    else 
     return @@fullPathsToFiles[aFullPath].length; 
    end 
    end 
end 
class REXML::Document 
    def analyze(node, aFatherNodeAnalyzer, aFile="") 
    anNodeAnalyzer = NodeAnalyzer.new(node.name, aFatherNodeAnalyzer, aFile) 
    node.elements.each() do |aSubNode| analyze(aSubNode, anNodeAnalyzer) end 
    NodeAnalyzer.recordNode(anNodeAnalyzer) 
    end 
end 

begin 
    anXmlFilesDirectory = "xmlfiles.com/examples/" 
    anXmlFilesRegExp = Regexp.new("http:\/\/" + anXmlFilesDirectory + "([^\"]*)") 
    a = Net::HTTP.get(URI("http://www.google.fr/search?q=site:"+anXmlFilesDirectory+"+filetype:xml&num=100&as_qdr=all&filter=0")) 
    someXmlFiles = a.scan(anXmlFilesRegExp) 
    someXmlFiles.each() do |anXmlFile| 
    anXmlFileContent = Net::HTTP.get(URI("http://" + anXmlFilesDirectory + anXmlFile.to_s)) 
    anUTF8XmlFileContent = Iconv.conv("ISO-8859-1//ignore", 'UTF-8', anXmlFileContent).gsub(/\s+encoding\s*=\s*\"[^\"]+\"\s*\?/,"?") 
    anXmlDocument = Document.new(anUTF8XmlFileContent) 
    puts "Analyzing #{anXmlFile}: #{NodeAnalyzer.nodesRootAnalyzer.name}" 
    anXmlDocument.analyze(anXmlDocument.root,NodeAnalyzer.nodesRootAnalyzer, anXmlFile.to_s) 
    end 
    NodeAnalyzer.recordNode(NodeAnalyzer.nodesRootAnalyzer) 
    puts NodeAnalyzer.displayStats 
end 
+0

Autsch. Wenn Sie nur ein paar kürzere Bezeichner verwendet haben, ist der Code möglicherweise sogar lesbar. – JesperE

0

Gehen Sie mit JeniT Antwort - sie ist eine der ersten XSLT-Gurus ist fing ich von hinten auf '02 zu lernen. Um die Leistungsfähigkeit von XML wirklich zu schätzen, sollten Sie mit XPath und XSLT arbeiten und lernen, die Knoten zu manipulieren.

+0

Ich stimme zu, JeniTs Antwort ist der richtige Track ... aber es ist kein komplettes Skript, das ich einfach starten kann und prüfen, ob es funktioniert. Fühlen Sie sich frei, ein vollständigeres xslt-Skript zu schreiben, um die Herausforderung zu beantworten;) – VonC

+0

Jeni geht davon aus, dass Sie die Dokumente verketten, bevor Sie das Skript/den Prozess ausführen. Wäre das nicht genug? Sie könnten die Dateien mit Dateiströmen kombinieren, den XML/XSLT-Namespace laden und ausführen. –

+0

Eigentlich gehe ich nicht davon aus, dass die Dokumente verkettet sind, sondern dass sie sich in einer XPath-Folge von Dokumentknoten befinden, die am einfachsten mit 'collection()' erzeugt wird. Ich habe meine Antwort oben aktualisiert, um das vollständige Stylesheet zu erhalten. – JeniT

Verwandte Themen