Most recent comments
2021 in Books -- a Miscellany
Are, 2 years, 11 months
Moldejazz 2018
Camilla, 5 years, 4 months
Romjulen 2018
Camilla, 5 years, 11 months
Liveblogg nyttårsaften 2017
Tor, 6 years, 11 months
Selvbygger
Camilla, 1 month, 4 weeks
Bekjempelse av skadedyr II
Camilla, 11 months
Kort hår
Tor, 3 years, 11 months
Ravelry
Camilla, 3 years, 6 months
Melody Gardot
Camilla, 5 years, 5 months
Den årlige påske-kommentaren
Tor, 5 years, 8 months
50 book challenge
Camilla, 11 months, 3 weeks
Ten years ago
Nissebading
Tor
Controls
Register
Archive
+ 2004
+ 2005
+ 2006
+ 2007
+ 2008
+ 2009
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ 2020
+ 2021
+ 2022
+ 2023
+ 2024

Datahøsting

Etter å ha lest litt om en debatt som går i Storbritannia om programmering i skolen, har jeg følt meg sånn halvveis inspirert til å skrive en sterk kronikk til Morgenbladet, der jeg planlegger å argumentere for at langt flere burde lære seg å programmere på et tidligere tidspunkt. Dessverre sliter jeg litt med å komme på overbevisende grunner, men jeg tror jeg har i alle fall to. For det første, programmering er en nyttig intellektuell øvelse, der man lærer seg å skrive oppskrifter på å løse problemer. Bra for karakteren, eller noe. Og for det andre, litt enkelt programmering kan sette deg i stand til å sjekke ting du ellers ikke ville kunne sjekket, fordi det ville tatt for lang tid. Dermed slipper du i noen tilfeller å stole på at statistikken noen vifter i ansiktet ditt stemmer, ettersom du bare kan sjekke selv.

Nå kan jeg ikke på stående fot komme på et godt eksempel på dette siste, men jeg tenkte likevel å gå gjennom noen verktøy man kan bruke for å grafse til seg store mengder data fra internett. Som eksempel skal jeg laste ned prisen på alle rødvinene på polet. Grunnen til at jeg bruker dette som eksempel er at vi begynte å snakke om det på jobb i dag. Det er en dårlig skjult hemmelighet at polet av og til setter ned prisen på noen av produktene sine uten å si fra til noen, og da kan det være gode sjanser til å gjøre et lite kupp. Du må imidlertid få med deg at prisene endres, og det er ikke alltid så lett, siden polet ikke akkurat deler ut en tilbudsavis.

En måte man kan gjøre dette på er å kjøre en cronjobb som laster ned alle prisene, og sammenligner med tidligere data. En annen, og mye enklere måte, er å gå hit, der noen andre allerede har gjort dette for deg. Så selv om dagens artikkel i prinsippet vil sette deg i stand til å laste ned prisene selv er det ingen grunn til å gjøre dette.

Mitt foretrukne verktøy til datahøsting er naturligvis Python. Python har som kjent uendelig mange fordeler, men de to viktigste i dette tilfellet er at det er lett å teste kode på kommandolinjen, og at det finnes et bredt utvalg av biblioteker. Vi skal bruke bibliotekene urllib2 og BeautifulSoup til å laste ned og håndtere html. I tillegg trenger vi re og codecs for å gjøre noen triks. Så la oss begynne med å importere disse, pluss definere et par konstanter.
import re
import codecs
import urllib2
import BeautifulSoup

datafile = codecs.open('prisliste.txt', 'w', encoding = 'utf-8')
base_url = 'http://www.vinmonopolet.no/vareutvalg/sok?query=*&sort=2&sortMode=0&page={page}&filterIds=25&filterValues=R%C3%B8dvin'

Det vi gjør her er altså først at vi importerer de nevnte bibliotekene, hvorpå vi åpner filen vi skal skrive dataene til, og lager en variabel som heter base_url. Dette er essensielt adressen til den første av 169 sider i polets prisliste for rødvin, med den forskjellen at page=0 er byttet ut med page={page}. Dette skal vi komme tilbake til senere.

