Aus Linux-Magazin 01/2005

Ein Perl-Skript sammelt automatisch Schlagzeilen

Statt in regelmäßigen Abständen alle interessanten Nachrichtenseiten selbst nach neuen Meldungen abzuklappern, setzt der Perl-Fan lieber einen News-Aggregator darauf an. Der macht seinen Herrn automatisch auf Neuigkeiten aufmerksam. Falls eine Website noch kein RSS-Feed hat, bastelt man es sich selber.

Die Informationsflut im Internet ist erdrückend: Wer täglich zwei Dutzend Websites nach neuen Nachrichten absucht, kann gleich seinen Job aufgeben. Nachrichten-Sites fassen darum zunehmend ihre Schlagzeilen mit Links zu den eigentlichen Artikeln in so genannten RSS-Feeds zusammen und stellen sie in maschinenlesbarem Format zur Verfügung. RSS steht für RDF Site Summary, wobei RDF seinerseits das Kürzel für Resource Description Framework ist. RSS-Dateien enthalten XML, das News-Aggregatoren maschinell einlesen. Die noch ungelesenen neuesten Neuigkeiten, präsentieren sie dem Benutzer als klickbare Schlagzeilen.

Diese Syndication, das Zusammenstellen von Nachrichten, die bereits woanders verfügbar sind, hilft dabei, der Informationsflut Herr zu werden, und spart enorm Zeit. Bekannte Sites wie Slashdot und neuerdings sogar die Bild-Zeitung[2] bieten RSS-Feeds an, die Aggregatoren wie Amphetadesk ([3] und Abbildung 1) in regelmäßigen Zeitabständen einsaugen, falls der Benutzer den Service abonniert, also den Subscribe-Knopf drückt. Weitere Informationen rund um RSS-Reader (in der Skriptsprache Tcl) bietet der Artikel “Appetithäppchen” in dieser Ausgabe.

News ohne Feed

Doch nicht jede News-Seite hat ein RSS-Feed. Erwarten deren Anbieter wirklich, dass Nutzer täglich vorbeischneien, um sich durch die angebotenen Informationen zu wühlen? Das heute vorgestellte Modul »RssMaker« stellt eine Funktion bereit, die mit rund zehn Zeilen Perl-Code aus einer Titelseite mit Schlagzeilen und URLs eine RSS-Datei generiert. Per Cronjob einmal täglich neu erzeugt, kann man diese Datei einem News-Aggregator übergeben, der dann eine Zusammenfassung liefert.

Die »make()«-Funktion aus Listing 1 »RssMaker.pm« (Zeile 20) nimmt eine URL entgegen, unter der die News-Site verfügbar ist, lädt die Webseite herunter und wühlt sich durch die darin enthaltenen HTML-Links. Diese Links bietet sie mit dem dargestellten Text einem vom Nutzer definierten Filter an. Der Filter entscheidet, ob die Schlagzeile samt Link in die RSS-Beschreibung aufgenommen wird oder nicht.

Listing 1:
»RssMaker.pm«

001 ###########################################
002 package RssMaker;
003 ###########################################
004 # Mike Schilli, 2004 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008 
009 use LWP::UserAgent;
010 use HTTP::Request::Common;
011 use XML::RSS;
012 use HTML::Entities qw(decode_entities);
013 use URI::URL;
014 use HTTP::Date;
015 use DateTime;
016 use HTML::TreeBuilder;
017 use Log::Log4perl qw(:easy);
018 
019 ###########################################
020 sub make {
021 ###########################################
022   my(%o) = @_;
023 
024   $o{url}      ||  LOGDIE "url missing";
025   $o{title}    ||  LOGDIE "title missing";
026   $o{output}   ||= "out.rdf";
027   $o{filter}   ||= sub { 1 };
028   $o{encoding} ||= 'utf-8';
029 
030   my $ua = LWP::UserAgent->new();
031 
032   INFO "Fetching $o{url}";
033   my $resp = $ua->request(GET $o{url});
034 
035   LOGDIE "Fetching $o{url} failed" if
036     $resp->is_error();
037 
038   my $http_time =
039             $resp->header('last-modified');
040   INFO "Last modified: $http_time";
041 
042   my $mtime   = str2time($http_time);
043   my $isotime = DateTime->from_epoch(
044                           epoch => $mtime);
045   DEBUG "Last modified: $isotime";
046 
047   my $rss = XML::RSS->new(
048     encoding => $o{encoding});
049 
050   $rss->channel(
051     title => $o{title},
052     link  => $o{url},
053     dc    => { date => $isotime . "Z"},
054   );
055 
056   foreach(exlinks($resp->content(),
057                   $o{url})) {
058 
059     my($lurl, $text) = @$_;
060 
061     $text = decode_entities($text);
062 
063     if($o{filter}->($lurl, $text)) {
064       INFO "Adding rss entry: $text $lurl";
065       $rss->add_item(
066         title => $text,
067         link  => $lurl,
068       );
069     }
070   }
071 
072   INFO "Saving output in $o{output}";
073   $rss->save($o{output}) or
074       die "Cannot write to $o{output}";
075 }
076 
077 ###########################################
078 sub exlinks {
079 ###########################################
080   my($html, $base_url) = @_;
081 
082   my @links = ();
083 
084   my $tree = HTML::TreeBuilder->new();
085 
086   $tree->parse($html) or return ();
087 
088   for(@{$tree->extract_links('a')}) {
089       my($link, $element, $attr,
090          $tag) = @$_;
091 
092       next unless $attr eq "href";
093 
094       my $uri = URI->new_abs($link,
095                              $base_url);
096       next unless
097         length $element->as_trimmed_text();
098 
099       push @links,
100            [URI->new_abs($link, $base_url),
101             $element->as_trimmed_text()];
102     }
103 
104     return @links;
105 }
106 
107 1;

