1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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}"
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"<title>(.*?) \| Natura 2000</title>", 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 <kml> 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}<br/>{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()
|