From 9daf8e0fa5db84988679606c9b4c8f26ef463db1 Mon Sep 17 00:00:00 2001 From: gdamms Date: Fri, 24 Jul 2026 13:25:21 +0200 Subject: clean the repo --- src/n2000.py | 180 ++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 105 insertions(+), 75 deletions(-) (limited to 'src/n2000.py') diff --git a/src/n2000.py b/src/n2000.py index 357f744..a0741a0 100644 --- a/src/n2000.py +++ b/src/n2000.py @@ -1,96 +1,126 @@ +import argparse +import json import re +import xml.etree.ElementTree as ET + import requests +from kml_utils import add_multi_polygon, add_placemark, new_document, write_kml + +MAP_URL = "https://www.natura2000.fr/carte-natura2000" +POPUP_URL_TEMPLATE = ( + "https://www.natura2000.fr/leaflet-ajax-popup/node/{entity_id}" + "/pop_up_n2000_field/und?_wrapper_format=drupal_ajax" +) +SITE_URL_TEMPLATE = "https://www.natura2000.fr/site-natura/{slug}" + -reponse = requests.get("https://www.natura2000.fr/carte-natura2000") -regex = r"\"entity_id\":\"(\d+)\"" -matches = re.findall(regex, reponse.text) +def get_entity_ids(map_url: str = MAP_URL) -> list[str]: + """Fetch the Natura 2000 map page and extract every site's numeric entity id. + Args: + map_url (str, optional): URL of the Natura 2000 map page. Defaults to MAP_URL. + Returns: + list[str]: Entity ids found on the page. + """ + response = requests.get(map_url) + return re.findall(r"\"entity_id\":\"(\d+)\"", response.text) -kml_file = open("natura_2000.kml", "w", encoding="utf-8") -kml_file.write(""" - - - Natura 2000 -""") +def get_site_slug(entity_id: str) -> str | None: + """Resolve an entity id to its site page URL slug via the popup endpoint. -for i, match in enumerate(matches): - url = f"https://www.natura2000.fr/leaflet-ajax-popup/node/{match}/pop_up_n2000_field/und?_wrapper_format=drupal_ajax" - response = requests.get(url) + Args: + entity_id (str): Numeric entity id, as found by get_entity_ids. + Returns: + str | None: The site URL slug, or None if it could not be found. + """ + response = requests.get(POPUP_URL_TEMPLATE.format(entity_id=entity_id)) + match = re.search(r"href=\\u0022\\/site-natura\\/(.*?)\\u0022", response.text) + return match.group(1) if match else None - regex = r"href=\\u0022\\/site-natura\\/(.*?)\\u0022" - match = re.search(regex, response.text) - if not match: - print(f"No match found for entity_id {match}.") - continue +def get_site_details(site_url: str) -> tuple[str, list[list[tuple[float, float]]]] | None: + """Fetch a site page and extract its name and outer polygon rings. - site_url = f"https://www.natura2000.fr/site-natura/{match.group(1)}" + Args: + site_url (str): URL of the site page. + Returns: + tuple[str, list[list[tuple[float, float]]]] | None: (name, rings), where each ring is + a list of (longitude, latitude) points, or None if the name, geometry type, or + coordinates could not be found on the page. + """ response = requests.get(site_url) - regex = r"(.*?) \| Natura 2000" - match = re.search(regex, response.text) - if not match: + name_match = re.search(r"(.*?) \| Natura 2000", response.text) + if not name_match: print(f"No name found for {site_url}.") - continue - name = match.group(1) + return None + name = name_match.group(1) - regex = r"\"type\":\"(multipolygon|polygon)\"" - match = re.search(regex, response.text) - if not match: + type_match = re.search(r"\"type\":\"(multipolygon|polygon)\"", response.text) + if not type_match: print(f"No geometry type found for {site_url}.") - continue - geometry_type = match.group(1) - + return None + geometry_type = type_match.group(1) + if geometry_type == "polygon": - regex = r"\"points\":(\[\[.*?\]\])" - elif geometry_type == "multipolygon": - regex = r"\"points\":(\[\[\[.*?\]\]\])" + points_regex = r"\"points\":(\[\[.*?\]\])" else: - print(f"Unsupported geometry type {geometry_type} for {site_url}.") - continue + points_regex = r"\"points\":(\[\[\[.*?\]\]\])" - match = re.search(regex, response.text) - if not match: + points_match = re.search(points_regex, response.text) + if not points_match: print(f"No coordinates found for {site_url}.") - continue - polygons = eval(match.group(1)) - + return None + + polygons = json.loads(points_match.group(1)) if geometry_type == "polygon": - # Wrap single polygon in a list to treat it as multipolygon - polygons = [polygons] - - kml_file.write(f""" - - {name} - - {name}
{site_url} -
- - """) - - for polygon in polygons: - kml_file.write(""" - - - - - """) - for point in polygon[0]: - kml_file.write(f"{point['lon']},{point['lat']},0\n") - kml_file.write(""" - - - - - """) - kml_file.write(f""" - -
- """) - - print(f"Processed: {name} ({i+1: 5d}/{len(matches)})") - -kml_file.write("""
-
""") + polygons = [polygons] # normalize to a list of polygons, like multipolygon + + rings = [[(point["lon"], point["lat"]) for point in polygon[0]] for polygon in polygons] + return name, rings + + +def scrape_natura2000(map_url: str = MAP_URL) -> ET.Element: + """Scrape natura2000.fr and build a KML tree with one Placemark per site. + + Args: + map_url (str, optional): URL of the Natura 2000 map page. Defaults to MAP_URL. + Returns: + ET.Element: The root element. + """ + root, document = new_document("Natura 2000") + + entity_ids = get_entity_ids(map_url) + for i, entity_id in enumerate(entity_ids): + slug = get_site_slug(entity_id) + if slug is None: + print(f"No site URL found for entity_id {entity_id}.") + continue + + site_url = SITE_URL_TEMPLATE.format(slug=slug) + details = get_site_details(site_url) + if details is None: + continue + name, rings = details + + placemark = add_placemark(document, name=name, description=f"{name}
{site_url}") + add_multi_polygon(placemark, rings) + + print(f"Processed: {name} ({i + 1:5d}/{len(entity_ids)})") + + return root + + +def main(): + parser = argparse.ArgumentParser(description="Scrape natura2000.fr and export sites to KML") + parser.add_argument("-o", "--output", default="natura_2000.kml", help="Path to the output KML file") + args = parser.parse_args() + + root = scrape_natura2000() + write_kml(root, args.output) + + +if __name__ == "__main__": + main() -- cgit v1.3.1