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 BeautifulSoupdatafile = 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 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))
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
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)
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 (&backslash;( og &backslash;)), 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&backslash;r&backslash;npr. liter, der &backslash;r&backslash;n betyr 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&backslash;.&backslash; ([0-9&backslash;.,-]*)&backslash;r&backslash;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)
Helt til slutt må vi faktisk kalle funksjonen main(), og det gjør vi slik:
if __name__ == '__main__':
main()
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