requests vs. urllib

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

ich habe mal etwas rumgespielt, und versucht eine Datei herunterzuladen. Dazu habe ich mich an requests und urllib bedient. Bei meiner Recherche bin ich hin und wieder darauf gestoßen, dass man von der urllib abrät. Grund sei, dass diese Bibliothek zu unsauber programmiert worden ist, und das bei wenig Aufwand zu viel im Hintergrund gearbeitet wird.

Nun, ich habe mir dann zwei Versionen erstellt, und hätte dann eure Meinung, bzw. eure Kritik, wo ich was falsch gemacht habe. Beide Varianten scheinen zu funktionieren. Dateien liegen nach dem Herunterladen vollständig auf meinem Rechner.

Variante: urllib

Code: Alles auswählen

import os
import shutil
import urllib

def download_and_save_urllib():
    #download file from url
    location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
    url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'
    testfile = urllib.URLopener()
    
    #save file in folder (location)
    testfile.retrieve(url, location)

download_and_save_urllib()
Die urllib-Variante ist hier recht übersichtlich. Um zu überprüfen, ob ich es wirklich verstanden habe:
Zeile 7: In die lokale Variable location wird der zusammengesetzte Pfad gespeichert.
Zeile 8: In die lojkale Variable url wird die URL gespeichert.
Zeile 9: In die lokale Variable file wird die URLopener()-Methode gespeichert
Zeile 12: Über die retrieve()-Methode wird heruntergeladene Datei (url) in einen entsprechenden Ort (location) gespeichert.
Zeile 14: Aufruf der download_and_save_urllib-Funktion

Variante: requests

Code: Alles auswählen

import os
import requests

def download_and_save_requests():
    #download file from url
    location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
    url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'
    file = requests.get(url, stream=True)
    chunk_size = "50"

    #save file in folder (location)
    with open(location, 'wb') as fd:
        for chunk in file.iter_content(chunk_size):
            fd.write(chunk)

download_and_save_requests()
Zeile 8: Über requests.get()-Methode wird die url geholt. Stream wurde hier auf True gesetzt, falls mal die Dateien "streamen" will. Ist besonders wichtig, wenn die Dateien richtig groß werden. Ab 1 GB wird es richtig problematisch, da ohne "Stream" die Datei komplett direkt in den Speicher geladen wird. Und da kann es richtig eng werden.
Zeile 9: Warum ich hier eine "chunk"-Größe angeben muss, habe ich nicht ganz verstanden. Ich habe mich hier an die folgende Dokumentation (http://docs.python-requests.org/en/v0.10.6/api/) orientiert, im Absatz "Raw Response Content".
Zeile 12: Mit einer with-Anweisung wird die Datei geöffnet, und zwar schreibend im Binär-Modus.
Zeile 13: Da ich Stream auf True gesetzt habe, so wird hier quasi Response.raw durch die For-Schleife ersetzt. Dadurch wird der Inhalt abgerufen.
Zeile 14: Anshcließend wird die Datei geschrieben und hinterher geschlossen
Zeile 16: Aufruf der download_and_save_requests()-Funktion.

Mein Fazit:
Wenn ich mir beide Varianten anschaue, so ist die urllib um einiges kürzer beim Programmieren, oder aber, ich habe einiges nicht berücksichtigt, was ja kein Zufall bei mir ist :-)

Jetzt überlasse ich euch das Feld. Tobt euch was. Was habe ich vergessen, was ist besser? Wie könnte man meine Varianten verbessern. Ich bin über jede Information erfreut.
BlackJack

@Sophus: Eine Chunkgrösse muss man angeben weil man ja irgendwie sagen muss wie gross man die Einzelteile gerne hätte. Wobei man da eine *Zahl* angeben muss und keine *Zeichenkette*. Man muss das auch nicht an einen Namen binden. Und vielleicht doch ein bisschen mehr als 50 Bytes. Sooo knapp wird der Speicher doch hoffentlich nicht sein.

Anstelle der ``for``-Schleife hätte man den Iterator der `writelines()`-Methode des Dateiobjekts übergeben können.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

