XML mit Namespace prefix (ALT: Sonderzeichen in diversen XML Knoten ersetzen)

Spelmann

Erfahrenes Mitglied
Hallo,
ich versuche gerade ein XML von mobile.de auszulesen.
Die Benennung der Knoten ist allerdings problematisch. Sie enthalten Doppelpunkte und Minuszeichen, die mir beim Auslesen via simpleXML Probleme bereiten.

So sieht das XML aus:
Code:
<search:search-result xmlns:resource="http://services.mobile.de/schema/resource" xmlns:ad="http://services.mobile.de/schema/ad" xmlns:seller="http://services.mobile.de/schema/seller" xmlns:search="http://services.mobile.de/schema/search" xmlns:financing="http://services.mobile.de/schema/common/financing-1.0" xmlns:error="http://services.mobile.de/schema/common/error-1.0">
    <search:total>33</search:total>
    <search:page-size>20</search:page-size>
    <search:current-page>1</search:current-page>
    <search:max-pages>2</search:max-pages>
    <search:ads>
        <ad:ad key="123456789" url="https://services.mobile.de/search-api/ad/123456789">
            <ad:creation-date value="2014-05-14T12:54:24+02:00"/>
            <ad:modification-date value="2015-03-14T13:17:18+01:00"/>
            <ad:detail-page url="http://suchen.mobile.de/auto-inserat/vw-golf-vi-1-4-tsi-comfortline.html"/>
            <ad:vehicle>
                <ad:class key="Car" url="https://services.mobile.de/refdata/classes/Car">
                    <resource:local-description xml-lang="de">Pkw</resource:local-description>
                </ad:class>
                <ad:category key="Limousine" url="https://services.mobile.de/refdata/categories/Limousine">
usw.

Mein Versuch einen Wert auszugeben:
PHP:
    $path ="/search:search-result/search:ads/ad:ad[@key=".$id."]/ad:vehicle/ad:make";
    if (!$res = $xml->xpath($path))
        {
        echo "Artikel nicht vorhanden!";
        }else {
        echo "<h1>".$res[0]->resource:local-description."</h1>";
    }
will ich den Knoten ad:class > resource:local-description auslesen,
bekomme ich eine Fehlermeldung da resource:local-description ja so nicht stehen darf.
(Parse error: syntax error, unexpected ':', expecting ',' or ';' in C:\xampp...)

Kann mir jemand auf die Sprünge helfen, wie ich in einem Rutsch bei allen Knoten den Doppelpunkt und das Minuszeichen in einen Unterstrich wandeln kann? Gibts da eine Kombination aus regulärem Ausdruck und replace?

Vielen Dank!
 
Zuletzt bearbeitet:
DOMDocument kommt mit Namespaces klar. Wenn du ein komplettes XML postest, kann man sogar ein Beispiel erstellen.

Ok, ich hab mal anhand der vorhandenen XML-Daten ein kleines Script erstellt, mit dem du experimentieren kannst. Vieles kannst du der Dokumentation für DOMDocument entnehmen. Die mobile.xml enthält die Daten, die du gepostet hast, allerdings wohlgeformt abgeschlossen (siehe unten)

PHP:
<?php
$doc = new DOMDocument();
$doc->load('mobile.xml');

$xpath = new DOMXPath($doc);
$nodes = $xpath->query('//resource:local-description');

foreach($nodes as $node) {

  echo "Text-Inhalt: {$node->textContent}\n";
  foreach ($node->attributes as $attribute) {
  echo "Attribute {$attribute->name}: " . $node->getAttribute($attribute->name) . "\n";
  }
}

