Mere databasemalkning med Perl

I sidste uges artikel trak vi data ud fra HTML-sider ved at analysere HTML-koderne og benytte Perls TreeBuilder-bibliotek. I artikel nummer to ser vi på, hvorledes man henter alle resultatsiderne fra en søgning, holder sig gode venner med webmasteren, og ser på ophavsrettigheder i forbindelse med anvendelser af dataudtræk.

Et kort resume

I den sidste artikel så vi på, hvorledes vi nemt kunne indlæse selv et ikke-velformet HTML-dokument i Perl-biblioteket TreeBuilders træ-objekt. Lad os her kort resumere den metode, vi brugte til at identificere og udtrække de data, vi ønsker at få fingrene i.

Først downloadede vi den side, som skulle bruges som udgangspunkt for fangsten. I almindelighed vil det være den første resultatside ved en søgning.

Ved at åbne dokumentet i en teksteditor, og søge på en tekststreng i et af de datasæt, vi ønsker at udtrække, kunne vi identificere omgivelserne for datasættet. Med omgivelserne menes her de HTML-tags, der står i forbindelse med tekststrengen. Som vi så, er det ikke nok med et enkelt <B>-tag eller tilsvarende - vi skal finde et eller andet, der er mere entydigt. I eksemplet kunne vi finde et <TABLE>-tag der havde attributten WIDTH sat til en bestemt værdi, og på den måde kunne vi kredse os ind på datasættet.

Derefter indlæste vi HTML-dokumentet i en træstruktur ved hjælp af TreeBuilders tree-objekt. Træstrukturen er defineret udfra den måde som HTML-tags er indlejret under hinanden, på samme facon som det kendes fra dokument objekt-modellen (DOM).

Så brugte vi metoden content_list, der returnerer alle undernoder i et deltræ, og derudover kan metoden parent være ganske nyttig. Den returnerer den overliggende node i HTML-træet, og på den måden kan man via content_list og parent navigere sig rundt i i træstrukturen. I praksis viser det sig ofte at være ganske nemt at isolere de relevante datasæt.

En sidste metode fra TreeBuilder der kan være nyttig, er dump, som udskriver et helt træ eller et deltræ. Det kan man benytte til at kredse sig ind på den information, man leder efter, og dermed finde ud af hvor man skal navigere hen i træet.

Masser af sider

Masser af sider
Som sagt fik vi udtrukket de relevante data fra den første resultatside. Men ved den søgning, vi udførte i eksemplet, er der faktisk i alt 10 sider med resultater. Lige som vi sidst isolerede datasættet fra den omgivende HTML, gør vi det samme med de links, der peger på de efterfølgende sider. Resultatsider er som regel organiseret ved, at der enten er et "Næste side"-link, nederst på siden, eller en nummeret liste, med links til side 2, 3 og så videre.

Før vi går videre, vil vi lige gøre opmærksom på en fælde, man kan falde i, og det er at kigge på parametrene i den querystring, der returnerer resultatsiderne. Som regel vil den indeholde en angivelse af hvor resultaterne starter, som i eksemplet:

/mos/mos_sog.actionquery?p_faggruppeid=30805&Z_START=26

Ved at kigge på de efterfølgende sider, kan man se, at parameteren Z_START vokser. Her af kunne man foranledes til at tro, at der er en direkte sammenhæng i mellem Z_START og antallet af resultater pr side, således at Z_START=1, Z_START=26 på side 2, Z_START=51 på side 3 og så videre. Det kan godt være at metoden rent faktisk virker i et konkret tilfælde, men i almindelighed er denne antagelse farlig, da man ikke reelt ved hvorledes den bagvedliggende logik bliver påvirket at parametrene. Således kunne man forestille sig, at en eller flere rækker i en indekseret tabel blev slettet ved opdatering, og så bliver det mere usikkert, præcist hvilke data, man faktisk får ud af det hele.

Det sikreste er altså at bruge de links som er beregnet til det samme. Og de links henter vi på samme måde, som da vi isolerede datasæt.

Nederst til højre på siden finder vi de links, der peger videre på de efterfølgende sider, og teksten på det første link er ganske enkelt "Side 1", så vi åbner HTML-dokumentet i WordPad, og søger på strengen "Side 1". Den befinder sig i omegnene af følgende tags:

