Files
matitos_news/utils/Schools-NL.ipynb
T

20 KiB

In [ ]:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import pandas as pd
import os
import json
import csv

headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"}
In [ ]:
# Function to decode Cloudflare-protected emails
def decode_email(encoded_email):
    """
    Decode an email protected by Cloudflare's email protection.
    :param encoded_email: The encoded email string from the data-cfemail attribute.
    :return: The decoded email address.
    """
    email = ""
    key = int(encoded_email[:2], 16)  # Extract the key (first two characters)
    for i in range(2, len(encoded_email), 2):
        # XOR each pair of hex characters with the key
        email += chr(int(encoded_email[i:i + 2], 16) ^ key)
    return email

def extract_emails(soup):
    # Find all visible email links (mailto:)
    visible_emails = []
    for link in soup.find_all('a', href=lambda href: href and href.startswith('mailto:')):
        email = link['href'].replace('mailto:', '')
        visible_emails.append(email)

    # Find all Cloudflare-protected emails
    protected_emails = []
    for span in soup.find_all('span', class_='__cf_email__', attrs={'data-cfemail': True}):
        encoded_email = span['data-cfemail']
        decoded_email = decode_email(encoded_email)
        protected_emails.append(decoded_email)

    # Combine all emails
    all_emails = visible_emails + protected_emails
    all_emails = list(set(all_emails))
    if (len(all_emails) == 0):
        return None
    elif (len(all_emails) == 1):
        return all_emails[0]
    else:
        return all_emails

def find_website(soup_school):
    # Find all <a> tags with href attributes
    for link in soup_school.find(class_="dl-horizontal dl-icons").find_all('a', href=True):
        href = link['href']
        # Filter out only valid URLs (e.g., starting with http or https)
        if href.startswith(('http://', 'https://')):
            # websites.append(href)
            return href

def get_num_students_per_zipcode(soup):
    list_zipcode_students_percentage = []

    h3_tag = soup.find("h3", string="In welk postcodegebied wonen de leerlingen van deze school?")
    if h3_tag:
        dialog = h3_tag.find_parent("dialog")

        if dialog:
            # print(dialog.prettify())
            table = dialog.find("table")
            if table:
                rows = table.find_all("tr")
                for row in rows:
                    cells = row.find_all(["th", "td"])
                    row_data = [cell.get_text(strip=True) for cell in cells]
                    zipcode, num_students, percentage = row_data
                    list_zipcode_students_percentage.append( (zipcode, num_students, percentage) )
    
    return list_zipcode_students_percentage

def get_num_students_trend(soup):
    # Step 1: Locate the <aantal-leerlingen-trend-line-chart> tag
    trend_chart_tag = soup.find("aantal-leerlingen-trend-line-chart")

    if trend_chart_tag:
        # Step 2: Extract the 'leerlingen-trend-data' attribute
        trend_data_attr = trend_chart_tag.get("leerlingen-trend-data")
        
        if trend_data_attr:
            # Step 3: Parse the JSON string into a Python object
            trend_data = json.loads(trend_data_attr)
            #print("Extracted leerlingen-trend-data:")
            #print(json.dumps(trend_data, indent=4))  # Pretty-print the JSON data
            return [ (e.get("key"), e.get("aantal") ) for e in trend_data]

def get_num_students_per_age_and_group(soup):
    num_students_per_group, num_students_per_age = [], []
    ############################################################################
    # Step 1: Locate the <aantal-leerlingen-leeftijd-bar-chart> tag
    chart_tag = soup.find('aantal-leerlingen-leeftijd-bar-chart', attrs={'aantal-per-leeftijd': True})
    # Step 2: Extract the 'aantal-per-leeftijd' attribute
    raw_data = chart_tag['aantal-per-leeftijd']

    # Step 3: Parse the JSON data
    try:
        data = json.loads(raw_data)
        # Step 4: Print the extracted data
        # print("Aantal per Leeftijd:")
        for entry in data:
            age = entry['key']
            num_students = entry['aantal']
            # school_data["num_students_age_{}".format(age)] = num_students
            num_students_per_age.append( (age, num_students) )
            # print(f"Age {entry['key']}: {entry['aantal']} leerlingen")
    except json.JSONDecodeError as e:
        print(f"Failed to parse JSON data: {e}")

    ############################################################################
    # Step 1: Locate the <aantal-leerlingen-leerjaar-bar-chart> tag
    chart_tag = soup.find('aantal-leerlingen-leerjaar-bar-chart', attrs={'aantal-per-leerjaar': True})

    if not chart_tag:
        print("Could not find the 'aantal per leerjaar' section.")
    else:
        # Step 2: Extract the 'aantal-per-leerjaar' attribute
        raw_data = chart_tag['aantal-per-leerjaar']
        
        # Step 3: Parse the JSON data
        try:
            data = json.loads(raw_data)
            # Step 4: Print the extracted data
            # print("Aantal per Leerjaar:")
            for entry in data:
                group = entry['key']
                num_students = entry['aantal']
                # school_data["num_students_group_{}".format(group)] = num_students
                num_students_per_group.append( (group, num_students) )
                # print(f"Groep {entry['key']}: {entry['aantal']} leerlingen")
        except json.JSONDecodeError as e:
            print(f"Failed to parse JSON data: {e}")
    ############################################################################
    return num_students_per_group, num_students_per_age