XML:
<?xml version="1.0" encoding="UTF-8"?>
<search:search-result xmlns:resource="http://services.mobile.de/schema/resource" xmlns:ad="http://services.mobile.de/schema/ad" xmlns:seller="http://services.mobile.de/schema/seller" xmlns:search="http://services.mobile.de/schema/search" xmlns:financing="http://services.mobile.de/schema/common/financing-1.0" xmlns:error="http://services.mobile.de/schema/common/error-1.0">
  <search:total>33</search:total>
  <search:page-size>20</search:page-size>
  <search:current-page>1</search:current-page>
  <search:max-pages>2</search:max-pages>
  <search:ads>
  <ad:ad key="123456789" url="https://services.mobile.de/search-api/ad/123456789">
  <ad:creation-date value="2014-05-14T12:54:24+02:00"/>
  <ad:modification-date value="2015-03-14T13:17:18+01:00"/>
  <ad:detail-page url="http://suchen.mobile.de/auto-inserat/vw-golf-vi-1-4-tsi-comfortline.html"/>
  <ad:vehicle>
  <ad:class key="Car" url="https://services.mobile.de/refdata/classes/Car">
  <resource:local-description xml-lang="de">Pkw</resource:local-description>
  </ad:class>
  <ad:category key="Limousine" url="https://services.mobile.de/refdata/categories/Limousine">
  </ad:category>
  </ad:vehicle>
  </ad:ad>
  </search:ads>
</search:search-result>

Ausgabe:

Code:
Text-Inhalt: Pkw
Attribute xml-lang: de
 
Zuletzt bearbeitet:
Ansonsten:

PHP:
echo "<h1>".$res[0]->{'resource:local-description'}."</h1>";

Zumindest prinzipiell. Ob das im hier vorgestellten Fall so exakt richtig ist, habe ich nicht getestet.
 
Zuletzt bearbeitet:
Vielen Dank für die Mühe, die Du Dir gemacht hast saftmeister!
An deinem Beispiel kann ich mich jetzt gut abarbeiten. Wenn ich zurechtkomme setze ich das Projekt vielleicht wirklich mit DOM um. Hab mich da nicht so rangetraut. SimpleXML sieht, wie der Name schon sagt, eben doch einfacher aus (ist mein erster Versuch XML Daten zu verarbeiten).

@mermshaus
Diese Schreibweise hab ich inzwischen auch gefunden, dennoch bekomme ich bis jetzt auch mit dieser Methode kein Ergebnis. Es gibt keine Fehlermeldung. Es wird einfach nichts angezeigt. Aber auch hier werde ich noch experimentieren.
 
(Sorry, hatte nicht gesehen, dass @saftmeister bereits eine einfach kopierbare XML-Datei gepostet hatte. Habe die dann mal genommen.)

Jedenfalls: Ich habe wieder gemerkt, warum ich SimpleXML niemals freiweillig verwende und immer die Nutzung von DOM empfehle. Es gibt zu viele Dinge in SimpleXML, die nicht so gestaltet sind oder funktionieren, wie ich das von einer „normalen“ Klasse erwarte.

Das liegt beispielsweise daran, dass Instanzen von SimpleXMLElement, der zentralen Klasse in SimpleXML, vielleicht gar keine echten Objekte sind.

- http://php.net/manual/en/class.simplexmlelement.php#100811

Ich habe gerade nicht die Lust, mich damit tiefgreifender zu befassen, aber der Gedanke, dass SimpleXML-„Objekte“ Fähigkeiten haben, die normale Objekte, wie man sie selbst im Rahmen der PHP-Syntax generieren könnte, nicht haben können, ist mir auch schon mehrfach gekommen. Ich weiß aber spontan kein überzeugendes Beispiel.

Wie auch immer:

PHP:
<?php

$xml = simplexml_load_string(<<<EOT
<?xml version="1.0" encoding="UTF-8"?>
<search:search-result xmlns:resource="http://services.mobile.de/schema/resource" xmlns:ad="http://services.mobile.de/schema/ad" xmlns:seller="http://services.mobile.de/schema/seller" xmlns:search="http://services.mobile.de/schema/search" xmlns:financing="http://services.mobile.de/schema/common/financing-1.0" xmlns:error="http://services.mobile.de/schema/common/error-1.0">
    <search:total>33</search:total>
    <search:page-size>20</search:page-size>
    <search:current-page>1</search:current-page>
    <search:max-pages>2</search:max-pages>
    <search:ads>
        <ad:ad key="123456789" url="https://services.mobile.de/search-api/ad/123456789">
            <ad:creation-date value="2014-05-14T12:54:24+02:00"/>
            <ad:modification-date value="2015-03-14T13:17:18+01:00"/>
            <ad:detail-page url="http://suchen.mobile.de/auto-inserat/vw-golf-vi-1-4-tsi-comfortline.html"/>
            <ad:vehicle>
                <ad:class key="Car" url="https://services.mobile.de/refdata/classes/Car">
                    <resource:local-description xml-lang="de">Pkw</resource:local-description>
                </ad:class>
                <ad:category key="Limousine" url="https://services.mobile.de/refdata/categories/Limousine">
                </ad:category>
            </ad:vehicle>
        </ad:ad>
    </search:ads>
</search:search-result>
EOT
);