Det neste jeg har gjort er å definere en klasse som heter Product. Dette er bare for å gjøre livet litt enklere når det kommer til å lagre dataene. Ta en titt på artikkelen min om objektorientert programmering om du ikke husker hva en klasse er. Den er ikke spesielt grundig, så om du virkelig er nysgjerrig anbefaler jeg å lese litt rundt på nettet, men den forklarer et par grunnleggende ting.
class Product:
    def save(self, datafile):
        datafile.write('%s\t%s\t%s\n' % (self.id, self.price, self.name))

Poenget med denne klassen er å lage et objekt som har en metode som heter save(), som lagrer egenskapene id, price og name til datafilen vi opprettet. Mer om dette også senere.

Hensikten med biblioteket BeautifulSoup er å lese en nettside og opprette noe som kalles et tre, der alle html-taggene er ordnet i en hierarisk struktur som gjør det lett å navigere og hente ut informasjon. Jeg definerer en hendig liten funksjon som tar inn en url, og returnerer et slikt tre, som av en eller annen grunn tradisjonelt kalles soup:
def return_soup(url):
    data = urllib2.urlopen(url)
    soup = BeautifulSoup.BeautifulSoup(data)
    return soup

Denne funksjonen bruker altså urllib2.urlopen til å laste ned en nettside, som så gis videre til BeautifulSoup.BeautifulSoup, som lager suppe av den. Akkurat disse tingene kan være greit å teste på kommandolinjen, for når vi skal gå videre til å prøve å lese ut data om ulike rødviner fra htmlen vi laster ned blir det fort litt prøving og feiling. BeautifulSoup er forresten ikke en del av standard Python, men du kan installere det ved å åpne en terminal og skrive easy_install BeautifulSoup. Dette installerer versjon 3, som er den samme som jeg bruker her. Versjon 4 kom nettopp, men der er syntaxen litt annerledes, og jeg har ikke giddet å bytte ennå.

Det neste vi skal gjøre er å lage suppe av første side av polets liste over rødviner, for deretter å hekle ut prisen på de 30 vinene som vises på denne siden. Denne funksjonen tar seg av det:
def parse_hits(soup):
    product_list = soup.findAll('tr', {'class' : re.compile('[odd|even]')})
    product = Product()
    for item in product_list:
       product.name = item.a.text
       product.id = int(re.findall('\((\d*)\)', item.p.text)[0])
       product.price = float(re.findall('Kr\.\ ([0-9\.,-]*)\r\n', item.find('td', {'class' : 'price'}).text)[0].replace('.', '').replace(',', '.').strip('-'))
       product.save(datafile)

Her kan vi benytte oss av at Vinmonopolet har vært så greie at de har hyret en høvelig kompetent webdesigner, som ser ut til å ha skrevet noenlunde korrekt og velformet html. Ved å kikke litt i kildekoden ser vi fort at alle produktene er samlet i en tabell, og at hvert produkt ligger i en <tr>, med class="odd" eller class="even". Ved hjelp av en er frekk liten regex henter vi ut alle disse taggene, og putter dem i product_list, som vi så kan loope over. Først oppretter vi imidlertid et objekt av klassen Product, som vi skal bruke til å holde og lagre data.

Fra hver av <tr>-taggene vi hentet ut skal vi finne frem et navn, et id-nummer og en pris, og her skal vi benytte oss tungt av den tidligere nevnte tre-strukturen i suppen vi lagde tidligere. Når vi går gjennom for-løkken inneholder objektet item etter tur hver av <tr>-taggene, med alt innhold. Ved å ta en kikk i kildekoden igjen, oppdager vi at navnet på hvert produkt alltid finnes i den første <a>-taggen i <tr>-taggen, med navnet som link-tekst. Vi får tak i navnet ved å skrive item.a.text. Spesifikt er item.a den første <a>-taggen, og item.a.text gir teksten inni. Navnet lagrer vi som egenskapen name i Product-objektet vi lagde tidligere.

Det neste er å finne id-nummeret. Igjen, ved å kikke i kilden finner vi at dette nummeret alltid står inni en parantes, i den første <p>-taggen i <tr>-taggen. Den småfrekke regexen '\((\d*)\)' finner vilkårlig mange tall inni en parantes (\( og \)), og bruker paranteser (( og )) til å hente ut tallene. Funksjonen re.findall() returnerer en liste med treff, selv om det bare er ett, og for å få ut det første (og i dette tilfellet eneste) elementet i listen bruker vi [0], siden Python praktiserer 0-indeksering. Til slutt bruker vi funksjonen int() for å gjøre om tallet fra en tekststreng som tilfeldigvis bare inneholder tall, til et faktisk tall. Det spiller ikke så stor rolle i dette tilfellet, men hvis vi for eksempel skulle lagre dataene i en database i stedet for en tekstfil hadde det vært et poeng. I alle tilfelle vil koden krasje og gi lyd fra seg hvis den får tilsendt noe som ikke lar seg konvertere til et tall, og det er jo greit, for i såfall har vi gjort noe feil.

