Databaser
Om et par år bliver det nemt at automatisere tilgangen til data på nettet. Når alle data er formateret som XML, er det bare at indlæse XML-kildedokumentet i en eller anden struktur, og så kan den information, man ønsker, nemt pilles ud og puttes i en database. Men denne mulighed er ikke til stede nu. HTML-koderne bag databasernes søgesider giver ikke data mening, og derfor kan det være svært umiddelbart at se, hvorledes man skal udtrække data fra en skønsom suppedas bestående af alle mulige tags.
Men det kan nu godt lade sig gøre alligevel, selvom det ikke er helt så nemt som det forhåbentligt bliver i XML-fremtiden. Scriptingsproget Perl er simpelthen toptunet til sådan en opgave. For det første er det forholdsvist nemt at gå til, for det andet er Perl rettet imod behandling af simpel ascii-tekst, og for det tredie indeholder det god understøttelse af internet-funktioner. Derudover indeholder Perl et modul, der gør det muligt at læse HTML-dokumentet ind i en træ-struktur, også selv om koden ikke er velformet, hvilket gør det lidt lettere at udtrække data fra HTML-dokumenter end ved at søge på tekststrenge.
Masser af databaser
Inspirationen til artiklen opstod, da PC World Onlines researchere skulle benytte et stort antal data fra en offentlig tilgængelig database. Det drejede sig om et udtræk af større omfang, og reseacheren henvente sig derfor til organisationen bag siden. Et dataudtræk ville koste 1300 kroner plus 500 kroner per løbende teknikertime, så derfor så vi os om efter alternative løsninger, og her lå Perl lige for hånden på grund af de tidligere nævnte egenskaber.
Perl-eksempler kan se frygteligt komplicerede ud, men her i artiklen benytter vi en stil, hvor de fleste der beskæftiger sig med Visual Basic, Javascript, PHP eller et tilsvarende scripting-sprog kan følge med uden problemer.
Der er masser af databaser med webadgang, så læseren kan sikkert selv finde på andre eksempler, hvor et dataudtræk ville være behændigt. Selvfølgelig kan man altid kopiere og indsætte, men hvis det drejer sig om flere hundrede sider, kan det hurtigt blive en kedelig opgave. I eksemplet fra før drejede det sig om cirka 20.000 rækker, og det kan man jo godt blive lidt træt i betrækket af.
Installer Perl
Perl over hele linien
Perl findes til alle populære styresystemer, både Windows, Macintosh og alle de mange Unix-varianter, Linux inklusive selvfølgelig. Her i artikelen benytter vi Perl under Windows, og det er fantastisk nemt at installere. Windows-versionen af Perl kan downloades fra firmaet ActiveStates hjemmeside, og da Perl er open source, er det ganske gratis.
Installationen er lige ud af landevejen som alle andre Windows-programmer, men brugere af NT4 og Windows 98 skal downloade og installere nogle ekstra komponenter. Det forløber nu også ganske problemfrit.
Efter installationen skal maskinen lige genstartes, så vi kan starte vores Perl-programmer fra alle mapper.
Lad os straks skrive vores første lille Perl-program, der består af en linie. Åbn en tekst-editor som for eksempel Notepad, skriv
print "Hallo verden";
gem filen under navnet hallo.pl og gem den.
Perl-programmer afvikles fra DOS-prompten, men det er der nu ingen grund til at blive angst for. Hvis vores program for eksempel ligger i mappen C:\scripts og hallo.pl, så afvikles det ved at åbne en DOS-prompt, og taste
C:\>cd C:\scripts
og derefter
C:\scripts>perl hallo.pl
Det skulle gerne skrive
Hallo verden
i vores DOS-vindue. Det kan forøvrigt godt være, at æ, ø og å samt andre ikke-engelske tegn ser lidt underlige ud i DOS-vinduet, men når vi gemmer resultaterne i en fil, ser det ud, som det skal - så ingen panik.
Vi haster videre med artiklens egentlige formål, nemlig at hente data fra web-databaser. Men først et lille DOS-tip: Hvis der er flere udddata-linier, end DOS-vinduet kan rumme, kan man tilføje DOS-direktivet more efter kommandoen, på denne facon:
C:\scripts>perl hallo.pl | more
- hvorefter man får en enkelt skærm uddata ad gangen, og man kan derefter navigere sig igennem uddata ved at trykke retur for hver enkelt skærm. Hvis man løber sur i det, kan programmet altid afsluttes med tastatur-kommandoen Ctrl+C.
Snup en side
Querystrings
For at forstå, hvordan vi kan hente de websider, vi vil udtrække data fra, må vi først kigge på, hvorledes brugerdata kommmer fra browseren og ind til webserveren. Der findes to måder, data kan sendes på. Den ene måde, kaldet Get, er som parametre, der tilføjes webadressen, som det ses i eksemplet:
/Default.asp?Mode=2&ArtikelID=2512
Her kaldes scriptet Default.asp med parametrene Mode, som har værdien 2, og ArtikelId, som har værdien 2512. Den del af adressen, der står efter spørgsmålstegnet, kaldes for en querystring eller på dansk en forespørgselsstreng. Det er websøgetjenster som benytter denne metode, som vi kigger på i denne artikel.
Den anden metode Post foregår ved at data sendes i headeren af browserens HTTP-forespørgsel til webserveren. Den er en del mere kompliceret at have med at gøre, så den ser vi bort fra her. Den gode nyhed er, at langt de fleste søgetjenester benytter querystrings som inddata-metode.
For at starte, skal vi finde en webside, der kan give os udgangspunktet for vores datafangst. For eksempel kan man gå ind på søgemaskinen Google og skrive "pc world" i søgefeltet. Adressen på resultatsiden, der kommer frem, ser nu sådan ud:
http://www.google.com/search?q=pc+world&hl=da&lr=
Hvis det var Google, vi ville malke data fra, kunne denne adresse bruges som udgangspunkt. Men først må vi lige tjekke, at tjenesten ikke benytter cookies eller Post-metoden. Det gøres meget simpelt ved at kopiere adressen over i en anden browser, hvis man har sådan en ved hånden.
Hvis den samme side nu bliver vist i den anden browser, benytter websiden querystrings som inddata-metode. Hvis man derimod får en fejlmeddelelse frem, er det tegn på at søgetjenesten benytter andre metoder til dataoverførsel som Post-data eller cookies, også selv om der optræder en querystring i adressen.
Hvis man kun har én browser til rådighed, må man udføre testen ved at bookmarke adressen, slette cache og eventuelle cookies fra det pågældende site, genstarte browseren og så udføre testen ved at åbne resultatsiden igen.
Det andet krav, vi må stille til tjenesten, er at resultatsiderne indeholder links til de efterfølgende sider, så vores script kan finde den næste side, når en side er behandlet.
Som sagt overholder langt de fleste database-grænseflader på web begge krav, blandt andet fordi det er nemmest at implementere for udvikleren, men nogen garanti er der altså ikke.
Analyse af HTML-dokumentet
Det første, vi skal gøre, er at downloade det HTML-dokument, der er udgangspunktet for datafangsten. Så kan vi analysere dokumentet uden at skulle belaste webserveren, hver gang vi tester.
Dokumentet skal downloades med Perl-scriptet, og ikke ved at downloade en kopi med browseren, da nogle servere leverer forskellige versioner af websiderne afhængigt af hvilken specifik brugeragent, man benytter.
Vi henter siden med følgende lille script:
use LWP::Simple;
$doc = get "http://www.søgetjenesten.dk/mos/mos_sog.actionquery?P_FAGGRUPPEID=30805&P_INTOMRID=8&P_KOMMUNE=";
open(HTMLFIL, '>', 'resultatside.html');
print HTMLFIL $doc;
Her skal adressen i get-funktionen udskiftes med adressen på den side, vi benytter som udgangspunkt for vores fangst. Ellers er scriptet lige ud af landevejen:
I første linie importerer vi biblioteket LWP::Simple, der indeholder funktioner til HTTP-manipulationer. Den næste linie henter HTML-koden, og gemmer den i variablen $doc. Derefter åbner vi en ny fil ved navn resultatside.html for skrivning, og den sidste linie udskriver variablen $doc til filen (som befinder sig i samme mappe som perl-scriptet).
Nu skulle der gerne ligge en kopi af siden på vores lokale harddisk. Vi kan nu gå i gang med at analysere koden, så vi forhåbentlig kan udtrække den relevante information. I eksemplet her i artiklen benytter vi en dansk erhvervsvejviser, hvor vi vil udtrække firmanavne og adresser i kategorien IT. Navnet på tjenesten er udeladt, for at den stakkels webserver ikke skal belastes unødigt af ivrige testere, men eksemplet er forholdsvist nemt at reproducere.
Analyse
Der er forskellige metoder til at finde signifikant information om de HTML-tags, hvorfra vi skal udtrække informationer. Ofte er det sådan, at HTML-koderne utilsigtet indeholder attributter, der kan bruges til at identificere de rigtige tags.
Lad os kigge på et eksempel. Det første resultat, vi ønsker i den side, vi har downloadet i eksemplet, hedder "ATC Data ApS". Ved at åbne HTML-dokumentet i for eksempel Wordpad og så søge efter strengen "ATC Data ApS", kan vi finde omgivelserne for den information, vi vil udtrække. Linierne lige omkring strengen ser sådan ud:
<tr><td width="190" valign="TOP" height="40" nowrap class="txtnormgulm">
<b>ATC Data ApS</b>
<br>Tangmosevej 87
<br>4600 Køge</td>
Ved at kigge på HTML-dokumentets visuelle udforming i browseren, kunne det godt se ud som om, at det første <td>-tag er specifikt for adresseoplysningerne, så vi vil prøve at pille de data ud, der befinder sig indlejret under <td>-tagget i HTML-dokumentet.
Lad os prøve at udtrække alle data, der befinder sig i <td>-tags af den ovenstående slags. Det ser sådan ud:
use LWP::Simple;
use HTML::TreeBuilder;
$tree = HTML::TreeBuilder->new; # nyt tomt træ
$tree->parse_file("resultatside.html");
@del_trae = $tree->look_down('_tag','td', 'width', '190');
for $node (@del_trae) {
print "\nHele noden:", $node->as_text, "\n";
}
$tree->delete; # tøm hukommelsen
Her importer vi udover LWP::Simple også modulet HTML:TreeBuilder, der giver os en træstruktur, som vi kan læse HTML-dokumentet ind i.
Derefter opretter vi et $tree-objekt og læser (parser) HTML-dokumentet ind i træet i den efterfølgende linie i programmet.
Så skaber vi et array, @del_trae, (snabel-a foran variabelnavnet betyder array i Perl). Det tildeler vi resultatet af metoden look_down('_tag','td', 'width', '190') udført på vores HTML-træ. Metoden look_down returnerer samtlige tags i træet, der opfylder betingelserne i parameterkaldet. Her i eksempelt kigger vi efter tags, der hedder td, og som har attributten width sat til værdien 190. Flere attributter og værdier kan tilføjes efter behag.
Derefter gennemløber vi de noder i træet, der blev udtrækket, ved brug af en for-løkke. Løkken viser iøvrigt Perls charme: Ved blot at skive sætningen
for $node (@del_trae)
gennemløbes elementerne i @del_trae-arrayet, med værdien af hvert element gemt i variablen $node - ganske nuttet.
Inde i løkken udskriver vi resultatet af hvert $node-objekt med metoden ->as_text, der udskriver tekst-elementerne i noden samt de sub-noder, den måtte indholde. Det lille "\n" foran og i slutningen af strengen:
print "\nHele noden:", $node->as_text, "\n";
tilføjer en blank linie før og efter, så uddata bliver overskuelige.
De første par linier af uddata fra scriptet ser sådan ud, når vi kører det i vores DOS-prompt:
Hele noden:ATC Data ApSTangmosevej 87 4600 Køge
Hele noden:PC-Repair CaféGentoftegade 47 2820 Gentofte
Hele noden:TOP ConsultingFeilbergvej 26 St. Darum 6740 Bramming
...og meget mere. Grunden til, at der ikke er mellemrum i mellem de forskellige data, er at de befinder sig i underliggende noder (tags), og det tager vi fat på om et øjeblik.
Noder
Ved at kigge endnu engang på en del af HTML-dokumentet:
<tr><td width="190" valign="TOP" height="40" nowrap class="txtnormgulm">
<b>ATC Data ApS</b>
<br>Tangmosevej 87
<br>4600 Køge</td>
- ser vi, at firmanavnet er indlejret i et <b>-tag, mens resten af adresse-felterne står adskilt med <br>-tags. Ved et kig på HTML-dokumentet i browseren, ser vi, at alle firmanavne står med fed skrift - altså er de indlejret i et <b>-tag, mens selve adressen blot er angivet linie for linie. Vi kan nok ikke regne med, at der altid er præcis to linier med adresseoplysninger, da man ude på landet undertiden også tilføjer et stednavn i adressen. Men det ser altså ud som om, at firmanavnets dataintegritet kan bevares, hvis man skal sige det på den flotte måde.
Vi ændrer nu vores for-løkke i scriptet, så det ser sådan ud:
for $node (@del_trae) {
$boldtag = $node->look_down('_tag','b');
print "\nFirmanavn: ", $boldtag->as_text;
}
Der er kun et bold-tag, nemlig fimanavnet, i hver node, så det fisker vi ud fra $node-variablen med metoden look_down lige som før, men nu er det jo et bold-tag, vi kigger efter, så derfor står der 'b' i parameterkaldet.
Nu ser uddata-linierne sådan ud:
Firmanavn: ATC Data ApS
Firmanavn: PC-Repair Café
Firmanavn: TOP Consulting
...og meget mere. Det går den rigtige vej! Det næste skridt er nu at opsnappe teksten i mellem de <br>-tags, der optræder i hver af vore $node-variable. Vi kan desværre ikke benytte vores sædvanlige metode her, da adresselinierne står imellem <br> tags, og ikke indlejret i et tag. Adresselinerne er såkaldte tekstnoder - de svømmer frit omkring inde i <td>-blokken - så vi må gøre noget, der er en anelse mere kompliceret.
Metoden content_list fra TreeBuilder-modulet giver os en liste af alle noder, der befinder sig i et givent del-træ. Vi skriver vores for-løkke om, så den ser sådan ud:
for $node (@del_trae) {
print "\n";
@del_node_array = $node->content_list;
for $del_node (@del_node_array) {
if(ref($del_node)) {
print "tagnode: ", $del_node->as_text, "\n";
} else {
print "tekstnode: ", $del_node, "\n";
}
}
}
I stedet for at kigge på specifikke tags, kigger vi nu på alle del-noderne inde i <td>-blokken. Vi opsamler alle noderne i arrayet @del_node_array, og udskriver dem i den næste for-løkke. If-betingelsen tester på, om $del_node er et objekt, eller om det er en tekststreng med funktionen $ref(), der returnerer sand (i denne kontekst) hvis $del_node er et objekt.
Nu ser uddata sådan ud:
tagnode: ATC Data ApS
tagnode:
tekstnode: Tangmosevej 87
tagnode:
tekstnode: 4600 Køge
tagnode: PC-Repair Café
tagnode:
tekstnode: Gentoftegade 47
tagnode:
tekstnode: 2820 Gentofte
...og mere endnu. Den første tag-node er firmanavnet, der er indlejret i bold-tags. De to tomme noder er vores <br>-tags, der separerer adresselinierne, og de to (eller om muligt flere) tekstnoder er selvfølgelig vores adresselinier. Vi er næsten ved vejs ende i første delprojekt - nu skal vi blot have oplysningerne formateret, så vi senere kan lægge dem ned i en tekstfil. Og bemærk: Vi har kun brugt 17 liniers kode indtil videre.
for $node (@del_trae) {
print "\n";
@del_node_array = $node->content_list;
for $del_node (@del_node_array) {
if(ref($del_node)) {
if($del_node->as_text ne "") {
print $del_node->as_text, "\t";
}
} else {
$del_node =~ s/\s$//;
print $del_node, ",";
}
}
}
I denne version af for-løkken tilføjer vi en ny if-betingelse på vores tagnoder fra før, således at det kun er den første node, som indeholdt firmanavnet, der bliver udskrevet. Det gør vi ved at se på, om $del_node->as_text returnerer en tom streng. Det gøres med tekstoperatoren ne (not equal) som modsvarer den numeriske != operator.
Vi separerer firmanavn og adressedata med tabulator, hvilket gøres ved at udskrive "\t" i syvende linie af løkken. Adresselinierne separerer vi med komma, og hver enkelt post separeres med linieskift. Så skulle det være nemt at importere og viderebehandle vores fangst i andre programmer som databaser eller regneark. Den lidt kryptiske line
$del_node =~ s/\s$//;
fjerner blot et afsluttende mellemrum i slutningen af hver adresselinie.
Nu ser uddata sådan ud:
ATC Data ApS Tangmosevej 87,4600 Køge,
PC-Repair Café Gentoftegade 47,2820 Gentofte,
TOP Consulting Feilbergvej 26,St. Darum ,6740 Bramming,
- og mere til. Så fik vi styr på dataudtrækningen!
I næste artikel, som kommer i den kommende uge, ser vi på, hvorledes vi fanger resten af resultat-siderne, lærer flere metoder fra HTML::TreeBuilder-biblioteket, og på, hvordan man sikrer sig, at webmasteren ikke bliver sur. Vi får også kigget på Perls subrutiner og andet sjov.