$id = '123456789';

$path = "/search:search-result/search:ads/ad:ad[@key=".$id."]/ad:vehicle/ad:class"; // War ganz hinten ad:make

if (!$res = $xml->xpath($path)) {
    echo "Artikel nicht vorhanden!";
} else {
    var_dump($res[0]->children('resource', true)->{'local-description'});
}

Das ist nicht die geschickteste Variante, was den Umgang mit XML-Namespaces angeht. Aber mach es im Zweifel so, wenn du bei SimpleXML bleiben möchtest. Die geschicktere Handhabung ist sehr umständlich.



Ich führe sie dennoch auf:

PHP:
$xml = simplexml_load_string(<<<EOT
...
EOT
);

$namespaces = array(
    'xsearch'   => 'http://services.mobile.de/schema/search',
    'xad'       => 'http://services.mobile.de/schema/ad',
    'xresource' => 'http://services.mobile.de/schema/resource'
);

foreach ($namespaces as $prefix => $ns) {
    $xml->registerXPathNamespace($prefix, $ns);
}

$id = '123456789';

$path = "/xsearch:search-result/xsearch:ads/xad:ad[@key=".$id."]/xad:vehicle/xad:class"; // War ganz hinten ad:make

if (!$res = $xml->xpath($path)) {
    echo "Artikel nicht vorhanden!";
} else {
    var_dump($res[0]->children($namespaces['xresource'])->{'local-description'});
}

Das „x“ zu Beginn der lokalen Namespace-Präfixe kann entfernt werden. Ich habe das für den Beispielcode hinzugefügt, um zu verdeutlichen, dass es sich um lokale Aliase handelt, die nicht den Namespace-Präfixen aus der XML-Datei entsprechen müssen (aber können).

Der Vorteil dieser Variante ist, dass der Code auch dann noch funktioniert, wenn sich in der XML-Datei die Namespace-Präfixe ändern sollten. Die wird der Anbieter wahrscheinlich niemals ändern, weil die sich auch denken können, wie Leute ihren Code schreiben, aber es wäre wohl durchaus legitim, das zu tun.

Das hier…

PHP:
var_dump($res[0]->children('xresource', true)->{'local-description'});

…funktioniert meines Wissens allerdings nicht. (Doku)

Der Grund dafür scheint zu sein, dass SimpleXMLElement::registerXPathNamespace nur für die jeweilige Instanz von SimpleXMLElement Namespaces registriert und dass diese Namespaces auch nur für SimpleXMLElement::xpath-Aufrufe gelten.



DOM handhabt derlei Dinge meiner Erfahrnung nach geschickter. Auch dort gibt es zwar Dinge, die eigenartig sind, aber meiner Meinung nach ist die Syntax und die Verhaltensweise der DOM-Klassen grundsätzlich sehr viel nachvollziehbarer.
 
Hier noch eine Variante mit DOM (aus einem aktuellen Thread auf phpforum.de).

Ich gebe zu, so viel gibt sich das nicht. (Das hatte ich anders in Erinnerung.) Ich würde dennoch DOMDocument und DOMXPath vorziehen.

PHP:
<?php

$data = <<<EOT
<SearchResultResponse
  xmlns="urn:veloconnect:catalog-1.1"
  xmlns:vco="urn:veloconnect:order-1.1"
  xmlns:vct="urn:veloconnect:transaction-1.0"
  xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-1.0"
  xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-1.0"
