Python unter Linux: Netzwerk

Einfacher Zugriff auf Webressourcen

Bearbeiten

"Wie ist das Wetter auf Hawaii?", "Welches Stück wird gerade im Radio gespielt?" oder "Wie lautet die aktuelle Linux-Kernelversion?" sind typische Fragen, deren Antworten sich zumeist mit Hilfe eines Browsers finden lassen. Möchte man jedoch in einer konkreten Anwendung nicht immer den Browser aufrufen, helfen zumeist kleine Programme dabei, die benötigten Antworten automatisch zu finden. Die aktuelle Kernelversion zum Beispiel ist leicht aus einer Webseite herauszuextrahieren. Hierzu sind allerdings Kenntnisse vom aktuellen Aufbau der Seite unerlässlich.

#!/usr/bin/python
import urllib2, re

url = urllib2.urlopen('http://www.kernel.org')
html = url.read()
url.close()

match = re.search("""<table id="releases">.*?<td>(?P<kertext>.*?)</td>\s*<td><strong>(?P<kerver>.*?)</strong></td>""", html, re.S)
if match is not None:
    print match.group('kertext'), match.group('kerver')
else:
    print 'Der Seitenaufbau hat sich vermutlich zwischenzeitlich geaendert.'
  Ausgabe

user@localhost:~$ ./url1.py
mainline: 4.12-rc4

Die Funktion urllib2.urlopen() öffnet eine angegebene Webseite, die wir mit url.read() vollständig lesen können. In der Variablen html ist nun der gesamte Inhalt der Webseite gespeichert. Wir benötigen also den weiteren Netzwerkzugriff nicht mehr und schließen daher die Verbindung mit url.close(). Schaut man sich die Webseite auf http://www.kernel.org im Quellcode an, so stellt man fest, dass die für uns interessanten Informationen in einer Tabelle hinterlegt sind, die mit <table id="releases"> beginnt. Innerhalb der ersten beiden <td>-Zeilen befinden sich die Informationen, die wir mit dem regulären Ausdruck herauslösen.

Lesen bei WikiBooks

Bearbeiten

Etwas komplizierter ist es, auf Wikibooks-Inhalte zuzugreifen. Die MediaWiki-Software verlangt ein Mindestmaß an übertragenen Browser-Informationen und das Protokoll GET. Das nun folgende Programm benutzt daher einen Request, dem wir einige Header hinzufügen.

#!/usr/bin/python

import urllib2, re
import xml.sax.saxutils as util

header = {'User-agent' : 'PythonUnterLinux', 'Accept-Charset' : 'utf-8'}
request = urllib2.Request('http://de.wikibooks.org/w/index.php?title=Diskussion:Python_unter_Linux:_Netzwerk&action=edit', headers=header)

print "DEBUG: ", request.get_method()
url = urllib2.urlopen(request)
html = url.read()
url.close()

found = re.search("<textarea .*?>(?P<inhalt>.*?)</textarea>", html, re.S)
print util.unescape(found.group('inhalt'))
  Ausgabe

user@localhost:~$ ./url2.py
DEBUG: GET
<!-- Dient zum Testen der hier vorgestellten Skripte -->
TESTINHALT

Die HTTP Request Header geben den Namen unseres selbstgeschriebenen Browsers mit "PythonUnterLinux" an. Accept-Charset ist eine Information darüber, welche Zeichencodierung dieser Browser versteht. Diese Header werden als Argument unserem Request mit auf den Weg gegeben. Die Verbindung wird wieder mit urlopen() geöffnet, der Rest des Programmes gleicht dem aus dem ersten Beispiel.

Wir öffnen die Diskussionsseite dieser Seite, um die dort enthaltenen Informationen bequem aus einer HTML-Textbox extrahieren zu können. Mit dem Argument action=edit in der URL geben wir an, dass wir die Seite eigentlich zum Schreiben öffnen. Das machen wir, da so die interessanten Informationen leichter zu finden sind. Der Reguläre Ausdruck extrahiert aus dem <textarea>-Bereich der Webseite den Inhalt. Dieser Inhalt ist HTML-codiert, weswegen wir den String mit xml.sax.saxutils.unescape() wieder in eine natürlich-lesbare Zeichenkette umwandeln.

Auf WikiBooks schreiben

Bearbeiten

