Register NOW: welke bedrijven hebben een voorschot aangevraagd?

Ingediend door Dirk Hornstra op 02-aug-2020 20:30

Toen in maart de maatregelen voor Corona actief werden, werden veel bedrijven getroffen. Restaurants, de kapper, het café, geen klanten meer, geen inkomsten, maar nog wel de (vaste) uitgaven. Hiervoor heeft de overheid toen de NOW-regeling opgezet, zodat bedrijven een voorschot van de overheid kunnen krijgen. Omdat dit publiek geld is, heeft het UWV de opdracht gekregen om dit inzichtelijk te maken. Informatie hierover is te vinden op deze pagina van het UWV: link en daar heb je de link naar het PDF bestand met het overzicht: link.

Het toont de bedrijven, de vestigingsplaats en het uitgekeerde voorschotbedrag. Dit is een voorlopige versie, bedrijven kunnen besluiten hun aanvraag in te trekken en definitieve bedragen kunnen afwijken van het aangevraagde bedrag. Maar toch ben ik wel benieuwd, want het zijn ongeveer 2.045 pagina's met bedrijven en hoewel ik aan het begin van mijn 3e jaar accountancy overgestapt ben naar Hogere Informatica word ik toch nog getriggerd door getallen.

Wat wil ik weten op basis van deze gegevens?

  • Hoeveel is er in totaal overgemaakt (staat misschien wel ergens, dan heb ik er overheen gekeken)?
  • In welke plaatsen zijn de meeste aanvragen gedaan (maar zoals mijn collega Anne terecht opmerkt: als het hoofdkantoor in Amsterdam zit, zal de aanvraag daar gedaan zijn, terwijl de vestigingen over het land verdeeld zijn)?
  • Wie zijn de 10 bedrijven die de hoogste bedragen aangevraagd hebben?
  • Kan ik een totaalbedrag per provincie bepalen?
  • Hoe zit het met de IT-sector in Friesland, waar ik ook in actief ben?

 

Voor de mensen die geen tijd hebben en de conclusie willen lezen, die zetten we dan hier neer. Hoe ik tot dit resultaat gekomen ben staat hieronder.

Conclusie

Ik ben met de verkeerde materialen aan de slag gegaan. Ik heb geprobeerd om in C# een gegenereerd tekstbestand van de PDF om te zetten naar een CSV-bestand, maar dat is jammerlijk mislukt. Dit komt doordat in het resultaat je niet netjes de kolommen naast elkaar krijgt, maar in een groot deel de bedragen onderaan de pagina (waarbij je ze zelf weer aan het juiste bedrijf moet koppelen). En in de code is niet altijd duidelijk of je dus op de plek van de plaats of van de naam van het bedrijf zit. Maar zo zitten er meer adders onder het gras.

Na mijn inschatting van "dit is 2 uur werk" ben ik er nu wel bijna 50 uur mee bezig geweest denk ik, ik heb nog andere dingen te doen, dus ik gooi de handdoek in de ring.
Hornstra is out.

Maar Ansien is "in". Deze gast heeft met PHP de boel verwerkt en het netjes op een site gezet: https://now-inzichtelijk.nl/ en daarvan zijn de gegevens ook op Github terug te vinden: https://github.com/now-inzichtelijk-nl/now-inzichtelijk-nl-website, ik heb natuurlijk even in de code gekeken en zag in now-inzichtelijk-nl-website/src/Command/ConvertFirstBatchCommand.php dat hij gebruik heeft gemaakt van Smalot\PdfParser\Parser, deze parser levert blijkbaar een betere tekst-export dan pdftotext.com die ik gebruikt heb. Ook hier zie ik een paar checks dat met user-input wordt gevalideerd of iets een vestigingsplaats is of een bedrijfsnaam. Met dank aan mijn collega Robin Bos die op GeenStijl een artikel met verwijzing naar die webpagina vond.

Omdat ik er wel "helemaal klaar mee ben" ben ik niet meer naar de antwoorden op mijn vragen gaan zoeken. Als je zelf interesse hebt in de data kun je gewoon naar now-inzichtelijk.nl gaan, je kunt daar ook de .CSV downloaden en helemaal los gaan. Succes!

