XML-Problem, Anzahl der Starttags und Endtags stimmen nicht - Wie sinnvoll ermitteln?

  • EDIT: Post #7 mit Zwischenlösung und neuer Problemstellung


    Hallo Community 【ツ】 ,


    ich muss ein klein wenig ausholen: Ich habe ein Word-Dokument, welches nicht mehr geöffnet werden kann (*.docx). Ich habe dieses Dokument entpackt, via "7z", da dies ja auch "nur" XML-Dateien etc. enthält. In diesem Archiv ist eine "document.xml" enthalten, welche fehlerhafte Start- bzw. Endtags enthält. Sprich die Anzahl der öffnenden und schließenden TAGs stimmt nicht überein.


    Woher ich dies weiß? Da ich nach der Fehlermeldung suchte, die Word ausgab - Ergebnis: https://support.microsoft.com/de-de/kb/2528942. Es besteht über eine FixIt-EXE von MS die Möglichkeit, solche Word-Dokumente reparieren zu lassen, was jedoch auch manuell getan werden kann.


    Damit komme ich nun zu meiner Frage bzw. zum Problem:
    Wie kann ich jedem öffnenden Starttag den passenden Endtag zuordnen, bzw. wenn ein Endtag fehlen sollte, diese Information (also Tag-Name und Position in der Datei) anzeigen? Die Datei zu parsen ist meine erste Idee, da die komplette Datei (im Anhang als "Unvalid.xml" bezeichnet) in keinem sinnvoll zu lesenden Format aufgebaut ist. Sprich es sind alle TAGs aneinander oder anders, es gibt keine Zeilenumbrüche (@CRLF bzw. \r\n) in der Datei.


    1. Bitte schaut euch zunächst die "Unvalid.xml" an (ich nutze Notepad++).
    2. Folgender Code um eine Übersicht zu erhalten.


    Übersicht der XML-Datei via Arrays:

    Spoiler anzeigen
    [autoit]

    ; ------------------------------------------------------------------------------; Au3_XmlCheck; ------------;; Über die DEBUG-Bereiche, gebe ich mir zunächst eine Übersicht (in Form von; Arrays aus, damit ich die TAGs analysieren kann).; ------------------------------------------------------------------------------#region includes#include <Array.au3>#endregion#region declarationLocal $sXmlFile = @ScriptDir & '\Unvalid.xml'Local $sXmlFileFormatted = @ScriptDir & '\Formatted.xml'Local $aStartTag[1], $aEndTag[1], $aMixedTag[1]#endregion#region init$hFile = FileOpen( $sXmlFile, 0 )$sRead = FileRead( $hFile )FileClose( $hFile )#endregion#region processingLocal $sFormatted = StringReplace( $sRead, '><', '>' & @CRLF & '<', 0 )$aFormatted = StringSplit( $sFormatted, @LF, 1 ); write new fileFileClose( FileWrite( FileOpen( $sXmlFileFormatted, 2 + 8 ), $sFormatted ) ); DEBUG_ArrayDisplay( $aFormatted, 'All' ) ; 14749 rowsFor $i = 1 To $aFormatted[0] Step 1 If StringInStr( $aFormatted[$i], '/>' ) <> 0 Then ; row number and <TAG /> _ArrayAdd( $aMixedTag, $i & '|' & $aFormatted[$i] ) ElseIf StringInStr( $aFormatted[$i], '</' ) <> 0 Then ; row number and </TAG> _ArrayAdd( $aEndTag, $i & '|' & $aFormatted[$i] ) Else ; row number and <TAG> _ArrayAdd( $aStartTag, $i & '|' & $aFormatted[$i] ) EndIfNext; DEBUG_ArrayDisplay( $aStartTag, 'Start' ) ; 4502 rows_ArrayDisplay( $aEndTag, 'End' ) ; 5328 rows_ArrayDisplay( $aMixedTag, 'Mixed' ) ; 4964 rows#endregion

    [/autoit]


    Wie sollte ich weiter vorgehen, um tatsächlich die nicht passenden TAGs zu finden? Gibt es evtl. entsprechende UDFs dafür (abgesehen von der "_XMLDomWrapper.au3", die mir nicht viel hilft)? Kann mir bitte jemand auf die Spünge helfen, wie man dieses Problem sinnvoll angeht bzw. umsetzt?


    Anhang:
    - Unvalid.xml.txt (Originaldatei)
    - Formatted.xml.txt (geht aus dem Skript heraus)
    - ".txt" kann weg (nur damit ich diese hier anhängen konnte)


    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!


    UserIsGrateful 【ツ】

  • Hi,
    erstmal "lesbar" machen..

    AutoIt
    $a=fileread("unvalid.xml.txt")
    $b=stringreplace($a,"><",">"&@crlf&"<",0,1)
    ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : $b = ' & stringleft($b,2000) & @CRLF & '>Error code: ' & @error & @CRLF) ;### Debug Console
  • Hallo Andy,


    Danke für deine Antwort, jedoch ist diese ehrlich gesagt weniger hilfreich, da ich genau dieses bereits in meinem Skript getan habe. Im Skript wird so die Datei "Formatted.xml" erzeugt. Meine Frage ist, wie ich sinnvoll weiter machen sollte? Ich will eigentlich nicht, jeden StartTAG und jeden EndTAG auswerten, diese zählen und mit einander vergleichen oder so. Über viele Einzelschritte komme ich sicherlich zum Ziel, doch vielleicht gibt es bessere Ansätze?


    Allgemein: Gebt bitte Bescheid, wenn ich mich mit der Problemstellung zu undeutlich ausgedrückt habe oder so. Gern formuliere ich meine Frage bzw. mein Ziel noch genauer - dachte das ich dies im Post #1 recht gut dargestellt habe, aber das ist natürlich nur meine Wahrnehmung.


    Danke für weitere Ansätze / Ideen.


    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!


    UserIsGrateful 【ツ】

    • Offizieller Beitrag

    Gebt bitte Bescheid, wenn ich mich mit der Problemstellung zu undeutlich ausgedrückt habe oder so.

    Ja, du hast nicht aufgezeigt, wie die korrekte Datei aussehen muß. Also nicht speziell diese, sondern das Muster einer korrekten *.docx.
    Ich habe z.B. noch nie eine *docx benutzt, weil die bei mir automatisch schon beim Abholen aus dem Postfach auf ordentliches *.doc umformatiert werden. Nicht abwärtskompatible Datenformate zu nutzen ist echt das letzte, aber bei M$ leider Standard. 90% aller User können bequem alle ihre Office-Aufgaben auch mit den Versionen vor MSO-2007 lösen.
    Also zeig mal, wie die Struktur richtig aussehen muss.

  • Hallo BugFix 【ツ】,


    erstmal ja, ich stimme dir zu das MS wiedermal einen schönen Bockmist gebaut hat, welcher sicherlich sogar gewollt war! Naja, nun aber zu deinen Hinweisen (merci dafür).


    Im Post #1 habe ich einen MS-Support-Link hinterlegt, welcher die Problemlösung über eine FixIt-Exe von MS ermöglicht. Damit habe ich die *.docx-Datei auch wieder lauffähig bekommen. Trotzdem möchte ich diese Aufgabe lieber selbst erledigen und ein Tool dafür schreiben. Konkret tritt dieser Fehler in einem *.docx-Word-Dokument nur in der "document.xml" auf. Mir ist diese Fehlerstellung auch nicht erst seit gestern bekannt, da ich manuell schon einige Word-Dokumente erneut gängig machen musste. Über manuellen Abgleich der TAGs -> nun soll es ein Tool werden.


    Die Datei "Unvalid.xml.txt" (ehemals "document.xml") sieht repariert so aus, dass diese ordentlich formatiert und mit passenden Start- und EndTags (Anzahlen stimmen) versehen ist. Dazu siehe Anhang:
    - "RepairedByMSFixIt.xml.txt" (ehemals "document.xml" aus der reparierten *.docx)


    Das "Wie" ist die Frage:
    Mir geht es darum, wie ich sinnvoll zu dem Ergebnis komme, welches das Tool von MS produziert hat. Bzw. kann ich somit einen generellen Check von XML-Dateien machen (zumindest für Start- und EndTags). Nur eben das Herausfinden der Position und welche(n) TAG(s) es betrifft, ist nicht so einfach.


    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!


    UserIsGrateful 【ツ】

  • Soa, hier mal ein Ansatz:

    [autoit]

    #include <Array.au3>
    #include <StringConstants.au3>
    Global $iFor, $sTmp, $iExt, $asF

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; Schritt 1: Datei einlesen und Zeile für Zeile in Array speichern
    Global $sXML = FileRead('Formatted.xml')
    Global $asXML = StringSplit($sXML, @CRLF, $STR_ENTIRESPLIT)

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; Schritt 2: Datei Zeile für Zeile lesen und offene Tags in Stack Objekt speichern.
    ; Fehlerhafte Daten und Zeilennummern ebenfalls speichern.
    Global $oTAG = ObjCreate('System.Collections.Stack')
    Global $oData = ObjCreate('System.Collections.Stack')

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; In der ersten Zeile wird die XML Versionsnummer etc. gespeichert... Interessiert uns nicht ^^
    For $iFor = 2 To $asXML[0]
    $sTmp = _XMLGetTag($asXML[$iFor])
    $iExt = @extended

    [/autoit][autoit][/autoit][autoit]

    If $sTmp Then
    If $iExt Then ; Wenn Tag geschlossen wird
    If StringSplit($oTAG.peek(), '|')[0] = $sTmp Then ; und alles gut ist ^^
    $oTAG.pop() ; entferne offenen Tag
    Else ; Ansonsten halt: Fehler :)
    $oData.push($sTmp & '|' & $iFor & '|' & $iExt)
    EndIf
    Else ; Wenn Tag geöffnet wird
    $oTAG.push($sTmp & '|' & $iFor)
    EndIf
    EndIf
    Next

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; Nun die ungeschlossenen Tags noch in die Liste einfügen:
    While $oTAG.count
    $oData.push($oTAG.pop() & '|' & '0')
    WEnd

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; Fehlerhafte Daten und deren Zeilennummern ausgeben:
    $asF = $oData.ToArray()
    _ArrayDisplay($asF)

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    ; Filtern den TAG-Namen sowie Klasse heraus
    ; @extended 0: TAG wird geöffnet
    ; 1: TAG wird geschlossen
    Func _XMLGetTag($sStr)
    ; Wenn sich der Tag wieder selber schließt (<tag /> bzw. <tag>xyz</tag>) kann man diesen ignorieren
    If StringRight($sStr, 2) = '/>' Or StringRegExp($sStr, '<.*?(?: |>).*</.*>') Then Return ''

    [/autoit][autoit][/autoit][autoit]

    Local $sTAG = StringRegExp($sStr, '<(.*?)(?: |>)', $STR_REGEXPARRAYGLOBALMATCH)[0]
    Local $bExt = StringLeft($sTAG, 1) = '/'
    Return SetExtended($bExt, ($bExt ? StringTrimLeft($sTAG, 1) : $sTAG))
    EndFunc

    [/autoit]

    Ist leider noch ein wenig fehlerhaft, mir fehlt aber gerade die Zeit das zu debuggen. Aber damit solltest du zmd. schon mal weiter kommen. Die Ausgabe sieht folgendermaßen aus:

    Name des Tags|Zeilennummer|offen oder geschlossen

    abc|123|0 << Tag abc in Zeile 123 wurde nicht geschlossen
    efg|456|1 << Tag efg in Zeile 456 wurde nicht geöffnet


    Ich setze mich nachher nochmal ran, ich hoffe dass ich dir damit schon mal einen Ansatz liefere. ^^

  • Hallo Make-Grafik 【ツ】, hallo Community 【ツ】,


    zunächst mal vielen Dank für deinen Ansatz. Werde diesen nochmals durchgehen und evtl. auch verwenden, doch zunächst (aus Zeitgründen) habe ich eine Lösung mit Arrays geschrieben (dies fiehl mir auf die Schnelle leichter) ;) .

    Meine Lösung:

    Spoiler anzeigen
    [autoit]


    ; ------------------------------------------------------------------------------
    ; Au3_XmlCheck
    ; ------------------------------------------------------------------------------
    #region includes
    #include <Array.au3>
    #include <File.au3>
    #endregion

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    #region declaration
    Local $sXmlFile = @ScriptDir & '\Unvalid.xml'
    Local $sXmlFileFormatted = @ScriptDir & '\Formatted.xml'
    Local $aStart[1], $aEnd[1]
    Local $aStartWithLine[1], $aEndWithLine[1]
    #endregion

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    #region init
    Local $hFile = FileOpen( $sXmlFile, 0 )
    Local $sRead = FileRead( $hFile )
    FileClose( $hFile )
    #endregion

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    #region processing
    ; write new file (change format)
    Local $sFormatted = StringReplace( $sRead, '><', '>' & @CRLF & '<', 0 )
    FileClose( FileWrite( FileOpen( $sXmlFileFormatted, 2 + 8 ), $sFormatted ) )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; read from new file and get TAGs to arrays
    $hFile = FileOpen( $sXmlFileFormatted, 0 )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; start at second line
    For $i = 2 To _FileCountLines( $sXmlFileFormatted ) Step 1
    Local $sLine = FileReadLine( $hFile, $i )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; no <TAG/> and no <TAG>STRING</TAG>
    If StringRight( $sLine, 2 ) <> '/>' And StringRegExp( $sLine, '<.+?>.+?</.+?>$', 0 ) <> 1 Then
    ; just the name of TAG (without attribute and values)
    Local $sStrippedRead = StringRegExp( $sLine, '<(.*?)(?: |>)', 3 )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    If StringInStr( $sLine, '</' ) = 0 Then
    ; StartTAGs
    ;~ ConsoleWrite( 'Start | ' & $i & ' | ' & $sStrippedRead[0] & @CRLF )
    _ArrayAdd( $aStart, $sStrippedRead[0] )
    _ArrayAdd( $aStartWithLine, $sStrippedRead[0] & ', Zeile: ' & $i )
    Else
    ; EndTAGs
    ;~ ConsoleWrite( 'End | ' & $i & ' | ' & StringTrimLeft( $sStrippedRead[0], 1 ) & @CRLF )
    _ArrayAdd( $aEnd, StringTrimLeft( $sStrippedRead[0], 1 ) )
    _ArrayAdd( $aEndWithLine, StringTrimLeft( $sStrippedRead[0], 1 ) & ', Zeile: ' & $i )
    EndIf
    EndIf
    Next
    FileClose( $hFile )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; DEBUG
    ;~ _ArrayDisplay( $aStart, 'Start' )
    ;~ _ArrayDisplay( $aStartWithLine, 'StartWithLine' )
    ;~ _ArrayDisplay( $aEnd, 'End' )
    ;~ _ArrayDisplay( $aEndWithLine, 'EndWithLine' )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; check counts and search all items through the arrays
    If UBound( $aStart ) - 1 = UBound( $aEnd ) - 1 Then
    For $i = 1 To UBound( $aStart ) - 1 Step 1
    Local $aIndizies = _ArrayFindAll( $aEnd, $aStart[$i] )
    If @error <> 0 Then ConsoleWrite( 'Kein EndTAG für StartTAG: ' & $aStartWithLine[$i] & @CRLF )

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    Local $aIndizies = _ArrayFindAll( $aStart, $aEnd[$i] )
    If @error <> 0 Then ConsoleWrite( 'Kein StartTAG für EndTAG: ' & $aEndWithLine[$i] & @CRLF )
    Next
    Else
    ConsoleWrite( 'Unterschiedliche Anzahl zwischen Start- und EndTags!' & @CRLF )
    EndIf
    ConsoleWrite( 'Verarbeitung beendet!' & @CRLF )
    #endregion

    [/autoit]

    Hinweis:
    Eine Ausgabe kann man durch Veränderung der "Unvalid.xml" bspw. bekommen. Dort einfach den öffnenden TAG "w:body" durch "w:body1" und den schließenden TAG durch "w:body2" ersetzen. Somit bleibt die Anzahl (zwischen Start- und EndTAGs) trotzdem gleich, aber es sind Unterschiede.

    Aber:
    Dadurch habe ich nun festgestellt, dass es gar keine Differenzen zwischen Start- und EndTAGs gibt. Also es gibt genauso viel Start- wie EndTAGs. Ausgenommen die Zeilen (in der generierten "Formatted.xml.txt"), die nicht den folgenden Aufbau haben:
    <TAG/> oder <TAG>STRING</TAG> (diese lasse ich gleich bei der Prüfung weg, da diese eindeutig sind.

    Hmmm ... es ist nun also so, dass ich kontrollieren muss, ob die Anordnung der TAGs in der Datei richtig ist. Also ob zu dem jeweiligen StartTAG auch ein richtiger EndTAG existiert und dieser an der richtigen Stelle in der XML steht.

    Wie mache ich dies nun #grübel #grübel #grübel ... ?

    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!


    UserIsGrateful 【ツ】

    • Offizieller Beitrag

    Ich dachte erst an einen rekursiven Ansatz, aber das führt zu einem Abbruch, wegen der Rekursionstiefe von AutoIt.
    Es geht aber auch einfacher mit einer Schleife:

  • Hallo Oscar 【ツ】,


    auch dir vielen Dank für dein Skriptbeispiel bzw. deine Lösung, merci. Es geht deutlich schneller als mein Ansatz und sah auf dem erstem Blick auch ganz gut aus und lief sehr gut ( RegEx hast du ja anscheinend bestens drauf #neid :) ).

    Aber:
    Als ich den StartTAG "w:body" in "w:body1" und den dazugehörigen EndTAG in "w:body2" umbenannt habe (siehe Post #7), brachte mir deine Lösung die richtigen Ausgaben. Jedoch wenn ich einen EndTAG entferne oder einen hinzufüge, kommt "XML OK? True" zurück bzw. wird ausgegeben. Daher glaube ich, dass es noch nicht einwandfrei funktioniert!

    Bitte nutze mal mein Skript vorher (Post #7), um die "Formatted.xml" (aus der "Unvalid.xml") zu generieren, damit wir die gleiche Ausgangslage haben und lass dann nochmals dein Skript mit einem zusätzlichen EndTAG (welchen du in die generierte "Formatted.xml" einfügst) laufen. Nun solltest du ebenfalls -> alles Okay zurückbekommen, was ja falsch ist.

    Mein Ziel:
    1.) Ich möchte die Zeilennummer und TAGName, der nicht passenden Start- bzw. EndTAGs erhalten, bei dem entweder kein EndTag zum StartTAG vorhanden ist oder umgekehrt (also kein StartTAG zum EndTAG).
    2.) Ich möchte irgendwie sicherstellen, dass strukturell (wenn allen StartTAGs je ein EndTAG zugeordnet ist) die Anordnung passt.

    Beispiel:
    Hier stimmt bspw. "1.)", "2.)" jedoch nicht:

    <TAG1>
    <TAG2>
    <TAG3>
    ...
    ...
    ...
    </TAG1>
    </TAG3>
    </TAG2>

    Danke weiterhin an alle mit Miträtselnden!

    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!


    UserIsGrateful 【ツ】

  • Nehmen wir an, du kannst die Formatted.xml.txt erstellen. JEDE Zeile nur ein einziges Tag. Den Fehler macht dein Code offensichtlich auf Zeile 27 mit deiner Formatted. ;)

    0. Du liest alles in einen Array.
    1. Du läufst nun Zeile für Zeile durch:
    a) Findest du ein öffnendes Tag, speicherst du dir den clean-wert davon in eine Variable, genauso wie die Zeile, in der es vorkommt.
    b) Findest du ein schließendes Tag:
    I: Schließt es das zuletzt geöffnete clean-Tag, ist dieser Code-Teil sauber. Lösche alle Zeilen vom öffnenden bis inklusive dem schließenden Tag. Beginne von vorne.
    II: Schließt es das zuletzt geöffnete clean-Tag NICHT, dann gib es als Fehler aus. Dieses geöffnete Tag wird an keiner Stelle geschlossen.

    Wenn du das ganze einmal exakt umdrehst, weißt du auch, wie du die Endtags bekommst, die niemals geöffnet wurden.

    Fehler an diesem Algorithmus ist einfach: Es zeigt dir einen Fehler DOPPELT an. Beispiel - in HTML:

    <html>
    <body>
    <div>
    <p>
    </div>
    </body>
    </html>

    Der erste Algorithmus wurde einen Fehler in Zeile 5 finden: Das div-Tag wurde niemals geöffnet.
    Der zweite Algorithmus würde feststellen, dass Tag wurde niemals geschlossen.


    Deine Aufgabe wäre hierbei nur noch die Fehler zu korrigieren - der Code kann das schwieriger bewerten. Er kann dir höchstens fehlerhafte Tags entfernen und dadurch weitere Fehler verursachen. Wenn beide Algorithmen durchlaufen, ist dein Dokument definitiv fehlerfrei..

    Es gibt Tage, da trete ich nicht ins Fettnäpfchen. Ich falle in die Friteuse.

  • Hallo Bioshade, hallo Community 【ツ】 ,

    vielen Dank für eure Antworten und Ideen. Ich habe auf Grund von anderen Prioritäten dieses Thema aus den Augen verloren (verlieren müssen). Trotzdem war es lehrreich und im Post #7 gibt es ja zum Thema auch eine Teillösung (falls jemand ein ähnliches Szenario mal haben sollte).

    Ich werde den Thread schließen, da ich auch in absehbarer Zeit nicht dazukommen werde, mich darum zu kümmern. Evtl. werde ich das Problem nochmals in C# angreifen oder wie auch immer. Danke und bis später, zur nächsten kniffligen Frage :D .

    Vielen Dank für etwaige Unterstützung (Ideen, Verbesserungsvorschläge, Kritik und Zustimmung).
    Einen angenehmen Tag noch!

    UserIsGrateful 【ツ】