Screenscrape av Øya-programmet

Forskningsprosjektet Sky & Scene, hvor jeg jobber, ser blant mye annet nærmere på strømmetallene fra WiMP før, under og etter Øya-festivalen. For å gjøre dette trenger vi en liste over hvilke artister som spiller, hvilken dag de spiller og når på dagen de spiller. Før dataene kan analyseres må disse dataene være tilgjengelige i Excel-ark og i CSV-format og i databasen hvor strømmetallene finnes. Dataene må hentes og struktureres i et bestemt format.

Et godt utgangspunkt er å samle dataene i et CSV-format. CSV står for Comma separated values, kommaseparerte verdier, og er en liste hvor verdiene er for en forekomst er samlet på en linje, og hvor forekomstens data-attributter, også kalt variabler, er separert med – you guessed it – komma. Et lignende format kan du finne i Excel hvor èn forekomst finnes på èn linje, og denne forekomstens variabler oppgis i kolonner.

Finne dataene

Ok, nok om formatering. Hvor kan vi finne dataene? Et naturlig utgangspunkt er festivalens hjemmesider. På oyafestivalen.com (den engelske hjemmesiden til festivalen) finner vi et menyvalg kalt “program“, og her finner vi også programmet.

developer_menu_oyafestival
Utviklerverktøyet til Chrome kan finnes i menyen. Dette er et veldig nyttig verktøy for både web-utvikling og screen scraping

For å screen scrape programmet hjelper det lite med den visuelle presentasjonen av siden og vi må derfor se på HTML kilden. I Google Chrome finner du denne ved å høyreklikke i web-vinduet for så å klikke på “vis sidekilde”, her kan vi finne HTML-koden. Eventuelt kan du kopiere denne lenken inn i din Chrome browser: “view-source:http://oyafestivalen.com/program/#all

Dersom du gikk inn i kildekoden vil du se at listen med artister mangler. Hvorfor? Jo, fordi listen er ganske lang og benyttes av flere kilder lastes ikke listen med programmet inn av selve program-siden. Den lastes inn asynkront med AJAX (Asynchronous Javascript and XML). Finn fram Chrome Developer Tools som finnes i menyen, og gå til Network fanen. Last siden igjen ved å klikke på sirkelen med pil til venstre for URL-feltet.

Her kan du se at en fil kalt getArtist.php er lastet (bilde 1), og at denne filen ikke lastes som en del av originalforespørselen vår til web-tjeneren, men istedet er lastet inn via Javascript. Dersom vi klikker for å se på hva denne URL-en leverer kan vi se at artistlisten kommer herifra. URLen til siden kan du finne ved å høyreklikke på navnet getArtist.php og så velge “copy link address”.