def update_school_data(school_url, school_data):
    try:
        # Process school (request contact details)
        response = requests.get(os.path.join(school_url, "contact/#inhoud"), headers=headers)
        response.raise_for_status()  # Raise an exception for HTTP errors
        # Parse the HTML content using BeautifulSoup
        soup_school = BeautifulSoup(response.text, 'html.parser')

        # School details
        school_details = soup_school.find(class_="school-details")
        for category_idx, li_detail in enumerate(school_details.find_all("li")):
            data = li_detail.find('span', class_='infotip-term')['data-dfn']
            text = li_detail.get_text(strip=True)
            # Set data
            school_data["category_{}".format(category_idx)] = text
            school_data["category_{}_description".format(category_idx)] = data
        
        school_address = soup_school.find(class_="school-adres").get_text(strip=True)
        school_postcode_city = soup_school.find(class_="school-postcode-woonplaats").get_text(strip=True)
        school_postcode = "".join(school_postcode_city.split(" ")[:2])
        school_city = " ".join(school_postcode_city.split(" ")[2:])

        school_data["city"] = school_city
        school_data["postcode"] = school_postcode
        school_data["address"] = school_address

        try:
            school_data["website"] = find_website(soup_school) # soup_school.find(class_="button button-primary").get('href')
        except Exception as e:
            pass
        try:
            school_data["phone"] = soup_school.find('a', href=lambda href: href and href.startswith('tel:')).text
        except Exception as e:
            pass
        try:
            school_data["email"] = extract_emails(soup_school)
        except Exception as e:
            pass

        # Process school main site
        response = requests.get(os.path.join(school_url), headers=headers)
        response.raise_for_status()  # Raise an exception for HTTP errors
        # Parse the HTML content using BeautifulSoup
        soup_school = BeautifulSoup(response.text, 'html.parser')

        try:
            school_data["students_per_zipcode"] = get_num_students_per_zipcode(soup_school)
        except Exception as e:
            pass
        try:
            school_data["students_per_year_trend"] = get_num_students_trend(soup_school)
        except Exception as e:
            pass

        if (school_data.get("category").lower() == "basisscholen"):
            try:
                num_students_per_group, num_students_per_age = get_num_students_per_age_and_group(soup_school)
                school_data["num_students_per_group"] = num_students_per_group if len(num_students_per_group)>0 else None
                school_data["num_students_per_age"] = num_students_per_age if len(num_students_per_age)>0 else None
            except Exception as e:
                pass
        
    except Exception as e:
        print(school_url, str(e))

def main():
    list_urls = [
        "https://scholenopdekaart.nl/Basisscholen/",
        "https://scholenopdekaart.nl/middelbare-scholen/"
    ]

    list_school_data_dicts = []

    # For each category
    for url in list_urls:
        # Fetch the HTML content of the page
        response = requests.get(url, headers=headers)
        response.raise_for_status()  # Raise an exception for HTTP errors
        # Parse the HTML content using BeautifulSoup
        soup = BeautifulSoup(response.text, 'html.parser')

        # Get category
        category = url.strip("/").split("/")[-1].lower()

        # Find all <a> tags with href attributes
        links_areas = []
        for a_tag in soup.find_all('a', href=True):
            href = a_tag['href']
            
            if (category not in href):
                continue
            
            # Convert relative URLs to absolute URLs
            area_full_url = urljoin(url, href)
            links_areas.append(area_full_url)

            # Area
            area = href.rstrip("/").split("/")[-1]

            ###############################################
            # Fetch the HTML content of the page
            print(".", end="")
            response = requests.get(area_full_url, headers=headers)
            response.raise_for_status()  # Raise an exception for HTTP errors

            # Parse the HTML content using BeautifulSoup
            soup_area= BeautifulSoup(response.text, 'html.parser')

            # Get schools in area
            for a_tag in soup_area.find_all('a', href=True):
                href = a_tag['href']

                school_url = urljoin(url, href)
                if (area_full_url not in school_url):
                    continue
                
                school_name = a_tag.text.rstrip(".")
                school_data = {
                    "category": category,
                    "area": area,
                    "name": school_name,
                    "url": school_url,
                }

                update_school_data(school_url, school_data)

                list_school_data_dicts.append(school_data)

                # Save per processed school to track progress
                df = pd.DataFrame(list_school_data_dicts)
                df.to_csv("scholenopdekaart_tmp.csv", encoding="utf-8", quoting=csv.QUOTE_ALL)

    df = pd.DataFrame(list_school_data_dicts)
    df.to_csv("scholenopdekaart.csv", encoding="utf-8", quoting=csv.QUOTE_ALL)
    # Without extra columns
    df.drop(columns=["students_per_zipcode", "students_per_year_trend", "num_students_per_group", "num_students_per_age"]).to_csv("scholenopdekaart_.csv", encoding="utf-8", quoting=csv.QUOTE_ALL)
