Zanurkuj w Pythonie/Obsługa skompresowanych danych

Z Wikibooks, biblioteki wolnych podręczników.

Obsługa skompresowanych danych[edytuj]

Ostatnią ważną właściwością HTTP, którą będziemy chcieli obsłużyć, będzie kompresja. Wiele serwisów sieciowych posiada zdolność wysyłania skompresowanych danych, dzięki czemu wielkość wysyłanych danych może zmaleć nawet o 60% lub więcej. Sprawdza się to w szczególności w XML-owych serwisach sieciowych, ponieważ dane XML kompresują się bardzo dobrze.

Serwery nie dadzą nam skompresowanych danych, jeśli im nie powiemy, że potrafimy je obsłużyć.

Przykład 11.14. Informowanie serwera, że chcielibyśmy otrzymać skompresowane dane

>>> import urllib2
>>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml')
>>> request.add_header('Accept-encoding', 'gzip')        #(1)
>>> opener = urllib2.build_opener()
>>> opener.handle_open['http'][0]._debuglevel=1
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
Accept-encoding: gzip                                    #(2)
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:24:39 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Vary: Accept-Encoding
header: Content-Encoding: gzip                           #(3)
header: Content-Length: 6289                             #(4)
header: Connection: close
header: Content-Type: application/atom+xml
  1. To jest najważniejsza część: kiedy utworzymy obiekt Request, dodajemy nagłówek Accept-encoding, aby powiedzieć serwerowi, że akceptujemy dane zakodowane jako gzip. gzip jest nazwą wykorzystanego algorytmu kompresji. Teoretycznie powinny być dostępne inne algorytmy kompresji, ale gzip jest algorytmem kompresji wykorzystywanym przez 99% serwisów sieciowych.
  2. W tym miejscu nagłówek idzie przez linie.
  3. I w tym miejscu otrzymujemy informacje o tym, co serwer przesyła nam z powrotem: nagłówek Content-Encoding: gzip oznacza, że dane które otrzymaliśmy zostały skompresowane jako gzip.
  4. Nagłówek Content-Length oznacza długość danych skompresowanych, a nie zdekompresowanych. Jak zobaczymy za minutkę, rzeczywista wielkość zdekompresowanych danych wynosi 15955, zatem dane zostały skompresowane o ponad 60%.

Przykład 11.15. Dekompresowanie danych

>>> compresseddata = f.read()                              #(1)
>>> len(compresseddata)
6289
>>> import StringIO
>>> compressedstream = StringIO.StringIO(compresseddata)   #(2)
>>> import gzip
>>> gzipper = gzip.GzipFile(fileobj=compressedstream)      #(3)
>>> data = gzipper.read()                                  #(4)
>>> print data                                             #(5)
<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
  xmlns="http://purl.org/atom/ns#"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xml:lang="en">
  <title mode="escaped">dive into mark</title>
  <link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
  <-- rest of feed omitted for brevity -->
>>> len(data)
15955
  1. Kontynuując z poprzedniego przykładu, f jest obiektem plikopodobnym zwróconym przez otwieracz URL-a. Korzystając z jego metody read() zazwyczaj dostaniemy nieskompresowane dane, ale ponieważ te dane będą skompresowane gzip-em, to jest dopiero pierwszy krok, aby otrzymać dane, które naprawdę chcemy.
  2. OK, ten krok jest troszeczkę okrężny. Python posiada moduł gzip, który czyta (i właściwie także zapisuje) pliki skompresowane jako gzip. Jednak my nie mamy pliku na dysku, mamy skompresowany bufor w pamięci, a nie chcemy tworzyć tymczasowego pliku, aby te dane dekompresować. Zatem tworzymy obiekt plikopodobny przechowujący w pamięci skompresowane dane (compresseddata) korzystając z modułu StringIO. Pierwszy raz wspomnieliśmy o StringIO w poprzednim rozdziale, ale teraz znaleźliśmy kolejny sposób, aby go wykorzystać.
  3. Teraz tworzymy instancję klasy GzipFile. Ten "plik" jest obiektem plikopodobnym compressedstream.
  4. To jest linia, która wykonuje całą właściwą pracę: "czyta" z GzipFile zdekompresowane dane. Ten "plik" nie jest prawdziwym plikiem na dysku. gzipper w rzeczywistości "czyta" z obiektu plikopodobnego, który stworzyliśmy za pomocą StringIO, aby opakować skompresowane dane znajdujące się tylko w pamięci w zmiennej compresseddata w obiekt plikopodobny. Ale skąd przyszły te skompresowane dane? Oryginalnie pobraliśmy je z odległego serwera HTTP dzięki "odczytaniu" obiektu plikopodobnego, który utworzyliśmy za pomocą urllib2.build_opener. I fantastycznie, to wszystko po prostu działa. Żaden element w tym łańcuchu nie ma pojęcia, że jego poprzednik tylko udaje, że jest tym za co się podaje.
  5. Zobaczmy, są to prawdziwe dane. (tak naprawdę 15955 bajtów)

"Lecz czekaj!" - usłyszałem Twój krzyk, "Można to nawet zrobić prościej". Myślisz, że skoro opener.open zwraca obiekt plikopodobny, więc dlaczego nie wyciąć pośrednika StringIO i po prostu przekazać f bezpośrednio do GzipFile? OK, może tak nie myślałeś, ale tak czy inaczej nie przejmuj się tym, ponieważ to nie działa.

Przykład 11.16. Dekompresowanie danych bezpośrednio z serwera

>>> f = opener.open(request)                  #(1)
>>> f.headers.get('Content-Encoding')         #(2)
'gzip'
>>> data = gzip.GzipFile(fileobj=f).read()    #(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "c:\python23\lib\gzip.py", line 217, in read
    self._read(readsize)
  File "c:\python23\lib\gzip.py", line 252, in _read
    pos = self.fileobj.tell()   # Save current position
AttributeError: addinfourl instance has no attribute 'tell'
  1. Kontynuując z poprzedniego przykładu, mamy już obiekt żądania Request z ustawionym nagłówkiem Accept-encoding: gzip header.
  2. Zaraz po otworzeniu żądania otrzymujemy nagłówki z serwera (ale nie pobieramy jeszcze żadnych danych). Jak możemy zobaczyć ze zwróconego nagłówka Content-Encoding, dane te zostały skompresowane na gzip.
  3. Ponieważ opener.open zwraca obiekt plikopodobny, a z dopiero co odczytanego nagłówka wynika, że otrzymane dane będą skompresowane na gzip-a, to dlaczego nie przekazać prosto otrzymany obiekt plikopodobny bezpośrednio do GzipFile? Kiedy "czytamy" z instancji GzipFile-a, będziemy "czytali" skompresowane dane z serwera HTTP i dekompresowali je w locie. Jest to dobry pomysł, ale niestety nie działa. GzipFile potrzebuje zapisywać swoją pozycję i przesuwać się bliżej lub dalej po skompresowanym pliku, ponieważ w taki sposób działa kompresja gzip. Nie działa to, kiedy "plikiem" jest strumień bajtów przychodzących z zewnętrznego serwera; jedynie co możemy z tym zrobić, to jednorazowo pobierać dane bajty nie przesuwając się wstecz lub do przodu w strumieniu danych. Zatem nieelegancki sposób z wykorzystaniem StringIO jest najlepszym rozwiązaniem: pobieramy skompresowane dane, tworzymy z nich obiekt plikopodobny za pomocą StringIO, a następnie dekompresujemy dane wewnątrz niego.