<td align="right" valign="MIDDLE" nowrap class="txtnorm">Side 1</b>
<A HREF="mos_sog.actionquery?p_faggruppeid=30805&Z_START=26"><font color="#000000"><b>2</b></font></A>
<A HREF="mos_sog.actionquery?p_faggruppeid=30805&Z_START=51"><font color="#000000"><b>3</b></font></A>

Der er ikke noget link på side 1 - det er jo den aktuelle side, som vi ser lige nu. Vi benytter som tidligere metoden look_down fra TreeBuilder, som kigger efter de noder, hvis egenskaber vi tilføjer som argumenter. Vi ser i HTML-koden herover, at der står et <font>-tag lige inde under det <A>-tag, hvis HREF-attribut vi ønsker at få fingrene i. Vi kigger nu på forældre-noden til font-tagget, med følgende stump:

use HTML::TreeBuilder;

my $tree = HTML::TreeBuilder->new; # empty tree
$tree->parse_file("testfil.html");

@del_trae = $tree->look_down('_tag','font', 'color', '#000000');

for $node (@del_trae) {      
   $foraelder = $node->parent;
   $foraelder->dump;
}

Det giver en masse linier af denne slags:

<a href="mos_sog.actionquery?&ampp_faggruppeid=30805&ampZ_START=26"> @0.1.0.0.2.0.3.0.0.1.0.0.0.0.0.0.0.0
.0.1.1
<font color="#000000"> @0.1.0.0.2.0.3.0.0.1.0.0.0.0.0.0.0.0.0.1.1.0
<b> @0.1.0.0.2.0.3.0.0.1.0.0.0.0.0.0.0.0.0.1.1.0.0
"2"

Det, vi ser her, er forælder-noden til <font>-tagget, og den giver os helt præcist de <A>-tags, vi ønsker.

Vi kigger nu på $foraelder->as_text i stedet for blot at dumpe. Så skal løkken se sådan ud:

for $node (@del_trae) {      
   $foraelder = $node->parent;
   print $foraelder->as_text, "\n";
}

Og det giver os tallene inden for linkene, nemlig 2, 3, 4, og så videre. Hvis vi så udskifter $foraelder->as_text med $foraelder->attr('href'), som returnerer værdien af HREF-attributten i <A>-tagget, skulle vi gerne få de links, der peger på de efterfølgende sider. Og det gør vi helt præcist:

a href="mos_sog.actionquery?&ampp_faggruppeid=30805&ampZ_START=26

Nu skal vi jo kun hente en side ad gangen, først side 1, så side 2 og så videre, så derfor skriver vi rutinen om til en subrutine, som Perl kalder underprogrammer.

Subrutiner og succes

Subrutinen ser sådan ud:

sub naesteHtmlDokument {
   $sideNummer = $_[0];

   @del_trae = $tree->look_down('_tag','font', 'color', '#000000');
   for $node (@del_trae) {      
      $foraelder = $node->parent;
      if($foraelder->as_text eq $sideNummer) {
         return $foraelder->attr('href');
      }

   }
}

Det reserverede ord sub deklarerer starten på subrutinen - det kender Visual Basic folket godt, og for os andre er det ligesom function i sprog som C, PHP og JavaScript.

Den første lidt kryptiske linie

$sideNummer = $_[0];

henter subrutinens første argument. I Perl hedder argumenterne til et subrutinekald nemlig $_[0], $_[1], $_[2] og så fremdeles. Senere kan vi så kalde naesteHtmlDokument() med sidenummeret som argument.

Linierne

if($foraelder->as_text eq $sideNummer) {
   return $foraelder->attr('href');
}

ser på, om teksten indenfor <A>-tagget er lig med tallet, der er gemt i $sideNummer, som altså er 2, når vi kalder naesteHtmlDokument(2). Operatoren "eq" (equal) er Perls strengoperator for sammenligning, som modsvarer den numeriske operator ==. Tallet 2 var linket til resultatside 2, tallet 3 linker til tredie resultatside og så videre.