BlackJack hat geschrieben: Anstelle der ``for``-Schleife hätte man den Iterator der `writelines()`-Methode des Dateiobjekts übergeben können.
Ich habe den Satz syntaktisch nicht verstanden. In meiner For-Schleife werden die Elemente iteriert und bei jeder Interation geschrieben. Was gleich eine nächste Frage aufwirft. Wieso muss hier geschrieben werden? Es handelt sich hierbei eine angefertigte zip-Datei. Warum muss die Datei geschrieben werden? Die Philosophie habe ich nicht ganz verstanden. Und zu den chunk-Größen. ich glaube bis zu 1024 Bytes kann man gehen oder?
BlackJack

@Sophus: Wieso sollte nicht geschrieben werden müssen? Irgendwie müssen die Daten von dem Webserver doch lokal bei Dir in eine Datei kommen. Und Daten kommen dadurch in Dateien in dem man sie hinein schreibt.

Man kann auch deutlich weiter als 1024 gehen. `urllib.URLopener.retrieve()` versucht 8 KiB grosse Datenhäppchen zu kopieren.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich habe ein kleines Problem. Es ist ja möglich und nicht selten, dass Dateien, die heruntergeladen werden, CRC-Fehler aufweisen. Vor diesem Hintergrund versuche ich eine Ausnahme-Behandlung einzuführen, jedoch bin ich ein wenig über die Fehlermeldung verwirrt, die ich euch am Ende zeige. Zunächst meinen Quelltext:

Code: Alles auswählen

import requests
import os

def crc_check_file(file_name):
    the_zip_file = zipfile.ZipFile(file_name, 'r')
    try:
        print "Check the downloaded zipfile"
        ret = the_zip_file.testzip()
        print "Zip file es good."
    except (zipfile.BadZipfile, zipfile.LargeZipFile), e:
        print e

    the_zip_file.close()
    print "Zipf-file was closed"

def download_and_save_requests():
    location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
    url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'
    file = requests.get(url, stream=True)
    
    file_size = int(requests.head(url).headers.get('content-length', None))
    result = float(int(file_size)/(1024.0*1024.0))
    print "Size: %s MB" %round(result, 2)

    print "Now saving the file"
    
    chunk_size = (10000)
    downloaded_bytes = 0

    with open(location, 'wb') as fd:
        for chunk in file.iter_content(chunk_size):
            fd.write(chunk)
            downloaded_bytes += chunk_size
            result = float(int(downloaded_bytes)/(1024.0*1024.0))

            print "%s MB" %round(result, 2)
    print "Finish"
    crc_check_file(location)
    
download_and_save_requests()
Wir sehen hier zwei Funktionen. In der download_and_save_requests()-Funktion wird die zip-Datei vom Webserver heruntergeladen, und anschließend wird der crc_check_file()-Funktion ein Argument namens location übergeben. Schließlich brauche ich den Pfad zu der Datei, die in der crc_check_file()-Funktion auf mögliche Fehler überprüft werden soll. Ich versuche im Try-Execption die Datei mit Hilfe der testzip()-Methode zu überprüfen. Bei mögliche Fehler wie BadZipfile oder LargeZipFile soll eine Ausnahme behandelt werden. Und nun kommt die Fehlermeldung:
Traceback (most recent call last):
File "D:\Dan\Python\Übung\unraren\test.py", line 129, in <module>
download_and_save_requests()
File "D:\Dan\Python\Übung\unraren\test.py", line 127, in download_and_save_requests
crc_check_file(location)
File "D:\Dan\Python\Übung\unraren\test.py", line 55, in crc_check_file
the_zip_file = zipfile.ZipFile(file_name, 'r')
File "C:\Python27\lib\zipfile.py", line 766, in __init__
self._RealGetContents()
File "C:\Python27\lib\zipfile.py", line 807, in _RealGetContents
raise BadZipfile, "File is not a zip file"
BadZipfile: File is not a zip file
Was mich hier ein wenig wundert, ist, dass ich in der Exception doch BadZipfile angegeben habe. Müsste da nicht meine Print-Anweisung ausgegeben werden? Wieso greift meine Try-Exception in diesem Falle nicht?
BlackJack

@Sophus: Warum sollte das ``try``/``except`` greifen? Dir ist klar in welcher Zeile die Ausnahme ausgelöst wird? Und in welchen Zeilen die Ausnahme ausgelöst werden dürfte damit der ``except``-Zweig ausgeführt würde?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Peinlich peinlich peinlich.

Code: Alles auswählen

def crc_check_file(file_name):
    try:
        the_zip_file = zipfile.ZipFile(file_name, 'r')
        print "Check the downloaded zipfile"
        ret = the_zip_file.testzip()
        print "Zip file es good."
    except (zipfile.BadZipfile, zipfile.LargeZipFile), e:
        print e