Um anonym[1] auf WB schreiben zu können, müssen zuerst einige Informationen von der Webseite geholt werden, auf der Sie schreiben möchten. Im HTML-Quellcode der Seite sind sie als <input... zu erkennen. Diese Daten müssen beim schreibenden Zugriff mitübergeben werden, es handelt sich in diesem Fall um ein POST-Request. Das folgende Programm fügt auf die Diskussionsseite dieses Kapitels einen kleinen Text hinzu, weitere Ausgabe erfolgt nicht.

#!/usr/bin/env python

import urllib, urllib2, re
import xml.sax.saxutils as util

# Lies die Seite
header = {'User-agent' : 'PythonUnterLinux', 'Accept-Charset' : 'utf-8'}
request = urllib2.Request('http://de.wikibooks.org/w/index.php?title=Diskussion:Python_unter_Linux:_Netzwerk&action=edit')
url = urllib2.urlopen(request)
html = url.read()
url.close()

# RegExp vorbereiten
s_str = "<form id=\"editform\" name=\"editform\" method=\"post\" action=\"(?P<actionurl>.*?)\".*?>"
s_str += ".*?<input type=\"text\" name=\"wpAntispam\" id=\"wpAntispam\" value=\"(?P<wpAntispamValue>)\" />"
s_str += ".*?<input type=\'hidden\' value=\"(?P<wpSectionValue>.*?)\" name=\"wpSection\" />"
s_str += ".*?<input type=\'hidden\' value=\"(?P<wpStarttimeValue>.*?)\" name=\"wpStarttime\" />"
s_str += ".*?<input type=\'hidden\' value=\"(?P<wpEdittimeValue>.*?)\" name=\"wpEdittime\" />"
s_str += ".*?<input type=\'hidden' value=\"(?P<wpScrolltopValue>.*?)\" name=\"wpScrolltop\" id=\"wpScrolltop\" />"
s_str += ".*?<textarea name=\"wpTextbox1\" id=\"wpTextbox1\" cols=\"80\" rows=\"25\" .*?>(?P<content>.*?)</textarea>"
s_str += ".*?<input tabindex=\'2\' type=\'text\' value=\"(?P<wpSummaryValue>.*?)\" name=\'wpSummary\' id=\'wpSummary\' maxlength=\'200\' size=\'60\' />"
s_str += ".*?<input name=\"wpAutoSummary\" type=\"hidden\" value=\"(?P<wpAutoSummaryValue>.*?)\" />"
s_str += ".*?<input type=\'hidden\' value=\"(?P<wpEditTokenValue>.{2})\" name=\"wpEditToken\" />"

# RegExp ausfuehren
found = re.search(s_str, html, re.S)

# Neuen Inhalt aufbauen
new_content = util.unescape(found.group("content")) + "\n===Neuer Inhalt===\ntest neues Skript"
summary = "Python unter Linux Browser"

# zu uebermittelnde Daten vorbereiten
data_dict = {"wpAntispam" : found.group("wpSectionValue"),
    "wpSection" : found.group("wpSectionValue"),
    "wpStarttime" : found.group("wpStarttimeValue"),
    "wpEdittime" : found.group("wpEdittimeValue"),
    "wpScrolltop" : found.group("wpScrolltopValue"),
    "wpTextbox1" : new_content,
    "wpSummary" : summary,
    "wpAutoSummary" : found.group("wpAutoSummaryValue"),
    "wpEditToken" : found.group("wpEditTokenValue")}

data = urllib.urlencode(data_dict)

# und abschicken...
request = urllib2.Request('http://de.wikibooks.org/w/index.php?title=Diskussion:Python_unter_Linux:_Netzwerk&action=edit')
url = urllib2.urlopen(request)
url.close()

Der erste Teil des Programmes ist dazu da, die Seite zu lesen. Der neue Inhalt, wie auch die Zusammenfassung der Änderung, werden in den Variablen new_content und summary vorbereitet. Auf der gelesenen Webseite befinden sich einige Daten wie wpSummary -Zusammenfassung der Änderung-, wpAutoSummary -Konstanter Wert, der zurückgegeben werden muss- und wpTextbox1, der den eigentlichen, zu übermittelnden Inhalt der Seite darstellt. Diese Felder müssen extrahiert und, gegebenenfalls modifiziert, zurückgeschickt werden. urllib.urlencode() erzeugt aus dem Dictionary einen String, der dem Request mitgegeben wird.

Anmelden mit Cookies

Bearbeiten