Nu, hvor vi er istand til at udtrække linket til den efterfølgende resultatside, er vi parat til at gennemføre vores udtræk. Vi pakker koden fra sidste artikel ind i en subrutine, så programmet bliver overskueligt. Så ser det færdige program sådan ud:

use LWP::Simple;
use HTML::TreeBuilder;

$tree = HTML::TreeBuilder->new; # empty tree

# lav en ny fil til at gemme resultaterne i
open(UDTRAEK, '>', 'udtraek.txt');

sub findData {
   $tree->parse($htmlDokument);
   @del_trae = $tree->look_down('_tag','td', 'width', '190');
   for $node (@del_trae) {
      print UDTRAEK "\n";
      @del_node_array = $node->content_list;
      for $del_node (@del_node_array) {
         if(ref($del_node)) {
            if($del_node->as_text ne "") {
               print UDTRAEK $del_node->as_text, "\t";
            }
         } else {
            print UDTRAEK $del_node, ",";
         }
      }
   }
}

sub naesteHtmlDokument {
   $tree->parse($htmlDokument);
   $sideNummer = $_[0];
   @del_trae = $tree->look_down('_tag','font', 'color', '#000000');
   for $node (@del_trae) {      
      $foraelder = $node->parent;
      if($foraelder->as_text eq $sideNummer) {
         return $foraelder->attr('href');
      }
   }
}

$url = "http://www.søgetjeneste.dk/mos/mos_sog.actionquery?P_FAGGRUPPEID=30805";

for $sider (1..10) {
   print "Henter nu side ", $sider, "\n";
   print "Adresse: ", $url, "\n";
   $htmlDokument = get $url;
   print "Behandler nu side ", $sider, "\n";
   findData();
   $url = "http://www.søgetjeneste.dk/mos/".naesteHtmlDokument($sider);
   print "Sover eet minut.\n";
   sleep(60);
}

print "\n\nFærdig.";

Koden i den første subrutine findData() er identisk med koden fra sidste artikel, med meget få ændringer: I stedet for at udskrive til konsollen - som er DOS-vinduet i denne sammenhæng - så udskriver vi til en fil, som vi kalder udtraek.txt, og som bliver oprettet i sammme mappe som den programmet ligger i.

Derudover læser vi ikke længere HTML-kode fra vores testfil, men fra variablen $htmlDokument, som bliver tildelt HTML-kildekoden fra det aktuelle dokument, og det foregår nede i hovedprogrammet.

Lad os kigge på hovedprogrammet. Først tildeler vi variablen $url adressen på den side, som er udgangspunktet for udtrækket. Så gennemløber vi en løkke. I dette tilfælde ved vi, at der er 10 resultatsider, for det skrev søgetjenesten på resultatsiderne. Hvis vi ikke ved, hvor mange sider der er, må vi kigge efter et "Næste"-link, og stoppe, når vi ikke længere finder et sådant link, men her er situationen altså lidt nemmere.

Vi udskriver nogle statusmeddelser til DOS-vinduet, så vi kan se hvor langt vi er kommet i processen. Vi henter det aktuelle dokument med linien

$htmlDokument = get $url;

Dernæst udtrækker vi data med subrutinen findData(), og så finder vi adressen på den næste side ved at benytte vores subrutine naesteHtmlDokument(), som vi kalder med sidenummeret, som er gemt i variablen $sider. Da linket i HTML-dokumentet er relativt, skal vi sætte "http://www.søgetjeneste.dk/mos/" for at få den absolutte adresse til søgesiden.

Og så sker der noget kryptisk: Sætningen sleep(60), som får programmet til at vente 60 sekunder, før den næste side hentes. Forklaringen kommer om et øjeblik, men først: Virker det overhovedet?

Følgende skærmdump, hvor tekstfilen "udtraek.txt" er kopieret og indsat i et regneark, taler for sig selv. Disse data er øjensynligt for fantastiske til at være fiktion.


46 liniers Perl-kode og et par timers arbejde giver over 200 linier i regnearket.

En god robot