Til slutt gjelder det å hekle ut prisen, som jo var hele poenget med denne øvelsen. Nok en gang kan vi benytte oss av at polet har hyret webutviklere som vet hvor David kjøpte øllet (ordspill tilsiktet). Prisen finnes nemlig i en <td>-tag med class="price". item.find('td', {'class' : 'price'}).text henter ut teksten som står inni denne taggen. Når vi kun ser på teksten er prisen oppgitt på formen Kr. 1.033,30Kr. 1.377,70\r\npr. liter, der \r\n betyr linjeskift på Windows-språk (historisk var \r en carriage return, altså en kommando til skriveren som sa at vognen med hodet skulle gå tilbake til utgangsposisjonen, mens \n var newline, altså at arket skulle flyttes en linje. På fornuftige operativsystemer bruker man i dag bare \n for linjeskift). Vi må imidlertid være oppmerksomme på at prisene skrives med punktum for hvert tredje siffer, komma som desimaltegn, og med bindestrek etter kommaet hvis prisen tilfeldigvis er et helt antall kroner. Regexen 'Kr\.\ ([0-9\.,-]*)\r\n' tar seg av dette. Så ønsker vi å gjøre om dette tallet til et flyttall, ikke bare en tekststreng som ser ut som et tall, og til det bruker vi float(). Først må vi imidlertid fjerne alle punktum, konvertere komma til punktum (det er punktum som er desimaltegn på engelsk) og i tillegg fjerne den tullete streken som dukker opp på slutten av noen priser.

Når alt dette er i boks bruker vi save()-metoden på product-objektet til å lagre dataene, og gjentar så prosessen for neste produkt på denne siden, til vi har vært gjennom alle.

Da er grovarbeidet unnagjort, og det som gjenstår er å sette sammen disse funksjonene og automatisere prosessen med å gå gjennom alle 169 sidene med rødviner. Det gjør vi slik:
def main():
    for x in xrange(1, 175):
       print 'Downloading page', x
       url = base_url.format(page = x)
       soup = return_soup(url)
       parse_hits(soup)

Her looper vi altså fra 1 til 175, for å få med oss alle 169 sidene med rødviner. Dette er ikke spesielt robust, siden det vil slutte å få med seg alle hvis polet skulle finne på å lansere et par hundre ekstra viner, men jeg gidder ærlig talt ikke å skrive kode som finner ut hvor høyt den må loope. De siste 5 siden som ikke har noen viner blir lastet ned, men koden finner ingen av de nødvendige taggene, så det skjer ingenting med dem. Først skriver vi ut hvilken side vi er på, siden det er veldig kjedelig å stirre på en svart skjerm uten noen idé om hvor langt programmet har kommet. Deretter konstruerer vi den første urlen, som består av å ta base_url som vi definerte tidligere, og erstatte {page} med det tallet vi har kommet til. Så laster vi ned og leser ut data fra denne urlen, og alt er bare velstand.

Helt til slutt må vi faktisk kalle funksjonen main(), og det gjør vi slik:
if __name__ == '__main__':
    main()

Hvis vi kjører dette programmet fra kommandolinjen vil navnet være __main__, og i såfall kjører funksjonen. Hvis vi derimot skulle ønske å importere noen av funksjonene vi har definert her til et annet program vil ikke navnet være __main__, og funksjonen vil ikke kjøre. Akkurat slik vi liker det.

Putt alt dette i en fil og lagre det, eventuelt last ned programmet herfra, og du er klar til å skaffe deg din egen kopi av polets prisliste for rødviner som ren tekst. Yiha!

-Tor Nordam
Are likes this

Comments

...

Camilla,  07.05.12 23:42

"Poenget med denne klassen er å lage et objekt som har en metode som heter save"?


Kommer det en artikkel til som forklarer hvordan man skriver et program som sammenligner resultatene over tid? Og kan jeg få det i et hendig brukergrensesnitt?