Andreas-Lindig: wann ruft der Parser 'expat' Funktionen auf?

Hallo Forum,

Ich versuche gerade eine XML-Datei zu parsen, mit expat. Schön - geht auch, aber ich verstehe das Vorgehen dieses Parsers nicht, besonders wann er den Handler für Textdaten aufruft.

//XML-Datei
<?xml version="1.0" encoding="UTF-8"?>
<sect1>
   <title>
      Absatztitel
   </title>

<para>
      Absatztext
      neue Zeile
   </para>
</sect1>

//Handler für Daten ( CDATA ) setzen
xml_set_character_data_handler( $parser, "daten" );

//Funktion "daten()"
function daten( $parser, $data )
{
   echo $data;
}

für einfache Beispiele funktioniert das soweit. Aber, wenn ich z.B. Zeilenumbrüche in der XML-Datei mit nl2br() umsetzen will:

function daten( $parser, $data )
{
   echo nl2br($data);
}

stelle ich fest, daß diese Funktion nicht etwa einmal für "Absatztitel" und einmal für "Absatztext" aufgerufen wird, sondern andauernd. Z.B. auch zwischen "</title>" und "<para>" und bei Leerzeichen im Text usw.

Keine Ahnung, wie bringe ich das Teil dazu den Text innerhalb zweier zusammengehöriger Tags als _ein_ Ganzes aufzufassen?

Gruß, Andreas

