wtorek, 16 października 2012

Syndykacja czyli jak wyświetlić treść na swojej stronie?

Kanały treści, feedy lub po prostu RSS/Atom*. Istnieje wiele określeń na nośnik informacji używany w syndykacji treści stron internetowych, które to nieraz są stosowane wymiennie. W zasadzie dziś mało kto zwraca uwagę na to co kryje się pod magicznym guzikiem symbolizującym właśnie kanał treści danej witryny. Ale czym jest owa syndykacja, której próżno szukać w słownikach języka polskiego (link | link)? W uproszczeniu jest to publikowanie treści podmiotu przez inny podmiot, który tej treści nie utworzył. Definicja może i krótka, ale pewnie nie do końca zrozumiała, dlatego posłużę się przykładem nie związanym z internetem:
Stacja telewizyjna A nakręciła materiał z koncertu super gwiazdy i udostępnia go na swoim kanale. Jednocześnie stacja ta pozwala innym stacjom na używanie tego materiału na swoich kanałach (zwykle za opłatą licencyjną). I właśnie takie udostępnianie innym nazywamy syndykacją.
W świecie internetu gro procesu syndykacji zostało uproszczone do generowania przez właściciela treści pliku z jej zawartością, który to umieszcza na swoim serwerze. Rzadko też spotykamy się z odpłatnymi kanałami, a jeśli już to są one konsekwencją płatnego dostępu do całego serwisu. Dość po słowie, że świat idzie na przód i syndykacja w obecnym obliczu jest jednym z elementów, które powodują odejście starej wizji mediów w niebyt.

JavaScript i AJAX

 Ale wracając do tematu wpisu, czyli wyświetlania kanału treści na swojej witrynie. Niestety najprostsze i najkorzystniejsze dla właściciela hostingu (transfer, czas procesora) podejście wykorzystujące JavaScript w wielu przypadkach nie sprawdzi się z racji polityki pochodzenia z tej samej domeny. Oczywiście jeśli chcemy agregować treści znajdujące się w serwisach tej samej domeny ten problem nas nie dotyczy i bez problemu możemy wykorzystać AJAX (Tak kanały w standardzie RSS jak i w Atom korzystają z XML) choćby z poniższym kodem:
function GetXHR()
{
 var xmlHttp = null;
 try
 {   // Firefox, Opera 8.0+, Safari
  xmlHttp=new XMLHttpRequest();
 }
 catch (e)
 {   // Internet Explorer
  try
  {
   xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");
  }
  catch (e)
  {
   xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
 }
 if(xmlHttp == null)
  alert("Browser does not support HTTP Request");
 return xmlHttp;
}

window.addEventListener('load', function()
{
 var chk=setInterval(function()
 {  
  doc = document.getElementsByTagName("body");
  if(doc.length == 0 || doc[0].innerHTML.length < 500)
  { return; }
  clearInterval(chk);
  
  var xhr = GetXHR();
  xhr.onreadystatechange=function()
  {
   if(this.readyState == 4 && this.status == 200)
   {
    document.getElementById("articleStuffShowRSSHere").innerHTML = "<a href='"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("link")[4].getAttribute("href")+"'>"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("title")[0].textContent+
     "</a>";
   }
   
  };
  xhr.open("GET" , '/feeds/posts/default', true);
  xhr.send();  
 },100);
}, false);
Przykład ten wyświetla obok link do najnowszego artykułu z tego bloga:

PHP i cache po stronie serwera

Jednak jeśli nasz serwis znajduje się w innej domenie to z usług JavaScript skorzystać nie będzie nam dane. Oczywiście możemy próbować korzystać z różnego rodzaju sztuczek lub zewnętrznych serwisów pobierających pliki syndykacji za nas... ale raczej nie o półśrodki tu chodzi, a o mniej lub bardziej profesjonalne i autonomiczne rozwiązanie. PHP na szczęście umożliwia nam pobranie plików z zewnętrznych serwisów i swobodną manipulację na nich, tak więc problem wspólnej domeny odpada. Pojawia się za to inny problem, a mianowicie problem z wydajnością - w przypadku JavaScript cały proces de facto zrzucaliśmy na użytkownika i z punktu widzenia serwera nie odczulibyśmy różnicy przy syndykacji z 2 kanałów lub z 200 kanałów (Choć użytkownik by zapewne odczuł). Przy korzystaniu z PHP natomiast jeśli każda wizyta użytkownika miałaby uruchamiać mechanizm pobierania plików, ich parsowania i transformacji do finalnej postaci prezentowanej użytkownikowi to nie potrzeba agregacji z 200 kanałów, ale wystarczy 1 kanał i 200 użytkowników naszego serwisu byśmy odczuli ten sam efekt. Co więcej istnieje realna groźba, że właściciel serwisu, z którego dane pobieramy uzna nas za źródło ataku DoS (ang. Denial-of-service), - bo wszak wszystkie żądania pobrania będą pochodzić z naszego serwera - i zablokuje nam dostęp do kanału.
Stąd też konieczny jest mechanizm zapisu pobranych danych w taki sposób aby odwiedziny nowych użytkowników nie generowały transferu danych pomiędzy serwisami, a korzystały z już ściągniętych danych. Tu należy się zastanowić na którym etapie procesu przygotowywania do prezentacji należy dane zapisać, bo w zależności od tego jak chcemy je wykorzystać inny moment tego procesu może okazać się najefektywniejszy. W poniższym przykładzie zapis do pliku na serwerze odbywa się po przygotowaniu całości prezentowanego komponentu, a podobny mechanizm można z powodzeniem wykorzystać do zapisu całych stron, których wygenerowanie wymaga wielu obliczeń lub zapytań do baz danych, a których treści nie ulegają częstej zmianie.
Generalnie kod składa się z dwóch części, pierwsza - dłuższa to funkcja przeprowadzająca pobranie, obróbkę i zapis treści na dysk serwera:
<?php
function prepare($ident)
{
 // download data and parse them with SimleXmlElement
 $content = file_get_contents('http://runaurufu.blogspot.com/feeds/posts/default');
 $x = new SimpleXmlElement($content);

 // get data which we want to use
 $entryName = $x->entry[0]->title."";
 $entryUrl = "";
 foreach ($x->entry[0]->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
   $entryUrl = $aux['href']."";
  }
 }

 $blogTitle = $x->title."";
 $blogSubTitle =  $x->subtitle."";
 $blogUrl = "";
 foreach ($x->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
  $blogUrl = $aux['href']."";
  }
 }

 // prepare for cacheing 

 //start output buffering
 ob_start();

 // here do entire presentation generation
 ?>
 <a href="<?php echo $entryUrl; ?>"><?php echo $entryName; ?></a> @ <a href="<?php echo $blogUrl; ?>" title="<?php echo $blogSubTitle; ?>"><?php echo $blogTitle; ?></a>

 <?php
 // save output buffer to $data
 $data = ob_get_contents();
 // clear output buffer and stop buffering
 // (ob_end_flush(); can be used to stop buffering and display buffer content)
 ob_end_clean();

 $handle = fopen($ident, 'wb');
 if(flock($handle, LOCK_EX))
 {
  $bytes = fwrite($handle, $data);
  flock($handle, LOCK_UN);
 }
 fclose($handle);
 return $data;
}
?>

