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.
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.