ugrás a tartalomhoz

Hírfolyamok feldolgozása

Pepita · 2012. Ápr. 12. (Cs), 23.57
Hírfolyamok feldolgozása

Manapság sok olyan magyar weblapot látunk, ami rendelkezik hírcsatornával, ezeket viszont kevés felhasználó olvassa, használja. Pedig hasznosak a hírfolyamok, ha nem az átlag internetezőknek, akkor nekünk, fejlesztőknek, mikor más oldalakról akarunk bizonyos tartalmakat kölcsönözni.

Léteznek egymással kapcsolatban lévő intézmények (klubok, egyesületek stb.), melyek szívesen megosztanák egymással híreiket, programajánlójukat, egyebet. (Régebben divat volt az időjárás is.) Ezt legegyszerűbben úgy lehet megvalósítani, ha feldolgozzák egymás hírforrásait. Igen, de milyen a jó feldolgozó? Igényeim a következők voltak:

  1. A lehető legtöbb hírfolyamtípust képes legyen feldolgozni,
  2. mégis egységes eredményhalmazt adjon vissza, használata során lehetőleg egyáltalán ne kelljen a formátummal foglalkozni;
  3. szűrje a tartalmat beállítások szerint (pl. képek, egyes HTML elemek);
  4. ha akarom, rendezze az elemeket dátum szerint csökkenő sorrendbe;
  5. a dátumokat magyarul, a hét napjaival jelenítse meg;
  6. rövidítsen a túl hosszú bevezetőkön, a benne esetleg szereplő HTML elemek helyes lezárásával;
  7. lehetőleg egyszerűbb PHP függvényeket használjon, a minél jobb hordozhatóság érdekében.

Nos, olyan feldolgozót nem találtam, ami mindezt tudná, ezért fogtam a fejszét és belevágtam.

A legnagyobb feladatnak a különböző szabványok szerint készült XML dokumentumok egységes struktúrába rendezése látszott, ezért először jó sokat olvastam róluk a Wikipédián és különböző RFC-k között. Számomra úgy tűnik, hogy az RSS 2.0 a legelterjedtebb szabvány, ezért az egységesített struktúra és a változók nevei többnyire ezt követik.

class RssReader
{
    var $title;
    var $link;
    var $description;
    var $lastBuildDate;
    var $pubDate;
    var $ttl;
    var $items;
    var $errors;
    
    private $document;
    private $root;
    private $itemCount;
    
    private $tags1 = array(
        '<a>',      '<ol>', '<h5>',
        '<b>',      '<dl>', '<h6>',
        '<i>',      '<dt>', '<pre>',
        '<u>',      '<dd>', '<span>',
        '<cite>',   '<li>', '<th>',
        '<code>',   '<p>',  '<td>',
        '<q>',      '<h1>', '<tr>',
        '<strong>', '<h2>', '<tbody>',
        '<div>',    '<h3>', '<table>',
        '<ul>',     '<h4>',
    );
    
    private $tags2 = array(
        '</a>',      '</ol>', '</h5>',
        '</b>',      '</dl>', '</h6>',
        '</i>',      '</dt>', '</pre>',
        '</u>',      '</dd>', '</span>',
        '</cite>',   '</li>', '</th>',
        '</code>',   '</p>',  '</td>',
        '</q>',      '</h1>', '</tr>',
        '</strong>', '</h2>', '</tbody>',
        '</div>',    '</h3>', '</table>',
        '</ul>',     '</h4>', 
    );
    
    private $days = array(
        'Vasárnap',
        'Hétfő',
        'Kedd',
        'Szerda',
        'Csütörtök',
        'Péntek',
        'Szombat'
    );
    
    function RssReader()
    {
        $this->errors = array();
        
        if (version_compare(phpversion(), '5.1.0', '>=')) {
            if (!date_default_timezone_set('Europe/Budapest')) {
                $this->errors[] = 'Nem sikerült az időzónát beállítani.';
            }
        }
        
        if (extension_loaded('mbstring') === false) {
            $this->errors[] = 'Nincs betöltve az "mbstring" modul.';
        }
        
        unset($this->document);
        unset($this->root);
        
        $this->document = new DomDocument();
        $this->items    = array();
    }
    
    private function resetAll()
    {
        $this->title         = '';
        $this->link          = '';
        $this->description   = '';
        $this->lastBuildDate = '';
        $this->pubDate       = '';
        $this->ttl           = 60;
        
        unset($this->items);
        $this->errors = array();
        
        $this->itemCount = 0;
    }
    
    function getItemCount()
    {
        return $this->itemCount;
    }
    
    // …
    
}

A hírfolyam egészére vonatkozó mezők:

title
a csatorna címe;
link
általában a forrásweblap főoldalának a címe, ritkábban egy aloldalé vagy a hírfolyamé;
description
a csatorna leírása, sok esetben üres;
lastBuildDate
a hírfolyam utolsó összeállításának ideje (pl. 2011-12-15 Csütörtök, 19:20:58);
pubDate
a hírfolyam közzétételének ideje (sok esetben csak egyik dátumot hozza a folyam, ilyenkor a két változó azonos értékű)
ttl
a csatorna ajánlott frissítési ideje (másodpercben) – sokszor nincs (vagy rosszul van) megadva, jobb, ha mi döntjük el a feldolgozás gyakoriságát.

A hírfolyam majdani elemeinek száma a getItemCount() függvénnyel kérdezhető le.