En god robot opfører sig ordentligt
Det, vi har konstrueret her, er hvad man kalder en robot. Den opfører sig om, der sad en person bag skærmen og sendte forespørgsler af sted til webserveren. Det smarte ved robotter er, at de kan udføre opgaver ganske meget hurtigere end mennesker. Men hvis man blot sender en masse forespørgsler af sted til webserveren i den anden ende, kan man i værste tilfælde risikere, at serveren går ned, og i bedste tilfælde, at andre brugere ikke kan komme i kontakt med serveren, fordi alle serverens processer er i gang med at udfører opgaver for vores sultne og hurtige robot.

Her kommer netetikette for robotter ind i billedet. Den siger, at man skal vente mindst et minut imellem forespørgsler. Det kan virke som langt tid, men i dette eksempel fik vi faktisk udtrukket over 200 rækker på blot 10 minutter, hvilket jo ikke kan siges at være galt. Der er også andre grunde til at opføre sig ordentligt ,udover det at være en god robot. Hvis webmasteren i den anden ende opdager, at tjenesten bliver blokeret af en robot, der systematisk laver forespørgsler, kan webmasteren faktisk blokere alle forespørgsler fra det IP-nummer, robotten sender fra. Hvis robotten sidder bag en firewall, kan det betyde, at hele virksomhedens adgang til søgetjenesten forsvinder.

Derudover er det simpelthen uprofessionelt ikke at tage hensyn til, hvorledes ens robotter opfører sig på nettet. Derfor bør robot-etiketten og de 60 sekunders pause overholdes nøje.

Man bør også foretage sine forespørgsler på et tidspunkt, hvor man forventer at der er mindst trafik til serveren, for eksempel klokken 1 om natten.

I Windows gøres det nemt med den indbyggede Opgavestyring, og på Unix har man Crontab til samme formål. Eller man kan blot starte programmet med følgende linie i toppen, hvis man sætter det i gang kl 17.00 om eftermiddagen:

sleep(60*60*8); # sov 8 timer

Ophavsrettigheder
Oplysninger i databaser er - ligesom det meste på nettet - underlagt de almindelige regler om ophavsret. Man må altså godt malke en database og benytte den til privat brug, men man må ikke distribuere eller publicere den information, man har hentet. Det omfatter i almindelighed også publicering på intranet eller andre typer af publicering i lukkede fora. Man må heller ikke bygge "front-ends" til for eksempel intranet-tjenester, der udtrækker og behandler andres data.

Der er dog undtagelser, som for eksempel Open Source Directory-projektet, som er en vejviser ligesom Yahoo, men med den forskel, at data er omfattet af en Open Source licens. I andre tilfælde kan information benyttes frit, mens den underliggende datastruktur eller anden type af forædling er omfattet af ophavsrettigheder. Hvis man tænker på at benytte sine lånte data til en eller anden type af offentliggørelse, er det altså klogt at konsultere juridisk bistand først.




Brancheguiden
Brancheguide logo
Opdateres dagligt:
Den største og
mest komplette
oversigt
over danske
it-virksomheder
Hvad kan de? Hvor store er de? Hvor bor de?
Ciklum ApS
Offshore software- og systemudvikling.

Nøgletal og mere info om virksomheden
Skal din virksomhed med i Guiden? Klik her

Kommende events
Computerworld Cloud & AI Festival 2025

Med den eksplosive udvikling indenfor cloud & AI er behovet for at følge med og vidensdeling større end nogensinde før. Glæd dig til to dage, hvor du kan netværke med over 2.400 it-professionelle, møde mere end 50 it-leverandører og høre indlæg fra +90 talere. Vi sætter fokus på emner som AI; infrastruktur, compliance, sikkerhed og løsninger for både private og offentlige organisationer.

17. september 2025 | Læs mere


IT og OT i harmoni: Sikring uden at gå på kompromis med effektiviteten

IT og OT smelter sammen – men med risiko for dyre fejl. Få metoder til sikker integration med ERP, kundesystemer og produktion. Tilmeld dig og få styr på forskellene og faldgruberne.

24. september 2025 | Læs mere


NIS2: Vi gør status efter tre måneder og lærer af erfaringerne

Vær med, når vi deler oplevelser med implementering af NIS2 og drøfter, hvordan du undgår at gentage erfaringerne fra GDPR – og særligt undgår kostbar overimplementering.

30. september 2025 | Læs mere