Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

22 July, 2009

Active Browsing, Teil 2

Programmiersprachen

Im letzten Beitrag habe ich über Active Browsing geschrieben und der Mächtigkeit, die jeder User im Grunde hat, diese aber nicht nutzt. Wir wollen uns nun das Leben mit ein paar Zeilen Code vereinfachen.

Installation der Komponenten

Um ruby-libnotify und mechanize nutzen zu können, müssen sie zuerst installiert werden. Bei Mechanize ist das kein Problem, da es schon als rubygem vorliegt. Ein simples gem install mechanize installiert das Paket, inklusive nokogiri für das Parsen der Webseiten. Bei ruby-libnotify muss man selbst Hand anlegen und mit dem Dreischritt ruby extconf.rb && make && make install das Paket zusammenbauen und installieren. Wer das Paket libopenssl-ruby noch nicht installiert hat, sollte das zuvor erledigen.

Login und Parsing

Der Erste Schritt ist nun, sich auf der Webseite anzumelden und die Titel aller Jobaufträge aus der HTML Datei herauszusuchen. Wir binden zuerst die Pakete mechanize und nokogiri ein, um damit arbeiten zu können.

require "mechanize"
require "nokogiri"

Als nächstes kümmern wir uns um den Login. Hier kommt uns mechanize sehr entgegen, denn es bietet ein simples Interface zur HTML-Seite. Machen wir zuerst Gebrauch von den Methoden get und submit:

class TextBroker
    def initialize(mail, pass)
        @agent = WWW::Mechanize.new
        @mail, @pass = mail, pass
        login
    end

    private
    def login
        page = @agent.get("http://www.textbroker.de/")
        loginForm = page.form("login-autoren")
        loginForm.author_mail = @mail
        loginForm.pass = @pass
        @agent.submit(loginForm, loginForm.buttons.first)
    end
end

Unter @agent ist die mechanize-Instanz abgelegt, die wir im Konstruktor initialisieren. Die private Methode login läd zuerst die Startseite mit get herunter und greift sich das Login Formular der Autoren anhand der ID. Die Attribute author_mail und pass sind Formularfelder, die sich so befüllen und auslesen lassen. mit submit wird das Login-Formular abgeschickt. In der mechanize Instanz wird nun der Cookie gesetzt, um dauerhaft eingeloggt zu sein. Ab jetzt können wir GET/POST-Anfragen innerhalb des passwortgeschützten Bereichs starten ohne sich erneut einzuloggen.

Sammeln wir nun die Jobs ein, die auf der Seite verfügbar sind. Ich habe die Methode fetch genannt, und in die TextBroker Klasse integriert. (In Ruby sind alle Klassen offen und können so erweitert werden, hier wird also keine neue Klasse angelegt, sondern die bestehende erweitert)

class TextBroker
    def fetch
        page = @agent.get("http://www.textbroker.de/a/search.php")
        links = page.links.select {|l| !l.href.nil? and l.href.include?("search.php?hdl_id=") }
        links.map {|l| l.text }
    end
end

Wir stellen einen neuen GET-Request, um uns die HTML-Seite mit den Jobangeboten zu besorgen. über das Attribut links erhalten wir ein Array von allen Links, die auf der Seite verfügbar sind. Daraus selektieren wir uns alle, die im href Attribut den String search.php?hdl_id= enthalten haben. Da wir nur am Linktext interessiert sind, erstellen wir mit map ein Array der Linktexte und geben diese zurück.

Damit sind wir schon mit dem ersten Teil fertig und können uns eine Liste der Aufträge holen.

wwwAccess = TextBroker.new("mail@aaron-fischer.net", "xxx")
pp wwwAccess.fetch #=> ["...", "...", ...]

Im nächsten Schritt werden wir diese Information auswerten und grafisch aufbereiten, so dass der User nur das sieht, was er wirklich sehen will: Die neu hinzugekommenen Jobaufträge.

Darstellung