A konstruktor mindössze beállítja az időzónát (ha a PHP kiadás későbbi, mint 5.1.0), ellenőrzi az mbstring kiterjesztés meglétét (az UTF-8 kódolás miatt szükséges), létrehoz egy DomDocument objektumot, és egyes változókat alaphelyzetbe állít. Ha valami hiba történik – itt és a többi függvénynél is – arról az errors tömbben olvashatunk (magyarul).

Ezután következhet a hírfolyam betöltése fájlból (ha a tartalmat időzítve mentjük egy könyvtárunkba), XML sztringből (ha egy változóba mentettük előzőleg) vagy közvetlenül URL alapján (ha rábízzuk az osztályra a távoli nyitást – ehhez be kell legyen kapcsolva az allow_url_fopen):

function loadFile($filename)
{
    $this->resetAll();
    
    if (!isset($filename) or (!file_exists($filename))) {
        $this->errors[] = 'Nincs megadva fájlnév, vagy a fájl nem létezik.';
        return false;
    }
    
    if ($this->document->load($filename) === false) {
        $this->errors[] = 'Nem sikerült a fájl betöltése, valószínűleg nem megfelelő XML dokumentum.';
        return false;
    }
    
    $this->read();
    
    return true;
}

function loadXml($xml)
{
    $this->resetAll();
    
    if ($this->document->loadXML($xml) === false) {
        $this->errors[] = 'Nem megfelelő a betöltött XML sztring.';
        return false;
    }
    
    $this->read();
    
    return true;
}

function loadUrl($url)
{
    $this->resetAll();
    
    if (!allow_url_fopen) {
        $this->errors[] = 'Nincs engedélyezve a távoli fájlnyitás (allow_url_fopen), így nem lehet URL-ről betölteni.';
        return false;
    }
    
    $xml = file_get_contents($url);
    
    if ($xml === false) {
        $this->errors[] = 'Sikertelen a távoli fájl olvasása a "file_get_contents()" függvénnyel.';
        return false;
    } else {
        return $this->loadXml($xml);
    }
}

Mindhárom függvény false-szal tér vissza sikertelenség esetén, ilyenkor az errors tömböt érdemes megnézni a fent említett módon. Sikeres betöltés után meghívódik a rejtett read() függvény, mely a gyökérelem tagName tulajdonsága alapján eldönti, hogy a dokumentum RSS, Atom vagy RDF típusú, és meghívja az ennek megfelelő feldolgozót:

private function read()
{
    $this->root = $this->document->documentElement;
    
    if ($this->root->tagName == 'rss') {
        $this->readRss();
    }
    
    if ($this->root->tagName == 'feed') {
        $this->readAtom();
    }
    
    if (
           (stristr($this->root->tagName, 'rdf'))
        or (stristr($this->root->tagName, 'RDF'))
    ) {
        $this->readRdf();
    }
}

