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}" 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) def get_site_slug(entity_id: str) -> str | None: """Resolve an entity id to its site page URL slug via the popup endpoint. 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 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. 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) name_match = re.search(r"(.*?) \| Natura 2000", response.text) if not name_match: print(f"No name found for {site_url}.") return None name = name_match.group(1) type_match = re.search(r"\"type\":\"(multipolygon|polygon)\"", response.text) if not type_match: print(f"No geometry type found for {site_url}.") return None geometry_type = type_match.group(1) if geometry_type == "polygon": points_regex = r"\"points\":(\[\[.*?\]\])" else: points_regex = r"\"points\":(\[\[\[.*?\]\]\])" points_match = re.search(points_regex, response.text) if not points_match: print(f"No coordinates found for {site_url}.") return None polygons = json.loads(points_match.group(1)) if geometry_type == "polygon": 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()