Druga część jest odpowiedzialna za sam proces wyboru, czy chcemy użyć dane zapisane na dysku, lub czy mimo wszystko pobieramy je od nowa:
<?php
$cacheTime = 86400;
$ident = "myFeed.cache";
if(!is_file($ident))
{
 echo prepare($ident);
}
else
{
 if(filemtime($ident) + $cacheTime > time())
 {
  // show content of file
  echo file_get_contents($ident);
 }
 else
 {
  echo prepare($ident);
 }
}
?>
Oczywiście kod ten można w znacznej mierze zmodyfikować i sprawić, aby cały proces obsługi naszego cache wyglądał tak np:
<?php$cache = new Chors_Cache($identifier, $time);
if($cache->check())
{
 return $cache->fetchRender();
}
else
{
 $cache->startOutputCaching();
 // generate cached content begin
 ...
 // generate cached content end
 return $cache->endOutputCachingClean();
}?> 

Słowo na koniec

Jak widać obsługa kanałów syndykacji nie jest czymś trudnym a jedynym problemem na jaki możemy natrafić to konieczność stworzenia parsera wyłuskującego te dane, które nas interesują. Warto jeszcze wspomnieć, że o ile używanie AJAXa nie obciąża naszego serwera, to generuje zbyteczny ruch na serwerze udostępniającym kanał treści i z tego względu będąc "miłym sąsiadem" należałoby mimo wszystko zaprzęgnąć swój serwer do obsługi tych danych.

* Słowem wyjaśnienia: RSS i Atom o ile bywają synonimem udostępniania zawartości witryny to w rzeczywistości są tego udostępniania standardami, a nie samą nazwą procesu.

2 komentarze:

  1. Jeżeli chodzi o obejście SOP to polecam http://en.wikipedia.org/wiki/Cross-origin_resource_sharing . Niestety wymaga zmian na serwerze z ktorego chcesz cos pobrac... Np coś takiego: http://software.dzhuvinov.com/cors-filter.html

    OdpowiedzUsuń
    Odpowiedzi
    1. To fakt jest kilka skutecznych sposobów na wydarcie plików z innej domeny, a cały ten proces można uprościć jeśli mamy dostęp do obu serwerów.
      Jednak jak dla mnie w przypadku stron WWW w ogóle zrzucanie na użytkownika obowiązku pobierania danych z innych stron jest pewnym brakiem odpowiedzialności i zbędnym obciążaniem docelowej domeny - nie ma sytuacji, w której wykonanie operacji pobrania ze strony serwera nie byłoby ogólnie efektywniejszym rozwiązaniem niż przerzucanie tego na użytkownika końcowego.

      Usuń