>
    <vct:BuyersID>MeineFirma</vct:BuyersID>
    <vct:ResponseCode>200</vct:ResponseCode>
    <vct:TransactionID>e644275f-b3f2-4b6c-b297-1ed6b6060cd9</vct:TransactionID>
    <vct:StatusCode>100</vct:StatusCode>
    <vct:IsTest>false</vct:IsTest>
    <StartIndex>0</StartIndex>
    <Count>2</Count>
    <TotalCount>2</TotalCount>
    <ResultFormat>ITEM_DETAIL</ResultFormat>
    <vco:ItemDetail>
        <cac:Item>
            <cbc:Description>
            QUICK BRICK REIFEN 26X2.125 BLACK Felt Cruiserreifen Quickbrick - Cruiserreifen mit Style und schönem Design - Größe: 26 Zoll - Breite: 2.125 Zoll - für vorne und hinten - Farbe: schwarz
            </cbc:Description>
            <cac:SellersItemIdentification>
                <cac:ID>H8450</cac:ID>
            </cac:SellersItemIdentification>
            <cac:BasePrice>
                <cbc:PriceAmount amountCurrencyID="CHF">15.00</cbc:PriceAmount>
                <cbc:BaseQuantity quantityUnitCode="EA">1</cbc:BaseQuantity>
            </cac:BasePrice>
            <cac:RecommendedRetailPrice>
                <cbc:PriceAmount amountCurrencyID="CHF">26.85</cbc:PriceAmount>
                <cbc:BaseQuantity quantityUnitCode="EA">1</cbc:BaseQuantity>
            </cac:RecommendedRetailPrice>
            <ItemInformation>
                <InformationURL>
                    <URI>
                    https://secure.abacuscity.ch/abavimage/H8450.jpg?s=168&i=h2nJnWTWBkrB7UNSPt7i&vtid=195&vrid=1413&vfn=H8450.jpg
                    </URI>
                    <Disposition>picture</Disposition>
                </InformationURL>
            </ItemInformation>
        </cac:Item>
        <vco:Availability>
            <vco:Code>available</vco:Code>
        </vco:Availability>
    </vco:ItemDetail>
</SearchResultResponse>
EOT;

// Notwendiger Hack, da das Beispiel-XML-Dokument syntaktisch fehlerhaft ist.
// Die "&"-Vorkommen aus dem "URI"-Element müssen in XML als "&amp;" kodiert
// werden oder es muss alternativ ein "<![CDATA[...]]>"-Bereich eingefügt
// werden. Bei Generierung ändern oder bei den Leuten beschweren, die es
// generieren
$data = str_replace('&', '&amp;', $data);

$doc = new DOMDocument('1.0', 'UTF-8');
$doc->loadXML($data);

// Variante via XPath
$f = function (DOMDocument $doc) {
    $xpath = new DOMXPath($doc);

    // Die Präfix-Aliase sind frei wählbar und können von den Präfixen im
    // XML-Dokument verschieden sein (müssen aber nicht, ist egal). Ich habe
    // hier bewusst nicht "default" (oder so, für den Default-Namespace), "vco"
    // und "cac" gewählt, um das zu verdeutlichen. Entscheidend ist immer der
    // Namespace-URI. Über den läuft die Zuordnung letztlich
    $xpath->registerNamespace('x', 'urn:veloconnect:catalog-1.1');
    $xpath->registerNamespace('a', 'urn:veloconnect:order-1.1');
    $xpath->registerNamespace('b', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-1.0');

    $uri = $xpath->query(
        '/x:SearchResultResponse/a:ItemDetail/b:Item/x:ItemInformation/x:InformationURL/x:URI'
    )->item(0);

    return trim($uri->nodeValue);
};

// Variante "zu Fuß"
$g = function (DOMDocument $doc) {
    $root            = $doc->documentElement;
    $itemDetail      = $root           ->getElementsByTagNameNS('urn:veloconnect:order-1.1'  , 'ItemDetail'     )->item(0);
    $item            = $itemDetail     ->getElementsByTagNameNS('urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-1.0', 'Item')->item(0);
    $itemInformation = $item           ->getElementsByTagNameNS('urn:veloconnect:catalog-1.1', 'ItemInformation')->item(0);
    $informationUrl  = $itemInformation->getElementsByTagNameNS('urn:veloconnect:catalog-1.1', 'InformationURL' )->item(0);
    $uri             = $informationUrl ->getElementsByTagNameNS('urn:veloconnect:catalog-1.1', 'URI'            )->item(0);

    return trim($uri->nodeValue);
};

var_dump(
    $f($doc),
    $g($doc)
);
 
Ich danke euch nochmal für euer großartiges Engagement!
Die Tage habe ich noch andere Sachen um die Ohren, aber am Wochenende setze ich mich wieder ran.
Vielen Dank.
 

Neue Beiträge

Zurück