Active Browsing, Teil 2
ProgrammiersprachenIm 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. :)