Ik heb in ieder geval kunnen lachen om bepaalde bedrijfsnamen, "Hotel Restaurant In Den Verdwaalden Koogel B.V." gaf mij de vraag of dat misschien de stamkroeg van Willem Holleeder zou zou en toen ik "Naturistenvereniging de Vrije Vogels" tegen kwam vroeg ik me af of er ook één met de naam "Naturistenvereniging de bungelende balzak" zou zijn, maar nee, die bestaat niet. Doe er je voordeel mee.

 

Hoe ben ik tot dit resultaat gekomen?

Het volgende verhaal laat meer de weg zien die ik gevolgd heb en wat developers over het algemeen het moeilijkste van hun werk vinden: "de tijd schatten die een opdracht nodig heeft".

De versie die 17 juli online stond heb ik als databron gebruikt. Een collega zei al, op zich kun je dit heel snel doen. Je kopieert alles naar Excel en dan kun je daar zaken optellen, sorteren en andere dingen met draaitabellen doen. Dus dat heb ik als "snelle actie" gedaan. Tenminste, ik dacht dat het een snelle actie zou zijn...

Als je me van tevoren had gevraagd, kopieer even de data uit de PDF naar een Excel bestand, hoeveel tijd kost je dat? Dan was mijn inschatting geweest:

 


Beetje kopiëren/plakken. Ik zie dat er wel op elke pagina een koptekst staat. En je zult ook de paginanummers mee nemen. Hmmm... 2 uren werk dan maar?

Dus ik ben eerst even gaan kijken of ik de boel kon overzetten. Het kopiëren van de teksten werkt niet als je de PDF in je browser toont. Als je deze in Acrobat Reader opent, alles selecteert en kopieert en vervolgens in Visual Studio Code plakt, dan heb je een goed begin. Wel zijn bedrijfsnaam, plaats en bedrag nu ieder op een losse regel geplaatst. Of toch niet, want er wordt maar 1 pagina geselecteerd. Oké, dit wordt dus een uitdaging. Dan maar even een zoekactie op PDF to TXT en ik kom zo op pdftotext.com. Deze levert me een tekstbestand op. Let op, als je zelf iets gaat doen met een PDF waar data in staat wat je niet met de rest van de wereld wilt delen, dan zou ik adviseren om hier een andere tool voor te nemen en het niet via een online partij uit te voeren. In mijn geval gaat het om een publiekelijke PDF, dus is het geen probleem. Het tekstbestand wat ik terug krijg bevat de bedrijfsnaam, plaats en bedrag nog steeds over regels gescheiden, dus ik moet een eigen parser hiervoor maken.

We hebben met onze back-end developers het boek "Clean Code" van Uncle Bob doorgenomen, laat ik dat dan in de praktijk brengen. Door het probleem gestructureerd aan te pakken komen we tot een goede oplossing (hoop ik).

Stap 1: de "kop-regel"

Elke pagina heeft een kop-regel met de tekst "BEDRIJFSNAAM", "VESTIGINGSPLAATS" en "UITBETAALD VOORSCHOTBEDRAG". Dus laat ik de hele tekst verwerken, en als ik deze teksten tegen kom weet ik: hier begint een nieuwe pagina (en is dus de huidige pagina afgelopen). Zo kunnen we in ieder geval elke pagina later "los" gaan aanpakken. Het begin is trouwens niet fijn, want hier heb je "BEDRIJFSNAAM", lege regel, echte bedrijfsnaam (dus eigenlijk regel 1) en dan "VESTIGINGSPLAATS". Dat vang ik af door als mijn huidige buffer kleiner dan 10 regels is, het bij de huidige pagina te voegen.


Register NOW
1
BEDRIJFSNAAM
T GULDEN BOEK ( DE CHRISTELIJKE BOEKENCENTRALE ) B.V.
VESTIGINGSPLAATS
UITBETAALD
VOORSCHOTBEDRAG
HOUTEN
29.646
!PET PROJECTINRICHTERS B.V.
HOOGEVEEN
31.956

Reset: stap 1: de "kop-regel"

Dit blijkt dus niet te werken. Ergens rond pagina 1056 komt "UITBETAALD VOORSCHOTBEDRAG" helemaal onderaan de pagina, waardoor 1056 en 1057 als 1 pagina beschouwd worden. We gaan voor een alternatief, je hebt eerst het paginanummer en dan "BEDRIJFSNAAM". Dat werkt een stuk beter.