So habe ich das gelöst. So wird die Zip-Datei auch im Try-Block gelesen. Frage hier, kann man in diesem Falle die Zip-Datei auch mit der with-Anweisung öffnen? Denn hierbei muss ich mich immer um das Schließen der Datei kümmern. Richtig?
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: ZipFile unterstützt nicht direkt das Context-Manager-Interface, aber mit contextlib.closing kann man jede Klasse, die die close-Methode implementiert, mit with benutzen.
BlackJack

@Sophus: ``with`` hättest Du einfach ausprobieren können — ja das geht.

Die Funktion hat allerdings noch einen Fehler: Wenn bei einer Datei in dem Archiv etwas nicht stimmt wird von `testzip()` der Name dieser Datei zurückgegeben. Das heisst bevor Du ausgibst das das Archiv in Ordnung ist, solltest Du prüfen dass der Rückgabewert von `testzip()` auch tatsächlich `None` ist.

@Sirius3: Also bei Python 2.7 gibt es `__enter__()` und `__exit__()` bei `ZipFile`.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo BlackJack und Sirius3

Zunächst meinen vorerst abgewandelten Quelltext, jedoch ungetestet, da ich gerade in der Uni sitze und keinen Python-Interpreter auf dem Rechner habe, sondern nur Notepad++.

Code: Alles auswählen

def crc_check_file(file_name):
    try:
        the_zip_file = zipfile.ZipFile(file_name, 'r')
        print "Check the downloaded zipfile"
        bad_file = the_zip_file.testzip()
		if bad_file not None:
			print "The file %s is corrupted" %bad_file
		else:
			print "Zip file is good."
    except (zipfile.BadZipfile, zipfile.LargeZipFile), e:
        print e
Ich benutze Python 2.7. Das heißt also, wenn ich BlackJack richtig interpretiert habe, dass ich mühelos mit der with-Anweisung arbeiten kann, also im Kontext des ZipFile? Wenn ja, würde ich das wie folgt (ungetestet) bewerkstelligen:

Code: Alles auswählen

def crc_check_file(file_name):
    try:
        with zipfile.ZipFile('file_name', 'r') as the_zip_file:
        print "Check the downloaded zipfile"
        ret = the_zip_file.testzip()
		if ret not None:
			print "The file %s is corrupted" %ret
		else:
			print "Zip file is good."
    except (zipfile.BadZipfile, zipfile.LargeZipFile), e:
        print e
Wenn aber Sirius3 Recht behält, so würde ich das (ebenfalls ungetestet) wie folgt umsetzen:

Code: Alles auswählen

import contextlib
	
def crc_check_file(file_name):
    try:
        with contextlib.closing(zipfile.ZipFile('file_name', 'r')) as the_zip_file:
        print "Check the downloaded zipfile"
        ret = the_zip_file.testzip()
		if ret not None:
			print "The file %s is corrupted" %ret
		else:
			print "Zip file is good."
    except (zipfile.BadZipfile, zipfile.LargeZipFile), e:
        print e
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Bis auf die Einrückungsebene, ja. (Was natürlich *bedeutsam* ist!)