Når du har URLen (http://oyafestivalen.com/wp-content/themes/oya13_new/includes/ajax/program/getArtists.php) kan du kopiere denne inn i nettleser vinduet ditt. Du skal nå få en liste uten spesiell formatering som ser omtrent slik ut:

artistliste_oyafestival
Øyafestivalens artistliste hentes fra serveren asynkront for å spare tid når hovedsiden lastes. Nå har vi funnet dataene vi trenger.

OK, nå har vi funnet dataene vi trenger. Nå må vi bare finne en god måte å hente de ut fra siden. La oss ta en titt på kilden bak konsertlista. Her finner vi både dataene og strukturen vi trenger:

Dataene vi trenger, men med en annen formatering. Uansett, nå gjenstår bare hentingen og reformateringen.
Dataene vi trenger, men med en annen formatering. Uansett, nå gjenstår bare hentingen og reformateringen.

Her kan vi se at:

  1. Ytterst har vi en div-tag med klassen “table title”. Denne innleder forklaringen som står over kolonnen i visningen.
  2. Vi har en uordnet liste (ul-tag) med klassen “table”
  3. Den uordnede listen har flere barn som er satt i liste elementer (li). Disse benytter seg av HTML5 data-attributter, men disse skal vi ikke bruke i denne omgang.
  4. Hvert liste-element har et span element med klassen “name”, hvor innholdet er navnet på artisten
  5. Liste-elementet har også en klasse “scene” med scene navnet som innhold.
  6. Sist har liste-elementet også en “date” klasse med de tre første bokstavene på dagen, tre non breaking spaces (HTML syntaks:  ) og tidspunkt for konsert-start.

Her finner vi alle dataene, og formateringen er også lik for alle elementene i lista med klassen “table”.

Når vi nå har funnet datakilden kan vi begynne å trekke ut dataene for videre bruk.

Screen scrape med Ruby og Nokogiri

Vi har nå funnet kilden og da kan vi benytte oss av Ruby og biblioteket (ruby-term: gem) Nokogiri.

Før vi begynner å hente dataene må vi gjøre klart scriptet som skal hente dataene fra festivalens hjemmeside. Vi inkluderer nokogiri som skal hjelpe oss å parsere datakilden. Samtidig laster vi også inn csv-bibliotek for å skrive ut filene og open-uri for å kunne lese URI-kilden som en fil.

[sourcecode language=”ruby”]
#!/usr/bin/ruby
# -*- encoding : utf-8 -*-

require ‘nokogiri’
require ‘open-uri’
require ‘csv’
[/sourcecode]

Konsert klassen

For å lagre og manipulere dataene lager vi en klasse for å lagre de fire verdiene vi trenger: artist, scene, date og datetime. Hos kilden finner vi de tre første verdiene og datetime konstruerer vi utfra date.

For klassen setter vi alle variablene vi skal benytte med en attr_accessor. Dette gjør at ruby selv genererer get og set-metoder for alle variablene listet etter funksjonen, noe som gjør at vi fritt kan hente og sette variablene fra instansene av klassen.

Vi skriver en initialize-metode, en konstruktør, som kalles når instansen opprettes. Siden vi allerede henter artist, scene og dato fra datakilden kaller vi konstruktøren med disse variablene slik at disse settes. For å oversette date til datetime, lager vi en dictionary med dagene og tilsvarende ISO-datoformat.

Legg merke til at når instans-variabelen @date settes, så gjøres det en del formatering. Fra kilden får vi datoformatet noe annerledes, så vi fjerner non-braking space, og bytter ut punktum med semikolon og sørger for at det er mellomrom mellom de tre bokstavene som angir dagen, og klokkeslettet. Når dette er gjort kaller vi en metode for å generere datetime-verdien basert på date-verdien. Vi bruker @ foran variabelnavnet for å markere at dette er en instanse-variabel.

metoden add_datetime gjør et oppslag i date_dict og bytter ut dag-bokstavene med ISO-dato, deretter henter den ut tidspunktet fra @date variabelen og interpolerer disse to verdiene til en datetime string.

Den siste metoden vi lager to_arr tar alle instanse-variablene og returnerer disse som en array. Siden CSV-funksjonen vi inkluderte tidligere kan lage en CSV-linje fra en array er dette en hendig måte å hente ut verdiene fra objektet.

[sourcecode language=”ruby”]
class Concert
attr_accessor :artist, :scene, :date, :datetime
def initialize(artist, scene, date)
@date_dict = {‘wed’ => ‘2013-08-07′ ,’thu’ => ‘2013-08-08′ ,’fri’ => ‘2013-08-09′ ,’sat’ => ‘2013-08-10’}
@artist = artist.strip
@scene = scene.strip
@date = date.gsub(/\u00a0/, ”).gsub(‘.’,’:’).gsub(/([a-zA-Z]{3})(.)/,’\1 \2′).strip
self.add_datetime
end

def to_arr
return [self.artist, self.scene, self.date, self.datetime]
end

def add_datetime
@datetime = "#{@date_dict[@date[0,3].downcase]} #{@date[4..9]}"
end

end
[/sourcecode]

Lese dokumentet, hente ut dataene og lage objektene

Når vi nå har en datastruktur hvor vi kan lagre informasjonen, kan vi begynne å hente informasjonen fra internett. Aller først lager vi igjen en tom dictionary hvor vi ønsker å lagre våre konsert-objekter etterhvert som vi lager disse.

Vi bruker Nokogiris HTML klasse og lagrer denne til doc variabelen. Til denne sender vi en tekst-strøm som hentes fra URLen. Vi sender altså samme tekst som vi fikk fra getArtist.php kildekoden til Nokogiri.

Nokogiri har en utmerket methode kalt css. Denne metoden tar en CSS (Cascading Style Sheet) selektor og finner riktig element fra DOMen (Document Object Model) som Nokogiri holder. Vi ønsker å iterere over alle “.table li”-nodene (alle li-nodene under table-klassen), og gjør dette ved enkelt med .each metoden.

For hver “.table li” vi itererer over, henter vi ut innholdet av elementene som har klassene .name, .scene og .date og oppretter et objekt av Concert-klassen. Det siste vi gjør for hver iterasjon er å lagre objektet med artisten som nøkkel i vår concerts dictionary.

[sourcecode language=”ruby”]
concerts = {}

doc = Nokogiri::HTML(open(‘http://oyafestivalen.com/wp-content/themes/oya13_new/includes/ajax/program/getArtists.php’))
doc.css(‘.table li’).each do |el|
a = Concert.new(el.css(‘.name a’).first.content,
el.css(‘.scene’).first.content,
el.css(‘.date’).first.content)
concerts[a.artist] = a
end
[/sourcecode]

Printe objektene som CSV

Når vi har opprettet alle objektene ønsker vi å skrive ut alle variablene i disse til fil. Vi gjør dette ved å åpne en fil kalt output.csv med skrivetilgang. Deretter itererer vi igjennom alle objektene og bruker nøkkelen fra k-variabelen til å hente ut hvert enkelt objekt som finnes i vår concerts dictionary. For å kun få Øya-festivalens konserter (ikke klubb-Øya) sjekker vi at konserten fant sted på enten scenene “Enga”, “Klubben”, “Sjøsiden” eller “Vika” (Sjøsiden har feil format her som vi senere korrigerer i Excel). For hvert objekt hvis scene er inkludert blant Øya-scenene skrives det en linje til csv-fila som tar en array med verdier. Denne arrayen hentes fra to_arr metoden vi skrev i Concert-klassen.

[sourcecode language=”ruby”]
CSV.open("output.csv", "wb") do |csv|
concerts.each do |k,v|
csv << concerts[k].to_arr if [‘Enga’,’Klubben’,’Sjøsiden’,’Vika’].include? concerts[k].scene
end
end

[/sourcecode]

Sånn. Nå burde du ha en CSV med alle Øya-artistene som du kan enten importere til en database eller åpne i Excel.

Hele scriptet:

[sourcecode language=”ruby”]

#!/usr/bin/ruby
# -*- encoding : utf-8 -*-

require ‘nokogiri’
require ‘open-uri’
require ‘csv’
require ‘open-uri’

class Concert
attr_accessor :artist, :scene, :date, :datetime
def initialize(artist, scene, date)
@date_dict = {‘wed’ => ‘2013-08-07′ ,’thu’ => ‘2013-08-08′ ,’fri’ => ‘2013-08-09′ ,’sat’ => ‘2013-08-10’}
@artist = artist.strip
@scene = scene.strip
@date = date.gsub(/\u00a0/, ”).gsub(‘.’,’:’).gsub(/([a-zA-Z]{3})(.)/,’\1 \2′).strip
self.add_datetime
end

def to_arr
return [self.artist, self.scene, self.date, self.datetime]
end

def add_datetime
@datetime = "#{@date_dict[@date[0,3].downcase]} #{@date[4..9]}"
end

end

concerts = {}

doc = Nokogiri::HTML(open(‘http://oyafestivalen.com/wp-content/themes/oya13_new/includes/ajax/program/getArtists.php’))
doc.css(‘.table li’).each do |el|
a = Concert.new(el.css(‘.name a’).first.content,
el.css(‘.scene’).first.content,
el.css(‘.date’).first.content)
concerts[a.artist] = a
end

CSV.open("output.csv", "wb") do |csv|
concerts.each do |k,v|
csv << concerts[k].to_arr if [‘Enga’,’Klubben’,’Sjøsiden’,’Vika’].include? concerts[k].scene
end
end

[/sourcecode]

Data-wrangling: find country based on artist name

At the Clouds & Concerts project at the University of Oslo we are working with really interesting topics, based on interesting empirical data. Through our collaboration with the Norwegian streaming service provider WiMP we are together with Telenor and WiMP analysing a vast collection of data. More about the project’s data-part, also the ‘Clouds’ part of the project’s name can be found on the project’s web sites.

Artist and Country

One of the tasks at hand was to find out which country an artist came from, and whether they came from Norway or not. One way of doing this is to manually go through each artist and use preexisting knowledge about music to determine their country of origin, if stuck, use online services (aren’t we all mostly using Google as initial source of wisdom). Another alternative is to use online services first and then to use human preexisting knowledge to quality assure the final result.

On the Internet, vast amount of sources can be found. However, if you want to get the data without too much fitting, testing, and nitty gritty adaptation for every source, you have an advantage if there is as consistently structured repository you can tap from. Luckily, Metabrainz foundation has a large repository of musical meta-data known as Musicbrainz.

Below you find a script which should (partially) solve our problem by combining the data from Musicbrainz with data exported from our research data. That being said, this script is more a method than a finished product. It should be very easy adaptable, but it is an advantage if you know Python and handling CSV-files. Codeacademy has a good introduction to Python.

The core idea of the script is to take input with name and number of streams and turn them into output with name from the original datasource, number of streams from the original datasource, as well as country of origin, Musicbrainz-ID, and name parsed by Musicbrainz search engine (for initial quality assurance).

To make things simple there is only one successful output and that is if the name sent to the Musicbrainz search engine return an answer, and if that answer has a country associated with it. Be advised (that is also why I have marked the title with ‘try’), that the search engine may not return a similar result. Of that reason we also print the name of the artist we find so this later can be juxtaposed with the original name in the Excel spreadsheet (you are going to transform the CSV to Excel before reviewing aren’t you. Good tool is Google Refine). Another problem is that popular cultural phenomena, common nouns and tribute bands (probably in that order, descending) have same name. This is why a human is always needed, or semantic absolute URI associated with each phenomena. This leads me on to the last step before the code.

Other ways this could have been solved (let me know if you solve the problem in any of these ways)

The semantic way:

The data found in the Musicbrainz database is made available through a SPARQL endpoint named LinkedBrainz.If you know the right ontologies and is comfortable with performing triplestore queries, this is perhaps the most innovative and new-thinking way to solve the problem.

The Virtual Machine Postgres way:

Instead of doing a query on the server, you can be a gentleman and download the server onto your own machine. If you have VirtualBox (if you don’t have it, download it for free) you can run the server locally. An image file with the complete Musicbrainz database can be found on their webpages.

The code:

Here is the code used to solve this task. It can also be cloned from the Cloud & Concerts GitHub-page

[sourcecode language=”python”]
#!/usr/bin/python
# -*- coding:utf-8 -*-

"""
Clouds & Concerts – 2012
Ola Loevholm

Called from command line:
The script reads a file named "topp1000_artister.csv" consisting of a list of artists and then tries to find out which country each artist comes from based on the name.
The name is given in the second column of the CSV file.

Called as a module:
The method getCountry() takes an artist name and checks this with musicbrainz seach engine. Returns the country if a) artist is found through the search engine b) artist has a country associated to the profile

"""

import sys, urllib, string, csv, time
import xml.etree.ElementTree as ET

# Loads a dictionary with ISO 3166-1 abbreviations and countries
COUNTRIES = {"AF":"AFGHANISTAN","AX":"ÅLAND ISLANDS","AL":"ALBANIA","DZ":"ALGERIA","AS":"AMERICAN SAMOA","AD":"ANDORRA","AO":"ANGOLA","AI":"ANGUILLA","AQ":"ANTARCTICA","AG":"ANTIGUA AND BARBUDA","AR":"ARGENTINA","AM":"ARMENIA","AW":"ARUBA","AU":"AUSTRALIA","AT":"AUSTRIA","AZ":"AZERBAIJAN","BS":"BAHAMAS","BH":"BAHRAIN","BD":"BANGLADESH","BB":"BARBADOS","BY":"BELARUS","BE":"BELGIUM","BZ":"BELIZE","BJ":"BENIN","BM":"BERMUDA","BT":"BHUTAN","BO":"BOLIVIA, PLURINATIONAL STATE OF","BQ":"BONAIRE, SINT EUSTATIUS AND SABA","BA":"BOSNIA AND HERZEGOVINA","BW":"BOTSWANA","BV":"BOUVET ISLAND","BR":"BRAZIL","IO":"BRITISH INDIAN OCEAN TERRITORY","BN":"BRUNEI DARUSSALAM","BG":"BULGARIA","BF":"BURKINA FASO","BI":"BURUNDI","KH":"CAMBODIA","CM":"CAMEROON","CA":"CANADA","CV":"CAPE VERDE","KY":"CAYMAN ISLANDS","CF":"CENTRAL AFRICAN REPUBLIC","TD":"CHAD","CL":"CHILE","CN":"CHINA","CX":"CHRISTMAS ISLAND",
"CC":"COCOS (KEELING) ISLANDS","CO":"COLOMBIA","KM":"COMOROS","CG":"CONGO","CD":"CONGO, THE DEMOCRATIC REPUBLIC OF THE","CK":"COOK ISLANDS","CR":"COSTA RICA","CI":"CÔTE D’IVOIRE","HR":"CROATIA","CU":"CUBA","CW":"CURAÇAO","CY":"CYPRUS","CZ":"CZECH REPUBLIC","DK":"DENMARK","DJ":"DJIBOUTI","DM":"DOMINICA","DO":"DOMINICAN REPUBLIC","EC":"ECUADOR","EG":"EGYPT","SV":"EL SALVADOR","GQ":"EQUATORIAL GUINEA","ER":"ERITREA","EE":"ESTONIA","ET":"ETHIOPIA","FK":"FALKLAND ISLANDS (MALVINAS)","FO":"FAROE ISLANDS","FJ":"FIJI","FI":"FINLAND","FR":"FRANCE","GF":"FRENCH GUIANA","PF":"FRENCH POLYNESIA","TF":"FRENCH SOUTHERN TERRITORIES","GA":"GABON","GM":"GAMBIA","GE":"GEORGIA","DE":"GERMANY","GH":"GHANA","GI":"GIBRALTAR","GR":"GREECE","GL":"GREENLAND","GD":"GRENADA","GP":"GUADELOUPE","GU":"GUAM","GT":"GUATEMALA","GG":"GUERNSEY","GN":"GUINEA","GW":"GUINEA-BISSAU","GY":"GUYANA","HT":"HAITI","HM":"HEARD ISLAND AND MCDONALD ISLANDS",
"VA":"HOLY SEE (VATICAN CITY STATE)","HN":"HONDURAS","HK":"HONG KONG","HU":"HUNGARY","IS":"ICELAND","IN":"INDIA","ID":"INDONESIA","IR":"IRAN, ISLAMIC REPUBLIC OF","IQ":"IRAQ","IE":"IRELAND","IM":"ISLE OF MAN","IL":"ISRAEL","IT":"ITALY","JM":"JAMAICA","JP":"JAPAN","JE":"JERSEY","JO":"JORDAN","KZ":"KAZAKHSTAN","KE":"KENYA","KI":"KIRIBATI","KP":"KOREA, DEMOCRATIC PEOPLE’S REPUBLIC OF","KR":"KOREA, REPUBLIC OF","KW":"KUWAIT","KG":"KYRGYZSTAN","LA":"LAO PEOPLE’S DEMOCRATIC REPUBLIC","LV":"LATVIA","LB":"LEBANON","LS":"LESOTHO","LR":"LIBERIA","LY":"LIBYA","LI":"LIECHTENSTEIN","LT":"LITHUANIA","LU":"LUXEMBOURG","MO":"MACAO","MK":"MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF","MG":"MADAGASCAR","MW":"MALAWI","MY":"MALAYSIA","MV":"MALDIVES","ML":"MALI","MT":"MALTA","MH":"MARSHALL ISLANDS","MQ":"MARTINIQUE","MR":"MAURITANIA","MU":"MAURITIUS","YT":"MAYOTTE","MX":"MEXICO","FM":"MICRONESIA, FEDERATED STATES OF",
"MD":"MOLDOVA, REPUBLIC OF","MC":"MONACO","MN":"MONGOLIA","ME":"MONTENEGRO","MS":"MONTSERRAT","MA":"MOROCCO","MZ":"MOZAMBIQUE","MM":"MYANMAR","NA":"NAMIBIA","NR":"NAURU","NP":"NEPAL","NL":"NETHERLANDS","NC":"NEW CALEDONIA","NZ":"NEW ZEALAND","NI":"NICARAGUA","NE":"NIGER","NG":"NIGERIA","NU":"NIUE","NF":"NORFOLK ISLAND","MP":"NORTHERN MARIANA ISLANDS","NO":"NORWAY","OM":"OMAN","PK":"PAKISTAN","PW":"PALAU","PS":"PALESTINIAN TERRITORY, OCCUPIED","PA":"PANAMA","PG":"PAPUA NEW GUINEA","PY":"PARAGUAY","PE":"PERU","PH":"PHILIPPINES","PN":"PITCAIRN","PL":"POLAND","PT":"PORTUGAL","PR":"PUERTO RICO","QA":"QATAR","RE":"RÉUNION","RO":"ROMANIA","RU":"RUSSIAN FEDERATION","RW":"RWANDA","BL":"SAINT BARTHÉLEMY","SH":"SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA","KN":"SAINT KITTS AND NEVIS","LC":"SAINT LUCIA","MF":"SAINT MARTIN (FRENCH PART)","PM":"SAINT PIERRE AND MIQUELON","VC":"SAINT VINCENT AND THE GRENADINES",
"WS":"SAMOA","SM":"SAN MARINO","ST":"SAO TOME AND PRINCIPE","SA":"SAUDI ARABIA","SN":"SENEGAL","RS":"SERBIA","SC":"SEYCHELLES","SL":"SIERRA LEONE","SG":"SINGAPORE","SX":"SINT MAARTEN (DUTCH PART)","SK":"SLOVAKIA","SI":"SLOVENIA","SB":"SOLOMON ISLANDS","SO":"SOMALIA","ZA":"SOUTH AFRICA","GS":"SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS","SS":"SOUTH SUDAN","ES":"SPAIN","LK":"SRI LANKA","SD":"SUDAN","SR":"SURINAME","SJ":"SVALBARD AND JAN MAYEN","SZ":"SWAZILAND","SE":"SWEDEN","CH":"SWITZERLAND","SY":"SYRIAN ARAB REPUBLIC","TW":"TAIWAN, PROVINCE OF CHINA","TJ":"TAJIKISTAN","TZ":"TANZANIA, UNITED REPUBLIC OF","TH":"THAILAND","TL":"TIMOR-LESTE","TG":"TOGO","TK":"TOKELAU","TO":"TONGA","TT":"TRINIDAD AND TOBAGO","TN":"TUNISIA","TR":"TURKEY","TM":"TURKMENISTAN","TC":"TURKS AND CAICOS ISLANDS","TV":"TUVALU","UG":"UGANDA","UA":"UKRAINE","AE":"UNITED ARAB EMIRATES","GB":"UNITED KINGDOM","US":"UNITED STATES",
"UM":"UNITED STATES MINOR OUTLYING ISLANDS","UY":"URUGUAY","UZ":"UZBEKISTAN","VU":"VANUATU","VE":"VENEZUELA, BOLIVARIAN REPUBLIC OF","VN":"VIET NAM","VG":"VIRGIN ISLANDS, BRITISH","VI":"VIRGIN ISLANDS, U.S.","WF":"WALLIS AND FUTUNA","EH":"WESTERN SAHARA","YE":"YEMEN","ZM":"ZAMBIA","ZW":"ZIMBABWE"}

# Iterates through XML-structure and removes the namespace, for easier navigation in getCountry()s ElementTree.findall()
def remove_namespace(doc, namespace):
"""Remove namespace in the passed document in place."""
ns = u'{%s}’ % namespace
nsl = len(ns)
for elem in doc.getiterator():
if elem.tag.startswith(ns):
elem.tag = elem.tag[nsl:]

# getCountry – where the magic happens. Encodes string with artistname to url, then query musicbrainz search engine.
# parses the XML-answer and get the name, id and country of the first returned element (with highest weight)
# returns country name i a) artist is found through the search engine b) artist has a country associated to the profile, otherwise returns False
def getCountry(name):
name = urllib.quote_plus(name)
BASE_URL = "http://musicbrainz.org/ws/2/artist/?query=%s&format=xml&method=advanced" % (name)
print "Querying: %s" % (BASE_URL)
try:
search_input = urllib.urlopen(BASE_URL)
# Checks whether HTTP Request Code is 200 – if not goes to sleep for 5 seconds // Inded for 503 Code
http_code = search_input.code
if http_code != 200:
# print "Could not access: %s \t Got HTTP Code: %s. 5 second cool-down" % (name, http_code)
time.sleep(5)
getCountry(name)
except Exception:
print "GETTING_ERROR: Something went wrong while getting HTTP"
return False
#search_xml = search_input.read()
#print search_xml
try:
tree = ET.parse(search_input)
remove_namespace(tree, u’http://musicbrainz.org/ns/mmd-2.0#’)
feed = tree.getroot()
elem = feed.findall("./artist-list/")
#print elem[0].find(‘name’).text
#print elem[0].get(‘id’)
except Exception:
print "PARSE_ERROR: Something went wrong while parsing HTTP"
return False
try:
if elem[0].find(‘country’) != None:
# print COUNTRIES[elem[0].find(‘country’).text]
try:
country = COUNTRIES[elem[0].find(‘country’).text]
except Exception:
print "Could not find key in countrylist error"
return False
return [country,elem[0].get(‘id’),elem[0].find(‘name’).text]
else:
print elem[0].find(‘name’).text + " has not any country associated\n"
return False
except IndexError, ValueError:
print "ERROR – COULD NOT GET DATA FROM %s\n" % (name)
return False

# If method is called from terminal. Iterates through topp1000 artists contained in a CSV-file in same directory.
if __name__ == "__main__":
#name = sys.argv[1]
csvfile = open("topp1000_artister.csv")
outfile = open("topp1000_output.csv","w")
artistlist = csv.reader(csvfile, delimiter=’,’, quotechar=’"’)
for line in artistlist:
result = getCountry(line[1])
try:
if result != False:
result_string = "%s,%s,%s,%s,%s,%s\n" % (line[0],line[1],line[2],result[0],result[1],result[2])
# print result_string
else:
result_string = "%s,%s,%s,%s\n" % (line[0],line[1],line[2],"No Country Found or fail occured")
# print result_string
except IndexError, ValueError:
print e
result_string = "Error on element: %s\n" % line[1]
try:
outfile.write(result_string)
except:
print "Write error happened with %s" % line[1]
[/sourcecode]

And as always, I am most greatful for feedback! Hope this may come in handy!

What does your Twitter followers look like

I like Twitter. It’s the virtual world’s answer to Post-it notes, well not really, but the nature of the site constrains people from droning on-and-on about a topic. The restrictions in the number of characters a user may put into a tweet causes brevity, which is ideal when the total number of people who you follow is increasing and the live-feed of Tweets is updated several times per minute. This restrictions in the number of characters have fostered a certain repurposing of signs. The @ (at-sign) is working as a reference to users and the # (hash-sign) is used to concentrate commentaries about a topic.

From a programming-wise perceptive the decoupled nature of Twitter is interesting. While Facebook.com and other Facebook-created applications are the favourite place to access Facebook-data, Twitter is built around a more open model. Facebook is complicated with several ways of interacting, many modalities, third-party applications and a requirement to login to get access. Twitter is opposite, it has very simple and easy to understand structure and is open (though user may protect their feed to accepted followers this is not default and luckily not widely spread). These differences leads, at least for me, to Facebook being a personal and private social network and Twitter being used more for interests, both professional and private.

The decoupled nature of Twitter has led to an abundance of third-party applications which interact with the Twitter API. It’s easy to access Twitter data, and more importantly, easy to understand what it all does. Twitter is in that sense a good place to start if you want to play more around with data, Internet protocols and programming. A couple of posts ago I wrote about Mining the Social Web and Screen Scraping with Python (in Norwegian). The post you now is reading goes into the same category. This code example does not however mine any of the data from Twitter, it is just finding the ID of your followers, then get data about each of them and download their pictures. The code here may be used for further data-analysis though if you save the data gathered by the requests to somewhere instead of doing like I do store them in the heap while the script executes and hence automatically remove them when it terminates. Writing the data to file takes just two lines of code, writing them to a database probably four or five.

[sourcecode language=”ruby”]

#encoding: utf-8

dependencies = %w(net/http active_support open-uri uri)
dependencies.each {|m| require m}

class Follower
attr_accessor :name, :created_at, :profile_image_url, :location, :url, :lang, :geo_enabled, :description
def initialize(id)
@id = id
end

def say_hi
puts "Hello, my name is #{@name}, and I have the id #{@id}. Oh, by the way. I was created at #{@created_at}"
end

def download_picture
puts "downloading #{@id} : #{@name} from #{@profile_image_url} \n"
unless @profile_image_url.nil?
open(URI.escape(@profile_image_url)) {|f|
File.open("pictures/#{@id}.jpg","wb") do |file|
file.puts f.read
end
}
end

end

end

class TwitterGetter

def initialize(name)
@name = name
#@followers = Array.new
unless File.directory?("pictures")
Dir.mkdir("pictures", 0755)
end
end

def get_follower_list
response = Net::HTTP.get("api.twitter.com" , "/1/followers/ids.json?cursor=-1&screen_name=#{@name}" )
@followers = ActiveSupport::JSON.decode(response)
sleep_time = ((60*60)/ 75) + 2
puts "set sleeptime to: ", sleep_time, "\n"

@followers[‘ids’].each do |id|
sleep(sleep_time)
lookup_user(id)
end

end

def lookup_user(id)
response = Net::HTTP.get("api.twitter.com", "/1/users/show.json?user_id=#{id}&include_entities=true")
info = ActiveSupport::JSON.decode(response)
f = Follower.new(id)
f.name = info["name"]
f.created_at = info["created_at"]
f.profile_image_url = info["profile_image_url"]
f.location = info["location"]
f.url = info["url"]
f.lang = info["lang"]
f.geo_enabled = info["geo_enabled"]
f.description = info["description"]
f.say_hi
f.download_picture
end
end

tg = TwitterGetter.new("olovholm")
tg.get_follower_list

[/sourcecode]

So, how does this little script work? First it instantiates the work-horse class TwitterGetter which takes the argument of the user which followers’ pictures you want to download. This class creates the directory in which the pictures will be downloaded into. Once instantiated we call the get_follower_list method which accesses twitter data through the REST-API  then parse the JSON stream from   response = Net::HTTP.get("api.twitter.com" , "/1/followers/ids.json?cursor=-1&screen_name=#{@name}" ). Once the list is downloaded a script runs through the new list of followers and gathers data from the Twitter API on each of the users. Due to limitation in how many requests one can call to twitter each hour I put the script to sleep for

total time units % requests allowed per hour

(You may request Twitter 150 times per hours, but the lookup user calls download_picture which downloads the image – It may be that this is excluded from the restrictions). Well, the user class stores data from each of the users and has the code and responsibility for downloading the users pictures. It also contains a say_hi method which perhaps would be better as a bit more formal to_s method. The code is more a proof of concept and does not contain error handling for JSON data returned by Twitter (which, believe me, happens quite often) so that would be a good place to start if you want to expand on this code.

Screen Scraping med Python

Tim Berners-Lee, Internetts pappa, snakker i et inspirerende TED foredrag om the web of data, og oppfordrer alle til å dele sine data. Ideen med web of data er at webben fram til i dag hovedsakelig har vært dokumentbasert og at denne nå endres til å bli mer databasert. I webbens dokumenter ligger det mye bakenforliggende informasjon hvor det semantiske innholdet ofte kan være utfordrende å hente ut, spesielt hvis du er en datamaskin. Selv om, ideeltt sett, all informasjon som ligger ute på Internett burde være lesbar for alle er det ikke slik, så av og til må vi være litt pragmatiske og gjøre det beste ut av situasjonen. Vi må rett og slett skrape ut den informasjonen – de dataene vi trenger – fra websiden, derav navnet screen scraping.

Et av de morsomste aspektene ved screen scraping, syntes jeg, er å forstå hvordan innholds-serveren er satt sammen og dekonstruere denne. Ettersom få organisasjoner (og personer for den saks skyld) håndkoder hver HTML-side de viser til offentligheten ligger det et system bak sidene som publiseres. Hvordan hele nettstedet er organisert og hvordan hver enkelt side er lagt opp er et godt utgangspunkt for å forstå strukturen. Mange nettsteder følger i dag også en REST-arkitektur, og dette gjør at vi kan finne ut mye bare av å se på URLen vi bruker for å få tilgang til sidens forskjellige funksjoner. Mange slike sider er laget med populære rammeverk med REST-implementering slik som Ruby on Rails og Django, som også har tilrettelagt for en enkel implementasjon av data-grensesnitt slik at spørringer kan resultere i et dataformatert svar som for eksempel XML eller JSON imotsetning til et template-basert HTML svar. Hvis du kan spørre om et datasvar kan du muligens finne dataene dine uten å scrape. Uansett, i mange tilfeller et klipp-og-lim eller filopplastning fra Excel eller Word klippet inn i et web-grensesnitt eller endret programmatisk til HTML, eller data hentet fra et annet datasystem matet til publiseringsportalen også i HTML. I disse tilfellene trenger vi å tweeke og endre litt for å hente ut data.

Et slag for demokratiet (?)

Francis Boulle, kanskje mest kjent fra TV-serien Made in Chelsea, har laget siden sexymp. På denne siden blir du presentert med bilder av to representanter fra det britiske underhuset. Din oppgave er å klikke på den mest attraktive personen, og basert på dine og andres klikk rangeres representantene. Dette systemet er nærmest identisk med siden som Mark Zuckerberg setter sammen i åpningsscenen i filmen The Social Network fra 2010. Det kan være spennende nok å se på vektingsalgoritmen og prøve å forstå hvordan Chris Evans fra Islwyn blir ansett som den mest og Steve McCabe fra Birmingham den minst attraktive, men for vår undersøkelse er det mer interessant å se hvordan vi kan innhente navn og bilder dersom vi ønsker å lage en tilsvarende side for Norge.

Stortinget.no

Et bra sted å begynne dersom vi skal utvikle en norsk sexymp (eller et annet atributt som f.eks troverdighet, ærlighet, classyness osv.) er Stortinget.no. Hvis vi går inn i listen over representanter kan vi finne ut at URLen har følgende oppbygning: http://stortinget.no/no/Representanter-og-komiteer/Representantene/Representantfordeling/Representant/?perid=SMY (for Sverre Myrli fra Arbeiderpartiet) og tilsvarende for en annen representant: http://stortinget.no/no/Representanter-og-komiteer/Representantene/Representantfordeling/Representant/?perid=IME (Ine Marie Eriksen fra Høyre). Hvis du studerer disse URLene nøye, hva er det som det er verdt å merke seg? … Ja, nemlig, representantene har en ID.

La oss nå gå inn å se på hvordan Ine Marie Eriksen sin profil egentlig ser ut. Siden nettleseren din, når du åpner en side, kun laster ned et dokument med angitte referanser skjer det ikke noe magisk (okey, med web 2.0 med Ajax og masse Javascript, eller Flash, Silverlight og andre plug-ins kan det virke mer mysterisk eller være veldig vanskelig å titte inn i boksen). I alle nettlesere kan du få tilgang til råteksten som senere skaper siden, og hvis du går inn å ser på denne kan du finne ut hvordan siden er bygget opp og hvilke andre ressurser (som f.eks bilder, stylesheets, javascript-kode) som kalles. Forsøk nå å se på HTML-filen til Eriksens stortingsbio og se om du kan finne bilde-ressursen.

Der ja, på linje 472 finner vi følgende HTML-snutt:
<img id="ctl00_MainRegion_RepShortInfo_imgRepresentative" src="/Personimages/PersonImages_Large/IME_stort.jpg" alt="Søreide, Ine M. Eriksen" style="border-width:0px;"
Dersom du følger denne filbanen med stortingsdomenet slik at URLen du benytter er: http://stortinget.no/Personimages/PersonImages_Large/IME_stort.jpg vil bare bildet åpnes. Du har nå funnet bildet som hentes inn i bio-siden vi tidligere åpnet. Nå kan du forsøke å åpne bildet til Sverre Myrli ved hjelp av kun hans system-ID som er SMY og URLen til Ine Marie Eriksen.

Hvis du nå har byttet “IME_stort.jpg” med “SMY_stort.jpg” har du forstått hvordan vårt detektivarbeid har bært frukter, og at vi nå kan få tak i alle bildene dersom vi har alle IDene. Vi trenger nå to ting: En liste over alle representantene med deres respektive IDer, og en måte å laste ned bildene til vår lokale maskin. Det er på tide å scripte litt.

Python

Det vi skal gjøre nå kan du gjøre i alle multi-purpose programmeringsspråk, men jeg har valgt å bruke Python. Jeg syntes Python er et flott språk å bruke til å løse små oppgaver hvor det er mye prøving og feiling involvert. Det finnes også veldig mye dokumentasjon og litteratur om Python i forskjellige roller, det kommer med mange nyttige biblioteker som kan brukes “right out of the box”, og det er allerede installert i de fleste Linux-distribusjoner og på alle Macer.

Jeg har lagt ut forskjellige kodeeksempler på GitHub, og du kan finne kildekoden vi bruker videre i denne lille artikkelen der. Dersom du ikke har installert Python eller mangler noen av bibliotekene jeg benytter finnes det mange gode guider på nettet på hvordan du kan få fikset dette.

Som tidligere nevnt er en av de første tingene vi trenger en liste over alle representantene. Heldigvis for oss finnes det en side med en slik liste. På siden med URL http://stortinget.no/no/Representanter-og-komiteer/Representantene/Representantfordeling/ finner vi en komplett liste over alle representantene med navn, parti, stortingsnummer og ID. Hvis vi åpner siden i en nettleser får vi ikke opp IDen, men denne finner vi dersom vi istedet ser på kildekoden. Dette er nok fordi IDen ikke er så viktig for de fleste besøkende, men for stortingssystemet er essensiell og derfor er den viktig for oss også. For å finne den informasjonen vi trenger før vi kan gå videre må vi gå inn å ta en nærmere titt på HTML-kilden.

Her kan du se at representantlista er lagt inn i en tabellstruktur. tr-taggen er table row og td-taggen er table data. Nedover kan vi se at tabellstrukturen er fast så vi kan ta utgangspunkt i denne, men i scriptet findPerId.py  har vi tatt utgangspunkt i et annet gjengående mønster. Alle representantoppføringene har en link-struktur som følger et bestemt mønster.
Amundsen, Per-Willy

Det at vi har et bestemt mønster bestående av “Representanter-og-komiteer/Representantene/Representantfordeling/Representant/”, etterfulgt av et spørsmålstegn som igjen følges av en HTML closing bracket (“>) og så to tekststrenger separert med et komma (,) og at dette mønsteret ikke finnes på andre steder enn akkurat der hvor vi ønsker å finne data er bra for oss. Dette kan vi nemlig representere med regulære utrykk

(r"Representanter-og-komiteer/Representantene/Representantfordeling/Representant/\?perid=(\w*)\">([a-å]*), ([a-å]*)", re.IGNORECASE)

Vi kan selvsagt hente hele HTML filen fra Stortingets nettsider og kjøre scriptet på all tekst, men hey, nettleseren din parser HTML så det kan jo vi gjøre også. Etter vi har lastet ned dokumentet via HTTP-protokollen med urllib2 sender vi HTML-teksten til BeautifulSoup som gjør at vi kan hente ut alle anchor-taggene og begrense RegEx sjekken vår til disse. Mange av linkene vil feile mønsteret vårt, og det er jo litt av poenget ettersom verken “tilbake til forsiden” eller “rettighetsinformasjon” er representanter vi ønsker å hente ut fra systemet. Siden vi har gruppert treffene i tre grupper ved hjelp av paranteser vil vi kunne hente ut henholdsvis brukernavn, etternavn og fornavn. Etter vi har hentet ut all informasjonen vi trenger skriver vi denne til en kommaseparert fil (populært referert til som CSV) som vi da kan hente inn i vårt neste script.

Laste ned bilder programmatisk

Etter å ha kjørt findPerId.py i forrige avsnitt har vi nå en lagret CSV-fil med fornavn, etternavn og ID på stortingsrepresentantene. Nå trenger vi bare å iterere igjennom denne listen og laste ned bilder for våre folkevalgte. Scriptet som gjør denne jobben heter downloadPictures.py og dette benytter metoden urlretrieve fra urllib biblioteket til å forespørre hvert enkelt bilde, laste dette ned og plassere det i en folder vi oppretter med os.makedirs (hvis ikke folderen vår allerede eksisterer). Bildene vi skal laste ned har før og etter representant IDen følgende tekst:

path_big_pre = "http://stortinget.no/Personimages/PersonImages_Large/"
path_big_post = "_stort.jpg"
path_little_pre = "http://stortinget.no/Personimages/PersonImages_Small/"
path_little_post = "_lite.jpg"

Som du sikkert kan forstå fra disse variabelnavnene og tekststrengene er at det for hver representant er både et stort og et lite bilde. Metoden urlretrive tar to argumenter hvorav det første må være den absolutte filbanen, altså med IDen samt teksten før og etter i URLen, outfolderen kan benytte en relativ filbane så her kan du endre filnavnet dersom du ønsker det.

Veien videre

Når du har kjørt begge scriptene har du både navn, id og bilder av alle stortingsrepresentantene, og hvis du har lest teksten og googlet ukjente navn og fremgangsmåter har du også lært noe nytt. Lykke til med videre bruk og andre eksperimenter.

Dette innlegget er skrevet på UiOs stand under The Gathering 2012 på Hamar, en myldreplass for kreativitet.