Web scraping mit Ruby/Mechanize

Praktisch jede Interaktion mit einer Website oder Webapplikation kann gescriptet, d.h. automatisiert werden. Das Abgrasen von Webseiten nach bestimmten Informationen wird auch als Scraping bezeichnet(für die nicht menschlichen Besucher dieser Seite sei das erwähnt *g*) Scripte können einem eine ganze Menge Arbeit abnehmen und sogar Dinge tun, die manuell unmöglich wären. Ich beschäftige mich mit dem Thema schon seit einer ganzen Weile und möchte hier nun die von mir favorisierte Methode dafür vorstellen, die Bibliothek Mechanize für die Scriptsprache Ruby.

Mechanize hat seinen Ursprung in Perl, mittlerweile gibt es jedoch auch Implementierungen der API für Python und eben Ruby. Für PHP gibt es mit Snoopy ein ähnliches Projekt, wenn es auch bei weitem nicht so fortgeschritten ist. Mechanize bietet die Möglichkeit mit einfachen Methoden eine art Webbrowser zu simulieren. Alle Beispiele wurden mit Mechanize Version 0.9.2 und Ruby 1.8.7 getestet.

Installation / Initialisierung

Mechanize kann mit Gems(ähnlich CPAN oder PEAR) installiert werden(# gem install mechanize --remote), einige Distributionen bieten aber auch eigene Pakete an. Ein Ruby-Script kann daraufhin Mechanize inkludieren und ein Objekt erstellen:

require 'rubygems' # ist unter Umständen notwendig
require 'mechanize'
agent = Mechanize.new

Jetzt ist Mechanize einsatzbereit, die folgenden Beispiele bauen darauf auf. Außerdem können mit dem agent nun noch grundlegende Einstellungen vorgenommen werden:

agent.set_proxy('localhost', '8000')
agent.user_agent = 'Individueller User-Agent'
agent.user_agent_alias = 'Linux Mozilla'

Die Einstellung ‘user_agent_alias‘ wählt einen User-Agent String aus dem folgenden Satz von Beispielen aus: Windows IE 6, Windows IE 7, Windows Mozilla, Mac Safari, Mac FireFox, Mac Mozilla, Linux Mozilla, Linux Konqueror, iPhone und Mechanize. Die Timing Einstellungen können ebenfalls sehr wichtig sein:

agent.open_timeout = 3 # setzt timeouts
agent.read_timeout = 4
agent.keep_alive = false # default ist true

Hier folgen nun einige Beispiele, vielleicht werde ich mit der Zeit auch noch ein paar ergänzen, falls jemand Vorschläge hat, immer her damit. Ich habe auf http://apoc.sixserv.org/requestinfo ein kleines Skript am laufen das nützliche Informationen zum HTTP-Request liefert, das kann zum Experimentieren mit Mechanize sehr nützlich sein. Einige Beispiele findet man auch in den GUIDE und EXAMPLES Dateien des Mechanize Pakets.

#01 – Manuelle Get Requests

url = 'http://apoc.sixserv.org/requestinfo/'
page = agent.get url # einfacher get request
# parameter können auch als Hash mitgeliefert werden:
page = agent.get(url, {"name" => "value", "key" => "val"})

#02 – Manuelle Post Requests

url = 'http://apoc.sixserv.org/requestinfo/'
page = agent.post(url, {"name" => "value", "key" => "val"})

In diesem Fall werden die Post-Daten manuell erzeugt, häufig ist es jedoch besser das eigentliche Formular zu verwenden. z.B. ein Login oder Suchformular:

#03 – Post Requests(/Submits) aus Formular erzeugen

page = agent.get 'https://twitter.com/login'
login_form = page.form_with(:action => 'https://twitter.com/sessions')
login_form['session[username_or_email]'] = '[Username]'
login_form['session[password]'] = '[Password]'
page = agent.submit login_form

Dieses kleine Beispiel loggt sich bei Twitter ein.

#04 – Navigation über Links und der History

page = agent.get 'http://www.heise.de/'
page = agent.click(page.link_with(:text => /Telepolis/))
page = agent.click(page.link_with(:href => /artikel/))
agent.back
agent.back
puts page.body

#05 – Exceptions

Wird z.B. eine nicht vorhandene Seite aufgerufen, wirft Mechanize eine Exception, diese sollte Abgefangen werden da das Skript sonst an dieser Stelle abbricht.

begin
  page = agent.get 'http://apoc.sixserv.org/diese/seite/gibt/es/nicht/'
rescue WWW::Mechanize::ResponseCodeError
  puts "ResponseCodeError - Code: #{$!}"
end

#06 – Referer

Mechanize benutzt seine Navigations-History auch zum senden eines passenden Referers, diesen kann man allerdings auch manuell bestimmen, hier für GET und POST Requests:

page = agent.get(:url => 'http://apoc.sixserv.org/requestinfo/',
:referer => 'http://google.com/this/is/a/custom/referer')
puts page.body

#07 – Headers / HEAD / Redirect

Der Request Header kann manuell erweitert werden, bspw. den “X-Requested-With” so zu setzen um einen Ajax-Aufruf zu simulieren. Es gibt leider keine wirklich gute Möglichkeit eigene Header zu setzen, das ganze wurde in den vergangenen Versionen oft geändert, die folgende Methode funktioniert mit älteren Versionen von Mechanize nicht.

agent.pre_connect_hooks << lambda do |params|
  params[:request]['X-Requested-With'] = 'XMLHttpRequest'
end

Dies ist übrigens auch die einzig wirklich einfache Möglichkeit manuell Cookies zu setzen/ zu übertragen. Die andere Seite, also das Abfragen des Response-Headers ist trivial, hier wird z.B. per HEAD-Request die Server und evtl. der PHP-Version abgefragt:

page = agent.head 'http://sixserv.org'
server_version = page.header['server']
puts "Server: #{server_version}"
if page.header.key? 'x-powered-by'
  php_version = page.header['x-powered-by']
  puts "X-Powered-By: #{php_version}"
end

Will man einen HTTP-Redirect(302) abfragen, muss man zunächst das automatische Weiterleiten im Mechanize deaktivieren, dann kann man den Zielort abfragen ohne das Ziel zu besuchen. (Nützlich z.B. wenn man auf einen Datei-Download weitergeleitet wird)

agent.redirect_ok = false
page = agent.get 'http://www.sixserv.org/' # leitet auf http://sixserv.org weiter
puts page.header['location']

#08 – Threads

Will man ein Script etwas beschleunigen kann man mit Threads arbeiten um mehrere Inhalte gleichzeitig abzufragen. Das folgende Grundgerüst für z.B. Foren mit vielen Seiten, bearbeitet 10 Seiten gleichzeitig und kann so ein Skript erheblich beschleunigen. In diesem Fall würde man allerdings wahrscheinlich eher nicht Mechanize einsetzen sondern eine Low-Level API fürs Netzwerk-Zugriff, das wäre deutlich effektiver ich will nur das Prinzip deutlich machen.

$threads = 0
last_page = 200
max_threads = 10
for page in 1..last_page
  Thread.new(page) do |page|
    $threads += 1
    puts " [*] Create new Thread for scanning Page ##{page}"
 
    page = $agent.get "http://example.com/datalist.php?page=#{page}"
 
    # process / save received data
 
    puts " [*] Destroy Thread"
    $threads -= 1
  end
  while $threads > max_threads
  end
  puts " [*] Next Page"
end

#09 – Parsing / XPath

Hat man eine Seite empfangen und will bestimmte Inhalte herausfiltern gibt es mehrere Möglichkeiten, meistens werden mehr oder weniger umfangreiche Reguläre Ausdrücke zum parsen verwendet:

page = agent.get 'http://example.com/'
page.body.match /< h3>([^<]+)< \/h3>/
puts "Überschrift 3: #{$1}"

Wesentlich stabiler sind jedoch XPath Ausdrücke die sich direkt auf die Struktur eines XML-Dokuments beziehen. Werden nur Kleinigkeiten am HTML-Code geändert sind RegEx’s häuft aufgeschmissen und müssen mühsam angepasst werden, XPath ausdrücke dagegen funktionieren häufig auch dann noch. Eine Einführung gibt es z.B. auf w3schools.com oder der offizielle Standard der W3C. Allerdings geht das alles auch sehr einfach mit entsprechenden Werkzeugen, so gibt es z.B. eine Menge Firefox-Addons(nützlich ist z.B. der XPath Checker) die einem, zu markierten Elementen einer Seite einen passenden XPath Ausdruck anzeigt oder umgekehrt, wenn auch die nur einfachen Ausdrücke gelegentlich Nachbearbeitung erfordern.

XPath mit Firebug

Firebug kann einem zu beliebigen Elementen einer Seite einen passenden XPath(Rot unterstrichen) anzeigen.

Der von Mechanize verwendete HTML-Parser Nokogiri(鋸) ist sehr robust und kann auch “kaputtes” HTML parsen, was XPath zu einem auserordentlich mächtigen Werkzeug macht, um jede beliebige Information aus noch so komplexen Seiten zu extrahieren.

Mit Firebug: Ein beliebiges Element auf einer Seite markieren und im Kontextmenü, “Inspect Element” aufrufen. In der oberen Zeile von Firebug(siehe Screenshot) kann im Kontextmenü mit “Copy XPath” der XPath geholt werden. Im Beispiel wird der XPath zum Comic-Strip auf XKCD angezeigt: /html/body/div/div[2]/div/div[2]/div/div/img In Mechanize kann so der IMG-Tag extrahiert werden:

page = agent.get 'http://xkcd.com/'
img = page.search '/html/body/div/div[2]/div/div[2]/div/div/img'
puts img
# Ausgabe: < img src="http://imgs.xkcd.com/comics/designated_drivers.png" title="Calling a cab means cutting into beer money." alt="Designated Drivers" >

Das wars damit vorerst. Anmerkungen oder Fragen zu den Beispielen oder Ideen für weitere, bitte in den Kommentaren hinterlassen.

7 thoughts on “Web scraping mit Ruby/Mechanize

  1. Pingback: Content Extraction Algorithmen: Density/CCB « sixserv blog

  2. Hey!

    Danke für den super Artikel. Ich wühl mich grad durch die Doku, und muss sagen, die ist leider für mich nciht all zu aufschlußreich, aber dein Artikel bereinigt da die ein, oder andere Unklarheit!

    Danke.

    Gruß,
    Nils

  3. Super Beitrag!
    Aber ich hätte da noch eine Frage: Ist es möglich ein Element anhand seiner styleclass oder seiner id zu holen?
    Also dass ich z.B. alle links einer Seite mit der selbe styleclass bekomme, bzw das Element mit der einen bestimmten id?

    Da das, was ich im Moment brauche alles in einem div eingeschlossen ist konnte ich es mit page.search ‘/html/body/….’ machen. Aber schöner wäre es anders.

  4. @RiPr: Mithilfe von X-Path Ausdrücken ist so gut wie alles möglich. Hier gibt es eine Einführung: http://www.w3schools.com/Xpath/ Beispiele:

    1
    2
    3
    4
    5
    6
    
    page.search "//a[@class='eine_klasse']" # alle Link-Elemente mit der Klasse "eine_klasse"
     
    page.search "//*[@id='eindeutige_id']" # alle Elemente mit der ID "eindeutige_id"
     
    # (das geht auch abgekürzt über die funktion id:)
    page.search "id('eindeutige_id')"
  5. Hallo!
    Für die, die es interessiert:
    So kann man sich ebenfalls ein Element über seine ID holen:

    target_form = (page/:form).find { |elem| elem['id'] == ‘id_name’ }

    Eine weitere Frage hätte ich jedoch noch:
    Wie kann ich einen Radiobutton checken?

  6. @RiPr:

    Für Radiobuttons gibt es die Methoden check/uncheck bzw. click (als toggle). Das wird aber auch im GUIDE erwähnt (unter Advanced Form Techniques).

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>