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
Comments