Skip to main content

HTML Scraping

rascal-0.42.2

Synopsis

Scraping HTML is to recover raw data from HTML documents

Description

In this example we see HTML as just another language that may contain relevant information. In the case of HTML it is smart to reuse existing parsers, so we use an Abstract Syntax format for HTML. It is described in AST and its IO interface is described in IO.

In this demo we extract information from the website of the Centraal Bureau voor Statistiek (CBS), the Dutch national centre for statistics.

We found an interesting page that lists how much biking the Dutchies do on average weekly:

rascal>import lang::html::IO;
ok
rascal>import IO;
ok
rascal>

we use a local copy instead of the live content for stability reasons

rascal>htmlExample = getResource("Languages/HTML/Scraping/fietsen.html");
loc: |file:///home/runner/work/rascal-website/rascal-website/courses/Recipes/Languages/HTML/Scraping/fietsen.html|
rascal>page = readHTMLFile(htmlExample);
No HTML constructor declared for svg
No HTML constructor declared for defs
No HTML constructor declared for clipPath
No HTML constructor declared for rect
No HTML constructor declared for mask
No HTML constructor declared for g
No HTML constructor declared for image
No HTML constructor declared for path
No HTML constructor declared for tspan
No HTML constructor declared for polygon
No HTML constructor declared for line
HTMLElement: html([
head([
text("\n "),
meta(charset="utf-8"),
text("\n "),
meta(http-equiv="x-ua-compatible",content="ie=edge"),
text("\n "),
meta(name="viewport",content="width=device-width, initial-scale=1.0"),
text("\n "),
title([text("Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS")]),
text("\n \n "),
meta(name="DCTERMS:identifier",title="XSD.anyURI",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/hoeveel-fietsen-we-gemiddeld-per-week"),
text("\n "),
meta(property="DCTERMS.title",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS"),
text("\n "),
meta(name="DCTERMS.type",title="OVERHEID.Informatietype",content="webpagina"),
text("\n "),
meta(name="DCTERMS.language",title="XSD.language",content="nl-NL"),
text("\n "),
meta(name="DCTERMS.authority",title="OVERHEID.Organisatie",content="CBS"),
text("\n "),
meta(name="DCTERMS.creator",title="OVERHEID.Organisatie",content="CBS"),
text("\n "),
meta(name="DCTERMS.modified",title="xsd:date",content="2022-09-09"),
text("\n "),
meta(name="DCTERMS.temporal",content="2022"),
text("\n "),
meta(name="DCTERMS.spatial",title="OVERHEID:Koninkrijksdeel",content="Nederland"),
text("\n "),
meta(name="DC.Rights",content="https://www.cbs.nl/nl-nl/over-ons/website"),
text("\n "),
meta(name="description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n\n "),
meta(property="og:title",content="Hoeveel fietsen we gemiddeld per week?"),
text("\n "),
meta(property="og:site_name",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS"),
text("\n "),
meta(property="og:type",content="article"),
text("\n "),
meta(property="og:image",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/assets/img/screenshot.jpg"),
text("\n "),
meta(property="og:image:width",content="1200"),
text("\n "),
meta(property="og:image:height",content="628"),
text("\n "),
meta(property="og:url",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/hoeveel-fietsen-we-gemiddeld-per-week"),
text("\n "),
meta(property="og:description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n "),
meta(property="og:author",content="CBS"),
text("\n\n "),
meta(name="twitter:card",content="summary_large_image"),
text("\n "),
meta(name="twitter:site",content="@statistiekcbs "),
text("\n "),
meta(name="twitter:title",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022"),
text("\n "),
meta(name="twitter:description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n "),
meta(name="twitter:image",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/assets/img/screenshot.jpg"),
text("\n\n "),
meta(name="ZOOMTITLE",content="Hoeveel fietsen we gemiddeld per week?"),
text("\n\t\n\t"),
meta(name="format-detection",content="telephone=no"),
text("\n\t\n "),
link(href="../assets/css/style.css",rel="stylesheet"),
text("\n\n "),
link(href="../assets/img/ico/apple-icon-57x57.png",sizes="57x57",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img...

As you can see the output is truncated with ..., to see more we can use Iprintln:

rascal>iprintln(page)
html([
head([
text("\n "),
meta(charset="utf-8"),
text("\n "),
meta(http-equiv="x-ua-compatible",content="ie=edge"),
text("\n "),
meta(name="viewport",content="width=device-width, initial-scale=1.0"),
text("\n "),
title([text("Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS")]),
text("\n \n "),
meta(name="DCTERMS:identifier",title="XSD.anyURI",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/hoeveel-fietsen-we-gemiddeld-per-week"),
text("\n "),
meta(property="DCTERMS.title",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS"),
text("\n "),
meta(name="DCTERMS.type",title="OVERHEID.Informatietype",content="webpagina"),
text("\n "),
meta(name="DCTERMS.language",title="XSD.language",content="nl-NL"),
text("\n "),
meta(name="DCTERMS.authority",title="OVERHEID.Organisatie",content="CBS"),
text("\n "),
meta(name="DCTERMS.creator",title="OVERHEID.Organisatie",content="CBS"),
text("\n "),
meta(name="DCTERMS.modified",title="xsd:date",content="2022-09-09"),
text("\n "),
meta(name="DCTERMS.temporal",content="2022"),
text("\n "),
meta(name="DCTERMS.spatial",title="OVERHEID:Koninkrijksdeel",content="Nederland"),
text("\n "),
meta(name="DC.Rights",content="https://www.cbs.nl/nl-nl/over-ons/website"),
text("\n "),
meta(name="description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n\n "),
meta(property="og:title",content="Hoeveel fietsen we gemiddeld per week?"),
text("\n "),
meta(property="og:site_name",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022 | CBS"),
text("\n "),
meta(property="og:type",content="article"),
text("\n "),
meta(property="og:image",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/assets/img/screenshot.jpg"),
text("\n "),
meta(property="og:image:width",content="1200"),
text("\n "),
meta(property="og:image:height",content="628"),
text("\n "),
meta(property="og:url",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/hoeveel-fietsen-we-gemiddeld-per-week"),
text("\n "),
meta(property="og:description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n "),
meta(property="og:author",content="CBS"),
text("\n\n "),
meta(name="twitter:card",content="summary_large_image"),
text("\n "),
meta(name="twitter:site",content="@statistiekcbs "),
text("\n "),
meta(name="twitter:title",content="Hoeveel fietsen we gemiddeld per week? - Nederland in cijfers 2022"),
text("\n "),
meta(name="twitter:description",content="In 2020 zaten 12- tot 18-jarigen het vaakst op de fiets. Zij fietsten gemiddeld 33 kilometer per persoon per week. 75-plussers fietsten het minst."),
text("\n "),
meta(name="twitter:image",content="https://longreads.cbs.nl/nederland-in-cijfers-2022/assets/img/screenshot.jpg"),
text("\n\n "),
meta(name="ZOOMTITLE",content="Hoeveel fietsen we gemiddeld per week?"),
text("\n\t\n\t"),
meta(name="format-detection",content="telephone=no"),
text("\n\t\n "),
link(href="../assets/css/style.css",rel="stylesheet"),
text("\n\n "),
link(href="../assets/img/ico/apple-icon-57x57.png",sizes="57x57",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-60x60.png",sizes="60x60",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-72x72.png",sizes="72x72",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-76x76.png",sizes="76x76",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-114x114.png",sizes="114x114",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-120x120.png",sizes="120x120",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-144x144.png",sizes="144x144",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-152x152.png",sizes="152x152",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/apple-icon-180x180.png",sizes="180x180",rel="apple-touch-icon"),
text("\n "),
link(href="../assets/img/ico/android-icon-192x192.png",sizes="192x192",rel="icon",type="image/png"),
text("\n "),
link(href="../assets/img/ico/favicon-32x32.png",sizes="32x32",rel="icon",type="image/png"),
text("\n "),
link(href="../assets/img/ico/favicon-96x96.png",sizes="96x96",rel="icon",type="image/png"),
text("\n "),
link(href="../assets/img/ico/favicon-16x16.png",sizes="16x16",rel="icon",type="image/png"),
text("\n "),
meta(name="msapplication-TileColor",content="#ffffff"),
text("\n "),
meta(name="msapplication-TileImage",content="../assets/img/ico/ms-icon-144x144.png"),
text("\n\n "),
link(href="../assets/css/cbs-infographics.min.css",rel="stylesheet"),
text("\n "),
script([data("\n var highchartsLogo = \'../assets/img/highcharts-logo.png\';\n window.Highcharts = undefined;\n ")]),
text("\n "),
script(
[],
src="../assets/js/lib/jquery-3.2.1.min.js"),
text("\n "),
script(
[data("\n MathJax.Hub.Config({\n messageStyle: \"none\"\n });\n ")],
type="text/x-mathjax-config"),
text("\n "),
script(
[],
src="https://longreads.cbs.nl/lib/MathJax/2.7.5/MathJax.js?config=MML_CHTML&locale=nl",
async=""),
text("\n \n ")
]),
body(
[
text("\n\n "),
text("\n "),
script(
[data("\n (function(window, document, dataLayerName, id) {\n window[dataLayerName]=window[dataLayerName]||[],window[dataLayerName].push({start:(new Date).getTime(),event:\"stg.start\"});var scripts=document.getElementsByTagName(\'script\')[0],tags=document.createElement(\'script\');\n function stgCreateCookie(a,b,c){var d=\"\";if(c){var e=new Date;e.setTime(e.getTime()+24*c*60*60*1e3),d=\"; expires=\"+e.toUTCString()}document.cookie=a+\"=\"+b+d+\"; path=/\"}\n var isStgDebug=(window.location.href.match(\"stg_debug\")||document.cookie.match(\"stg_debug\"))&&!window.location.href.match(\"stg_disable_debug\");stgCreateCookie(\"stg_debug\",isStgDebug?1:\"\",isStgDebug?14:-1);\n var qP=[];dataLayerName!==\"dataLayer\"&&qP.push(\"data_layer_name=\"+dataLayerName),isStgDebug&&qP.push(\"stg_debug\");var qPString=qP.length\>0?(\"?\"+qP.join(\"&\")):\"\";\n tags.async=!0,tags.src=\"https://cbs.containers.piwik.pro/\"+id+\".js\"+qPString,scripts.parentNode.insertBefore(tags,scripts);\n !function(a,n,i){a[n]=a[n]||{};for(var c=0;c\<i.length;c++)!function(i){a[n][i]=a[n][i]||{},a[n][i].api=a[n][i].api||function(){var a=[].slice.call(arguments,0);\"string\"==typeof a[0]&&window[dataLayerName].push({event:n+\".\"+i+\":\"+a[0],parameters:[].slice.call(arguments,1)})}}(i[c])}(window,\"ppms\",[\"tm\",\"cm\"]);\n })(window, document, \'dataLayer\', \'2c166c88-5f8b-465e-b713-f0341b357879\');\n ")],
type="text/javascript"),
text("\n\n "),
div(
[
text("\n "),
text("\n \n \n "),
header(
[
text("\n "),
div(
[
text("\n "),
div(
[
text("\n "),
div(
[
text("\n "),
a(
[
text("\n "),
span([
text("Nederland"),
br(),
text("in cijfers")
]),
text("\n ")
],
href="../"),
text("\n ")
],
class="bar bar-link",
id="bar-title"),
text("\n ")
],
class="title-container"),
text("\n "),
div(
[
text("\n "),
svg(
[
title(
[text("Editie 2021")],
id="58f5fc32dd1c"),
defs([
clipPath(
[rect(
[],
width="274.56",
height="215.04",
y="-21",
x="-23",
class="aa62d3b4-1c62-463b-a520-15d85a880803")],
id="fcbb5612-bce2-4e07-9226-7f6eecad512c"),
clipPath(
[rect(
[],
width="274.17",
height="214.06",
y="-21",
x="-23",
class="aa62d3b4-1c62-463b-a520-15d85a880803")],
id="aa4f58de-3f57-49d9-8a2b-9cc0b893c55a"),
mask(
[g(
[image(
[],
width="572",
height="448",
transform="translate(-23 -21) scale(0.48)",
xlink:href="data:image/png;base64,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")],
class="ad18db4e-f623-4748-a878-879541761840")],
maskUnits="userSpaceOnUse",
width="274.56",
height="215.04",
x="-23",
y="-21",
id="b51b1ae6-0be4-4c40-bb4b-8fc8e3db95cd"),
clipPath(
[rect(
[],
width="178.08",
height="177.6",
y="0.12",
x="0.04",
class="aa62d3b4-1c62-463b-a520-15d85a880803")],
id="fd2462ca-87b2-4945-9a11-45cb70e207c6")
]),
g(
[g(
[
g(
[
g(
[g(
[rect(
[],
width="178.08",
height="177.6",
y="0.12",
x="0.04",
class="f77b1b45-9c4e-4cdf-b7f8-96f682dd1ed1")],
class="bb91c95d-d0d4-4d84-a253-e3e7d68237c2")],
class="eb04ebd8-756d-4b3d-838c-201b340d0d18"),
path(
[],
d="M99.53,168.26A83.5,83.5,0,1,0,2.8,100.53a83.49,83.49,0,0,0,96.73,67.73",
class="efdbc49b-472a-469d-88d9-beca98a5b118")
],
class="f094c21f-8c2f-489e-84f3-6f2c145b6c66"),
text(
[
tspan(
[text("E")],
class="e7450217-dbb5-433a-b408-cb597d3ae108"),
tspan(
[text("d")],
y="0",
x="18.87",
class="ad2fdf84-570a-4f23-8f14-195dc87476ec"),
tspan(
[text("i")],
y="0",
x="38.58",
class="bfaa76cd-4cad-4f0b-8096-255521be4074"),
tspan(
[text("t")],
y="0",
x="47.88",
class="b502cfd3-4eff-4174-9e94-5f876af875b2"),
tspan(
[text("ie ")],
x="62.25",
y="0")
],
transform="translate(36.01 70.48) rotate(-10)",
class="a4177483-9ebb-437f-acd3-4c7278a73baa"),
text(
[
text("2"),
tspan(
[text("0")],
y="0",
x="33.34",
class="b0e9cace-8fc6-438b-bb0b-f339561b8640"),
tspan(
[text("2")],
x="69.06",
y="0"),
tspan(
[text("2")],
y="0",
x="102.4",
class="b755dd28-4004-4cdb-ac0d-ed046b78d856")
],
transform="matrix(0.98, -0.17, 0.17, 0.98, 21.52, 128.88)",
class="e1d1559a-6454-4e5a-8ca9-32bc4dc3a1cb")
],
data-name="Layer 1",
id="a1516c78-2b83-4724-a578-746db537d10f")],
class="ba4d5243-dfba-488f-92fd-536587d9077c")
],
viewBox="0 0 179 179",
aria-labelledby="58f5fc32dd1c",
xmlns:xlink="http://www.w3.org/1999/xlink",
role="img",
xmlns="http://www.w3.org/2000/svg"),
text(" ")
],
id="edition"),
text("\n "),
div(
[
text("\n "),
a(
[
text("\n "),
span([text("English")]),
text("\n ")
],
href="https://longreads.cbs.nl/the-netherlands-in-numbers-2022/how-much-do-we-cycle-per-week-on-average",
title="Change language to English"),
text("\n ")
],
class="bar bar-link",
id="bar-langswitch"),
text(" \n "),
p(
[text("Foto omschrijving: Senioren op de elektriche fiets, e-bike op een groene dijk met een blauwe lucht en veel witte wolken.")],
class="show-for-sr"),
text("\n \n ")
],
class="header-container"),
text(" \n "),
div(
[
span(
[a(
[span([img(src="../assets/img/cbs-brand.svg",alt="CBS")])],
href="https://www.cbs.nl")],
class="bar bar-link",
id="topbar-logo"),
span(
[a(
[span([text("Nederland in cijfers 2022")])],
href="../")],
class="bar bar-link",
id="topbar-title"),
text("\n \n "),
div(
[
text("\n "),
a(
[i(
[],
aria-hidden="true",
class="fa fa-search")],
data-searchoverlay-open="",
class="top-button search",
title="Zoeken"),
text("\n "),
a(
[span(
[svg(
[
defs([
style([text(".d6f7efe6-3a34-48b1-9e54-39be799d9b1c{fill:none;}.9619b980-7a58-435d-b7e4-aca0d985b0c9{clip-path:url(#clip-path);}")]),
clipPath(
[rect(
[],
width="131.52",
height="84.34",
y="0.44",
id="pdf-icon-SVGID",
x="-34.99",
class="d6f7efe6-3a34-48b1-9e54-39be799d9b1c")],
id="clip-path")
]),
title([text("Download PDF")]),
g(
[path(
[],
d="M36.32,22.33V.44H4.12A3.86,3.86,0,0,0,.25,4.3V68.71a3.86,3.86,0,0,0,3.87,3.86H58.21a3.86,3.86,0,0,0,3.87-3.86V26.2H40.18a3.86,3.86,0,0,1-3.86-3.87m5.15-1.28h19a7.84,7.84,0,0,0-1.13-1.45L42.92,3.17a8.65,8.65,0,0,0-1.45-1.12Z",
class="pdf-icon-bg")],
class="9619b980-7a58-435d-b7e4-aca0d985b0c9"),
path(
[],
d="M15.23,39.92c4.13,0,6.81,1.54,6.81,6.33C22,52,19,53.09,14.9,53.09H13.48c-.36,0-.42.06-.42.21v6.81a.28.28,0,0,1-.3.3H9.35a.28.28,0,0,1-.3-.3V40.22a.28.28,0,0,1,.3-.3Zm-.51,10.15c2.41,0,3.4-.66,3.4-3.73,0-2.78-1-3.53-3.34-3.53H13.27c-.18,0-.21.09-.21.21v6.84c0,.15,0,.21.21.21Z",
class="pdf-icon-fg"),
path(
[],
d="M33.52,39.92c6.12,0,7.39,5.33,7.39,10.25s-1.27,10.24-7.39,10.24H25.93a.28.28,0,0,1-.3-.3V40.22a.28.28,0,0,1,.3-.3ZM33,43H29.82c-.13,0-.19.06-.19.18v14c0,.15,0,.18.15.18H33c3.07,0,3.77-3.08,3.77-7.3C36.78,46,36.08,43,33,43Z",
class="pdf-icon-fg"),
path(
[],
d="M45,40.22a.28.28,0,0,1,.3-.3H55.43a.29.29,0,0,1,.31.3V42.9a.29.29,0,0,1-.31.3H49.16a.16.16,0,0,0-.18.18v5.31a.16.16,0,0,0,.18.18H55a.28.28,0,0,1,.3.3v2.5a.28.28,0,0,1-.3.3H49.16a.16.16,0,0,0-.18.18v8a.28.28,0,0,1-.3.3H45.25a.28.28,0,0,1-.3-.3Z",
class="pdf-icon-fg")
],
viewBox="0 0 62 73",
data-name="pdf-icon",
xmlns:xlink="http://www.w3.org/1999/xlink",
id="pdf-icon",
xmlns="http://www.w3.org/2000/svg")],
aria-hidden="true")],
href="https://www.cbs.nl/-/media/_pdf/2022/36/cbs-nederland-in-cijfers-2022.pdf",
class="top-button facebook",
title="Download PDF"),
text("\n ")
],
class="top-button-container"),
text("\n "),
a(
[span([text("Open of sluit menu")])],
class="menu-toggle-button burger",
id="topbar-menutoggle",
data-menu-toggle="menu")
],
class="topbar"),
text("\n "),
div(
[
text("\n "),
div(
[
text("\n "),
div(
[
text("\n "),
form(
[
text("\n "),
input(spellcheck="false",autocomplete="off",name="zoom_query",dir="auto",value="",placeholder="Zoekterm",class="search-overlay-input",type="search",id="overlay-search-input"),
text("\n "),
button(
[
text("\n "),
text("\n ")
],
aria-label="Zoeken",
value="",
class="search-overlay-submit",
type="submit")
],
method="get",
action="../zoeken/",
class="search-overlay-form",
id="search-overlay-form")
],
class="search-overlay-form-container"),
text("\n "),
button(
[span(
[text("×")],
aria-hidden="true")],
class="close-button search-overlay-close",
type="button",
aria-label="Sluiten",
data-searchoverlay-close=""),
text("\n ")
],
class="search-overlay-content"),
text("\n ")
],
class="search-overlay"),
text(" \n "),
div(
[span([text("© ANP / Rob Voss")])],
class="photo-copyright"),
text("\n "),
div(
[
text("\n "),
a(
[i(
[],
aria-hidden="true",
class="fa fa-search")],
data-searchoverlay-open="",
class="top-button search",
title="Zoeken"),
text("\n "),
a(
[span(
[svg(
[
defs([
style([text(".d6f7efe6-3a34-48b1-9e54-39be799d9b1c{fill:none;}.9619b980-7a58-435d-b7e4-aca0d985b0c9{clip-path:url(#clip-path);}")]),
clipPath(
[rect(
[],
width="131.52",
height="84.34",
y="0.44",
id="pdf-icon-SVGID",
x="-34.99",
class="d6f7efe6-3a34-48b1-9e54-39be799d9b1c")],
id="clip-path")
]),
title([text("Download PDF")]),
g(
[path(
[],
d="M36.32,22.33V.44H4.12A3.86,3.86,0,0,0,.25,4.3V68.71a3.86,3.86,0,0,0,3.87,3.86H58.21a3.86,3.86,0,0,0,3.87-3.86V26.2H40.18a3.86,3.86,0,0,1-3.86-3.87m5.15-1.28h19a7.84,7.84,0,0,0-1.13-1.45L42.92,3.17a8.65,8.65,0,0,0-1.45-1.12Z",
class="pdf-icon-bg")],
class="9619b980-7a58-435d-b7e4-aca0d985b0c9"),
path(
[],
d="M15.23,39.92c4.13,0,6.81,1.54,6.81,6.33C22,52,19,53.09,14.9,53.09H13.48c-.36,0-.42.06-.42.21v6.81a.28.28,0,0,1-.3.3H9.35a.28.28,0,0,1-.3-.3V40.22a.28.28,0,0,1,.3-.3Zm-.51,10.15c2.41,0,3.4-.66,3.4-3.73,0-2.78-1-3.53-3.34-3.53H13.27c-.18,0-.21.09-.21.21v6.84c0,.15,0,.21.21.21Z",
class="pdf-icon-fg"),
path(
[],
d="M33.52,39.92c6.12,0,7.39,5.33,7.39,10.25s-1.27,10.24-7.39,10.24H25.93a.28.28,0,0,1-.3-.3V40.22a.28.28,0,0,1,.3-.3ZM33,43H29.82c-.13,0-.19.06-.19.18v14c0,.15,0,.18.15.18H33c3.07,0,3.77-3.08,3.77-7.3C36.78,46,36.08,43,33,43Z",
class="pdf-icon-fg"),
path(
[],
d="M45,40.22a.28.28,0,0,1,.3-.3H55.43a.29.29,0,0,1,.31.3V42.9a.29.29,0,0,1-.31.3H49.16a.16.16,0,0,0-.18.18v5.31a.16.16,0,0,0,.18.18H55a.28.28,0,0,1,.3.3v2.5a.28.28,0,0,1-.3.3H49.16a.16.16,0,0,0-.18.18v8a.28.28,0,0,1-.3.3H45.25a.28.28,0,0,1-.3-.3Z",
class="pdf-icon-fg")
],
ok

We used Chrome's "Inspect" feature to figure out that the div class datatable-container is of interest. So let's select that using a deep match operator and bind that div to the tab variable:

rascal>if (/tab:div(_,class=/datatable-container/) := page)
|1 >>>> iprintln(tab);
div(
[
text("\n "),
table(
[
text("\n "),
caption([text("Gemiddelde fietsafstand per persoon per week, 2020 (kilometer)")]),
text("\n "),
thead([
text("\n "),
tr([
text("\n "),
th(
[text("Persoonskenmerken")],
scope="col"),
text("\n "),
th(
[text("Fietskilometers")],
scope="col"),
text("\n ")
]),
text("\n ")
]),
text("\n "),
tbody([
text("\n "),
tr([
text("\n "),
th(
[text("Student of scholier")],
scope="row"),
text("\n "),
td([text("22,45")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Gepensioneerd of VUT")],
scope="row"),
text("\n "),
td([text("20,69")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Werkzaam, 12 tot 30 uur per week")],
scope="row"),
text("\n "),
td([text("18,79")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Werkzaam, 30 uur per week of meer")],
scope="row"),
text("\n "),
td([text("16,69")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Werkloos")],
scope="row"),
text("\n "),
td([text("14,64")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Arbeidsongeschikt")],
scope="row"),
text("\n "),
td([text("11,77")]),
text("\n ")
]),
text("\n "),
tr([
text("\n "),
th(
[text("Anders")],
scope="row"),
text("\n "),
td([text("13,14")]),
text("\n ")
]),
text("\n ")
]),
text("\n ")
],
class="figure-table"),
text("\n ")
],
class="datatable-container collapse",
id="datatable-4ysmm5r8l7t2lxvj")
ok

We used a deep match pattern and then a regular expression pattern to select all class attributes that have datatable-container somewhere in the string.

Every row in the table contains data, except the header row. Let's convert this entire table to a relation of type rel[str persoonskenmerken, real fietskilometers].

We create the match pattern by step-wise refinement. First let's just list all the rows:

rascal>if (/tab:div(rows,class=/datatable-container/) := page) {        
|1 >>>> for (/r:tr(_) := rows) {
|2 >>>> println(r);
|3 >>>> }
|4 >>>>}
tr([text("\n "),th([text("Persoonskenmerken")],scope="col"),text("\n "),th([text("Fietskilometers")],scope="col"),text("\n ")])
tr([text("\n "),th([text("Student of scholier")],scope="row"),text("\n "),td([text("22,45")]),text("\n ")])
tr([text("\n "),th([text("Gepensioneerd of VUT")],scope="row"),text("\n "),td([text("20,69")]),text("\n ")])
tr([text("\n "),th([text("Werkzaam, 12 tot 30 uur per week")],scope="row"),text("\n "),td([text("18,79")]),text("\n ")])
tr([text("\n "),th([text("Werkzaam, 30 uur per week of meer")],scope="row"),text("\n "),td([text("16,69")]),text("\n ")])
tr([text("\n "),th([text("Werkloos")],scope="row"),text("\n "),td([text("14,64")]),text("\n ")])
tr([text("\n "),th([text("Arbeidsongeschikt")],scope="row"),text("\n "),td([text("11,77")]),text("\n ")])
tr([text("\n "),th([text("Anders")],scope="row"),text("\n "),td([text("13,14")]),text("\n ")])
list[void]: []
  • ❶ binds the children of the div to rows;
  • ❷ uses deep match / to quickly jump to all the nested tr nodes;

Now we refined the pattern to filter out the non-header rows:

rascal>if (/tab:div(rows,class=/datatable-container/) := page) { 
|1 >>>> for (/r:tr([text(_),th(_,scope="row"), text(_), td(_), text(_)]) := rows) {
|2 >>>> println(r);
|3 >>>> }
|4 >>>>}
tr([text("\n "),th([text("Student of scholier")],scope="row"),text("\n "),td([text("22,45")]),text("\n ")])
tr([text("\n "),th([text("Gepensioneerd of VUT")],scope="row"),text("\n "),td([text("20,69")]),text("\n ")])
tr([text("\n "),th([text("Werkzaam, 12 tot 30 uur per week")],scope="row"),text("\n "),td([text("18,79")]),text("\n ")])
tr([text("\n "),th([text("Werkzaam, 30 uur per week of meer")],scope="row"),text("\n "),td([text("16,69")]),text("\n ")])
tr([text("\n "),th([text("Werkloos")],scope="row"),text("\n "),td([text("14,64")]),text("\n ")])
tr([text("\n "),th([text("Arbeidsongeschikt")],scope="row"),text("\n "),td([text("11,77")]),text("\n ")])
tr([text("\n "),th([text("Anders")],scope="row"),text("\n "),td([text("13,14")]),text("\n ")])
list[void]: []
  • ❸ we matching only those tr that have two children, one th and one td. To be sure we also limit the first th to have the scope attribute equal to "row".

Now it's time to get the final data out. The category is in the first column and the numbers are in the second. We could make the query deeper and more complex, but we choose to add another nesting level for the sake of clarity:

rascal>if (/tab:div(rows,class=/datatable-container/) := page) { 
|1 >>>> for (/r:tr([text(_),category:th(_,scope="row"), text(_), number:td(_), text(_)]) := rows) {
|2 >>>> if (/text(str c) := category, /text(str n) := number) {
|3 >>>> println("<c> --- <n>");
|4 >>>> }
|5 >>>> }
|6 >>>>}
Student of scholier --- 22,45
Gepensioneerd of VUT --- 20,69
Werkzaam, 12 tot 30 uur per week --- 18,79
Werkzaam, 30 uur per week of meer --- 16,69
Werkloos --- 14,64
Arbeidsongeschikt --- 11,77
Anders --- 13,14
list[void]: []

Now we have scraped the data out of the HTML syntax tree, we have to convert it to raw data. But the Dutch use comma's as decimal separators:

rascal>import String;
ok
rascal>import util::Math;
ok
rascal>toReal("18,79");
|std:///String.rsc|(12059,320,<486,0>,<499,31>): IllegalArgument()
at *** somewhere ***(|std:///String.rsc|(12059,320,<486,0>,<499,31>))
at toReal(|prompt:///|(7,7,<1,7>,<1,14>))
rascal>toReal("18.79");
real: 18.79
rascal>replaceAll("18,79", ",", ".")
str: "18.79"
───
18.79
───
rascal>rel[str persoonskenmerken, real fietskilometers] myData = {};
rel[str persoonskenmerken,real fietskilometers]: {}
rascal>if (/tab:div(rows,class=/datatable-container/) := page) {
|1 >>>> for (/r:tr([text(_),category:th(_,scope="row"), text(_), number:td(_), text(_)]) := rows) {
|2 >>>> if (/text(str c) := category, /text(str n) := number) {
|3 >>>> println("<c> --- <n>");
|4 >>>> myData += <c, toReal(replaceAll(n, ",", "."))>;
|5 >>>> }
|6 >>>> }
|7 >>>>}
Student of scholier --- 22,45
Gepensioneerd of VUT --- 20,69
Werkzaam, 12 tot 30 uur per week --- 18,79
Werkzaam, 30 uur per week of meer --- 16,69
Werkloos --- 14,64
Arbeidsongeschikt --- 11,77
Anders --- 13,14
list[void]: []
rascal>myData;
rel[str persoonskenmerken,real fietskilometers]: {
<"Student of scholier",22.45>,
<"Werkzaam, 30 uur per week of meer",16.69>,
<"Gepensioneerd of VUT",20.69>,
<"Werkloos",14.64>,
<"Anders",13.14>,
<"Werkzaam, 12 tot 30 uur per week",18.79>,
<"Arbeidsongeschikt",11.77>
}

Now we have the data in a format that we can compute with:

rascal>myData<persoonskenmerken>
set[str]: {"Student of scholier","Werkzaam, 30 uur per week of meer","Gepensioneerd of VUT","Werkloos","Anders","Werkzaam, 12 tot 30 uur per week","Arbeidsongeschikt"}
rascal>import Set;
ok
rascal>theSum = sum(myData<fietskilometers>);
real: 118.17
rascal>relativeData = { <pk, round(avg / theSum * 100.0, 0.1) > | <pk, avg> <- myData};
rel[str,real]: {
<"Student of scholier",19.0>,
<"Werkzaam, 30 uur per week of meer",14.1>,
<"Gepensioneerd of VUT",17.5>,
<"Werkloos",12.4>,
<"Anders",11.1>,
<"Werkzaam, 12 tot 30 uur per week",15.9>,
<"Arbeidsongeschikt",10.0>
}

To keep this analysis for the future, for example when new data is published on the site, we can store the query in a function. It is also ready to be rewritten from structured programming style into a functional comprehension. Let's do that first:

rascal>{ <c, toReal(replaceAll(n, ",", "."))>                      
|1 >>>>| /tab:div(rows,class=/datatable-container/) := page
|2 >>>>, /r:tr([text(_),category:th(_,scope="row"), text(_), number:td(_), text(_)]) := rows
|3 >>>>, /text(str c) := category, /text(str n) := number
|4 >>>>}
rel[str,real]: {
<"boodschappen doen",2.18>,
<"kinderopvang",1.85>,
<"Werkzaam, 30 uur per week of meer",16.69>,
<"Zakelijk, beroepsmatig",0.23>,
<"Gepensioneerd of VUT",20.69>,
<"Visite, logeren",1.33>,
<"Winkelen,",2.18>,
<"Werkloos",14.64>,
<"Werkzaam, 12 tot 30 uur per week",18.79>,
<"Van en naar het werk",3.07>,
<"Diensten,",0.33>,
<"Onderwijs,",1.85>,
<"Student of scholier",22.45>,
<"Uitgaan, sport, hobby",3.60>,
<"Anders",13.14>,
<"persoonlijke",0.33>,
<"Toeren, wandelen",4.67>,
<"verzorging",0.33>,
<"Overige reismotieven",0.99>,
<"cursus,",1.85>,
<"Arbeidsongeschikt",11.77>
}

The patterns have not changed, only they have been copied to the generator/filter side of a Comprehension:

  • ❶ here we have the resulting tuple that uses c and n which have been selected by pattern matching
  • ❷ this is the first selector that finds the table in the page
  • ❸ here we iterate over the rows that are not the header
  • ❹ finally we project out the text from the two cells.

Now we wrap it all up in a reusable function:

rascal>rel[str persoonskenmerken, real fietskilometers] scrapeFietsKilometers(loc address=htmlExample) 
|1 >>>> = { <c, toReal(replaceAll(n, ",", "."))>
|2 >>>> | /tab:div(rows,class=/datatable-container/) := readHTMLFile(address)
|3 >>>> , /r:tr([text(_),category:th(_,scope="row"), text(_), number:td(_), text(_)]) := rows
|4 >>>> , /text(str c) := category, /text(str n) := number
|5 >>>> };
rel[str persoonskenmerken,real fietskilometers] (, loc address = ...): function(|prompt:///|(0,438,<1,0>,<6,6>))
rascal>scrapeFietsKilometers()
rel[str persoonskenmerken,real fietskilometers]: {
<"boodschappen doen",2.18>,
<"kinderopvang",1.85>,
<"Werkzaam, 30 uur per week of meer",16.69>,
<"Zakelijk, beroepsmatig",0.23>,
<"Gepensioneerd of VUT",20.69>,
<"Visite, logeren",1.33>,
<"Winkelen,",2.18>,
<"Werkloos",14.64>,
<"Werkzaam, 12 tot 30 uur per week",18.79>,
<"Van en naar het werk",3.07>,
<"Diensten,",0.33>,
<"Onderwijs,",1.85>,
<"Student of scholier",22.45>,
<"Uitgaan, sport, hobby",3.60>,
<"Anders",13.14>,
<"persoonlijke",0.33>,
<"Toeren, wandelen",4.67>,
<"verzorging",0.33>,
<"Overige reismotieven",0.99>,
<"cursus,",1.85>,
<"Arbeidsongeschikt",11.77>
}

Every time the function is called, the HTML is retrieved again from the site. We coded the URL in a default parameter, just in case a similar page exists that we might try our analysis on.

Benefits

  • Rascal has a lot of powerful Pattern Matching operators to dissect a HTML page with;
  • Skills used in the analysis of programming languages, like traversal and pattern matching, are equally useful for HTML scraping;
  • Deep matching and Visit skip over all uninteresting content without depending on it. The more you use these "structure shy" primitives, the more robust the scraper will be against sudden changes in the HTML.

Pitfalls

  • HTML scraping is a brittle business. If the page changes, then it's likely the query will not work anymore. The function will start returning empty sets of tuples in that case, most likely. If we look at the structural dependencies then the word datatable-container is very important. Also this query matches only tables with two columns, and the first cell is always a th and the second cell is td. Finally the actual data is stored in a single text cell under the th and td. If any of these properties change, this scraper breaks. However, if anything else changes, the scraper keeps working;
  • The HTML parser skips SVG elements;