Her måtte Bash og Grep give op: Vi skriver et værktøj til at udtrække data fra aktindsigt

20. marts 2020 kl. 05:0015
Her måtte Bash og Grep give op: Vi skriver et værktøj til at udtrække data fra aktindsigt
Illustration: Bigstock/REDPIXEL.PL.
Kommandolinje-værktøjer kan bruges til meget. Men hvis man ikke kan finde det rigtige til opgaven - hvorfor så ikke bare skrive sit eget?
Artiklen er ældre end 30 dage
Manglende links i teksten kan sandsynligvis findes i bunden af artiklen.

Nu skal det jo ikke handle om COVID-19, det hele. Sådan tænker jeg i det mindste.

En dag fik min kollega en ordentlig bunke aktindsigt fra Datatilsynet, i forbindelse med Aula-lækket, som Version2 afslørede for nylig.

Der var tale om de blanketter, der skal udfyldes, når en virksomhed eller myndighed ved, at der har fundet et databrud sted, og skal anmelde dette til Datatilsynet.

Dokumenterne er som regel PDF’er i et maskinlæsbart format, hvor teksten kan udtrækkes i en eller anden form.

Masser af tekst

Der er tale om 168 dokumenter. Det vil tage en hulens lang tid at copy-paste de relevante informationer til en form, vi kan bruge til noget. Og det er næppe heller sundt for håndleddet. PDF'er er skidt for os, der skal hive tekst ud.

Artiklen fortsætter efter annoncen

I første omgang vil vi gerne have en oversigt over

  • det enkelte dokuments filnavn,
  • myndigheden, der har indsendt aktindsigten,
  • dato og tidspunkt,
  • og så vil vi gerne have det, der står i punktet ‘Beskriv hændelsen’.

Og gerne det hele på en nem og overskuelig facon, såsom en kommasepareret fil, som man kan åbne i et regnearksprogram.

Problemet er i og for sig ikke så svært. Alle blanketterne er ens, og de felter, vi vil udtrække, er omkranset af samme tekst. Altså lige et job for go’e gamle grep.

Første punkt på dagsordenen er at udtrække teksten fra PDF-filerne. Jeg bruger Bash-shell’en i Ubuntu, under Windows Subsystem for Linux (WSL), som jeg plejer.

Artiklen fortsætter efter annoncen

Her kan jeg udtrække teksten med Pdftotext-programmet, ved at navigere hen til mappen med dokumenterne og bruge one-liner’en:

  1. for file in *.pdf; do pdftotext "$file" "$file.txt"; done

Nu er der en tekst-udgave af hver af PDF-dokumenterne, med ‘.txt’ i enden af filnavnet, i mappen.

Tekstmønstre trækker tekst ud

Det går jo strygende. Nu skal vi kigge nærmere på tekstfilerne. Vi vil som sagt gerne opsnappe dato og tid. Vi finder tidspunktet i tekstklumpen:

  1. Skema til indberetning af sikkerhedshændelser
  2.  
  3. Identifikation
  4. Tidspunkt for indberetningen
  5. onsdag maj 15, 2019 13:20:03
  6.  
  7. Referencenummer
  8. a93068d77aca5b0ae2991b8af07021ed6f93da6e
  9.  
  10. Anmelderinformation

Vores tekstmønster i regular expressions - programmeringsverdens universelle sprog til mønstergenkendelse i tekst - skal altså være noget i stil med

  1. Tidspunkt for indberetningen(.*)Referencenummer

Punktum betyder ‘alle tegn’, stjerne betyder ‘nul eller flere gange’, og parentesen angiver en ‘capture group’, som er den tekst, vi vil indfange.

Grep kan ikke finde ud af kun at returnere capture group’en, så jeg bruger alternativet Pcregrep - ‘a grep with Perl-compatible regular expressions,’ som der står på manual-siden.

Det betyder, at jeg kan bruge den helt fantastiske og gratis webapp Regex101.com til på interaktiv vis at forfine mit søgemønster, så den gør det, jeg vil have. Efter lidt nørkleri når jeg frem til dette:

  1. pcregrep -M -o1 "(?s)Tidspunkt for indberetningen(.*?)Referencenummer" 2019-442-4960.pdf.txt