--
<img src="http://was-ist-das.andreas-lindig.de/was_ist_das_fetzen.jpg" border="0" alt="">
http://was-ist-das.andreas-lindig.de
  1. Halihallo Andreas

    //Handler für Daten ( CDATA ) setzen

    Der wird auch bei PCDATA aufgerufen ;)
    Und zwar, was du noch merken wirst, falls ein Entity geparsed werden
    soll genau einmal für das Entity alleine.

    z.B. wird 'hello&world' dreimal data() aufrufen.

    stelle ich fest, daß diese Funktion nicht etwa einmal für "Absatztitel" und einmal für "Absatztext" aufgerufen wird, sondern andauernd. Z.B. auch zwischen "</title>" und "<para>" und bei Leerzeichen im Text usw.

    Richtig.
    http://www.xml.com/pub/a/1999/09/expat/index.html?page=3
    <cite>
    A single block of contiguous text free of markup may still result in a sequence of calls to this handler. In other words, if you're searching for a pattern in the text, it may be split across calls to this handler.
    </cite>

    Soweit ich es gesehen habe, wird für jeden Zeilenumbruch, für
    jedes Entity, sowie nach dem Zeilenumbruch die Leerschläge bis zum
    ersten nicht-Leerschlag der CharacterData-Handler aufgerufen.

    Es ist eigentlich auch völlig egal, wann genau er aufgerufen wird.
    Beim Programmieren muss man sich einfach bewusst sein, dass er
    einfach "irgendwie" aufgerufen wird. Dass die Reihenfolge stimmt und
    der Inhalt komplett ist, ist das einzige was man zu wissen braucht.

    Das Zusammenhängen der einzelnen, separaten CharacterData-Strings,
    nennt man normalize. Bei XML-DOM gibt es eine entsprechende Methode,
    die dies dem Programmierer abnimmt. Bei Expat musst du eine
    entsprechende Funktion selber schreiben.

    Keine Ahnung, wie bringe ich das Teil dazu den Text innerhalb zweier zusammengehöriger Tags als _ein_ Ganzes aufzufassen?

    Schreib dir eine eigene normalize-Funktion...

    Ich kann dir hierbei einen Ansatz geben:
    Wann immer der CharacterHandler aufgerufen wird, hängst du den Text
    an eine (globale) Variable an. Falls ein anderer Handler (z.B. Start)
    aufgerufen wird, rufst du einen eigenen Handler "RealCharacterData"
    auf, dem du diese (globale) Variable übergibst (natürlich bevor der
    andere Handler Aktionen zum Verarbeiten vornimmt).

    So hast du sozusagen ein erweitertes Expat-Handler-Handling, wo
    der (dein eigener) CharacterData-Handler auf normalisierten
    Textnodes operiert.

    Viele Grüsse

    Philipp

    --
    The only program that runs perfectly every time, is a virus.
    1. Hallo Philipp,

      vielen Dank erstmal, ich muß mir das gleich zuhause nochmal reintun ;-)

      Aber eins kann ich nicht nachvollziehen:

      ...Wann immer der CharacterHandler aufgerufen wird, hängst du den Text an eine (globale) Variable an.

      soweit sogut,
      aaaber:

      Falls ein anderer Handler (z.B. Start) aufgerufen wird, rufst du einen eigenen Handler "RealCharacterData" auf, dem du diese (globale) Variable übergibst (natürlich bevor der andere Handler Aktionen zum Verarbeiten vornimmt).

      warum soll ich "start()" den Inhaltstext übergeben? start arbeitet bei mir doch richtig, also macht aus z.B. <titel> ein <h1>

      [...Vorschau lesen...]

      oder habe ich das jetzt falsch verstanden: "RealCharacterData()" soll den bisherigen Inhaltsstring - der ja jetzt komplett ist, weil eine andere Funktion aufgerufen wurde - so umformen, wie ich ihn haben will? Also ich rufe "RealCharacterData()" aus "start()" heraus als erstes auf?

      Gruß, Andreas

      --
      <img src="http://was-ist-das.andreas-lindig.de/was_ist_das_fetzen.jpg" border="0" alt="">
      hier könnte auch ruhig mal'n neues Bild stehen.
      1. Halihallo Andreas

        oder habe ich das jetzt falsch verstanden: "RealCharacterData()" soll den bisherigen Inhaltsstring - der ja jetzt komplett ist, weil eine andere Funktion aufgerufen wurde - so umformen, wie ich ihn haben will? Also ich rufe "RealCharacterData()" aus "start()" heraus als erstes auf?

        Ich glaube nicht ganz:

        Wenn du den normalen Callback CharacterData() verwendest bekommst du
        wie bereits festgestellt die Text-Stückchen happenweise zugesendet...
        Daran wird sich auch nichts ändern, da du die Expat-Internals nicht
        verändern kannst/willst.
        Was ich nun vorschlage ist ein zweiter "externer" Callback zu
        definieren, welcher nur dann aufgerufen wird, wenn ein Text-Stück
        "komplett" also normalisiert ist. Wie kann dies umgesetzt werden?
        Nun, wie ist ein ganzes Text-Stück definiert?

        <definition type="versuch">
        Es wird solange Text eingelesen, bis z.B. ein Element, Kommentar o.ä.
        auftaucht. D.h. die vom CharacterData() Callback [Expat-Callback!]
        gelieferten Texte werden solange zusammengehängt, bis ein Element
        oder Kommentar (oder PI) auftaucht.
        </definition>

        Nun gut, das heisst für unseren Expat-CharacterData-Callback, dass
        er die ankommenden Textstückchen solange in einer globalen Variable
        "zusammenbastelt", bis dann einmal ein anderer Callback ausgelöst
        wird.
        Falls dann also ein anderer Callback ausgelöst wird
        (Element,PI,Comment), ist der normalisierte Text, der sich in der
        globalen Variablen angesammelt hat, fertig. Bevor jetzt aber
        irgendwelche Aktionen für das Verarbeiten des Elementes/Kommentar
        ausgeführt werden, muss dein Programm den normalisierten String
        verarbeiten => ein "NormalizedCharacterData()" Callback muss
        ausgelöst werden (dieser kommt jetzt aber nicht von Expat, sondern
        von deinem Programm selber).

        Dieser NormalizedCharacterData() Callback wird von jedem
        Callback (ausser dem "Standard-Data-Callback") ausgelöst werden, denn
        das ist ja unsere Definition von "normalisiertem Text", erst wenn
        dein Programm den NormalizedCharacterData() verarbeitet hat, wird
        der eigentliche Callback von Expat ausgeführt (Comment|Element etc).

        Lange Rede kurzer Sinn. Ein Beispiel spricht 1000 Worte:

        xml_set_character_data_handler( $parser, "CharacterData" );
        xml_set_element_handler($xml_parser, "StartElement", "EndElement");
        xml_set_processing_instruction_handler($xml_parser, "PIData");
        ...

        function CharacterData($parser,$data) {
            global $normalizedString;
            // Normalisierungsprozess ist hier:
            $normalizedString .= $data;
            // Keine weitere Verarbeitung! - Der normalisierte String wird
            // NormalizedCharacterData() verarbeitet!
        }

        function StartElement($parser,$name,$attributes) {
            // Wurde vor dem Start dieses Elementes irgendwelche CDATA's
            // empfangen? - So können diese (zusammengehängt in $normStr)
            // dem NormalizedCharacterData()-Callback übergeben werden und
            // dort so verarbeitet werden, als wäre es der normale
            // Expat-Callback für 'Char'.
            if (strlen($normalizedString)) {
                NormalizedCharacterData($parser,$normalizedString);
                $normalizedString = '';
            }
            // hier jetzt das "normale Verarbeiten von StartElement".
            echo("found element $name\n");
        }

        function EndElement($parser,$name) {
            if (strlen($normalizedString)) {
                NormalizedCharacterData($parser,$normalizedString);
                $normalizedString = '';
            }
            // hier jetzt das "normale Verarbeiten von EndElement".
            echo("element end for $name\n");
        }

        function PIData($parser,$target,$data) {
            if (strlen($normalizedString)) {
                NormalizedCharacterData($parser,$normalizedString);
                $normalizedString = '';
            }
            // hier jetzt das "normale Verarbeiten von PIData".
            echo("ProcessingInstruction for $target with $data.\n");
        }

        function NormalizedCharacterData($parser,$data) {
            // so, das ist jetzt unser "erweiterter CharacterData-Callback"
            // hier kommen aber nur die wirklich normalisierten Strings hin!

        // hier einfach das hinschreiben, was du sonst in den Expat-
            // 'Char'-Handler geschrieben hättest.
            echo("Endlich habe ich die TextNodes normalisiert: $data\n");
        }

        Das ist jetzt alles ungetestet. Es sollte aber so (ggf. mit kleinen
        Anpassungen) funktionieren...
        Ich hoffe, du weisst jetzt, was ich mit Worten vergeblich versucht
        habe zu erklären (das ligt nicht an dir - Programmieren konnte ich
        schon immer besser als Reden :-) - Nicht zuletzt, weil meine
        Muttersprache Perl ist...).

        Viele Grüsse

        Philipp

        --
        The only program that runs perfectly every time, is a virus.
        1. Ich hoffe, du weisst jetzt, was ich mit Worten vergeblich versucht
          habe zu erklären (das ligt nicht an dir - Programmieren konnte ich
          schon immer besser als Reden :-) - Nicht zuletzt, weil meine
          Muttersprache Perl ist...).

          *g*...
          war gut. Im Grunde habe ich Dich auch vorher schon so verstanden. Ich habe gerade was ausprobiert und es funktioniert auch! Ich habe aber die "NormalizedCharacterData()"-Funktion in den anderen Handlern immer aufgerufen und das untersuchen des Strings mache ich dann _in_ "NormalizedCharacterData()". Eins wundert mich aber noch: warum übergibst Du an "NormalizedCharacterData()" nochmal den Parser?

          Gruß, Andreas

          --
          <img src="http://was-ist-das.andreas-lindig.de/was_ist_das_fetzen.jpg" border="0" alt="">
          hier könnte auch ruhig mal'n neues Bild stehen.
          1. Halihallo Andreas

            war gut. Im Grunde habe ich Dich auch vorher schon so verstanden. Ich habe gerade was ausprobiert und es funktioniert auch!

            Nein! - Du warst schneller :-)
            Ich hab's soeben auch erfolgreich umsetzen können. Zu meiner
            Verteidigung: Es war purer selbstnutzen, ich wollte auch mal ein
            XML-Dokument mit PHP Parsen ;-)

            Ich habe aber die "NormalizedCharacterData()"-Funktion in den anderen Handlern immer aufgerufen und das untersuchen des Strings mache ich dann _in_ "NormalizedCharacterData()".

            Genau! - Gut.

            Eins wundert mich aber noch: warum übergibst Du an "NormalizedCharacterData()" nochmal den Parser?

            Vielleicht brauchst du Parser-Informationen. Linie oder byteOffset...
            Zudem ist es Expat-CharacterData-Callback "kompatibel".

            Ich hatte noch ein paar Fehler in meinem Beispiel: In den anderen
            Callbacks muss natürlich auch ein 'global $normalizedString' stehen.

            Hm. Falls sich sonst grad noch wer für den Source interessiert:
               (normalerweise gibts das von mir nicht!)

            <source>
            <?

            $xml_source = '<?xml version="1.0" encoding="UTF-8"?>
            <sect1>
               <title>
                  Absatztitel
               </title>

            <para>
                  Absatztext
                  neue Zeile
               </para>
            </sect1>
            ';

            $normalizedString = '';

            $parser = xml_parser_create();

            xml_set_character_data_handler( $parser, "CharacterData" );
            xml_set_element_handler($parser, "StartElement", "EndElement");
            xml_set_processing_instruction_handler($parser, "PIData");

            if (!xml_parse($parser,$xml_source,1)) {
                die( 'Fehler im XML-Parsing: ' .
                xml_error_string(xml_get_error_code($parser)) .
                ' in Linie: ' .
                xml_get_current_line_number($parser));
            }

            function CharacterData($parser,$data) {
                global $normalizedString;
                // Normalisierungsprozess ist hier:
                $normalizedString .= $data;
                // Keine weitere Verarbeitung! - Der normalisierte String wird
                // NormalizedCharacterData() verarbeitet!
            }

            function StartElement($parser,$name,$attributes) {
                // Wurde vor dem Start dieses Elementes irgendwelche CDATA's
                // empfangen? - So können diese (zusammengehängt in $normStr)
                // dem NormalizedCharacterData()-Callback übergeben werden und
                // dort so verarbeitet werden, als wäre es der normale
                // Expat-Callback für 'Char'.
                global $normalizedString;
                if (strlen($normalizedString)) {
                    NormalizedCharacterData($parser,$normalizedString);
                    $normalizedString = '';
                }
                // hier jetzt das "normale Verarbeiten von StartElement".
                echo("found element $name\n");
            }

            function EndElement($parser,$name) {
                global $normalizedString;
                if (strlen($normalizedString)) {
                    NormalizedCharacterData($parser,$normalizedString);
                    $normalizedString = '';
                }
                // hier jetzt das "normale Verarbeiten von EndElement".
                echo("element end for $name\n");
            }

            function PIData($parser,$target,$data) {
                global $normalizedString;
                if (strlen($normalizedString)) {
                    NormalizedCharacterData($parser,$normalizedString);
                    $normalizedString = '';
                }
                // hier jetzt das "normale Verarbeiten von PIData".
                echo("ProcessingInstruction for $target with $data.\n");
            }

            function NormalizedCharacterData($parser,$data) {
                // so, das ist jetzt unser "erweiterter CharacterData-Callback"
                // hier kommen aber nur die wirklich normalisierten Strings hin!

            // hier einfach das hinschreiben, was du sonst in den Expat-
                // 'Char'-Handler geschrieben hättest.

            // Hm. Dem Design zu Liebe einfach mal nur Strings ausgeben, wie auch
                // wirklich was sinnvolles enthalten...

            if (preg_match( '/[1]*$/', $data )) { return; }

            echo("Endlich habe ich die TextNodes normalisiert: '$data'\n");
            }

            ?>
            </source>

            Viele Grüsse

            Philipp

            --
            The only program that runs perfectly every time, is a virus.

            1. \n ↩︎

            1. Hm. Falls sich sonst grad noch wer für den Source interessiert:
                 (normalerweise gibts das von mir nicht!)

              Du bist ja echt fix! wie wär's mit einem "Tips und Tricks"-Artikel? Einfach kopieren - überschrift: XML-Parser in PHP.

              Gruß, Andreas

              --
              <img src="http://was-ist-das.andreas-lindig.de/was_ist_das_fetzen.jpg" border="0" alt="">
              hier könnte auch ruhig mal'n neues Bild stehen.
              1. Halihallo Andreas

                Hm. Falls sich sonst grad noch wer für den Source interessiert:
                   (normalerweise gibts das von mir nicht!)
                Du bist ja echt fix!

                Copy&Past mit Erfahrung, das ist alles ;-)

                wie wär's mit einem "Tips und Tricks"-Artikel? Einfach kopieren - überschrift: XML-Parser in PHP.

                Hm... Steht IMHO alles in der PHP-Doku. Ich meine: Ich habe mir jetzt
                genau das zweite Mal die PHP-Expat-Doku durchgelesen und habe bereits
                den "Parser" geschrieben. Natürlich habe ich bereits Erfahrungen von
                Perl's XML::Parser::Expat, aber ich weiss ehrlichgesagt nicht,
                welchen Mehrwert bzgl. der Doku ich in einem Tipps&Tricks-Artikel
                beitragen könnte.

                Das mit dem Normalisieren wäre ein mögliches Thema für einen
                Tipps&Tricks-Artikel, denn das steht IMHO nicht in der PHP-Doku und
                hat auch ein konkreteres (nicht so allgemeines) Anwendungsgebiet.

                Eigentlich wollte ich schon immer mal einen Artikel, sei es
                Tipps&Tricks, oder Feature-Artikel schreiben, aber irgendwie fehlt
                mir für das immer die Zeit (komischerweise im Forum nicht :-)). Ich
                habe sogar schon einmal früher was angefangen, aber diese Artikel
                brauchen immer viel, viel mehr Zeit, als man denkt.

                Wie ging das noch gleich bei EAV?
                "Ja, morgen, ja, morgen, da fängt ein neues Leben an. Und wenn nicht
                morgen, dann übermorgen, oder vielleicht auch irgendwann..."

                ... irgendwann werde auch ich mal einen Feature-Artikel
                schreiben... ;)

                Viele Grüsse

                Philipp

                --
                The only program that runs perfectly every time, is a virus.