Wobei ich die Semantik der Funktion ändern würde, so dass diese einen booleschen Wert zurückliefert - evtl. will man nicht immer etwas printen, wenn alles ok ist, und zum anderen kann man die Funktion aktuell nicht zur weiteren Workflowsteuerung verwenden, eben weil nach außen nicht mehr erkennbar ist, ob das Zip File korrupt ist oder nicht.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Hyperion: Das Wort "Arbeitsfluss" scheint dir nicht zu gefallen, überhaupt, wenn etwas im Fluß ist? Aber lassen wir, heutzutage ist es ein Flow im Work :-P Aber zurück, zum "Workflow". Wird man nicht dazu angehaölten weitestgehend alles lokal in der Funktion abzuwickeln, und die Ergebnisse über Argumente weiter zu leiten? Konkret auf meinem Beispiel. "Wen" könnte es denn noch interessieren, ob die *.zip-Datei nun beschädigt ist oder nicht? Und das mit dem "printen" (hach, was vermisse ich die Formulierung 'Ausgabe über Print-Anweisung') dient hier nur zur Testzwecke. Im Bereich der GUI wäre eine Print-Anweisung natürlich sinnlos. Ich denke, das sollte ich mittlweile verstanden haben, auch wenn ich nicht immer den Eindruck erwecke :-)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Sophus hat geschrieben:@Hyperion: Das Wort "Arbeitsfluss" scheint dir nicht zu gefallen
In diesem Kontext: nein ;-)
Sophus hat geschrieben: Wird man nicht dazu angehaölten weitestgehend alles lokal in der Funktion abzuwickeln, und die Ergebnisse über Argumente weiter zu leiten?
Nein. Ergebnisse gibt man per ``return`` zurück! (Argumente sollte man allerdings als Parameter übergeben!)
Sophus hat geschrieben: Konkret auf meinem Beispiel. "Wen" könnte es denn noch interessieren, ob die *.zip-Datei nun beschädigt ist oder nicht?
K.A. Aber aktuell sehe ich den Nutzen nicht! Oder ist das der einzige Nutzen des Programms? (Wobei selbst dann würde ich die Logik zum Prüfen und die Ausgabe / Auswertung trennen)
Mal als Gegenfrage: Was nützt Dir diese Funktion? Du rufst sie auf, sie schreibt etwas, und dann?
Sophus hat geschrieben: Und das mit dem "printen" (hach, was vermisse ich die Formulierung 'Ausgabe über Print-Anweisung') dient hier nur zur Testzwecke. Im Bereich der GUI wäre eine Print-Anweisung natürlich sinnlos. Ich denke, das sollte ich mittlweile verstanden haben, auch wenn ich nicht immer den Eindruck erwecke :-)
Der Gedankenansatz ist ja schon mal gut! Aber wie würdest Du das denn in eine GUI integrieren? Mal angenommen man hat einfach einen Button, auf dem "Integrität prüfen" steht. Den drücke ich, um die Aufgabe zu erledigen. Wie genau würdest Du jetzt diese Funktion nutzen? (Ich meine nicht das ``print`` entfernen, sondern wie "ersetzt" du das?)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@Sophus: Na den Aufrufer der das Ergebnis das auf dem Terminal oder in einer GUI ausgibt interessiert das. Du sagst ja selbst das ``print`` bei Dir nur zu Testzwecken drin ist, wenn man das rausnimmt, dann wird die Funktion doch komplett nutzlos.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Nun, ich würde anstelle der Print-Anweisung eine QMessagebox setzen, damit der Anwender auch weiß, dass alles in Ordnung ist. Genauso, wenn Fehler auftauchen, dann wird der Anwender ebenfalls über die Fehler benachrichtigt.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@Sophus: Und was passiert danach? Ich meine die Prüfung ist ja kein Selbstzweck, sondern soll ja sicherlich mehr bewirken als das simple Anzeigen von Informationen! Wie sieht denn der Prozess "drum herum" aus?

(Im übrigen würdest Du damit dann auch wieder GUI und Logik vermischen - was an sich bereits ein indiz ist, dass Du ein nicht so tolles Vorgehen wählst)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ähm, die Datei soll im Anschluss entpackt werden. Logisch irgendwie, denn es handelt sich hierbei um eine *.zip-Datei. Und das die GUI vermischt wird, wäre ja nur eine Vermutung, denn wie es umgesetzt wird, sehen wir ja noch nicht. Nicht mal Ich habe es noch nicht umgesetzt.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Sophus hat geschrieben:Ähm, die Datei soll im Anschluss entpackt werden.
Und wenn die Datei korrupt ist :K Dann wird man ja in einen Fehler reinlaufen...
Sophus hat geschrieben: Und das die GUI vermischt wird, wäre ja nur eine Vermutung, denn wie es umgesetzt wird, sehen wir ja noch nicht. Nicht mal Ich habe es noch nicht umgesetzt.
Du hast ja gerade geschrieben, dass Du die ``print``-Anweisungen durch MessageBoxen ersetzen würdest - damit wäre die Logik offensichtlich mit GUI-Komponenten vermischt!
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Na ja, ich würde die Messagebox in eine weitere Funktion auslagern, und den Fehler in Form eines Argument mittel Parameter an die Funktion übergeben. So wäre da eine Trennung?
BlackJack

@Sophus: Die Aufrufrichtung ist aber falsch. Denn wenn die Programmlogik GUI-Funktionen aufruft, also die dafür hart im Code kodiert kennt, dann ist ja immer noch Programmlogik mit GUI vermischt.
Antworten