Flaget M står for ‘søg i hele teksten og ikke blot enkelte linjer’, o1 betyder ‘udskriv første capture group’. Det mystiske ‘?s’ i starten af selve søgestregen siger, at punktum-symbolet, som altså betyder 'alle tegn', også matcher linjeskift. Vores match kan altså løbe over linjeskift.

Vores capture group ser nu sådan ud: (.*?) og her betyder spørgsmålstegnet, at gruppen ikke skal være ‘grådig.’

Artiklen fortsætter efter annoncen

Det kræver en lille forklaring. I nogle af PDF’erne er der mere end én blanket, fordi myndighederne har indsendt flere anmeldelser til Datatilsynet og samlet dem i ét PDF-dokument.

Hvis spørgsmålstegnet udelades, finder mønsteret den størst mulige streng, og det er så den, der går fra starten af mønsteret i første dokument og frem til slutningen af mønsteret i det andet dokument. Med spørgsmålstegnet finder vi kun lige det, vi vil have - nemlig dato og tid.

Øv, mit dumme program

Ak! Det er ikke lige så nemt. Jeg bruger en tåbelig mængde tid på at forfine min kommando, som nu er gået hen og blevet til et helt lille script i Bash:

  1. echo "" > ../ud2.txt
  2. for file in \*; do
  3. echo $file >> ../ud2.txt
  4. pcregrep -M -o1 -e "(?s)Tidspunkt for indberetningen(.\*?)Referencenummer" "$file" | sed -e 's/^[[:space:]]\*//' >> ../ud2.txt
  5. echo -e "," >> ../ud2.csv
  6. pcregrep -M -o1 -e "(?s)virksomhedens navn(.\*?)Afdeling" "$file" | sed -e 's/^[[:space:]]\*//' >> ../ud2.txt
  7. done

(For enkelthedens skyld er det her kun de to første felter i filerne, som vi udtrækker.)

Men resultatet er slet ikke så raffineret som det, jeg gerne vil have. Jeg kan ikke se, hvordan jeg kommer fra et tekstfil, der har felterne med, og over i et kommasepareret format.

Det kan dog sagtens lade sig gøre, for bash-scripting og sed er regulære programmeringssprog. Men det bliver næppe enkelt eller elegant.

Jeg må smide håndklædet i ringen og se i øjnene, at det for mig er hurtigere at skrive et program i et sprog, jeg kender.

Men hvorfor ikke gøre en dyd af nødvendigheden og skrive et værktøj, der på simpel vis kan udtrække tekst og pløje det lige ind i et csv-dokument? Det er der nok andre, der kunne få brug for, såsom journalister og andet godtfolk.

Ligesom i eventyret om konen med æggene svimler jeg hen, og forestiller mig en dag, hvor man blot skal skrive

  1. extract2csv

og så

  1. Command 'extract2csv' not found, but can be installed with:
  2.  
  3. sudo apt install extract2csv

Derefter følger berømmelsen og pengene, naturligvis. I hvert fald i fantasien.

Nemme tekstmønstre

Men fantasi eller ej, programmet skal skrives først. Som udgangspunkt forestiller jeg mig, at teksten er udtrukket fra kildedokumenterne med pdftotext, eller docx2txt eller andre programmer der kan udtrække tekst fra forskellige slags formater.

Jeg vender tilbage til webappen Regex101.com, som jeg altså bruger til at udvikle mit søgemønster. Webappen har en rigtig smart funktion, som skriver søgemønsteret om til kode i Perl, Javascript og mere til, som man lige kan kopiere ind i sit program. Jeg bruger Java, og kan godt huske, at jeg syntes at api’et til regular expressions kan være lidt kringlet. Her klikker jeg bare på en knap, og så er mit program halvt færdigt.

Det næste punkt går ud på at skrive den fundne tekst i et kommasepareret dokument. Det gjorde vi i en Version2-artikel for et par år siden, og det var ganske nemt.

Så det springer vi over her.

Jeg tænker mig at den nemmeste syntaks for mønstre er noget i stil med:

  1. extract2csv "Tidspunkt for indberetningen\*Referencenummer" "virksomhedens navn\*Afdeling" "Beskriv hændelsen\*Hvor fandt hændelsen fysisk sted"

Bum - så er der en kommasepareret fil med de rigtige felter, med navnet out.csv, i mappen.