Falls ja, fügt das Skript neben der Schlagzeile und dem Link auch noch das letzte Modifikationsdatum der Webseite in das Newsfeed ein – ein kleiner Trick, um Nachrichten mit einem Datum zu versehen, auch wenn diese selbst kein Datum führen. Am Ende schreibt »make« die XML-Ausgabe in eine mit dem Parameter »output« spezifizierte Datei, die ein News-Aggregator später aufgreifen kann.

Datum in zwei Formaten

Um den HTTP-Zeitstempel des Webdokuments in das vom RSS-Format geforderte ISO-8601-Format umzuwandeln, scannt die Funktion »str2time« aus dem Modul »HTTP::Date« zunächst das Stringformat (zum Beispiel »Tue, 26 Oct 2004 05:10:08 GMT«) ein und gibt die Unix-Zeit in Sekunden zurück. Den Wert schnappt sich die »from_epoch()«-Funktion aus dem »DateTime«-Modul und erzeugt ein neues »DateTime«-Objekt, das sich im Stringkontext magisch in das ISO-8601-Format verwandelt: »2004-10-26T05:10:08«.

XML erwartet UTF-8-kodierten Text. UTF-8 ist kompatibel mit regulärem Ascii – solange keine Zeichen aus der zweiten Hälfte der 256-Zeichen-Tabelle enthalten sind. Die deutschen Umlaute sind also wieder mal ein Problem: Werden nach ISO-8859-1 alle 256 Zeichen der Ascii-Tabelle mit den Umlauten genutzt, ist die zweite Hälfte (129 bis 256) nicht UTF-8-kompatibel.

Kodierung

»RssMaker« geht diesem Problem aus dem Weg, indem es dem Programmierer erlaubt, die Kodierung im später erzeugten RSS-Dokument festzulegen. Enthält die analysierte Webseite HTML-Kodierungen wie »ü« für das ü, macht der »HTML::TreeBuilder« aus den extrahierten Link-Texten ISO-8859-1. Das ü im Link-Text »Bohlen rügt Teppichluder« wird mit 252 kodiert. Allerdings ergäbe sich ein Problem, wenn die ü-Zeichen mit Ascii 252 kodiert wären und in der später erzeugten RSS-Datei stünden:

<?xml version="1.0" encoding="utf-8"?>

Gibt der Programmierer jedoch der Make-Funktion »encoding => “iso-8859-1″« mit, steht später im XML-Dokument:

<?xml version="1.0" encoding="iso-8859-1"?>

Danach wird der News-Aggregator die als 252 kodierten ü-Zeichen korrekt interpretieren.

RSS aufgedrückt

Wie entsteht aus einer Nachrichtenseite mittels »RssMaker.pm« ein RSS-Feed? Listing 2 zeigt am Beispiel des auf Perlmeister.com publizierten Amerika-Rundbriefs, dass hierzu wirklich nur eine Hand voll Codezeilen erforderlich ist. Die Make-Funktion des »RssMaker«-Moduls erledigt alles Wesentliche: Der »url«-Parameter legt die URL zu der Nachrichtenseite fest, auf der die Schlagzeilen mit den zugehörigen Links stehen, und »output« gibt den Namen der entstehenden RSS-Datei an. »title« ist der später im News-Aggregator angezeigte Titel des Feed.

Listing 2:
»rb2rss«