Da ich unter GNOME arbeite, habe ich mich für das GTK Binding entschieden. Zusammen mit ruby-libnotify ist das Zusammenstecken einer GUI ganz einfach. Zuerst einmal binden wir wieder die benötigten Bibliotheken ein:

require "gtk2"
require "RNotify"

Da wir unser Programm in der Notification Area positionieren wollen, brauchen wir grundlegend nur ein Gtk::StatusIcon und die Notification selbst.

statusIcon = Gtk::StatusIcon.new
statusIcon.stock = Gtk::Stock::DND_MULTIPLE
statusIcon.tooltip = "Textbroker.de Benachrichtigung"
notify = Notify::Notification.new("Header", "Content", nil, statusIcon)
notify.timeout = 5000

Um nun in regelmäßigen Abständen nach neuen Aufträgen zu suchen, und um diesen Vorgang starten und stoppen zu können, verbinden wir das Signal activate (was für einen Linksklick steht) mit einem Stück Code:

wwwAccess = TextBroker.new("mail@aaron-fischer.net", "xxx")
fetchThread = nil

statusIcon.signal_connect("activate") {|widget, event|
    if fetchThread.instance_of? Thread
        fetchThread.terminate
        fetchThread = nil
        statusIcon.stock = Gtk::Stock::DND_MULTIPLE
    else
        statusIcon.stock = Gtk::Stock::JUMP_TO
        fetchThread = Thread.new do
            oldJobs = []
            loop do
                newJobs = (jobs = wwwAccess.fetch) - oldJobs
                oldJobs = jobs
                if !newJobs.empty?
                    content = "\\n\\n"
                    newJobs.each {|job| content += job + "\\n" }
                    notify.update("Neue Schreibaufträge vorhanden", content, nil)
                    notify.show
                end
                sleep 60
            end
        end
    end
}

Dies bedarf einer Erklärung: Jedes mal, wenn das Icon gedrückt wird, soll es entweder den fetchThread starten und in regelmäßigen Abständen nach neuen Jobs suchen, oder den aktuellen Thread beenden. Um dies zu visualisieren, ändern wir das Icon bei jeden Klick. Wird der Thread gestartet, läuft er in eine Endlosschleife, die alle 60 Sekunden unsere fetch Methode aus der TextBroker Klasse aufruft und dann mit dem letzten Stand überprüft. Gibt es Veränderungen (ist also das newJobs Array nicht leer), rufen wir die Methode update vom Notification Objekt auf und zeigen diese an.

Damit man das Programm auch anständig beenden kann, geben wir dem Icon noch ein Kontextmenü mit einem Beenden Eintrag.

menu = Gtk::Menu.new
menuQuit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
menuQuit.signal_connect("activate") {|widget, event| Gtk.main_quit }
menu.append(menuQuit)

statusIcon.signal_connect("popup-menu") {|widget, button, time|
    if button == 3
        menu.show_all
        menu.popup(nil, nil, button, time)        
    end
}

Am Ende müssen wir nur noch die Notification initialisieren und in den Event-Loop von GTK hineinspringen. Danach übernimmt GTK die Ablaufkontrolle.

Notify.init("textbroker-notification")
Gtk.main

Zusammengezählt sind das nun ca. 80 Zeilen Code, nicht viel für eine solche Aufgabe. Dieses Vorgehen lässt sich auf beliebige Resourcen anwenden und erleichtert den Alltag ungemein. Allerdings kann man nicht oft genug darauf hinweisen, dass man hier Funktionalität erstellt, die vom Autor der Webseite (oder einer anderen Resource) so nicht vorgesehen wurden, entweder aus Faulheit oder aber aus gutem Grund.

Habt ihr Ideen, wo man eine solche Technik einsetzen kann? Oder habt ihr selbst schon das ein oder andere Tool geschrieben? Soll ich öfters solche Beispielprojekte vorstellen? Lasst es mich in den Kommentaren wissen. :)