METAALDRAAIERIJ DONDORFF B.V.
BEILEN
30.741
METAALGIETERIJ VAN GILST V.O.F.
KERKRADE
10.158
MESSAGE AS
UITBETAALD
VOORSCHOTBEDRAG
7.908
6.291
4.701
115.404
4.500
2.337
5.343
2.502
32.277
4.389
220.986
74.709
24.816
110.469
1.986
31.200
8.577
1056
BEDRIJFSNAAM

Voor de volledigheid even wat snippets uit mijn code, zodat je kunt zien hoe de pagina-objecten gevuld worden. Je ziet dat er voor bedrijfsnaam nog een line-feed commando zit (\f).


        private static readonly string[] _PdfLines = File.ReadAllLines(@"C:\locatie\register-now-100720.txt");
        private static List<string> _Pages = new List<string>();

        public void ParseTextToPages()
        {
            var triggers = new string[] { "BEDRIJFSNAAM", "VESTIGINGSPLAATS", "UITBETAALD", "VOORSCHOTBEDRAG" };
            var pageContent = new StringBuilder();
            var pageCounter = 2;
            var started = false;
            var containsData = false;
            foreach (var line in _PdfLines)
            {
                if (triggers.Contains(line.Replace("\f", "")))
                {
                    started = true;
                    containsData = true;
                    pageContent.Append("\n");
                }
                else if (started)
                {
                    if (line.Trim() == pageCounter.ToString())
                    {
                        _Pages.Add(pageContent.ToString());
                        pageContent.Clear();
                        pageCounter++;
                        containsData = false;
                    }
                    else
                    {
                        pageContent.Append(line + "\n");
                    }
                }
            }
            if (containsData)
            {
                _Pages.Add(pageContent.ToString());
            }
        }

Stap 2: volgorde van de regel: bedrijfsnaam - plaats - bedrag

We gaan per pagina elke regel verwerken. Het probleem met de bedragen die soms niet als "3e regel" staan, maar onderaan de pagina staan, vang ik op door in de code, als ik op de 3e regel zit en deze regel niet een bedrag is, dan zijn de eerste 2 regels een "onverwerkt bedrijf". Die plaats ik in een queue en verwerk deze regel alsof het regel 1 is. Aan het einde komt ik straks als 1e regel bedragen tegen, dan zit ik (dus) onderaan de pagina en kan ik de onverwerkte bedrijven van de queue afhalen, het bedrag eraan koppelen en daarmee het bedrijf compleet maken. Dit gaat best goed, ik maak een export (mijn code is in 5 seconden klaar of zoiets, zelfs ik ben verbaasd over de snelheid), ik importeer de tekst in een Excel-bestand, zie onderaan nog een copyright-regeltje wat ik weg haal en denk dat het klaar is.

Ik scroll door de PDF en zie dan halverwege wat gaten vallen. Als ik daar weer heen scroll zie ik dat op sommige plekken bedrijfsnaam leeg is en in de kolom plaatsnaam het bedrag ingevuld is. Er zijn zaken verschoven... waarom?

Het blijkt dat er bedrijven zijn die gevestigd zijn in Rotterdam Albrandswaard. In de PDF zie je alleen Rotterdam- en Albrandswaard niet, omdat dit "afgebroken" en buiten beeld staat. Maar in het tekstbestand wordt het wel geplaatst. En op een volledig afwijkende plaats in de tekst waardoor mijn in 90% van de gevallen werkende algoritme "regel 1: bedrijf, regel 2: plaats, regel 3: bedrag" daar dus niet werkt...


NATURAL TALENT B.V.

TILBURG
23.232
NATURALESA B.V.
BARENDRECHT
NATURALIA B.V.
BORNE
NATURALITE BENELUX B.V.
ANSEN
NATURALLY BEDS LEIDERDORP B.V.
LEIDERDORP
NATURALS KRALINGEN B.V.
NATURANA MODE C.V.
ROTTERDAMALBRANDSWAARD
ROTTERDAMALBRANDSWAARD
ROTTERDAMALBRANDSWAARD
ROTTERDAMALBRANDSWAARD
DUIVEN
NATURE'S CHOICE B.V.

Het is inmiddels woensdagavond 23.40 uur, heb nog even 5 minuten wat geprobeerd, maar bedenk me: nu zit je te hacken en ga je niet het gewenste resultaat krijgen. Dit is geen Clean Code meer Hornstra, morgen een nieuwe poging!