Sådan en simpel kommando kan journalister vel godt finde ud af - i hvert fald sådan nogen af slagsen, vi har her på Version2 og Ingeniøren.

Inde i mit lille program udskifter jeg blot stjernen i det forsimplede søgemønster med regexp-udtrykket (.*?) som vi så tidligere.

Koden er lagt på Github, og der er plads til forbedringer. Jeg har ikke nået at skrive en eneste unit-test, og jeg kan allerede få øje på en kæmpe fejl.

Det ville også være smart med lidt flere muligheder, så som at brugeren selv kan vælge ud-filen, med mere. Og det ville være godt at producere noget nemmere eksekverbart, så som en jar-fil, eller måske compile til maskinkode?

Og hvad med Githubs indbyggede continuous integration, det kan vel også noget? Vi kigger nærmer på mulighederne i næste artikel, så bliv hængende på kanalen.

15 kommentarer.  Hop til debatten
Denne artikel er gratis...

...men det er dyrt at lave god journalistik. Derfor beder vi dig overveje at tegne abonnement på Version2.

Digitaliseringen buldrer derudaf, og it-folkene tegner fremtidens Danmark. Derfor er det vigtigere end nogensinde med et kvalificeret bud på, hvordan it bedst kan være med til at udvikle det danske samfund og erhvervsliv.

Og der har aldrig været mere akut brug for en kritisk vagthund, der råber op, når der tages forkerte it-beslutninger.

Den rolle har Version2 indtaget siden 2006 - og det bliver vi ved med.

Debatten
Log ind eller opret en bruger for at deltage i debatten.
settingsDebatindstillinger
15
24. marts 2020 kl. 10:30

Hvor er det trist at se, at Datatilsynet ...

Det ved jeg ikke helt. Måske er der fornuftigt nok.

Nu ved jeg ikke hvad format de aktuelle PDF-filer er, men en delmängde af PDF formaterne (PDF/A ...) er en ISO-standard, ISO 19005-1:2005, som er udviklet til 'long term archiving' og formatet bruges en hel del rundt omkring, isär i USA.

"Det Offentlige" burde derfor også bruge PDF/A. Jeg mener at man bare må leve med at lobbyisterne også vandt denne runde. PDF er en standard i dag, så man kan lige så godt få det bedste ud af situationen som den nu er.

Standardiseringen betyder at man kan anvende färdigt arkiv-software i stedet (måske udvikle noget Open Source API til offentlige dokumenter eftersom skatteyderne betaler, men det sker jo nok ikke)!


PS:

Man kan (det meste af tiden) konvertere de her stupide PDF-er som kun består af en TIFF-blok indpakket i PDF (som min billige scanner laver) til PDF/A format med noget software der hedder OCRmyPDF.

Det er også meget nyttigt hvis man lige har väret til en konference og har fået et par tusind "aller-laveste-fällesnävner digitaliserede" papers som man da også gerne ville lade sin MacBook söge i bagefter.

14
23. marts 2020 kl. 08:38

@Jens, For dokumentets/pdf'ens indhold er det jo nok at klikke >File > Export to > og så vælge sit format; Rich text eller andet. Det giver en glimrende tekst-fil som kan manipuleres efter forgodtbefindende. Efter at have nærlæst, så ser det ud til at vi i første omgang skal bruge meta-data. Det kan vi ikke umiddelbart trække ud uden at "programmere". Det kan gøres med vb-scripting (det har jeg selv tidlgiere benyttet) eller det kan også gøres med java-scripting inde fra Acrobat.

13
22. marts 2020 kl. 04:23

Gentagne kald til sed vil ikke virke hvis du skal finde tre felter (og det var opgaven), men hvis vi skal lave det hack (og det er et hack) så kan det gøres i et huk, ligesom det her:

  1. baldur@DESKTOP-5F7IMRQ:~$ amm
  2. Loading...
  3. Welcome to the Ammonite Repl 2.0.4 (Scala 2.13.1 Java 11.0.6)
  4.  
  5. @ scala.io.Source.fromFile("test.txt").getLines.mkString(" ").replaceFirst(".<em>indberetningen (.</em>?) +Referencenummer +(.<em>?) +Anmelderinformation.</em>","test.txt;$1;$2")
  6.  
  7. res0: String = "test.txt;onsdag maj 15, 2019 13:20:03;a93068d77aca5b0ae2991b8af07021ed6f93da6e"