01 #!/usr/bin/perl
02 ###########################################
03 # rb2rss - Rundbrief to RSS feed
04 # Mike Schilli, 2004 (m@perlmeister.com)
05 ###########################################
06 use warnings;
07 use strict;
08 
09 use RssMaker;
10 use Log::Log4perl qw(:easy);
11 Log::Log4perl->easy_init($INFO);
12 
13 RssMaker::make(
14   url      =>
15     "http://perlmeister.com/rundbrief",
16   title    => "Perlmeister",
17   filter   => sub {
18     my($url, $text) = @_;
19     $url =~ /rundbrief#d+$/;
20   },
21   encoding => 'iso-8859-1',
22   output   => 'rundbrief.rss',
23 );

Links der Reihe nach

Die mit »filter« angegebene anonyme Subroutine ruft »RssMaker« pro entdecktem Link auf. Jedes Mal schiebt »RssMaker« zwei Parameter hinein: Die URL des Links und den zugehörigen Text. Mit diesen Informationen kann die Subroutine nun bestimmen, ob der Link eine Schlagzeile ist, die ins Feed aufzunehmen ist oder nicht. Gibt der Filter »1« zurück, wird der Link ins Feed übernommen, bei »0« nicht.

Im Fall der USA-Rundbrief-Seite prüft »rb2rss« einfach, ob die URL mit »rundbrief#d+« endet, denn das sind die dort vorherrschenden Konventionen für die Zwischenüberschriften. Für andere Websites ist die Filterfunktion natürlich so anzupassen, dass sie nach den dort geltenden Konventionen Schlagzeilen aufzuspüren vermag. Konkret demonstriert dies das folgende Beispiel. Mehr ist nicht zu tun – fertig ist das erste automatische erzeugte RSS-Feed.

Als etwas komplexeres Beispiel dient die Startseite des Online-Auftritts der Bild-Zeitung auf [bild.de] – es ist lediglich als Anschauungsbeispiel zu verstehen, denn wie gesagt existiert seit neuestem ein extra RSS-Feed[2].

Im Bild

Unter der angegebenen URL findet sich eine Reihe von Links, die allerdings mit Javascript verziert sind. »bild2rss« (siehe Listing 3) prüft einfach, ob die URL das Pattern »java-script« enthält. Ist dem so, parst es hart aber herzlich die darin enthaltene »http://…«-URL heraus.

Listing 3:
»bild2rss«

01 #!/usr/bin/perl
02 ###########################################
03 # bild2rss - bild.de as *.rss
04 # Mike Schilli, 2004 (m@perlmeister.com)
05 ###########################################
06 use warnings;
07 use strict;
08 
09 use RssMaker;
10 use Log::Log4perl qw(:easy);
11 
12 Log::Log4perl->easy_init($INFO);
13 
14 my $url = "http://www.bild.t-online.de/" .
15           "BTO/Startseite/StartBuehne," .
16           "templateId=renderKomplett.html";
17 
18 RssMaker::make(
19   url    => $url,
20   title  => "Bildzeitung",
21   filter => sub {
22     my($link, $text) = @_;
23     if($link =~ /javascript/) {
24       if($link =~ /'(http:/.*?)'/) {
25         $link = $1;
26         $_[0] = $link;
27       } else {
28         return 0;
29       }
30     }
31     $_[1] =~ s/[^[:print:]äöüÄÖÜß]//g;
32     $link =~ m#BTO#i ? 1 : 0;
33   },
34   output => "bild.rss",
35   encoding => "iso-8859-1");

Wer in Perl einer Subroutine einen Parameter übergibt, kann innerhalb des Unterprogramms auf diesen nicht nur lesend, sondern auch schreibend zugreifen.

Setzt man »« in der Funktion auf einen neuen Wert, hat ein Aufruf von »filter(, )« für » in dem Hauptprogramm einen vielleicht unerwarteten Nebeneffekt: Der neue Wert wird tatsächlich der Variablen zugewiesen. Diesen Hackentrick nutzt die »filter«-Funktion in »bild2rss«, um die URL zu manipulieren, bevor sie ins Feed wandert. Der zweite Parameter, der der URL im HTML zugewiesene Link-Text, wird ebenfalls manipuliert: Zeile 31 von Listing 3 wirft alle Sonderzeichen raus, die das XML verhunzen könnten.

Als Aufnahmekriterium in das Feed dient dem Programm »bild2rss« der Test in Zeile 32, der nachsieht, ob die URL die Zeichenfolge »BTO« enthält – das scheint allen auf der Startseite angezeigten Schlagzeilen gemeinsam zu sein.

Aggregatoren