Mijn initiële inschatting was 2 uur werk.
Om bovenstaande uit te werken en tegen problemen aan te lopen, daar ben ik ongeveer van 21.15 tot 23.45 uur mee bezig geweest.
2 uur en 3 kwartier, zegt maar "3 uur".
En we zijn er dus (nog lang?) niet.
Ik zet de verdubbelaar en "gooi er nog wat marge bin"  in, dit kan wel eens 8 uren duren, een volledige werkdag dus.

Reset: stap 2: volgorde van de regel: bedrijfsnaam - plaats - bedrag

Ok, we moeten die regels dus "slim" gaan verwerken.

Mijn aanname was:
regel 1: bedrijfsnaam
regel 2: plaats
regel 3: bedrag

Maar omdat bedragen onderaan de pagina kunnen staan en omdat we door de lange plaatsnaam soms plaatsnamen 4x opeenvolgend in de tekst kunnen hebben staan, gaan we naar de pagina kijken.

Regel: tekst? dan is het of bedrijfsnaam of plaats.
Regel: numeriek? dan is het een bedrag.

Zoals ik al eerder had aangegeven is in 9 van de 10 gevallen mijn eerste aanname juist, dus bedrijfsnaam - plaats - bedrag. Dus als we die volgorde tegenkomen, no problem.

Maar als we op de 1e of 2e regel een bedrag tegenkomen of op de 3e regel geen bedrag, dan moeten we extra controles uit gaan voeren. Een hulpmiddel hierbij is dat de lijst alfabetisch is.
Dus de bedrijfsnaam zou met dezelfde letter in het alfabet moeten beginnen of een opvolgende. Of daarna volgend, mocht het zo zijn dat er geen bedrijven met bijvoorbeeld een Q als eerste letter zich aangemeld hebben.

We hebben geluk dat het probleem niet bij de eerste bedrijven optreedt. Want deze bedrijven beginnen met dubbele quotes, een @, een &-teken.
Bij een visuele controle lijkt qua plaatsnaam wat getoond kan worden een reeks van 24 tekens het maximum:
WESTERHAAR-VRIEZENV WIJK

Reset: stap 2: volgorde van de regel: bedrijfsnaam - plaats - bedrag

Het lijkt erop dat ik op deze manier er niet uit kom. Dit wordt teveel controle "hier is iets anders, wat moet ik nu gaan doen". Dus ik ga terug naar de tekentafel. En zie dat ik in het begin eigenlijk al wat fout doe. Ik dacht slim te zijn door alle lege regels eruit te halen. Maar door die lege regels zie je juist of ergens juist 2 regels "zonder" lege tussenregel er tussen staan. Dat geeft aan dat daar 2 plaatsnamen of iets dergelijks onder elkaar staan en dat je dus een probleem hebt.

Dit is wel mooi, want het is tevens een stukje "refactoring". In plaats van allemaal extra controles in het verwerken van de pagina's, regel-voor-regel, moet ik "gewoon" zorgen dat een pagina een juiste volgorde heeft. Ik moet dus al een soort "pre-formatting" uitvoeren, zodat de invoer netjes is en gewoon verwerkt kan worden!

Mijn nieuwe functie noem ik "ReformatPages" en daarmee ga per pagina de regels door. Kom ik afwijkingen tegen, dan vul ik lege plekken op met "_BEDRIJFSNAAM PLAATSEN!", "_VESTIGINGSPLAATS PLAATSEN!" en "_BEDRAG PLAATSEN!". En op plekken waar 2 plaatsnamen na elkaar staan of 2 bedrijfsnamen na elkaar staan, is het dus ook een kwestie van het uit elkaar trekken en zo de juiste volgorde afdwingen.

In de functie ProcessPages verwerkt ik alle pagina's. Als ik dan zo'n waarde tegen kom, zoek ik naar waar volgens mij in de tekst de waarde zou moeten staan en vervang zo de "placeholder". In het geval van bedragen die niet gevuld kunnen worden, geef ik deze automatisch de waarde -999. Op die manier kan ik, als ik in Excel de data importeer meteen zien of er iets nog niet klopt. En dat lijkt ook zo te zijn. Bedrijfsnamen en vestigingsplaatsen zijn allemaal gevuld, maar er zijn nog wel bedrijven met een bedrag van -999. Daar is de koppeling nog niet correct. Bij nadere controle van het eerste geval blijkt dat een vestigingsplaats helemaal onderaan het document staat en als bedrijfsnaam gebruikt wordt. Ook hier nog een regel voor gebouwd, bij een nieuwe export blijkt daarmee alles opgelost te zijn!