In manchen Foren und Wikis kann oder muss man sich anmelden, um Beiträge zu schreiben. Die Anmeldedaten werden häufig in einem Cookie gespeichert. So auch bei Wikibooks. Das folgende Beispiel zeigt eine Möglichkeit, sich bei Wikibooks anzumelden. Auf eine Testausgabe auf der Diskussionsseite haben wir diesmal verzichtet, den Code dazu kennen Sie schon aus dem vorherigen Beispiel. Es wird lediglich eine Bestätigung über den erfolgreichen Anmeldevorgang ausgegeben:

#!/usr/bin/python

import urllib, urllib2, re
import cookielib

USERNAME = "Testaccount007"
PASSWORD = "test123"

header = {'User-agent' : 'PythonUnterLinux', 'Accept-Charset' : 'utf-8'}
data_dict = {"wpName" : USERNAME, "wpPassword" : PASSWORD, "wpRemember" : "1"}
data = urllib.urlencode(data_dict)

cookieMonster = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookieMonster))
urllib2.install_opener(opener)
request = urllib2.Request('http://de.wikibooks.org/w/index.php?title=Spezial:Anmelden&action=submitlogin&type=login', data, header)
url = urllib2.urlopen(request)
html = url.read()
url.close()

res = re.search("<p>(?P<ret>.*?)</p>", html, re.S)
print res.group('ret')
  Ausgabe

user@localhost:~$ ./url4.py
Du bist jetzt als „Testaccount007“ bei Wikibooks angemeldet.

Auf der Anmeldeseite stehen im Formular unter anderem die Eingabezeilen, die mit wpName und wpPassword im HTML-Code benannt sind. Diese Variablen werden mittels urllib.urlencode() in Daten für den anstehenden POST-Request umgewandelt. Bei der Verwaltung von Cookies hilft uns die Klasse cookielib.CookieJar(). Mit ihrer Hilfe bauen wir einen Opener, den man sich als eine Art Schlüssel vorstellen kann. Mit opener.open() könnten wir die Webseite schon öffnen, jedoch haben wir es an der Stelle vorgezogen, den Opener global mit urllib2.install_opener() zu installieren.

Zeitserver

Bearbeiten

Selbstverständlich kann man mit Python nicht nur Browser schreiben, sondern ganz bequem auch Server. Wir implementieren hier einen Server, bei dem sich Clients anmelden müssen und zur Belohnung die Zeit dauerhaft angezeigt bekommen. Clients sind hier einfache Telnet-Sitzungen, die sie nach dem Starten des Servers in einem anderen Fenster öffnen können.

Unser Server wartet auf zwei Verbindungen. Die Clients müssen sich durch die Eingabe von HELO ausweisen. Sind zwei Clients akzeptiert worden, wird an beide die aktuelle Zeit geschickt.

#!/usr/bin/python

import socket, select, time

host = '127.0.0.1'
port = 6000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))

s.listen(2)

allConnected = False
allClients = []

# Clients verbinden
while not allConnected:
    clientsock, clientaddr = s.accept()
    print "Verbindungswunsch von", clientsock.getpeername(),
    data = clientsock.recv(4096)
    data.strip()
    if data.startswith("HELO"):
        allClients.append(clientsock)
        print "akzeptiert"
    else:
        clientsock.close()
        print "nicht akzeptiert"
    print "Wir haben ", len(allClients), "Clients"
    if len(allClients) == 2:
        allConnected = True

# Daten senden, empfangen
while True:
    time.sleep(2)
    listIn, listOut, listTmp = select.select(allClients, allClients, [], 1)
    for sock in listIn:
        data = sock.recv(4096)
        print "--\nAnkommende Daten:", len(data), "Zeichen. Inhalt: ", data
        if len(data) == 0:
            print "Client hat die Verbindung getrennt"
            sock.close()
            allClients.remove(sock)
            listIn.remove(sock)
            listOut.remove(sock)
    for sock in listOut:
        msg = time.strftime("%H:%M:%S\n")
        num = sock.send(msg)

Mit socket.socket(socket.AF_INET, socket.SOCK_STREAM) wird ein Socket erzeugt, eine Struktur, die eine Netzwerkverbindung repräsentiert. In diesem Fall soll es eine dauerhafte TCP/IP-Verbindung sein. Der Port, auf dem diese Verbindung lauscht, soll wiederverwertbar sein. Wenn der Server sich beendet, soll der Port wieder genutzt werden können. Dazu dient setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1). Anschließend binden wir den Port an die IP-Adresse und die Portnummer (bind((host, port))). Solange nicht alle Clients verbunden sind, werden Verbindungswünsche akzeptiert (accept()). Sendet der Client etwas, das mit HELO beginnt, so wird er aufgenommen, sonst abgelehnt.

