{"id":982,"date":"2019-03-18T09:43:35","date_gmt":"2019-03-18T07:43:35","guid":{"rendered":"https:\/\/www.bartbarnard.nl\/blog\/?p=982"},"modified":"2019-07-15T15:51:41","modified_gmt":"2019-07-15T13:51:41","slug":"tweede-kamerleden","status":"publish","type":"post","link":"https:\/\/www.bartbarnard.nl\/blog\/tweede-kamerleden\/","title":{"rendered":"Tweede-kamerleden"},"content":{"rendered":"<p>Afgelopen zaterdag was er in Nieuwsweekend <a href=\"https:\/\/www.nporadio1.nl\/nieuwsweekend\/onderwerpen\/494385-kees-boonman-over-politieke-flexibiliteit\">een aardig gesprek met Kees Boonman<\/a>, waarin onder andere gesproken werd over het gegeven dat slechts weinig tweede-kamerleden buiten de Randstad wonen. Op zich is dit niet zo vreemd, want Nederland heeft natuurlijk geen districtenstelsel zoals in de VS of het VK. Maar het wierp bij mij wel de vraag op: waar wonen die tweede-kamerleden zoal&#8230; Die vraag was de aanleiding voor een aangename middag code schrijven.<\/p>\n<p>Geruimte tijd geleden <a href=\"https:\/\/www.bartbarnard.nl\/blog\/?p=599\">heb ik een harverster geschreven voor Funda<\/a>. Dat was een behoorlijk complex ding geworden, want dat was ook de uitdaging die ik me toen had gesteld. Nu wilde ik gewoon even snel die data hebben, dus ik besloot een eenvoudige scraper in Python te schrijven. Een <a href=\"https:\/\/www.tweedekamer.nl\/kamerleden_en_commissies\/alle_kamerleden\">site waar de betreffende gegevens staan<\/a> was snel gevonden.<\/p>\n<p>Om het lezen van die pagina door het script wat te versnellen, haalde ik hem eerst maar eens naar mijn locale machine. Dit zou er ook voor zorgen dat ik geen last zou krijgen van een eventuele IP-block, hoewel me dat bij een dergelijke site niet heel waarschijnlijk leek. Voor het binnenhalen van de html maakte ik gebruik van <a href=\"http:\/\/docs.python-requests.org\/en\/master\/\">requests<\/a>; het parsen deed ik met <a href=\"https:\/\/lxml.de\/\">html\/lxml<\/a>.<\/p>\n<p>Het overzicht van de kamerleden is vanzelfsprekend gegenereerde code, wat ook duidelijk bleek uit de html view van de pagina. Ik begon dus maar met \u00e9\u00e9n persoon binnen te halen, met het idee dat naderhand te veralgemeniseren. Een eerste stap was om eens te kijken wat voor XPath FireFox zelf van zo&#8217;n kaartje maakte:<\/p>\n<p><img loading=\"lazy\" class=\"aligncenter size-medium wp-image-983\" src=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding1-300x255.png\" alt=\"\" width=\"300\" height=\"255\" srcset=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding1-300x255.png 300w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding1-768x652.png 768w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding1-1024x869.png 1024w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding1.png 1610w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><br \/>\nHet resultaat hiervan was <tt>\/html\/body\/div[1]\/main\/div[2]\/div\/section\/div\/div[2]\/div[1]\/div<\/tt>. Dat werkt natuurlijk wel, maar was wel erg specifiek. Het ging mij om de data die staat binnen de div met als klasse &#8216;card&#8217;. Dat was natuurlijk een eenvoudige opgave:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nkamerleden = tree.xpath(&quot;\/html\/body\/\/div[@class='card']&quot;)\r\nprint (len(kamerleden))\r\n<\/pre>\n<p>Het resultaat hiervan bleek 151 te zijn, waar we eigenlijk 150 zouden verwachten. Die ene afwijking zou ik later nog wel uitzoeken.<\/p>\n<p>De data die ik van die site kon (en wilde) ophalen was de volledige naam, de partij, de woonplaats, de leeftijd, de anci\u00ebnniteit en de url naar de foto. Niet alles was voor het eigenlijke vraagstuk even relevant, maar ik was er nou toch. Om te beginnen kon ik de onderstaande XPath gebruiken om de volledige naam te achterhalen. Een opvallend verschijnsel was dat het resultaat hiervan <tt>['Aalst, R.R. van']<\/tt> was, hoewel die naam op die manier geschreven in het hele html-document niet voorkomt.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\ntest = kamerleden[0]\r\nnaam = test.xpath(&quot;div[1]\/div[1]\/div[1]\/a[1]\/text()&quot;)\r\n<\/pre>\n<p>Dit werkte wel, maar ik vond de XPath wel erg specifiek en besloot dit wat te veralgemeniseren. Omdat die test feitelijk een sub-tree is van de originele html, zou je verwachten dat je via een wildcard binnen deze subtree zou moeten kunnen zoeken. Maar wanneer ik <tt>test.xpath(\"\/\/a[@class='member__name']\")<\/tt> deed, kreeg ik weer alle 151 resultaten terug.<\/p>\n<p><img loading=\"lazy\" class=\"aligncenter size-medium wp-image-984\" src=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding2-300x153.png\" alt=\"\" width=\"300\" height=\"153\" srcset=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding2-300x153.png 300w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding2-768x391.png 768w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding2-1024x521.png 1024w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2019\/03\/afbeelding2.png 1360w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/p>\n<p>Nadat ik hier te lang mee had ge\u00ebxperimenteerd, besloot ik dan maar eieren voor m&#8217;n geld te kiezen en de concrete XPath naar de verschillende data-elementen binnen de <tt>card-div<\/tt> uit te schrijven \u2013 dat was niet zo gek veel werk en ik wilde gewoon die data hebben.<\/p>\n<p>Ik maakte dus een eenvoudige loop door de kamerleden die ik al eerder had gedefinieerd en haalde de specifieke data uit elke iteratie. Omdat die methode <tt>xpath()<\/tt> een lijst (van als het goed is \u00e9\u00e9n element) teruggeeft, haal ik telkens het eerste element hiervan op. Ik voegde ook even een tellertje bij, wat een goed idee bleek; hierdoor kwam ik er al snel achter dat de loop bij zevenenveertigste iteratie ophield.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nctr = 0;\r\nfor lid in kamerleden:\r\n  ctr += 1\r\n  url = lid.xpath(&quot;div[1]\/div[1]\/img[1]\/@src&quot;)[0]\r\n  naam = lid.xpath(&quot;div[1]\/div[1]\/div[1]\/a[1]\/text()&quot;)[0]\r\n  partij = lid.xpath(&quot;div[1]\/div[1]\/div[1]\/span[1]\/text()&quot;)[0]\r\n  woonplaats = lid.xpath(&quot;div[1]\/table[1]\/\/tr[1]\/td[2]\/text()&quot;)[0]\r\n  leeftijd = lid.xpath(&quot;div[1]\/table[1]\/\/tr[2]\/td[2]\/text()&quot;)[0]\r\n  ancien = lid.xpath(&quot;div[1]\/table[1]\/\/tr[3]\/td[2]\/text()&quot;)[0]\r\n<\/pre>\n<p>Na wat inspectie bleek dat de zevenenveertigste persoon in de lijst geen woonplaats had opgegeven. Verder onderzoek wees uit dat dit drie personen betrof. De meest eenvoudige oplossing hiervoor was natuurlijk de hele boel in een try-catch-block te zetten en bij een exceptie de woonplaats op &#8216;onbekend&#8217; te zetten (wat ook past binnen het Python-adagium dat toestemming vragen lastiger is dan sorry zeggen).<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfor lid in kamerleden:\r\n  ctr += 1\r\n  try:\r\n    # doe al die dingen zoals hierboven\r\n  except:\r\n    woonplaats = 'onbekend'\r\n<\/pre>\n<p>Er staan twee data in dat overzicht die berekend zijn: de leeftijd en de anci\u00ebnniteit. De leeftijd wordt weergegeven in aantal jaren, dus het is lastig om hier een specifieke datum uit te destilleren (om dat voor elkaar te krijgen zouden we de pagina van de bewindpersoon in kwestie moeten bezoeken om de geboortedatum op te halen \u2013 leuk om te doen, maar nu even niet). De anci\u00ebnniteit wordt weergegeven in aantal dagen, dus dat is eenvoudig terug te rekenen. Daarvoor moest ik natuurlijk wel even het woord &#8216;dagen&#8217; uit die string halen. Het terugrekenen naar de datum waarop de persoon in kwestie is begonnen laat ik dan door mysql zelf doen (<tt>date_sub<\/tt> met <tt>interval<\/tt>).<\/p>\n<p>Omdat ik toch bezig was die data wat op te schonen, haalde ik ook de GET-variabel uit de url van het plaatje, en ook het woordje &#8216;jaar&#8217; uit de leeftijd (dan zou ik ook gelijk gemiddelde leeftijd en zo uit kunnen rekenen).<\/p>\n<p>Omdat ik het te veel gedoe vond om de data via python zelf in mysql te stoppen, besloot ik gewoon insert-statements uit te printen. Die kon ik dan later in het proces wel pipen naar een bestand en die weer inlezen. Het totale plaatje werd dan als volgt:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfor lid in kamerleden:\r\n  ctr += 1\r\n  try:\r\n    # doe al die dingen zoals hierboven\r\n  except:\r\n    woonplaats = 'onbekend'\r\n\r\n  str = 'insert into kamerleden values (&quot;{}&quot;,&quot;{}&quot;,&quot;{}&quot;,&quot;{}&quot;,{},date_sub(date(now()), interval {} day));'.format(url,naam,partij,woonplaats,leeftijd,ancien)\r\n  print (str)\r\n\r\nprint (&quot;In totaal {} leden.&quot;.format(ctr))\r\n<\/pre>\n<p>Nu was het tijd om de data in mysql te laden. Het datamodel was natuurlijk bijzonder eenvoudig (zie hieronder), maar er bleek nog wel een probleem te zijn met de naam van VVD tweede-kamerlid Ye\u015filg\u00f6z-Zegerius. Hoewel alle verdere encoding goed leek te gaan, ging mysql over z&#8217;n nek bij de \u015f. Blijkbaar is utf-8 niet de juiste encoding voor dergelijke karakters, maar <a href=\"https:\/\/stackoverflow.com\/a\/202246\/10974490\">dankzij onze trouwe vriend stackoverflow<\/a> kwam ik er snel genoeg achter dat je gebruik moet maken van utf8mb4. Ik moest nog wel even de hele tabel opnieuw defini\u00ebren omdat deze wijziging blijkbaar geen effect heeft op tabellen die al gemaakt zijn, maar daarna stond eindelijk alle data er goed in.<\/p>\n<p><strong>Resultaten<\/strong><br \/>\nOmdat dit project ontstaan was door de vraag naar de woonplaatsen van de tweede-kamerleden besloot ik eerst hier maar eens naar te kijken. Het bleek dat de leden in 69 verschillende woonplaatsen wonen, waarbij de top tien niet heel veel verrassingen herbergt:<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nselect count(distinct woonplaats) as wp from kamerleden;\r\n+----+\r\n| wp |\r\n+----+\r\n| 69 |\r\n+----+\r\n1 row in set (0,00 sec)\r\n\r\nselect woonplaats,count(woonplaats) as aantal from kamerleden group by woonplaats order by aantal desc limit 10;\r\n+------------+--------+\r\n| woonplaats | aantal |\r\n+------------+--------+\r\n| Amsterdam\u00a0 |\u00a0\u00a0\u00a0\u00a0 23 |\r\n| Den Haag\u00a0\u00a0 |\u00a0\u00a0\u00a0\u00a0 18 |\r\n| Utrecht\u00a0\u00a0\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 9 |\r\n| Groningen\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 6 |\r\n| Rotterdam\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 5 |\r\n| Breda\u00a0\u00a0\u00a0\u00a0\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 4 |\r\n| Haarlem\u00a0\u00a0\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 4 |\r\n| Middelburg |\u00a0\u00a0\u00a0\u00a0\u00a0 3 |\r\n| Leiden\u00a0\u00a0\u00a0\u00a0 |\u00a0\u00a0\u00a0\u00a0\u00a0 3 |\r\n| Leeuwarden |\u00a0\u00a0\u00a0\u00a0\u00a0 3 |\r\n+------------+--------+\r\n10 rows in set (0,00 sec)\r\n<\/pre>\n<p>Het enige opvallende aan deze tabel is dat Groningen op de vierde plek staat, terwijl dat de <a href=\"https:\/\/nl.wikipedia.org\/wiki\/Lijst_van_grootste_gemeenten_in_Nederland\">achtste stad van het land<\/a> is. Verder zien we toch best nog wel wat steden van buiten de Randstad in de top tien.<\/p>\n<p>En omdat we toch die leeftijd hadden meegenomen, konden we daar ook even wat data uit ophalen:<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\n\r\nselect min(leeftijd), max(leeftijd),avg(leeftijd) from kamerleden;\r\n+---------------+---------------+---------------+\r\n| min(leeftijd) | max(leeftijd) | avg(leeftijd) |\r\n+---------------+---------------+---------------+\r\n|\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 27 |\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 76 |\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 45.9400 |\r\n+---------------+---------------+---------------+\r\n<\/pre>\n<p>De gemiddelde leeftijd van de kamerleden valt gelukkig nog wel mee.<\/p>\n<p>De database is <a href=\"http:\/\/mandarin.nl\/presentaties\/tweedekamer.sql\">hier te downloaden<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Afgelopen zaterdag was er in Nieuwsweekend een aardig gesprek met Kees Boonman, waarin onder andere gesproken werd over het gegeven dat slechts weinig tweede-kamerleden buiten de Randstad wonen. Op zich is dit niet zo vreemd, want Nederland heeft natuurlijk geen districtenstelsel zoals in de VS of het VK. Maar het wierp bij mij wel de<\/p>\n<p class=\"more-link\"><a href=\"https:\/\/www.bartbarnard.nl\/blog\/tweede-kamerleden\/\" class=\"themebutton2\">Read More<\/a><\/p>\n","protected":false},"author":1,"featured_media":983,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[19],"tags":[],"_links":{"self":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/982"}],"collection":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/comments?post=982"}],"version-history":[{"count":4,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/982\/revisions"}],"predecessor-version":[{"id":989,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/982\/revisions\/989"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/media\/983"}],"wp:attachment":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/media?parent=982"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/categories?post=982"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/tags?post=982"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}