In [ ]:
""" # Issues with URL:
https://scholenopdekaart.nl/middelbare-scholen/grave/1900/merletcollege-grave/
https://scholenopdekaart.nl/middelbare-scholen/lent/4386/citadel-college-locatie-griftdijk/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/24527/montessori-college-k33-nijmegen/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/26368/aventurijn-park-neerbosch/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/26187/kandinsky-college-voor-lyceum-havo-mavo-vbo-lwoo/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/1791/karel-de-grote-college/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/2040/mondial-college-locatie-leuvensbroek/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/2041/mondial-college-meeuwse-acker/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/2036/stedelijk-gymnasium-nijmegen/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/2038/stedelijke-scholengemeenschap-nijmegen/
https://scholenopdekaart.nl/middelbare-scholen/nijmegen/26184/yuverta-vmbo-het-groene-lyceum-nijmegen/
https://scholenopdekaart.nl/middelbare-scholen/oss/23719/het-hooghuis-locatie-mondriaan-college/
https://scholenopdekaart.nl/middelbare-scholen/oss/943/het-hooghuis-locatie-oss-stadion/
https://scholenopdekaart.nl/middelbare-scholen/oss/947/het-hooghuis-zuidwest-gebouw-west/
https://scholenopdekaart.nl/middelbare-scholen/oss/946/het-hooghuis-zuidwest-gebouw-zuid/
https://scholenopdekaart.nl/middelbare-scholen/oss/1929/het-maaslandcollege-scholengemeenschap-voor-tweetalig-mavo-havo-vwo/
https://scholenopdekaart.nl/middelbare-scholen/oss/25783/sonnewijser-unit-route-arbeid/
https://scholenopdekaart.nl/middelbare-scholen/oss/11432/sonnewijser-unit-vervolgonderwijs-oss/
https://scholenopdekaart.nl/middelbare-scholen/oss/942/titus-brandsmalyceum/
https://scholenopdekaart.nl/middelbare-scholen/velp-noord-brabant/24545/merletcollege-eerste-opvang-anderstaligen-eoa/
https://scholenopdekaart.nl/middelbare-scholen/wijchen/2018/maaswaal-college-havo-atheneum-gymnasium/
https://scholenopdekaart.nl/middelbare-scholen/wijchen/2020/maaswaal-college-vmbo-basis-kader-mavo/
https://scholenopdekaart.nl/middelbare-scholen/wijchen/1781/pro-college-wijchen/
"""

if __name__ == "__main__":
    main()
In [ ]:
In [ ]:
import pandas as pd

df = pd.read_csv("~/Downloads/scholenopdekaart.csv", index_col=0)

df.head()
In [ ]:
def to_dict(row):
    # Empty?
    if (pd.isna(row)):
        return {}
    # Evaluate, to dict
    dict_data = dict(eval(row))
    # Remove None values
    for k in list(dict_data.keys()):
        if dict_data[k] is None:
            del dict_data[k]
    # Prefix
    return {f"{column}_{k}": v for k, v in dict_data.items()}

for column in ["students_per_year_trend", "num_students_per_group", "num_students_per_age"]:
    print(column)
    # Convert the list of tuples into a dictionary per row
    df_dicts = df[column].apply(to_dict)
    # Expand into separate columns
    df_expanded = pd.json_normalize(df_dicts)
    # Sort
    df_expanded = df_expanded[sorted(df_expanded.columns)]
    # Combine with original columns
    df = pd.concat([df.drop(columns=[column]), df_expanded], axis=1)
In [ ]:
def to_dict(row):
    # Empty?
    if (pd.isna(row)):
        return {}
    # Evaluate, to dict
    data = eval(row)
    # Remove first useless data
    data = data[1:]

    # Generate dict
    dict_data = {}
    for (zipcode, num, percentage) in data:
        dict_data[f"num_students_zipcode_{zipcode}"] = num
        dict_data[f"percentage_students_zipcode_{zipcode}"] = percentage

    # Remove None values
    for k in list(dict_data.keys()):
        if dict_data[k] is None:
            del dict_data[k]
    return dict_data

for column in ["students_per_zipcode"]:
    print(column)
    # Convert the list of tuples into a dictionary per row
    df_dicts = df[column].apply(to_dict)
    # Expand into separate columns
    df_expanded = pd.json_normalize(df_dicts)
    # Sort
    df_expanded = df_expanded[sorted(df_expanded.columns)]
    # Combine with original columns
    df = pd.concat([df.drop(columns=[column]), df_expanded], axis=1)
In [ ]:
df.to_csv("schools_nl.csv")
In [ ]:
df.head()
In [ ]:
list(df.columns)