Die folgende Schleife wird alle zwei Sekunden ausgeführt. select.select(allClients, allClients, [], 1) wartet höchstens eine Sekunde auf Clients, die zum Senden oder Empfangen bereit sind. Die Funktion gibt drei Listen zurück. Die ersten beiden Listen enthalten dabei diejenigen Sockets, die zum Senden oder zum Empfangen bereit sind. Sendet ein Socket eine leere Zeichenkette, dann bedeutet das, dass er sich beendet hat. Sonst wird an alle Clients die Zeit ausgegeben.

  Ausgabe

user@localhost:~$ ./server.py
Verbindungswunsch von ('127.0.0.1', 40510) akzeptiert
Wir haben 1 Clients
Verbindungswunsch von ('127.0.0.1', 40511) akzeptiert
Wir haben 2 Clients

Auf zwei anderen Konsolen (Hier wird HELO eingegeben):

  Ausgabe

user@localhost:~$ telnet localhost 6000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
HELO
15:46:20
...


Chatserver

Bearbeiten

Einen Server fürs Chatten zu schreiben ist nur wenig schwerer. Hierbei geht es darum, dass sich einige Clients während einer Chat-Sitzung verbinden, wieder abmelden und Daten von einem Client an alle anderen Clients übertragen werden. Folgendes Beispiel verdeutlicht das:

#!/usr/bin/python

import socket, select, time

host = '127.0.0.1'
port = 6000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(1)
allClients = []

# Daten senden, empfangen
while True:
    time.sleep(2)
    # Server socket hinzufuegen
    listIn, listOut, listTmp = select.select(allClients + [s], allClients, [], 1)
    for sock in listIn:
        if sock is s:
            clientsock, clientaddr = s.accept()
            print "+ Verbindungswunsch von", clientsock.getpeername(), "akzeptiert"
            allClients.append(clientsock)
        else:
            data = sock.recv(4096)
            if len(data) == 0 or data.startswith("quit"):
                print "- Client", clientsock.getpeername(), "hat die Verbindung getrennt"
                sock.close()
                allClients.remove(sock)
                listIn.remove(sock)
                listOut.remove(sock)
            else:
                # nicht an den Sender schreiben
                listOut.remove(sock)
                for sout in listOut:
                    sout.send(data)

Der Server läuft anfangs ohne Verbindung. Wenn ein Client sich verbinden möchte, so wird der Server-Socket (s) lesebereit. Voraussetzung dafür ist, dass wir im Aufruf von select() den Server als im Prinzip empfangsbereit einstufen.

Ein Client kann sich nun durch Eingabe von quit beenden. Falls der Client Daten sendet, werden diese Daten an alle anderen Clients ebenfalls verschickt, nicht jedoch an denjenigen, der gesendet hat. Dadurch soll ein weiteres Echo der Eingabe vermieden werden.

Die Ausgabe des Programms ist bei einer Telnet-Sitzung das, was man von einem Chat erwartet: Es werden Daten an alle Clients übertragen. Deswegen verzichten wir hier auf die Darstellung.

Zusammenfassung

Bearbeiten

Um spezialisierte Browser zu schreiben, benötigt man Wissen über die Webseite und Kenntnisse der benötigten Protokolle. Beides kann man zumeist aus den Quelltexten der Webseiten herauslesen. Zumeist sind nur wenige Zeilen Programmcode nötig, um eine Webseite komplett einzulesen, dafür viele Programmzeilen, die Daten aus dem HTML-Code zu extrahieren. Falls Sie die hier vorgestellten und gegebenenfalls modifizierten Programme auf Wikibooks zu mehr, als nur zu Testzwecken benutzen wollen, fragen Sie bitte die Administratoren um Erlaubnis. Python enthält ebenfalls eine vollständige Anbindung an Netzwerkprotokollen. Hiermit lassen sich Clients und Server schreiben.

Anmerkung

Bearbeiten
  1. So anonym sind Sie gar nicht. Auf WB werden alle Änderungen, die ein Nutzer oder eine IP macht protokoliert