12
21. marts 2020 kl. 19:17

mere hug :-)

1000 filer 4 sekunder :-)

output i fil

  1. det er en linie :-)
  2. onsdag maj 15, 2019 13:20:03 ; a93068d77aca5b0ae2991b8af07021ed6f93da6e

kode

  1. #!/bin/bash
  2.  
  3. if [ "$#" -ne 1 ]; then
  4. echo "Illegal number of parameters"
  5. exit
  6. fi
  7.  
  8. head -n 1000 $1 | tr '\n\r' ' ' | sed 's/.<em>beretningen//' | sed 's/Referencenummer/ ; /' | sed 's/Anmelder.</em>//' | sed 's/$/\n/' > $1.csv
  9.  

11
21. marts 2020 kl. 17:22

meget flot og god kode folk skriver - jeg mener det :-). Men tror måske at newcommers til scripting verden står af-

så tilladt mig et enkelt simpelt bondescript :-)

Problemet er tilfældige linineskift så vidt jeg kan gennemskue. TIl gengæld er der kendte faste ord som "delimiters". Så jeg snupper lige de første 1000 linier i filen , fjerner linie skift og indsætter så linieskift før efter de kendte ord. Klipper filen(linier) fra starten af og hver gang der er en interesant linie klipper jeg den ud gemmer den. bash, head, tail, sed, cat og tr er vist det hele.

Det kunne være folk der ikke er experter kan stave sig igennem det. Helt efter KISS :-)

Det dumpes filer hele vejen så uden alt det echo ca 40 linier der sagtens kan komprimeres ineffektiv kode, men på en anden side

1000 filer 15 sekunder(uden udskrift) - en hel dagsrejse :-) på min gamle latitude e7440

men så kan jeg også forstå mit lille micro script om 3 år ;-)

Så ej til produktion men til diy hjemme :-) og måske til at komme igang med gamle linux/unix progs :-)

mvh JEns

  1. #!/bin/bash
  2.  
  3. if [ "$#" -ne 1 ]; then
  4. echo "Illegal number of parameters"
  5. exit
  6. fi
  7.  
  8. rm -f tmp*
  9.  
  10. head -n 1000 $1 > tmp.1000
  11.  
  12.  
  13. echo "remove line shift"
  14. tr '\n\r' ' ' < tmp.1000 > tmp.step1
  15. cat tmp.step1
  16. echo ""
  17.  
  18. echo "rm all uptil beretningen"
  19. sed 's/.*beretningen//' < tmp.step1 > tmp.step2
  20. cat tmp.step2
  21. echo ""
  22.  
  23. echo "insert line shift before Ref(erencenummer)"
  24. sed 's/Ref/\nRef/' < tmp.step2 > tmp.step3
  25. cat tmp.step3
  26. echo ""
  27.  
  28.  
  29. echo "first line is date - save it"
  30. head -1 tmp.step3 | sed 's/ *//' > tmp.date
  31. cat tmp.date
  32.  
  33. echo "remove first line (date line)"
  34. tail -n +2 tmp.step3 > tmp.step4
  35.  
  36. echo ""
  37.  
  38. echo "insert lineshifte after ...nummer"
  39. sed 's/nummer/nummer\n/' < tmp.step4 > tmp.step5
  40. cat tmp.step5
  41. echo ""
  42.  
  43. echo "insert lineshifte bef Anmelderr"
  44. sed 's/Anmelder/\nAnmelder/' < tmp.step5 > tmp.step6
  45. cat tmp.step6
  46. echo ""
  47.  
  48. echo "take line no 2 = ref nummer"
  49. tail -n +2 tmp.step6 | head -1 | sed 's/ <em>//' > tmp.refnummer
  50. echo "result"
  51. cat tmp.refnummer
  52. cat tmp.date
  53.  
  54. echo "make oneliner with ext cvs ins linefeed at end"
  55. cat tmp.date tmp.refnummer | tr '\n' ';' | sed 's/;$/\n/' > $1.cvs
  56.  
  57. rm tmp.</em>
  58.  

9
20. marts 2020 kl. 19:59