Stap 3: Testen

Hoe kan ik zeker zijn dat mijn code correct is? Door voldoende testen te maken die mij tonen dat we de juiste waardes eruit filteren. Maar bij hoeveel testen is dat? Volgens Scott Hanselman is dat op het moment dat je als developer zelf denkt: "ok, nu heb ik er wel genoeg gemaakt". Ik heb in mijn testproject nu 10 testcases gemaakt, omdat dit degene waren die ik in de voorgaande stappen ben tegengekomen. Fouten zouden alleen kunnen optreden (volgens mij) in de delen waar ik zelf correcties moet doorvoeren. De pagina's waarbij de volgordes correct zijn leveren correcte input en dus ook correcte output. Vijf hiervan gaan goed, de overige vijf niet. Na het op maat maken van de code gaan 3 van die 5 wel goed, maar gaan de eerste 5 niet meer goed. Dit is geen stabiele oplossing.

Reset

Ik ben opnieuw begonnen. Omdat de output onvoorspelbaar is (dan staan er een aantal bedrijfsnamen onder elkaar, dan een aantal vestigingsplaatsen), dat is niet in code af te vangen. Misschien met "kunstmatige intelligentie" die snapt wat iets is, maar dat is overkill om dat te gaan programmeren. Ik maak nu code die de normale flow verwerkt, en als die op problemen stuit, geef ik een foutmelding en plaats informatie in een error.txt-bestand. Zo kan ik bepalen wat er aan de hand is. Met 2 tekstbestanden "companies.txt" en "cities.txt" kan ik het programma voeden op het moment dat er een keuze gemaakt moet worden. Dit gaat beter, want zo gaat na het oplossen van de fout op pagina 38 ik door naar pagina 107, dus de tussenliggende kunnen zonder problemen verwerkt worden.

System.Exception: Error on page 107, on line 250!
LINE SHOULD BE EMPTY
245: _REPLACE-WITH-AMOUNT!
246:
247: AUTOBEDRIJF JER. DE FONKERT B.V.
248:
249: OUDEGA GEM
250: SMALLINGERLND
251: NUMANSDORP
252:
253: AUTOBEDRIJF JOHANSSON V.O.F.
254:

   bij NowTextParser.NowTextParser.ThrowErrorMessage(Int32 pageNumber, Int32 lineNumber, Nullable`1 lineHasText, Nullable`1 emptyLine) in C:\NowTextParser\NowTextParser.cs:regel 840
   bij NowTextParser.NowTextParser.ReformatPages() in C:\NowTextParser\NowTextParser.cs:regel 197
   bij NowTextParser.Program.Main(String[] args) in C:\NowTextParser\Program.cs:regel 31

En toen waren we zondag 2 augustus. Ik heb hier op vrijdagavond 24 juli, zaterdagavond 25 juli, zondagavond 26 juli en maandagavond 27 juli aan gewerkt. Ook het weekend van 1 en 2 augustus en kom langzaam aan tot het besef dat ik beter kan stoppen. Want ik heb nu code waar ik wel tevreden mee ben, maar tijdens het verwerken van de data kom ik dit tegen:


COOP. VERENIGING SAMENWERKINGSVER- BAND PRAKTIJKOPLEIDING SCHILDEREN INDE REGIO GRONINGEN EN ASSEN
DRENTHE B.A.

In deze regel maakt de export het helemaal mooi, want je denkt dat dit één lange bedrijfsnaam is. Maar dat is niet zo, "ASSEN" is hier de vestigingsplek, COOP.VERENIGING... GRONINGEN DRENTHE B.A. is dus de bedrijfsnaam.

Laat ik dit lange verhaal dus als voorbeeld gebruiken voor al die bedrijven die een klus voor hun IT-partner hebben en waarbij hun eerste reactie is "wat, zolang hoeft dat toch niet te duren?". Iets wat er simpel uit kan zien, kan als het uitgewerkt moet worden ineens "a hell of a job" worden, iets waar je IT-partner als het goed is bij de start al rekening mee houdt. Want anders krijg je dat er elke keer extra budget gevraagd wordt omdat de opdracht "toch niet" binnen de afgesproken tijd uitgevoerd kon worden.

Check dus vooral https://now-inzichtelijk.nl/ en geef net als mij Ansien een biertje voor de moeite: https://www.buymeacoffee.com/ansien