private function readRss()
{
    $channels = $this->root->getElementsByTagName('channel');
    $channel  = $channels->item(0);
    
    foreach ($channel->getElementsByTagName('title') as $s) {
        if ($s->nodeValue > '') {
            $this->title = htmlspecialchars($s->nodeValue);
            break;
        }
    }
    
    foreach ($channel->getElementsByTagName('link') as $s) {
        if ($s->nodeValue > '') {
            $this->link = $s->nodeValue;
            break;
        }
    }
    
    $this->description = '';
    
    if (isset(
        $channel
        ->getElementsByTagName('description')
        ->item(0)
        ->nodeValue
    )) {
        $this->description =
            $channel
            ->getElementsByTagName('description')
            ->item(0)
            ->nodeValue
        ;
    }
    
    $time = false;
    
    foreach ($channel->getElementsByTagName('lastBuildDate') as $s) {
        if ($s->nodeValue > '') {
            $time = strtotime($s->nodeValue);
            break;
        }
    }
    
    if ($time) {
        $this->lastBuildDate =
              date('Y-m-d ', $time)
            . $this->days[date('w', $time)]
            . date(', H:i:s', $time)
        ;
    } else {
        $this->lastBuildDate = '';
    }
    
    $time = false;
    
    foreach ($channel->getElementsByTagName('pubDate') as $s) {
        if ($s->nodeValue > '') {
            $time = strtotime($s->nodeValue);
            break;
        }
    }
    
    if ($time) {
        $this->pubDate =
              date('Y-m-d ', $time)
            . $this->days[date('w', $time)]
            . date(', H:i:s', $time)
        ;
    } else {
        $this->pubDate = '';
    }
    
    if ($this->pubDate == '') {
        $this->pubDate = $this->lastBuildDate;
    }
    
    if ($this->lastBuildDate == '') {
        $this->lastBuildDate = $this->pubDate;
    }
    
    foreach ($channel->getElementsByTagName('ttl') as $s) {
        if ($s->nodeValue > '') {
            $this->ttl = $s->nodeValue;
            break;
        }
    }
    
    foreach ($channel->getElementsByTagName('item') as $s) {
        $this->items[$this->itemCount]['title'] = '';
        
        if (isset($s->getElementsByTagName('title')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['title'] = htmlspecialchars(
                $s
                ->getElementsByTagName('title')
                ->item(0)
                ->nodeValue
            );
        }
        
        $this->items[$this->itemCount]['link'] = '';
        
        if (isset($s->getElementsByTagName('link')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['link'] =
                $s
                ->getElementsByTagName('link')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->items[$this->itemCount]['author'] = '';
        
        if (isset($s->getElementsByTagName('author')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['author'] =
                $s
                ->getElementsByTagName('author')
                ->item(0)
                ->nodeValue
            ;
        } else if (isset(
            $s
            ->getElementsByTagName('creator')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['author'] =
                $s
                ->getElementsByTagName('creator')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $time = false;
        
        if (isset(
            $s
            ->getElementsByTagName('pubDate')
            ->item(0)
            ->nodeValue
        )) {
            $time = strtotime(
                $s
                ->getElementsByTagName('pubDate')
                ->item(0)
                ->nodeValue
            );
        }
        
        if ($time) {
            $this->items[$this->itemCount]['pubDate'] =
                  date('Y-m-d ', $time)
                . $this->days[date('w', $time)]
                . date(', H:i:s', $time)
            ;
        } else {
            $this->items[$this->itemCount]['pubDate'] = '';
        }
        
        $this->items[$this->itemCount]['comments'] = '';
        
        if (isset(
            $s
            ->getElementsByTagName('comments')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['comments'] =
                $s
                ->getElementsByTagName('comments')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->items[$this->itemCount]['description'] = '';
        
        if (isset(
            $s
            ->getElementsByTagName('description')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['description'] =
                $s
                ->getElementsByTagName('description')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->itemCount++;
    }
}

private function readAtom() {
    $feed = $this->root;
    
    $this->title = '';
    
    if (isset($feed->getElementsByTagName('title')->item(0)->nodeValue)) {
        $this->title = htmlspecialchars(
            $feed
            ->getElementsByTagName('title')
            ->item(0)
            ->nodeValue
        );
    }
    
    foreach ($feed->getElementsByTagName('link') as $s) {
        if (
                (
                        ($s->getAttribute('rel')  == 'alternate')
                    and ($s->getAttribute('type') == 'text/html')
                )
            or  ($s->getAttribute('rel') == 'self')
        ) {
            $this->link = $s->getAttribute('href');
            break;
        }
    }
    
    $this->description = '';
    
    if (isset(
        $feed
        ->getElementsByTagName('subtitle')
        ->item(0)
        ->nodeValue
    )) {
        $this->description = htmlspecialchars(
            $feed
            ->getElementsByTagName('subtitle')
            ->item(0)
            ->nodeValue
        );
    }
    
    $time = false;
    
    if (isset($feed->getElementsByTagName('updated')->item(0)->nodeValue)) {
        $time = strtotime(
            $feed
            ->getElementsByTagName('updated')
            ->item(0)
            ->nodeValue
        );
    }
    
    if ($time) {
        $this->pubDate =
              date('Y-m-d ', $time)
            . $this->days[date('w', $time)]
            . date(', H:i:s', $time)
        ;
    } else {
        $this->pubDate = '';
    }
    
    $this->lastBuildDate = $this->pubDate;
    
    foreach ($feed->getElementsByTagName('entry') as $s) {
        if (isset($s->getElementsByTagName('title')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['title'] = htmlspecialchars(
                $s
                ->getElementsByTagName('title')
                ->item(0)
                ->nodeValue
            );
        } else {
            $this->items[$this->itemCount]['title'] = '';
        }
        
        foreach ($s->getElementsByTagName('link') as $ss) {
            if ($ss->getAttribute('rel') == 'alternate') {
                $this->items[$this->itemCount]['link'] =
                    $ss->getAttribute('href')
                ;
                
                break;
            }
        }
        
        $time = false;
        
        if (isset(
            $s
            ->getElementsByTagName('published')
            ->item(0)
            ->nodeValue
        )) {
            $time = strtotime(
                $s
                ->getElementsByTagName('published')
                ->item(0)
                ->nodeValue
            );
        }
        
        if ($time) {
            $this->items[$this->itemCount]['pubDate'] =
                  date('Y-m-d ', $time)
                . $this->days[date('w', $time)]
                . date(', H:i:s', $time)
            ;
        } else {
            $this->items[$this->itemCount]['pubDate'] = '';
        }
        
        if ($this->items[$this->itemCount]['pubDate'] == '') {
            $time = false;
            
            if (isset(
                $s
                ->getElementsByTagName('updated')
                ->item(0)
                ->nodeValue
            )) {
                $time = strtotime(
                    $s
                    ->getElementsByTagName('updated')
                    ->item(0)
                    ->nodeValue
                );
            }
            
            if ($time) {
                $this->items[$this->itemCount]['pubDate'] =
                      date('Y-m-d ', $time)
                    . $this->days[date('w', $time)]
                    . date(', H:i:s', $time)
                ;
            } else {
                $this->items[$this->itemCount]['pubDate'] = '';
            }
        }
    
        $this->items[$this->itemCount]['author'] = '';
        
        if (isset(
            $s
            ->getElementsByTagName('author')
            ->item(0)
            ->getElementsByTagName('name')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['author'] =
                $s
                ->getElementsByTagName('author')
                ->item(0)
                ->getElementsByTagName('name')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->items[$this->itemCount]['comments']    = '';
        $this->items[$this->itemCount]['description'] = '';
        
        if (isset(
            $s
            ->getElementsByTagName('content')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['description'] =
                $s
                ->getElementsByTagName('content')
                ->item(0)
                ->nodeValue
            ;
        }
        
        if (isset(
            $s
            ->getElementsByTagName('summary')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['description'] .=
                $s
                ->getElementsByTagName('summary')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->itemCount++;
    }
}

private function readRdf() {
    $xmlns = $this->root->getAttribute('xmlns');
    $rdf   = $this->root->getAttribute('xmlns:rdf');
    $dc    = $this->root->getAttribute('xmlns:dc');
    
    if (($xmlns == '') or ($rdf == '') or ($dc == '')) {
        $this->errors[] = 'Nem szabványos RDF dokumentum.';
        return false;
    }
    
    $channel = $this->root->getElementsByTagName('channel')->item(0);
    
    $this->title = '';
    
    if (isset(
        $channel
        ->getElementsByTagName('title')
        ->item(0)
        ->nodeValue
    )) {
        $this->title = htmlspecialchars(
            $channel
            ->getElementsByTagName('title')
            ->item(0)
            ->nodeValue
        );
    }
    
    $this->link = $channel->getAttribute('rdf:about');
    
    if (
            ($this->link == '')
        and (isset($channel->getElementsByTagName('link')->item(0)->nodeValue))
    ) {
        $this->link =
            $channel
            ->getElementsByTagName('link')
            ->item(0)
            ->nodeValue
        ;
    }
    
    $this->description = '';
    
    if (isset(
        $channel
        ->getElementsByTagName('description')
        ->item(0)
        ->nodeValue)
    ) {
        $this->description =
            $channel
            ->getElementsByTagName('description')
            ->item(0)
            ->nodeValue
        ;
    }
    
    $time = false;
    
    if (isset(
        $channel
        ->getElementsByTagNameNS('*','date')
        ->item(0)
        ->nodeValue
    )) {
        $time = strtotime(
            $channel
            ->getElementsByTagNameNS('*','date')
            ->item(0)
            ->nodeValue
        );
    }
    
    if ($time) {
        $this->pubDate =
              date('Y-m-d ', $time)
            . $this->days[date('w', $time)]
            . date(', H:i:s', $time)
        ;
    } else {
        $this->pubDate = '';
    }
    
    $this->lastBuildDate = $this->pubDate;
    
    foreach ($this->root->getElementsByTagName('item') as $s) {
        $this->items[$this->itemCount]['title'] = '';
        
        if (isset($s->getElementsByTagName('title')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['title'] = htmlspecialchars(
                $s
                ->getElementsByTagName('title')
                ->item(0)
                ->nodeValue
            );
        }
        
        $this->items[$this->itemCount]['link'] = '';
        
        if (isset($s->getElementsByTagName('link')->item(0)->nodeValue)) {
            $this->items[$this->itemCount]['link'] =
                $s
                ->getElementsByTagName('link')
                ->item(0)
                ->nodeValue
            ;
        }
        
        if (isset(
            $s
            ->getElementsByTagName('creator')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['author'] =
                $s
                ->getElementsByTagName('creator')
                ->item(0)
                ->nodeValue
            ;
        } else if (isset(
            $s
            ->getElementsByTagName('author')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['author'] =
                $s
                ->getElementsByTagName('author')
                ->item(0)
                ->nodeValue
            ;
        } else {
            $this->items[$this->itemCount]['author'] = '';
        }
        
        $this->items[$this->itemCount]['comments'] = '';
        
        $time = false;
        
        if (isset($s->getElementsByTagName('date')->item(0)->nodeValue)) {
            $time = strtotime(
                $s
                ->getElementsByTagName('date')
                ->item(0)
                ->nodeValue
            );
        }
        
        if ($time) {
            $this->items[$this->itemCount]['pubDate'] =
                  date('Y-m-d ', $time)
                . $this->days[date('w', $time)]
                . date(', H:i:s', $time)
            ;
        } else {
            $this->items[$this->itemCount]['pubDate'] = '';
        }
        
        $this->items[$this->itemCount]['description'] = '';
        
        if (isset(
            $s
            ->getElementsByTagName('description')
            ->item(0)
            ->nodeValue
        )) {
            $this->items[$this->itemCount]['description'] =
                $s
                ->getElementsByTagName('description')
                ->item(0)
                ->nodeValue
            ;
        }
        
        $this->itemCount++;
    }
}

Ekkortól a csatorna teljes hírtartalma elérhető az items tömb elemein keresztül:

title
az elem címe;
link
az elem eredetijének elérhetősége;
pubDate
a közzététel dátuma (ez is magyarul)
author
az elem szerzője (név, de akár e-mail is lehet)
description
a hír szövege, ez sok esetben HTML-t is tartalmaz;
comments
ha az elemhez tartoznak hozzászólások, akkor azok elérhetősége (a Weblabor hírfolyamába például az új fórumtémák kerülnek így).

A betöltés után, ha nem minden elemet akarunk megjeleníteni, javaslom az rsortDate() függvény használatát, mely megfordítja az elemek (pubDate szerinti) sorrendjét:

function rsortDate() {
    for ($n = 0; $n < $this->itemCount - 1; $n++) {
        for ($m = $this->itemCount - 1; $m > 0; $m--) {
            if (
                $this->items[$m]['pubDate']
                >
                $this->items[$m - 1]['pubDate']
            ) {
                $s = $this->items[$m];
                $this->items[$m] = $this->items[$m - 1];
                $this->items[$m - 1] = $s;
            }
        }
    }
}

Kitérnék itt még a a getShortText() függvényre, mely egy elem szövegét rövidítve, szűrve adja vissza:

function getShortText($itemIndex = -1, $length = 0, $allowTags = 'all') {
    $row = chr(13) . chr(10);
    
    if (
           ($itemIndex < 0)
        or ($itemIndex >= $this->itemCount)
        or ($length < 1)
        or (!isset($allowTags))
    ) {
        $this->errors[] = 'Hibás paraméter(ek) a getShortText() függvényben.';
        return false;
    }
    
    $input = $this->items[$itemIndex]['description'];
    $maxlength = mb_strlen(strip_tags($input));
    
    if ($maxlength > $length) {
        $cut    = $length;
        $found  = false;
        $s2     = strip_tags($input);
        $source = mb_substr($s2, $cut, 8);
        
        while ($found === false) {
            $position = mb_strpos($input, $source, $cut - 1);
            
            if ($position === false) {
                $cut += 5;
                
                if ($cut >= $maxlength) {
                    $found = true;
                    $cut   = false;
                    $s1    = $input;
                } else {
                    $source = mb_substr($s2, $cut, 8);
                }
            } else {
                $found = true;
                $s1 = mb_substr($input, 0, $position);
            }
        }
    } else {
        $s1  = $input;
        $cut = false;
    }
    
    if ($allowTags <> 'all') {
        $s2 = strip_tags($s1, $allowTags);
    } else {
        $s2 = $s1;
    }
    
    $position = mb_strrpos($s2, '<');
    
    if ($position >= mb_strlen($s2) - 8) {
        $s2 = mb_substr($s2, 0, $position);
    } else if ($cut) {
        $position = mb_strrpos($s2, ' ');
        $s2       = mb_substr($s2, 0, $position);
        $position = mb_strrpos($s2, '<');
        
        if ($position >= mb_strlen($s2) - 8) {
            $s2 = mb_substr($s2, 0, $position);
        }
    }
    
    if ($cut) {
        $s2 .= '…';
    }
    
    for ($n = 0; $n < count($this->tags1); $n++) {
        $open  = mb_substr_count($s2, $this->tags1[$n], 'utf-8');
        $close = mb_substr_count($s2, $this->tags2[$n], 'utf-8');
        
        if ($open > $close) {
            for ($m = 0; $m < ($open - $close); $m++) {
                $s2 .= $this->tags2[$n] . $row;
            }
        }
    }
    
    return $s2;
}

Paraméterei:

itemIndex
ezen sorszámú elem szövegét fogja használni;
length
a kívánt hossz karakterben;
allowTags
megengedett HTML elemek, ezek maradhatnak a szövegben, a többit kiveszi (ha 'all'-t adunk meg, mindent benthagy).

A length hosszt közelítésekkel keresi a függvény, ebbe nem számítanak bele a címkék, de a szóköz stb. igen; az esetleges entitások, speciális karakterek miatt lehet némi eltérés (kb. 5-12 karakter) a megadott és a tényleges (hasznos) hossz között. Ha a rövidítendő description rövidebb length-nél, akkor nem történik levágás. Egyébként a szöveg szóvégnél lesz elvágva, a függvény pótolja a szükséges záró címkéket, a szöveg végéhez pedig …-t fűz. A címkék számolásához és zárásához a $tags1 és $tags2 rejtett tömböket használja, ezeket bárki kiegészítheti szükség szerint. (Én kb. 30 db, különböző helyről származó hírforrással teszteltem, sikerrel.)

Végül – a példa kedvéért – betöltés után írjunk ki maximum öt elemet, 150 karakter maximális hosszal:

$rss->rsortDate();

if ($rss->getItemCount() > 5) {
    $m = 5;
} else {
    $m = $rss->getItemCount();
}

for ($n = 0; $n < $m; $n++) {
    echo '<div class="feed_item">';
    echo '	<h3><a href="' . $rss->items[$n]['link'] . '">' . $rss->items[$n]['title'] . '</a></h3>';
    echo '	<p><strong>' . $rss->items[$n]['author'] . ' </strong> ' . $rss->items[$n]['pubDate'] . '</p>';
    
    echo $rss->getShortText($n, 150, '<p><i><b><span><strong>');
    
    if ($rss->items[$n]['comments'] > '') {
        echo '	<p><a href="' . $rss->items[$n]['comments'] . '">Hozzászólások</a></p>');
    }
    
    echo "</div>\r\n";
}

Megfontolásra érdemes, hogy egyes webhelyek nem követik azt az ajánlást, miszerint egy hírcsatornában maximum húsz elemet jelenítsenek meg, teljes mérete pedig ne haladja meg a 300 kB-ot. Ez az osztály az egész dokumentumot egyszerre tölti be, ezért a nagyobb méretű hírfolyamok feldolgozása lassú lehet, extrém esetben memóriafoglalási hibát is előidézhet.

Fontos tudni: a hírcsatornák felhasználása nem mindig ingyenes! Minden esetben alaposan járjunk utána, hogy az adott oldal hírcsatornáját milyen feltételekkel használhatjuk fel! Ha az XML dokumentum forrásában nem találunk erre vonatkozó információt, még nem jelenti azt, hogy szabadon felhasználhatjuk. Érdeklődjünk az üzemeltetőnél.

Köszönöm Poetronak a sok hasznos információt, amely segítségével elindultam, Hidvégi Gábornak, aki a készülő osztályt ellenőrizve ellátott ötletekkel a javításhoz, valamint Joó Ádámnak, aki ezt a cikket olvashatóvá varázsolta.

A cikkben bemutatott osztály, és a hírfolyamok közzétételére szolgáló párjának forráskódja egyben letölthető. A bélyegképen a szerző fényképe látható.

 
Pepita arcképe
Pepita
Horváth Péter webfejlesztő-programozóként dolgozik, főként adatbázis-tervezési és backend fejlesztési területen.
1

Jobban jártál volna, ha

inf3rno · 2012. Ápr. 13. (P), 00.44
Jobban jártál volna, ha csinálsz külön Reader, FileLoader, stb... osztályokat, ezeket külön egyszerűbb tesztelni, mint így egyben kideríteni, hogy hol a gond, ha hibát jelez a rendszer. Persze ez csak kéretlen tanács... :-)

A cikk nagyon hasznos, én is csináltam rss olvasót, tényleg nagyon utána kell járni mindennek. Amivel még kiegészíteném, hogy nem minden feed tartja be a karakterkódolást, velem előfordult már, hogy utf-8-at írtak az XML-ben, egyébként meg teljesen más volt, szóval azt detektálni kell. A másik, ami előfordult, hogy pubDate szerint nem voltak sorbarakva a bejegyzések, szóval azt is érdemes rendezni, amikor kiolvasod őket. A feed-ek nagy átlaga azért képes használható adatot nyújtani, ezek csak extrém esetek...
4

Szabványok

Pepita · 2012. Ápr. 13. (P), 13.24
Köszönöm a véleményed - hasznos számomra. Tesztelni valóban könnyebb lett volna külön, így viszont - szerintem - használhatóbb.

nem minden feed tartja be a karakterkódolást
Igen, itt felmerül a kérdés, hogy fel akarunk-e használni olyan feed-et, amelyik nem szabványos, ill. nem követi az ajánlásokat. Én ezt nem javaslom.

előfordult, hogy pubDate szerint nem voltak sorbarakva a bejegyzések
Ez nem is "előírás", de azt hiszem átsiklottál a cikk ezen részén:
A betöltés után, ha nem minden elemet akarunk megjeleníteni, javaslom az rsortDate() függvény használatát, mely megfordítja az elemek (pubDate szerinti) sorrendjét:
6

A betöltés után, ha nem

inf3rno · 2012. Ápr. 13. (P), 13.32
A betöltés után, ha nem minden elemet akarunk megjeleníteni, javaslom az rsortDate() függvény használatát, mely megfordítja az elemek (pubDate szerinti) sorrendjét:

Jah úgy néz ki, pedig emlékszem, hogy olvastam, csak már éjjel volt :-)
2

Ügyes. De azért a

kuka · 2012. Ápr. 13. (P), 10.18
Ügyes. De azért a Weblabor-féle RSS 2.0 csak kifog rajta. ;-)

Például ennek a cikknek a feedje ezt tartalmazza:

<pubDate> <key>pubDate</key>
 <value>Fri, 13 Apr 2012 00:44:55 +0200</value>
</pubDate>
(Mondjuk itt feltenném a kérdést, hogy mi a jó szagú életnek kell oda az a key és value aberráció, de feltételezem, hogy több éves múlttal rendelkezhet amire már senki sem emlékszik.)
5

Hiba

Pepita · 2012. Ápr. 13. (P), 13.29
"A hiba az Ön készülékében van."

Innentől kezdve ez a feed nem RSS 2.0, csak "hasonlít". Szerintem vmi miatt (talán a cikkben szereplő tag-ek?) a Drupal kutyulja össze ezt a feed-et. Mellesleg WL-ről mentett feed-del is teszteltem, tehát ez új dolog...
8

"A hiba az Ön készülékében

kuka · 2012. Ápr. 13. (P), 13.57
"A hiba az Ön készülékében van."
Ebben teljesen igazad van. De ahogy a hibás HTML-t kénytelenek vagyunk feldogozni, úgy a hibás RSS-t is. Nem lehet mindent elutasítani ami a W3C Feed Validation Service szerint hibás. Sajnos.
Mellesleg WL-ről mentett feed-del is teszteltem, tehát ez új dolog...
A Weblabor trükkös, nem minden feed ilyen ronda. Ahogy elnézem te a másik fajta feeddel teszteltél.
Nem tudom, hogy új-e ez a dolog, de márciusban már így volt amikor a Weblabor - Friss tartalom szkriptemmel játszottam.
10

Igen,

Pepita · 2012. Ápr. 13. (P), 14.23
neked is igazad van - sajnos. De pl. az IE8 sem ír ki ehhez a feed-hez dátumot, az RssReader szintúgy. Nem kapsz vissza se hibát, se krix-krax-ot elvileg.

Az osztályt és a cikket január-februárban írtam, asszem két akkori WL-es feed volt a tesztben, de erről a (weblabor.hu/rss...) címről. Ha jól emlékszem.
12

De pl. az IE8 sem ír ki ehhez

kuka · 2012. Ápr. 13. (P), 14.34
De pl. az IE8 sem ír ki ehhez a feed-hez dátumot
Nem lehetne inkább Opera a mérvadó? Ő kiírja.
13

De, akár lehetne,

Pepita · 2012. Ápr. 13. (P), 14.44
ez nézőpont kérdése.
Én így hirtelen IE8-al néztem meg.
Mindegy, elsősorban nem ezen (dátumon) múlik egy megjelenített tartalom olvasottsága, ha pedig mégis, akkor a tartalom előállítójának jobban érdeke a helyes feed elkészítése.
14

Valóban nézőpont kérdése. Én

kuka · 2012. Ápr. 13. (P), 14.55
Valóban nézőpont kérdése. Én úgy nézem, hogy ha a tartalom a te oldaladon jelenik meg akkor a látogató majd úgy értékeli, hogy te kurtad el.

Míg el nem felejtem, a dátum csak az egyik. A dc:creator is ugyanúgy el van rondítva a feedben, ezért dc:creator Pepita formában jeleníted meg.
15

A détumnál az extra html tag

inf3rno · 2012. Ápr. 13. (P), 16.47
A détumnál az extra html tag szűrés segíthet. Gondolom opera is azért jeleníti meg úgy, mert az ismeretlen tageket szimplán kihagyja és tovább megy a text-re.
18

Ok,

Pepita · 2012. Ápr. 13. (P), 21.24
ha lesz időm, alaposabban utánanézek ennek. Bár a creator-t sem helyettesítem, az is üres string lesz, ha nem található, vagy a "tag" is kiírásra kerül. Köszi az észrevételeket, eredetileg nem volt célom rossz feed-ekkel foglalkozni, de úgy látom (hála neked), muszály lesz.
19

Nálam olyan 10% szokott hibás

inf3rno · 2012. Ápr. 13. (P), 21.55
Nálam olyan 5-10% szokott hibás lenni, uh. kénytelen vagyok foglalkozni vele. Persze nem mindenhol muszáj begyűjteni az adatokat hibás feedekről, projektje válogatja... Olyan feed olvasót meg nem lehet írni, ami az összes létező hibát szűri, szóval sok esetben muszáj egyedi parsert készíteni az ilyen feed-ekhez.
3

Miért saját?

Bártházi András · 2012. Ápr. 13. (P), 10.59
Tanulás céljából nagyon jó dolog ilyen dolgokat írni, de sokkal célszerűbb egy már olyan megoldást választani, amit feedek milliárdjain teszteltek már. Nekem a LastRSS a bevált megoldásom (a Miner kb. ezt használja), de van más hasonló is:
http://planetozh.com/blog/2004/11/magpierss-vs-lastrss-comparison-of-php-rss-parsers/

Miért nem éri meg sajátot írni? Mert elég sok fajta feed létezik (RSS különböző verziói, meg ott az Atom is), és elég sok dolgot le kell kezelni, ha korrekt támogatást akarsz csinálni. Még ezek a sokat próbált kódok is elhalnak néha, pl. a blogspot tud olyat, hogy nem ha nagy egy feed tartalma, akkor a végét x kB-nál simán levágja, nem törődve az XML-lel, meg azzal, hogy félbevág egy bejegyzést. Az XML parserrel még ott is tud probléma lenni, hogy vannak olyan karakter kódok, amik az XML szerint nem megengedettek, de az UTF-8 szerint igen, és simán belekerülnek egy "XML" feedbe, a parser meg dob egy hátast. Ésatöbbi. :)
7

Mert jónak tartom

Pepita · 2012. Ápr. 13. (P), 13.47
Persze a tanulási szempont sem elhanyagolható, de - mint írtam - én nem találtam olyat, ami ilyen könnyen telepíthető, használható, kisméretű, stb., és ezeket a számomra fontos funkciókat (pl. rövidítés) mind tudja (és így).

Ez az x kB-os levágás felkeltette az érdeklődésem, lehet emiatt lesz következő verzió...

Mert elég sok fajta feed létezik (RSS különböző verziói, meg ott az Atom is)
Az a három fajta (RDF, RSS 2.0, Atom), amiket ez az olvasó egyformára alakít, lefedi az elérhető feed-ek kb. 98%-át. Ha ennél nagyobb a cél, illetve ha a szabványoknak/ajánlásoknak fittyet hányó feed-eket is le akarod kezelni, akkor valóban ez nem nagyon erős eszköz. De ha olyat találsz, ami mindezt "tudja"..., akkor az már egy nagy monstrum, és igen sok időt el kell tölteni vele, mire beüzemeled. Nekem mások voltak a szempontjaim, azért írtam sajátot.
9

Én inkább úgy csinálnám, hogy

inf3rno · 2012. Ápr. 13. (P), 14.01
Én inkább úgy csinálnám, hogy minden feed-hez hozzá rendelném a típusát, és a megfelelő parser-t töltené be a rendszer. Így lehetőség van külön parser osztályokat csinálni az extrém esetekre is...
11

Igen, te

Pepita · 2012. Ápr. 13. (P), 14.32
egyébként is szereted jól elkülöníteni a dolgokat... :)

De igazad van. Nekem - egyelőre - ennyi volt a célom, ha bővebb/nagyobb/másik olvasót írok, lehet, hogy úgy csinálom. Ennél viszont szempont volt a lehető leggyorsabb lefutás (is), aminek nem kedvez a több fájlból történő betöltés. (Pl. olyan, kiegészítő jellegű felhasználáskor, mint az én oldalamon, csak nagyon kicsi erőforrást/forgalmat akarok elhasználni rá.)
16

Persze, más az, ha leszeded a

inf3rno · 2012. Ápr. 13. (P), 16.48
Persze, más az, ha leszeded a feedeket, adatbázisban tárolod, feldolgozod, és úgy szolgálsz ki, mintha minden lekérésnél feldolgozod a feed cache-ét, esetleg magát a feed-et.
17

Igen, az már amolyan

Pepita · 2012. Ápr. 13. (P), 21.16
online feed-olvasó-szolgáltatás-féle (szépen, magyarul...) lenne. Én jóval kisebb/másmilyen kaliberben gondolkodtam: leginkább 1-3 feed, "ritka" megtekintés, viszont az épp-most-friss tartalommal.
20

A kód minősége?

T.G · 2012. Ápr. 14. (Szo), 09.10
Hál’Isten én még komolyan nem foglalkoztam az RSS ezeregy verziójának a nyűgjével... ilyen szempontból érdekes olvasmány... ám maga a forrás szerintem nem lett valami szép. Sok helyen kifejezetten „erős szaga” van.

A három féle olvasó, három hosszú függvényként tényleg nem túl elegáns, ám ennek következményeként sok apró ismétlés is van. Szinte megszámlálhatatlanul sokszor szerepel, hogy 1. változó értéke üres string, 2. létezik-e az adott node adott attribútumú eleme, 3. ha, igen, akkor legyen az az értéke. Ha semmi mást nem csinálnánk, csak ezt kiemelnénk, akkor sokkal rövidebb lenne a kód. Ahogy a dátum átalakítás is mindegyik esetben pont ugyanúgy megy.

Illetve kicsit zavar, hogy a típusok sem egészen egyértelműek, a $time először boolean, majd string. Majd utána ezt a stringet minden esetben ugyanazon a műveleten hajtjuk végre. Értem, hogy miről szól, de egy Weblabor-os mintapéldában azért mégsem szerencsés. Ahogy a $s->nodeValue > '' sem az.

Az „allow_url_fopen” az beépített konstans? Ott nem inkább ini_get('allow_url_fopen') lenne?
21

Engem a god object-en kívül

inf3rno · 2012. Ápr. 14. (Szo), 13.38
Engem a god object-en kívül leginkább az zavar, hogy nem dob kivételt, hanem visszatérő értékkel akarja eldönteni, hogy pl sikerült e betölteni az xml fájlt. Ettől függetlenül én már láttam sokkal rosszabb kódokat, mint ez... Refaktorálni kell, és ennyi. Mivel van osztály, ezért ez elég könnyen meg is oldható. Amint lesz időm rá meg is teszem (ez mondjuk akár még 1 hónap múlva is lehet, úgy néznek ki a dolgok).
22

Minden kódnál van

Pepita · 2012. Ápr. 14. (Szo), 14.54
- jobb;
- rosszabb;
- szebb / csúnyább.
Optimalizálni mindent lehet a végtelenségig, de igazad van: lehetne (kellene) szebb, rövidebb.
Köszönöm szépen a kritikát, tanultam belőle. A dátumoknál - utólag - én is gondoltam függvényre, de aztán számomra nem tűnt fontosnak. Már más a véleményem.

$time: megengedtem magamnak a nem típusos nyelv adta előnyt.
allow_url_fopen: konstans, csak php.ini-ben állítható, tehát használható így. ini_get-tel is lehetne olvasni, tetszés dolga.
23

allow_url_fopen: konstans,

kuka · 2012. Ápr. 14. (Szo), 15.38
allow_url_fopen: konstans, csak php.ini-ben állítható, tehát használható így.
Szerintem T.G kérdése nem érdeklődő hanem rávezető volt.

A deklarálatlan konstansok használatkor a saját nevüket kapják értékként. A helyes használat ini_get('allow_url_fopen'), de az előző mondatbeli kijelentés miatt ini_get(allow_url_fopen) is működik. Viszont allow_url_fopen nem.

Bekapcsolt allow_url_fopen estében:
Interactive shell

php > echo ini_get('allow_url_fopen');
1
php > echo ini_get(allow_url_fopen);
1
php > echo allow_url_fopen;
allow_url_fopen
Kikapcsolt allow_url_fopen estében:
Interactive shell

php > echo ini_get('allow_url_fopen');
php > echo ini_get(allow_url_fopen);
php > echo allow_url_fopen;
allow_url_fopen
24

Köszi, de akkor ez

Pepita · 2012. Ápr. 14. (Szo), 16.21
if (!allow_url_fopen) false? Mert akkor jelenlegi helyén sem működne! Esetleg különbség van kiírás és feltétel/vizsgálat között? (Nem lennék meglepve.)
25

Nincs különbség. A feltétel

kuka · 2012. Ápr. 14. (Szo), 16.39
Nincs különbség. A feltétel nem működik:
bash-4.2$ grep allow_url_fopen /etc/php.ini 
allow_url_fopen = On

bash-4.2$ php -r 'if (!ini_get("allow_url_fopen")) echo "Nincs engedelyezve\n"; else echo "Rendben\n";'
Rendben

bash-4.2$ php -r 'if (!allow_url_fopen) echo "Nincs engedelyezve\n"; else echo "Rendben\n";'
Rendben

bash-4.2$ sed -i '/allow_url_fopen/s/On/Off/' /etc/php.ini 

bash-4.2$ grep allow_url_fopen /etc/php.ini 
allow_url_fopen = Off

bash-4.2$ php -r 'if (!ini_get("allow_url_fopen")) echo "Nincs engedelyezve\n"; else echo "Rendben\n";'
Nincs engedelyezve

bash-4.2$ php -r 'if (!allow_url_fopen) echo "Nincs engedelyezve\n"; else echo "Rendben\n";'
Rendben
26

Köszönöm szépen!

Pepita · 2012. Ápr. 16. (H), 20.44
Teljesen igazad van, ezt én néztem el.
Nem mentegetőzni akarok, de kikapcsolt allow_url_fopen esetén így ugyan nem lesz benne az $errors tömbben a megfelelő üzenet, de lesz helyette a sikertelen távoli fájlnyitásos - és sajnos egy warningot is dobni fog.
Mihelyst lesz rá időm, a zip-ben javítom. Itt a cikkben nem tudom, talán ott kéne hagyni(?).
27

Javítás

Bártházi András · 2012. Ápr. 18. (Sze), 11.41
A cikkben is javítani szoktuk a hibákat, nem feltétlenül olvassa el mindenki a megjegyzéseket (főként ha sok van), ne szívjanak ezek a látogatók. Írd meg a javítandókat.
28

Rendben,

Pepita · 2012. Ápr. 18. (Sze), 16.52
megírtam, a letölthető rss.zip-ben is javítottam.

Köszönettel:
Pepita.