Hvorfor ikke bare eksportere en txt fra pdf'en og behandle den som man har behov for? Acrobat kan det meste selv. Ja, der er 168 dokumenter, men Acrobat kan behandle "multiple documents" uden bøvl. Som skrevet: Eli5, jeg forstår ikke behovet for at fedte med kode for at udtrække den slags fra en pdf?

8
20. marts 2020 kl. 16:42

Måske kunne awk have været brugt?

7
20. marts 2020 kl. 14:10

Traditionelt bruger mange Perl eller Python når Bash ikke er nok. Jeg har fundet at Ammonite er et stærkt alternativ hvis man har baggrund i Java. Det er godt nok Scala men man har automatisk adgang til alle Scala og Java biblioteker. Det er bare en script fil ligesom bash og det skal ikke oversættes/compiles før det køres.

Den første import henter automatisk det nødvendige CSV bibliotek første gang scriptet køres.

Lidt længere end Perl programmet men Perl programmet gør hellere ikke præcis det samme som Java programmet.

Ammonite: https://ammonite.io/

  1. #!/usr/bin/env amm
  2.  
  3. import $ivy.<code>com.github.tototoshi::scala-csv:1.3.6</code>
  4. import com.github.tototoshi.csv.CSVWriter
  5. import java.nio.file.Files
  6. import java.nio.file.Paths
  7.  
  8. val defaultFilename = "out.csv"
  9.  
  10. @main
  11. def text2csv(patternList: String*) = {
  12. val csv = CSVWriter.open(defaultFilename)
  13.  
  14. val filesInDir = Paths.get("").toAbsolutePath.toFile.listFiles.toList.map(<em>.toPath).filter { file =>
  15. Option(Files.probeContentType(file)).map(</em>.contains("text/plain")).getOrElse(false)
  16. }
  17.  
  18. val regexList = patternList.map(_.replace("<em>", "(.</em>?)")).map( p => s"(?s).<em>?$p.</em>".r ).toList
  19.  
  20. for ( file <- filesInDir ) {
  21. val text = scala.io.Source.fromFile(file.toString).mkString
  22. val row = for (regex <- regexList) yield text match {
  23. case regex(str) => str
  24. case _ => ""
  25. }
  26. csv.writeRow(file.toString :: row)
  27. }
  28.  
  29. csv.close
  30. }

6
20. marts 2020 kl. 13:36

Smukt, Simon! Og dejligt fri for forstyrrende kommentarer :)

5
20. marts 2020 kl. 13:35

Det er da ikke så lidt bedre end at aflevere word-filer!

Men du har da ret i at noget passende grupperet JSON ville være meget lettere at håndtere.

4
20. marts 2020 kl. 13:32

Hvor er det trist at se, at Datatilsynet af alle myndigheder fortsat benytter indrapportering via PDF ... i 2020!!!

3
20. marts 2020 kl. 12:55

  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4.  
  5. print("filename;time;refnum\n");
  6. while (<*.pdf.txt>) {
  7. my ($filename, $filehdl, $text);
  8. $filename = $_;
  9. open($filehdl, $filename) or die;
  10. $text = join('', <$filehdl>);
  11. print($filename);
  12. print(($text =~ /Tidspunkt for indberetningen[\r\n]+(^.+$)/gm) ? ";$1" : ";ERROR");
  13. print(($text =~ /^Referencenummer[\r\n]+(^[^ ]+$)/gm) ? ";$1" : ";ERROR");
  14. print("\n");
  15. }

2
20. marts 2020 kl. 11:26

Jeg har selv tusinder af PDF-filer, der er elendigt navngivet. Dem graver jeg på samme måde ned i, og udtrækker relevante stumper. Da de modsat jeres opgave er meget forskellige, skraber jeg et antal muligheder fri, og tester dem så mekanisk for brugbarhed.

Herefter sidder jeg med en kommasepareret fil som i jeres eksempel, hvor gode navne på måske 80 procent af PDF-filerne relativt nem kan klippes fri. Herfra navngives dokumenterne, og Document Properties udfyldes. Jeg kalder diverse hjælpeprogrammer via PHP, som jeg bruger til at afvikle det hele med.

Samme mekanik har jeg brugt til at rense og ensrette gamle HTML-sider med, som var skrevet i et utal af editorer. En del af dem er så puttet pænt på plads i CMS-kasser med CSV hvor det var muligt, og ellers med en trænet auto-mus. Dejligt systematisk arbejde :).