Services wie zum Beispiel Blogline [http://www.blogline.com] betreiben im Internet Webapplikationen, die es registrierten Nutzern erlauben, Feeds zu abonnieren und deren Änderungen aktiv zu verfolgen. Alternativ sei als lokal laufende Applikation Amphetadesk[3] empfohlen. Dabei handelt es sich um ein Perl-Skript, das auf dem heimischen Rechner als HTTP-Server läuft und in einem darauf gerichteten Browser eine schöne Übersicht aller News-würdigen Schlagzeilen anzeigt (Abbildung 1).

Abbildung 1: Das Perlmeister-Newsfeed, wie es der lokale News-Aggregator Amphetadesk präsentiert.

Abbildung 1: Das Perlmeister-Newsfeed, wie es der lokale News-Aggregator Amphetadesk präsentiert.

Kontrolle ist besser

Wer prüfen will, ob die erzeugten RSS-Dateien den strengen Vorschriften des Standards entsprechen, kann sie auf einfache Weise online validieren lassen. Einen entsprechenden, kostenlosen Service bietet die Website [http://feeds.archive.org/validator]. Sie checkt die übermittelten RSS-Dokumente in Echtzeit. Den Erfolg bescheinigt ein formschönes Siegel (Abbildung 2).

Abbildung 2: Genehmigt: Der RSS-Validierer auf Feeds.archive.org prüft die erzeugten RSS-Files auf ihre Standard-Konformität. Er beherrscht übrigens bereits das kommende Format Atom.

Abbildung 2: Genehmigt: Der RSS-Validierer auf Feeds.archive.org prüft die erzeugten RSS-Files auf ihre Standard-Konformität. Er beherrscht übrigens bereits das kommende Format Atom.

Das Skript »RssMaker.pm« nutzt die Module »Log4perl« im »easy«-Modus fürs Debuggen, »LWP::UserAgent« zum Einholen von URLs und »XML:RSS« zum Erstellen der RSS-Datei. »decode_entities« aus »HTML::Entities« dekodiert HTML-Escape-Sequenzen wie etwa »ü«. Die Extraktion der Link erledigt die Funktion »exlinks« mit »HTML::TreeBuilder«. Die Funktion »as_trimmed_ text()« fieselt den Text aus den gefundenen »href«-Ankern hervor.

Ein neues, Atom genanntes Format wird wohl in absehbarer Zeit den RSS-Standard ablösen. Die entsprechenden Gremien sind bereits zugange. Falls irgendwann die unter[7] aufgelisteten Atom-kompatiblen Clients eine kritische Masse erreichen, wird wahrscheinlich auch ein mit »RssMaker« vergleichbares »AtomMaker«-Modul vom CPAN erhältlich sein, das intern das bereits verfügbare »XML::Atom«-Modul nutzt.

Zurzeit unterstützen viele populäre Clients das Atom-Format allerdings noch nicht und einige ihrer aufgerüsteten Kollegen sind extrem buggy. Eine kurze Ausführung über Atom in Buchform findet sich unter[4], unter[5] ein einfaches Tutorial zum Thema.

Abbildung 3: Die informativen Rundbriefe der Perlmeister-Website verwandelt das hier vorgestellte Perl-Skript automatisch in ein Newsfeed, das sich mit einem News-Aggregator anzeigen lässt.

Abbildung 3: Die informativen Rundbriefe der Perlmeister-Website verwandelt das hier vorgestellte Perl-Skript automatisch in ein Newsfeed, das sich mit einem News-Aggregator anzeigen lässt.

Installation

Die von »RssMaker.pm« geforderten Module sind allesamt vom CPAN erhältlich. Scraper-Skripte wie »rb2rss« sollten als Cronjob laufen, typisch einmal täglich. Die entstehende RSS-Datei legt man am besten im Intranet ab. Das Publizieren von RSS-Dateien im Zusammenhang mit öffentlichem Deep-Linking könnte nämlich unter Umständen rechtliche Probleme aufwerfen.

Während der Debug-Phase ist es hilfreich, die »Log4perl«-Einstellung des Skripts auf » zu setzen und so das Einholen, die Link-Extraktion und das Erstellen des RSS-Feed am Terminal mitzuverfolgen. Im Produktionsbetrieb kann dann mit » aller überflüssiger Ballast ausgeblendet werden, damit der Cronjob keine unnötigen Mails aussendet. (jcb)

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2005/01/Perl]

[2] RSS-Feed der Bild-Zeitung: [http://www.bild.t-online.de/BTO/,templateId=rss/rss091.jsp.htm]

[3] Amphetadesk, “Syndicated Aggregator”: [http://www.disobey.com/amphetadesk]

[4] Michael Fitzgerald, “XML Hacks”: O\’Reilly

[5] Reuven Lerner, “Aggregating with Atom”: Linux-Journal 11/04, S. 18

[6] Ben Hammersley, “Content Syndication with RSS”: O\’Reilly 2003

[7] Liste von Applikationen, die bereits Atom unterstützen: [http://atomenabled.org]

Der
Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. Seine Homepage: [http://perlmeister.com]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben