Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Flickr RSS feed inlezen met SimpleXML

Net zoals zovelen heb ook ik een account bij Flickr voor foto's die ik zo af en toe maak. Aangezien Flickr zo ongeveer het poster-child voor alles web2.0 is, is het alleen uploaden van foto's naar Flickr eigenlijk not-done.

Dus in dit artikeltje kijken we naar hoe we de RSS feeds van Flickr kunnen gebruiken om onze eigen Flickr overzicht te maken voor op bijv. je persoonlijke blog. Hiervoor zullen we PHP5's SimpleXML gebruiken.

Een van de dingen die werd aangepakt voor PHP 5 was het verwerken van XML. In PHP4 had je hier al twee methodes voor (SAX parsing en DOM parsing) maar deze waren ofwel enigzins ingewikkeld of gewoon niet zo stabiel. Voor PHP5 werd daarom de SimpleXML extensie in het leven geroepen.

Zoals je van de naam SimpleXML mag verwachten is het verwerken van XML met deze extensie behoorlijk eenvoudig. Om een XML bestand in te laden gebruiken we slechts één functie: simplexml_load_file. Deze functie geeft een object terug welke we kunnen gebruiken om het document verder te verwerken:

PHP:
  1. $xmlObj = simplexml_load_file("http://api.flickr.com/services/feeds/photos_public.gne?id=82896416@N00&lang=en-us&format=rss_200")

De individuele XML elementen kunnen we vervolgens benaderen als eigenschappen van het $xmlObj object. Neem bijvoorbeeld deze voorbeeld RSS file:

XML:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <rss version="2.0">
  3.     <channel>
  4.         <title>Test RSS feed</title>
  5.         <link>http://scriptorama.nl/</link>
  6.     
  7.         <item>
  8.             <title>Flickr RSS feed inlezen met SimpleXML</title>
  9.             <link>http://scriptorama.nl/flickr-rss-feed-inlezen-met-simplexml</link>
  10.             <description>Leer werken met SimpleXML</description>
  11.         </item>
  12.        
  13.         <item>
  14.             <title>Webconferences in 2008</title>
  15.             <link>http://www.scriptorama.nl/events/webdeveloper-conferences-in-2008</link>
  16.             <description>Overzicht van conferences voor Webdevelopers in Nederland</description>
  17.         </item>
  18.        
  19.     </channel>
  20. </rss>

Om de titel van het eerste item te lezen gebruik je simpelweg:

PHP:
  1. echo $xmlObj->channel->item[0]->title;

Zoals je ziet slaan we het eerste element van het XML document, het zogenaamde document element, over. Dit komt omdat $xmlObj in feite al de verwijzing naar het eerste element is. Later zullen we zien hoe je de details van het document element kunt uitlezen.

Vervolgens benaderen we het channel element en daarin het item element. Zoals je ziet corresponderen deze namen exact met de XML element namen in de RSS feed. Er is maar een ding speciaal en dat is $xmlObj->channel->item. Deze ontstaat doordat er meerdere item elementen op hetzelfde niveau staan. Je kunt de individuele item element benaderen door simpel wel door $xmlObj->channel->item te lopen.

Met SimpleXML kunnen we dus met een paar regels een RSS feed verwerken:

PHP:
  1. $xmlObj = simplexml_load_file("http://api.flickr.com/services/feeds/photos_public.gne?id=82896416@N00&lang=en-us&format=rss_200");
  2.  
  3. foreach ($xmlObj->channel->item as $item)
  4. {
  5.     echo $item->title, '<br />';
  6. }

Maar, we hebben het hier over een Flickr RSS feed en dus moeten we wel de foto's die in deze RSS feed staan gebruiken. Om de foto's beschikbaar te maken gebruikt Flickr de Media RSS uitbreiding en om deze te verwerken zullen we het nog iets anders moeten aan pakken.

De Media RSS namespace in Flickr RSS feeds

De Media RSS specificatie is een door Yahoo! ontwikkelde toevoeging op de RSS standaard waarbij media, zoals fotos en video's, duidelijk beschreven kunnen worden toegevoegd aan een RSS feed.

In de Flickr RSS feed ziet dat er als volgt uit. Het voorbeeld is ingekort om het duidelijk te houden:

XML:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
  3.     xmlns:dc="http://purl.org/dc/elements/1.1/"
  4.        >
  5.     <channel>
  6.         <title>Photos from Mathieu K</title>
  7.         <link>http://www.flickr.com/photos/hoppa/</link>
  8.        <description></description>
  9.    
  10.         <item>
  11.             <title>Sunbeds, 3 euro</title>
  12.             <link>http://www.flickr.com/photos/hoppa/1243277966/</link>
  13.  
  14.             <pubDate>Sun, 26 Aug 2007 13:40:24 -0800</pubDate>
  15.             <author>nobody@flickr.com (Mathieu K)</author>
  16.             <guid isPermaLink="false">tag:flickr.com,2004:/photo/1243277966</guid>
  17.             <media:content url="http://farm2.static.flickr.com/1428/1243277966_6e87d19813_o.jpg" type="image/jpeg" height="1944" width="2592"/>
  18.             <media:title>Sunbeds, 3 euro</media:title>
  19.             <media:thumbnail url="http://farm2.static.flickr.com/1428/1243277966_2f111f0457_s.jpg" height="75" width="75" />
  20.             <media:credit role="photographer">Mathieu K</media:credit>
  21.             <media:category scheme="urn:flickr:tags">greece crete preveli</media:category>
  22.  
  23.         </item>
  24.     </channel>
  25. </rss>

In het document element worden de namespaces media en dc gedefinieerd. In dit geval gaat het ons alleen om de media namespace. In het eerste item element zie je vervolgens enkele elementen waarvan de naam begint met media:. Dit zijn elementen uit de Media RSS namespace.

In ons voorbeeld zullen we een lijst van thumbnails tonen. In dit geval hebben we dus interesse in het <media:thumbnail /> element. Om deze te kunnen benaderen zullen we een trucje moeten uithalen want in PHP kunnen object eigenschappen eigenlijk geen dubbele punt in hun naam hebben.

Om namespace elementen te verwerken gebruiken we de children methode en aan deze methode geven we de namespace waaruit we graag de children willen hebben. Om alle media RSS children te krijgen schrijven we dan:

PHP:
  1. foreach ($xmlObj->channel->item as $item)
  2. {
  3.     $mediaChildren = $item->children('http://search.yahoo.com/mrss/');
  4. }

Als namespace identifier geven we de URL op die bij de media namespace staat gedeclareerd in het document element. Deze aanroep geeft een object met alle elementen onder het item element die in de media namespace zitten. Deze elementen zijn weer te benaderen als object eigenschappen.

Zoals gezegd zijn wij nu voornamelijk geinteresseerd in het media:thumbnail element en specifiek daaruit willen we graag de URL weten. Hiervoor gebruiken we nog een andere methode: SimpleXMLElement::attributes().

Deze methode geeft ons alle attributen van het huidige xml element in een handige associatieve array. Dus om alle thumbnails weer te geven schrijven we:

PHP:
  1. foreach ($xmlObj->channel->item as $item)
  2. {
  3.     $mediaChildren = $item->children('http://search.yahoo.com/mrss/');
  4.     $thumbnailAttr = $mediaChildren->thumbnail->attributes();
  5.    
  6.     echo '<img src="', $thumbnailAttr['url'], '" title="', htmlspecialchars($item->title), '" /><br />';
  7. }

Het is belangrijk om je te realiseren dat $thumbnailAttr['url'] nu wel als string gebruikt wordt, maar dat de waarde ervan eigenlijk een SimpleXMLElement instantie is. Doordat $thumbnailAttr['url'] in een echo statement gebruikt wordt, roept PHP intern direct de magic method __toString() aan op het object waardoor we toch het gewenste effect krijgen.

Om de waarde toch als string in handen te krijgen zul je $thumbnailAttr['url'] moeten casten naar een string:

PHP:
  1. foreach ($xmlObj->channel->item as $item)
  2. {
  3.   $mediaChildren = $item->children('http://search.yahoo.com/mrss/');
  4.   $thumbnailAttr = $mediaChildren->thumbnail->attributes();
  5.  
  6.   $thumbnails[] = (string) $thumbnailAttr['url'];   
  7. }

Controleren of een XML document wel een RSS feed is

Eerder in dit artikel schreef ik dat het eerst element van een XML feed, het document element, niet wordt opgenomen als apart object eigenschap. Maar in sommige gevallen wil je toch graag dit document element benaderen, in ons geval bijvoorbeeld om te bepalen of het XML document wel een RSS feed is.

Hiervoor kun je het $xmlObj object direct aanspreken:

PHP:
  1. $xmlObj = simplexml_load_file("http://api.flickr.com/services/feeds/photos_public.gne?id=82896416@N00&lang=en-us&format=rss_200");
  2.  
  3. $docElAttr = $xmlObj->attributes();
  4.  
  5. if ($xmlObj->getName() != 'rss' || !isset($docElAttr['version']) || $docElAttr['version'] != '2.0')
  6.   die("Dit is geen RSS 2.0 feed.");

Maar dat is natuurlijk nog niet alles. We willen ook weten of deze RSS feed wel de Media RSS extensie gebruikt. Om dit te doen moeten we controleren of de Media RSS namespace definieert wordt in het document element en dit kunnen we doen met de methode getDocNamespaces():

PHP:
  1. $namespaces = $xmlObj->getDocNamespaces();
  2.  
  3. if (!in_array('http://search.yahoo.com/mrss/', $namespaces))
  4.     die("Deze feed is geen RSS feed met Media RSS");

Het volledige script

Ter volledigheid nog even een compleet script, inclusief controle of het document wel een RSS feed is:

PHP:
  1. <?php
  2. $xmlObj = simplexml_load_file("http://api.flickr.com/services/feeds/photos_public.gne?id=82896416@N00&lang=en-us&format=rss_200");
  3.  
  4. $docElAttr = $xmlObj->attributes();
  5.  
  6. if ($xmlObj->getName() != 'rss' || !isset($docElAttr['version']) || $docElAttr['version'] != '2.0')
  7.   die("Dit is geen RSS 2.0 feed.");
  8.  
  9. $namespaces = $xmlObj->getDocNamespaces();
  10.  
  11. if (!in_array('http://search.yahoo.com/mrss/', $namespaces))
  12.     die("Deze feed is geen RSS feed met Media RSS");
  13.  
  14. $thumbnails = array();
  15. foreach ($xmlObj->channel->item as $item)
  16. {
  17.     $mediaChildren = $item->children('http://search.yahoo.com/mrss/');
  18.     $thumbnailAttr = $mediaChildren->thumbnail->attributes();
  19.    
  20.     $thumbnails[ (string) $item->title ] = array (
  21.         'thumbnail' => (string) $thumbnailAttr['url'],
  22.         'link'      => (string) $item->link
  23.     );
  24. }
  25.  
  26. ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  27.     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  28. <html>
  29.     <head>
  30.         <title>Flickr RSS overzicht</title>
  31.         <style type="text/css">
  32.         body { font-family: helvetica; font-size: 0.8em; }
  33.         ul li { list-style-type: none; border: 1px dashed #c0c0c0; width: 500px; margin: 5px 0; }
  34.         ul li.odd  { background-color: #ebebeb; }
  35.         ul li img { vertical-align: middle; padding-right: 15px; border: 0;}
  36.         </style>
  37.        
  38.     </head>
  39.     <body>
  40.         <h1><?php echo htmlspecialchars($xmlObj->channel->title); ?></h1>
  41.         <ul>
  42. <?php
  43.     $i = 0;
  44.     foreach ($thumbnails as $title => $data)
  45.     {
  46.         $class = ($i++ % 2) == 0 ? 'even' : 'odd';
  47.         echo '<li class="', $class, '">';
  48.         echo '<a href="', $data['link'], '">';
  49.         echo '<img src="', $data['thumbnail'], '" /></a>', htmlspecialchars($title);
  50.         echo '</li>';
  51.    
  52.  
  53.     }
  54. ?>
  55.         </ul>
  56.     </body>
  57. </html>

Conclusie

Met SimpleXML zijn XML documenten erg simpel te verwerken. Zelfs als het document in kwestie gebruik maakt van wat geavanceerdere features, zoals bijvoorbeeld XML namespaces.

Een grappig demo-tje vind je hier. Deze moet bekeken worden met een recente WebKit nightly, aangezien deze CSS Transforms, een nieuwe webkit specifieke feature bevat. Of, als je daar geen zin in hebt, kun je ook gewoon dit screenshot bekijken:

Happy parsing!

Reageer ook!

Nice artikel :) zelf ben ik op het moment druk bezig met SOAP koppelingen ed. Als er nog iemand een artikel heeft om een SOAP connectie zo min mogelijk te belasten houd ik me aanbevolen.

Uh, cachen? :P

Relaxed artikel, just what I needed =D

Nog wel voor zorgen dat je thumbnail linked naar de flickr foto pagina. Dit is verplicht bij flickr.

Cachen gaat moeilijk omdat er een externe bron is die de gegevens ook kan aanpassen. Hierdoor zal ik steeds het request moeten sturen (per onderdeel zijn dat 6 requests)

Je geeft hier de namespace url aan children() mee, en dus is de check of de prefix wel 'media' is overbodig. Als die prefix op beide punten gewijzigd wordt, kan je script in nog prima werken.

Ik denk dat als je in je main code enkel met namespace urls werkt, in_array($namespace_url, $namespaces) een voldoende check is. :)

Marten: Er zijn genoeg manieren te bedenken, maar zonder meer informatie over het systeem, de requests en de houdbaarheid van de data, kan dat niet gericht gebeuren. Hoewel ik je graag wil helpen, is dit niet wellicht niet de beste plek om je probleem uitgebreid te tackelen. ;)

@ Maarten: je hebt helemaal gelijk. Ik ga het even aanpassen.

Wederom net artikel!

persoonlijk zou ik dan wel denken aan:
http://www.php.net/~helly/php/ext/spl/classSimpleXMLIterator.html

SimpleXMLElement is vaak al een iterator, $xmlObj->channel->item uit het artikel is eigenlijk geen array, maar een iterator.

@ Maarten: Jouw methode werkt overigens niet altijd. Sommige implementaties gebruiken http://search.yahoo.com/mrss als namespace, terwijl anderen http://search.yahoo.com/mrss/ gebruiken.

Dat probleem was al aanwezig omdat je juist al op basis van url werkte. Je kan ook werken children($namespace_prefix, true), als de prefix tenminste wel constant is. :)

Mooi simpel artikel!

simplexml_load_file werkt bij mij niet, en misschien bij meerdere, gebruik daarvoor gwoon cURL

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>