avant codex
This commit is contained in:
9154
calendrier_lunaire/calendrier_lunaire_2026.json
Normal file
9154
calendrier_lunaire/calendrier_lunaire_2026.json
Normal file
File diff suppressed because it is too large
Load Diff
9154
calendrier_lunaire/calendrier_lunaire_2027.json
Normal file
9154
calendrier_lunaire/calendrier_lunaire_2027.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
calendrier_lunaire/de421.bsp
Normal file
BIN
calendrier_lunaire/de421.bsp
Normal file
Binary file not shown.
185
calendrier_lunaire/deep_search.md
Normal file
185
calendrier_lunaire/deep_search.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Résumé (executive summary)
|
||||
Le **calendrier lunaire de jardinage** se base sur les cycles astronomiques de la Lune pour répartir les travaux (semis, plantations, récoltes) en «jours racine/feuille/fleur/fruit» et selon la **montée/descente de la sève**. Cette méthode traditionnelle, reprise par l’agriculture biodynamique, prétend optimiser la croissance. Toutefois, des sources scientifiques avertissent qu’**aucune influence directe mesurable** n’a été démontrée【6†L373-L381】【5†L108-L116】. Malgré tout, le calendrier lunaire sert souvent de repère pratique pour planifier les travaux. Ce document explique les concepts astronomiques (phases, illumination, etc.), leur calcul en Python (avec *skyfield*), et leur traduction en règles de jardinage, tout en restant critique et rigoureux.
|
||||
|
||||
## Concepts astronomiques clés
|
||||
1. **Phases de la Lune** : La phase lunaire se définit par l’angle entre la Lune et le Soleil vus depuis la Terre, mesuré le long de l’écliptique. En pratique, on calcule la différence de longitude écliptique Soleil–Lune【14†L123-L131】. Ce résultat vaut 0° (Nouvelle Lune), ≈90° (Premier Quartier), ≈180° (Pleine Lune) ou ≈270° (Dernier Quartier) modulo 360°【14†L123-L131】.
|
||||
La **fraction illuminée** (illumination) peut être obtenue à partir de l’angle de phase : si θ est la séparation géocentrique Soleil–Lune (en radians), alors l’illumination ≈ (1–cosθ)/2. (Skyfield propose aussi `m.fraction_illuminated(sun)` pour obtenir directement ce pourcentage.)
|
||||
|
||||
2. **Lune montante / descendante** : Traditionnellement, on dit «lune montante» quand la Lune «monte» dans le ciel, c’est-à-dire que sa déclinaison géocentrique augmente au fil du jour. En pratique, on calcule la déclinaison (angle au‑dessus du plan équatorial) pour midi d’un jour et du lendemain : si la déclinaison augmente, on est en **période montante**, sinon descendante. La montée (ou descente) de la Lune coïncide grosso modo avec le flux de sève vers le haut (ou le bas) dans les plantes.
|
||||
|
||||
3. **Longitude écliptique et signes zodiacaux** : La position de la Lune dans le **zodiaque** (son signe astrologique) se déduit de sa longitude écliptique : on divise l’écliptique (360°) en 12 signes de 30°. Par exemple, Taureau (30°–60°), Gémeaux (60°–90°), etc. Chaque signe est associé à un élément (Terre, Eau, Air, Feu) selon la tradition agricole. On définit alors les **jours «racine/feuille/fleur/fruit»** :
|
||||
- **Racine** : signes de Terre (Taureau, Vierge, Capricorne) – favorise légumes racines.
|
||||
- **Feuille** : signes d’Eau (Cancer, Scorpion, Poissons) – favorise feuillage (salades, épinards).
|
||||
- **Fleur** : signes d’Air (Gémeaux, Balance, Verseau) – favorise floraison et plantes ornementales.
|
||||
- **Fruit** : signes de Feu (Bélier, Lion, Sagittaire) – favorise légumes-fruits (tomates, haricots)【24†L44-L53】.
|
||||
|
||||
4. **Périgée et apogée** : La Lune suit une orbite elliptique (période anomalistique ≈ 27.55 j). Le **périgée** est le point où la Lune est la plus proche de la Terre, l’**apogée** le plus éloigné【18†L108-L112】. Chaque lunaison comporte un périgée et un apogée. On peut les repérer en cherchant les minima/maxima locaux de la distance Terre–Lune jour par jour. La tradition conseille d’**éviter** ces jours (trop d’énergie au périgée, croissance ralentie à l’apogée)【24†L66-L73】.
|
||||
|
||||
5. **Nœuds lunaires** : Ce sont les deux points où l’orbite lunaire coupe l’écliptique (plan de l’orbite terrestre)【16†L155-L163】. Les nœuds correspondent aux périodes d’éclipses (noeud ascendant/descendant). Traditionnellement, on considère ces jours comme «perturbés» et on déconseille le jardinage【24†L66-L73】.
|
||||
|
||||
**Calculs utilisés (Skyfield)** : Le script Python charge l’éphéméride NASA DE421 pour Soleil, Terre, Lune. Il utilise `almanac.find_discrete` pour les **phases exactes** et les **nœuds**. Pour chaque jour (heure de référence = midi locale Europe/Paris), on calcule : l’illumination (via la séparation Soleil-Lune), la variation de déclinaison (montante/descendante), la longitude écliptique (pour le signe). Le code exemple montre :
|
||||
|
||||
```python
|
||||
# Phases exactes
|
||||
f_phase = almanac.moon_phases(eph)
|
||||
times, events = almanac.find_discrete(t0, t1, f_phase)
|
||||
for t, ev in zip(times, events):
|
||||
local_day = t.utc_datetime().astimezone(TZ).date()
|
||||
phase_by_day[local_day] = ["Nouvelle Lune","1er Quart.","Pleine L.","Dernier Quart."][ev]
|
||||
```
|
||||
Cela enregistre l’événement de phase (le jour local de NL/Ple/Q1/Q3).
|
||||
|
||||
```python
|
||||
# Illumination et montante/descendante
|
||||
e = earth.at(t); v_sun = e.observe(sun).apparent(); v_moon = e.observe(moon).apparent()
|
||||
sep = v_sun.separation_from(v_moon).radians
|
||||
illum = (1 - math.cos(sep)) / 2 # fraction éclairée
|
||||
illum2 = (1 - math.cos(v_sun2.separation_from(v_moon2).radians)) / 2
|
||||
croissante = "Croissante" if illum2 >= illum else "Décroissante"
|
||||
|
||||
dec = v_moon.radec()[1].degrees
|
||||
dec2 = v_moon2.radec()[1].degrees
|
||||
montante = "Montante" if dec2 >= dec else "Descendante"
|
||||
```
|
||||
On compare l’illumination et la déclinaison d’un jour à ceux du lendemain pour décider «croissante/décroissante» et «montante/descendante».
|
||||
|
||||
```python
|
||||
# Signe zodiacal (longitude écliptique)
|
||||
lat, lon, dist = v_moon.ecliptic_latlon()
|
||||
signe = SIGN_NAMES[int(lon.degrees // 30)]
|
||||
type_jour = SIGN_TO_TYPE[signe] # "Racine"/"Feuille"/"Fleur"/"Fruit"
|
||||
```
|
||||
Cela associe chaque jour à un «type de jour» agricole selon le signe (Taureau→Racine, etc【24†L44-L53】).
|
||||
|
||||
Le calcul du **périgée/apogée** se fait manuellement : on mesure la distance Terre-Lune à midi chaque jour, puis on repère les minima/maxima locaux (via comparaison avec le jour précédent/suivant) pour marquer périgée et apogée. Ce choix manuel évite l’API `almanac.moon_distance` obsolète, tout en restant suffisant pour le jardinage (un point extrême par lunaison).
|
||||
|
||||
## Liens avec le jardinage
|
||||
Les cycles ci-dessus se traduisent en règles traditionnelles (non vérifiées scientifiquement【6†L373-L381】) :
|
||||
|
||||
- **Lune montante** : la sève monte, donc on **sème et récolte** (plantes aériennes, légumes-fruits)【7†L208-L214】【20†L24-L32】.
|
||||
- **Lune descendante** : la sève descend, donc on **plante, repique, taille, travaille le sol** (consolidation racinaire)【7†L208-L214】【20†L24-L32】.
|
||||
- **Croissante vs décroissante** : complément à montante/descendante. La lune croissante favorise les tiges/feuilles/fruits, la décroissante les racines【7†L208-L214】【20†L24-L32】.
|
||||
- **Jours racine/feuille/fleur/fruit** : selon le signe zodiacal, on privilégie les cultures correspondantes【24†L44-L53】 (par ex. Taureau/Vierge/Capricorne = légumes racines, Cancer/Scorpion/Poissons = salades et choux, Gémeaux/Balance/Verseau = fleurs, Bélier/Lion/Sagittaire = tomates et haricots).
|
||||
- **Éviter certains jours** : tradition recommande de ne rien faire lors des **nœuds lunaires, du périgée et de l’apogée**【24†L66-L73】. Par exemple, on évite semer au périgée (supposé trop d’énergie) et en période d’éclipse (nœuds)【22†L66-L73】.
|
||||
|
||||
En résumé, on obtiendrait un tableau synthétique :
|
||||
|
||||
| **Cycle lunaire** | **Action jardin** | **Exemple** |
|
||||
|---------------------------|------------------------------------------|----------------------|
|
||||
| Phase croissante | Développement aérien (semis, greffe) | Semis de tomates au 1er quartier【7†L208-L214】 |
|
||||
| Phase décroissante | Consolidation racinaire (plantation) | Repiquage en lune descendante【7†L208-L214】 |
|
||||
| Lune montante (ascendante)| Semis et récoltes (au-dessus du sol) | Semer haricots en lune montante【7†L208-L214】 |
|
||||
| Lune descendante | Planter, tailler, travailler le sol | Planter pommes de terre en lune descendante【7†L208-L214】 |
|
||||
| Jour **Racine** (signe Terre) | Légumes-racines (carottes, betteraves) | Semez carottes (Lune en Vierge)【24†L44-L53】 |
|
||||
| Jour **Feuille** (signe Eau) | Feuillage (salades, épinards) | Semez laitues (Lune en Cancer)【24†L44-L53】 |
|
||||
| Jour **Fleur** (signe Air) | Fleurs, plantes ornementales | Repiquer vivaces (Lune en Gémeaux)【24†L50-L53】 |
|
||||
| Jour **Fruit** (signe Feu) | Légumes-fruits (tomates, haricots) | Semis tomates (Lune en Lion)【24†L52-L55】 |
|
||||
| **Nœuds lunaires** | **Éviter le jardinage** (énergies perturbées) | 18+ mars (noeuds)【22†L66-L73】 |
|
||||
| **Périgée / Apogée** | **Éviter ou prudence** (croissance ralentie ou maladies) | Ex.: 25 mars (périgée)【22†L69-L73】 |
|
||||
|
||||
Ces recommandations proviennent de la tradition jardinière et sont souvent condensées dans des calendriers lunaires grand public【24†L44-L53】【22†L66-L73】.
|
||||
|
||||
## Script Python : structure et explications
|
||||
Le script `lunar_calendar.py` donné utilise **Skyfield** pour la précision astronomique. Principaux points techniques :
|
||||
- **Dépendances** : `skyfield`, `pytz`, `numpy`. Le fichier d’éphémérides `de421.bsp` (NASA) couvre jusqu’en 2050 environ.
|
||||
- **Fuseau horaire** : on fixe l’heure locale «midi à Paris» pour chaque jour, afin de ne pas rater un changement de date à l’UTC. On convertit en UTC pour l’analyse Skyfield (voir `TZ.localize(datetime(...)).astimezone(pytz.utc)`).
|
||||
- **Phases exactes** : on utilise `almanac.moon_phases(eph)` et `find_discrete(t0,t1,f_phase)` pour obtenir les instants (UTC) des quatre phases principales. On associe ensuite la date locale correspondante :
|
||||
|
||||
```python
|
||||
f_phase = almanac.moon_phases(eph)
|
||||
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
|
||||
phase_by_day = {}
|
||||
for t, ev in zip(phase_times, phase_events):
|
||||
local_day = t.utc_datetime().astimezone(TZ).date()
|
||||
phase_by_day[local_day] = ["Nouvelle Lune","Premier Quartier","Pleine Lune","Dernier Quartier"][int(ev)]
|
||||
```
|
||||
Cette méthode assure la précision astronomique des phases (décalage horaire et lieux pris en compte).
|
||||
|
||||
- **Illumination (pour «croissante/décroissante»)** : on calcule à midi la fraction illuminée de la Lune par rapport au Soleil vu de la Terre. La séparation angulaire géocentrique Lune–Soleil (radians) donne l’illumination via `(1 - cos(sep))/2`. On compare cette fraction au jour suivant pour déterminer si la Lune croît ou décroît.
|
||||
|
||||
- **Montante/descendante** : on récupère la déclinaison géocentrique de la Lune (`v_moon.radec()[1].degrees`) pour deux jours consécutifs. Si elle augmente, on est en période «montante», sinon «descendante».
|
||||
|
||||
- **Longitude écliptique (signe)** : Skyfield fournit la longitude écliptique (`v_moon.ecliptic_latlon()`). La division par 30° détermine le signe zodiacal (0=Bélier, 30=Taureau, etc.). On mappe ensuite le signe au type de jour (racine/feuille/fleur/fruit) via une table (comme vu ci-dessus【24†L44-L53】).
|
||||
|
||||
- **Périgée/Apogée manuels** : comme la fonction `almanac.moon_distance` n’existait plus, on calcule la distance Terre–Lune chaque jour à midi. On parcourt ce tableau de distances pour repérer les minima locaux (périgée) et maxima locaux (apogée). C’est une approximation suffisante pour marquer environ un périgée et un apogée par mois.
|
||||
|
||||
**Limites et précisions** :
|
||||
- L’algorithme suppose un calcul quotidien à midi : il peut ne pas être précis à l’heure, mais on obtient les bons jours. Pour une précision à l’heure près (rarement nécessaire pour le jardinage), on pourrait affiner la recherche d’événements.
|
||||
- Le fuseau Europe/Paris est appliqué partout pour obtenir la date locale. En hiver comme en été, on fixe à midi (CET ou CEST).
|
||||
- Les **jours racine/feuille/fleur/fruit** sont purement conventionnels (astrologiques)【24†L44-L53】. Le choix des signes et l’attribution aux «éléments» viennent de la tradition, pas de l’astronomie. D’autres écoles pourraient varier légèrement ces mappings.
|
||||
- Le script génère un JSON/CSV qui inclut, pour chaque date : phase, %illumination, status croissante/descroissante et montante/descendante, signe zodiacal et type de jour, périgée/apogée, nœud lunaire.
|
||||
|
||||
Extrait du fichier JSON produit (format JSON compatible API) :
|
||||
```json
|
||||
{
|
||||
"date": "2026-03-14",
|
||||
"phase": "",
|
||||
"illumination": 67.34,
|
||||
"croissante_decroissante": "Croissante",
|
||||
"montante_descendante": "Montante",
|
||||
"signe": "Taureau",
|
||||
"type_jour": "Racine",
|
||||
"perigee": false,
|
||||
"apogee": false,
|
||||
"noeud_lunaire": false
|
||||
}
|
||||
```
|
||||
|
||||
## Installation et test
|
||||
1. **Prérequis** : Python 3.9+ installé (nous avons testé sur Python 3.13). Ouvrir un terminal.
|
||||
2. **Environnement virtuel** (recommandé) :
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
3. **Installer dépendances** :
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install skyfield numpy pytz
|
||||
```
|
||||
4. **Vérifier** que `lunar_calendar.py` se trouve dans le dossier de travail.
|
||||
5. **Lancer le script** :
|
||||
```bash
|
||||
python lunar_calendar.py
|
||||
```
|
||||
– Au premier lancement, Skyfield télécharge `de421.bsp`.
|
||||
– Le script affiche «Calendrier lunaire généré» et crée `calendrier_lunaire_2026.json`.
|
||||
6. **Vérifier le contenu** :
|
||||
```bash
|
||||
head -n 5 calendrier_lunaire_2026.json
|
||||
```
|
||||
ou
|
||||
```bash
|
||||
cat calendrier_lunaire_2026.json | jq . # (avec jq pour formatage)
|
||||
```
|
||||
|
||||
Si une erreur survient (par ex. `ModuleNotFoundError`), vérifier l’environnement virtuel et l’installation des librairies.
|
||||
|
||||
## Exemples d’usage et formats de sortie
|
||||
- Le script, en l’état, génère un **JSON** (tableau d’objets journaliers) et peut être modifié pour produire du CSV.
|
||||
- Exemple CSV attendu (point-virgule séparateur) :
|
||||
```
|
||||
date;phase_exacte;croissante_décroissante;montante_descendante;signe;jour_plante
|
||||
2026-03-14;;Croissante;Montante;Taureau;Racine
|
||||
2026-03-15;Dernier Quartier;Décroissante;Descendante;Bélier;Fruit
|
||||
...
|
||||
```
|
||||
- Ce fichier JSON/CSV peut être importé dans une base (SQLite) ou exposé via une API (FastAPI) pour alimenter un frontend.
|
||||
|
||||
## Améliorations possibles et pièges à éviter
|
||||
- **CLI ou paramètres** : ajouter des arguments (`--start YEAR-MON-DAY --end ...`) et `--output` pour rendre le script plus flexible.
|
||||
- **FastAPI / Backend** : intégrer le calcul dans un endpoint (par ex. `/api/lune/{year}`) pour générer le calendrier à la demande ou en consulter un pré-calculé.
|
||||
- **Base de données** : pré-calculer 5-10 ans et stocker dans SQLite pour accès rapide (partition par année).
|
||||
- **Gestion du fuseau et locales** : tester en CET/CEST pour prendre en compte DST. Éviter l’heure d’hiver/été mal appliquée.
|
||||
- **Front-end** : colorer le calendrier (ex. style Gruvbox : orangé=Racine, vert=Feuille, violet=Fleur, jaune=Fruit, rouge discret=Nœud). Rendre responsive (mobile/tablette).
|
||||
- **Documentation** : ajouter un README dans le dépôt GitHub, expliquer les conventions (zodiaque, type de jour) et référencer les sources.
|
||||
- **Précision** : pour du calcul horaire fin, on pourrait itérer en minutes autour de l’heure approximative, mais pour le jardinage, le jour suffit.
|
||||
|
||||
## Références et lectures suggérées
|
||||
- **Documentation Skyfield** – exemples de calcul d’angles et phases【14†L123-L131】.
|
||||
- **Science et scepticisme** – Détecteur de rumeurs SciencePresse (2022) et SNHF (2020) concluent à l’absence d’effet mesurable de la Lune sur les plantes【5†L108-L116】【6†L373-L381】.
|
||||
- **Guides en français** – Semencemag (2025) explique l’usage pratique (jours racine/feuille/fleur/fruit, nœuds, apogée, périgée)【24†L44-L53】【22†L66-L73】. Rustica/Gerbeaud publient chaque mois des calendriers lunaires détaillés (ex. Gerbeaud, semis en «jour feuille, lune montante»【7†L93-L101】).
|
||||
- **Éphémérides officielles** – US Naval Observatory (phases et fraction illuminée)【10†L86-L94】, NASA HORIZONS, etc.
|
||||
- **Recherche astronomique** – pour approfondir : littérature sur l’orbite lunaire, astronomie du calendrier, mais aussi le rapport SNHF «Jardiner avec la lune : mythe ou réalité» pour le contexte.
|
||||
|
||||
Ce document vise à guider à la fois les développeurs (algorithmes, code) et les jardiniers (règles pratiques). Il reste essentiel d’expérimenter et d’adapter les recommandations à son jardin : un bon sol, de l’eau et du soleil restent les facteurs clés du succès, plus que toute influence lunaire【6†L390-L394】【22†L75-L84】.
|
||||
293
calendrier_lunaire/deep_search1.md
Normal file
293
calendrier_lunaire/deep_search1.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Calendrier lunaire de jardinage – Guide complet
|
||||
|
||||
## Résumé exécutif
|
||||
Le **calendrier lunaire** de jardinage exploite la position et le cycle de la Lune (phases, déclinaison, périgée/apogée, nœuds) pour rythmer semis, plantations et récoltes. Cette approche traditionnelle, popularisée depuis le b.a.-ba de l’agriculture biodynamique, associe chaque jour lunaire à un type de culture (racine/feuille/fleur/fruit) et tient compte de la Lune montante ou descendante. **Attention toutefois**: la science moderne ne confirme aucune influence directe significative de la Lune sur la croissance des plantes【6†L373-L381】【5†L108-L116】. Néanmoins, beaucoup de jardiniers l’utilisent comme repère complémentaire. Ce document explique les notions astronomiques (phases, illumination, déclinaison, signes zodiacaux, périgée/apogée, nœuds), leur calcul en Python, les règles de jardinage associées, ainsi que le fonctionnement du script fourni (algorithme, limites, sortie). Des exemples de configuration (JSON/CSV) et des conseils d’amélioration (CLI, API, base, front-end) sont détaillés, ainsi qu’une section sur les dictons français du jardinage et les « saints de glace ».
|
||||
|
||||
## Concepts astronomiques du calendrier lunaire
|
||||
|
||||
- **Phases lunaires** : La phase se définit par l’angle Lunaire-Solaire autour de la Terre. Concrètement, on calcule la différence de longitude écliptique entre la Lune et le Soleil【14†L123-L131】. Cette différence vaut 0° pour la Nouvelle Lune, 90° pour le Premier Quartier, 180° pour la Pleine Lune, 270° pour le Dernier Quartier (modulo 360°)【14†L123-L131】. En Python (Skyfield), on utilise `almanac.find_discrete(ts0,ts1, almanac.moon_phases(eph))` pour trouver les instants précis (UTC) de chaque phase.
|
||||
|
||||
- **Illumination de la Lune** : Le pourcentage du disque lunaire éclairé se calcule par la géométrie Soleil–Terre–Lune. Si θ est la séparation angulaire (en radians) entre la Lune et le Soleil vue de la Terre, alors la fraction illuminée = (1–cos θ)/2. En code, `sep = v_sun.separation_from(v_moon).radians; illum = (1 - math.cos(sep))/2`. Skyfield offre aussi `moon.fraction_illuminated(sun)`, mais la formule ci-dessus est équivalente. Le script compare l’illumination d’un jour au lendemain pour déterminer si la Lune croît ou décroît.
|
||||
|
||||
- **Lune montante / descendante** : On dit « Lune montante » si la déclinaison géocentrique de la Lune (angle par rapport à l’équateur céleste) augmente d’un jour sur l’autre. Sinon elle est « descendante ». Dans le script on calcule la déclinaison (`v_moon.radec()[1].degrees`) à midi un jour et le jour suivant. Exemple :
|
||||
```python
|
||||
dec = v_moon.radec()[1].degrees
|
||||
dec2 = v_moon2.radec()[1].degrees
|
||||
montante = dec2 >= dec # True si Lune "montante"
|
||||
```
|
||||
La lune montante est traditionnellement favorable aux travaux aériens (semis, récoltes), la descendante aux travaux racinaires (plantation, taille).
|
||||
|
||||
- **Signe zodiacal (longitude écliptique)** : La position de la Lune devant le zodiaque sert à définir le type de jour (racine/feuille/fleur/fruit). On calcule la longitude écliptique lunaire (0°–360°) via Skyfield (`v_moon.ecliptic_latlon()`). Le signe astrologique = int(longitude/30) (0=Bélier, 1=Taureau, …). Par convention :
|
||||
- **Terre (Taureau, Vierge, Capricorne)** → *Jour Racine* (légumes-racines)【24†L44-L53】.
|
||||
- **Eau (Cancer, Scorpion, Poissons)** → *Jour Feuille* (plantes feuillues)【24†L44-L53】.
|
||||
- **Air (Gémeaux, Balance, Verseau)** → *Jour Fleur* (fleurs, choux-fleurs)【24†L50-L53】.
|
||||
- **Feu (Bélier, Lion, Sagittaire)** → *Jour Fruit* (légumes-fruits)【24†L52-L55】.
|
||||
Ces correspondances sont purement traditionnelles. Le script possède une table Python :
|
||||
```python
|
||||
SIGN_NAMES = ["Bélier","Taureau",…,"Poissons"]
|
||||
SIGN_TO_TYPE = {
|
||||
"Taureau":"Racine","Vierge":"Racine","Capricorne":"Racine",
|
||||
"Cancer":"Feuille","Scorpion":"Feuille","Poissons":"Feuille",
|
||||
"Gémeaux":"Fleur","Balance":"Fleur","Verseau":"Fleur",
|
||||
"Bélier":"Fruit","Lion":"Fruit","Sagittaire":"Fruit"
|
||||
}
|
||||
signe = SIGN_NAMES[int(lon.degrees//30)]
|
||||
type_jour = SIGN_TO_TYPE[signe]
|
||||
```
|
||||
|
||||
- **Périgée / Apogée de la Lune** : L’orbite lunaire est elliptique. *Périgée* = point le plus proche de la Terre, *apogée* = point le plus éloigné【18†L108-L112】. Chaque lunaison comporte un périgée et un apogée. Skyfield n’a plus `almanac.moon_distance`, donc on calcule la distance Terre–Lune à midi chaque jour :
|
||||
```python
|
||||
dist = earth.at(ts.utc(date)).observe(moon).distance().km
|
||||
```
|
||||
On repère les minima locaux (périgée) et maxima locaux (apogée) dans la liste journalière. Ex.:
|
||||
```python
|
||||
distances = [earth.at(ts.utc(d.year,d.month,d.day,12,0,0)).observe(moon).distance().km for d in days]
|
||||
# repérer indices i tels que dist[i] < dist[i±1] → périgée
|
||||
```
|
||||
Traditionnellement, on **évite de jardiner** durant ces jours (le périgée apporterait «trop d’énergie», l’apogée «ralentissement de croissance»)【24†L66-L73】.
|
||||
|
||||
- **Nœuds lunaires** : Ce sont les deux points où l’orbite de la Lune coupe l’écliptique【16†L155-L163】 (juste avant/après éclipses). On peut utiliser `almanac.moon_nodes(eph)` et `find_discrete` pour obtenir ces dates. Dans la pratique, les jours de nœuds sont considérés «perturbés» et déconseillés au jardinage【24†L66-L73】.
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Phases lunaires (Mars 2026)
|
||||
section Phases
|
||||
Nouvelle lune : 2026-03-03, 1d
|
||||
Premier quartier : 2026-03-10, 1d
|
||||
Pleine lune : 2026-03-18, 1d
|
||||
Dernier quartier : 2026-03-25, 1d
|
||||
```
|
||||
|
||||
## Liens vers les pratiques de jardinage
|
||||
Les astronomes jardiniers ont formulé ces règles pratiques (purement empiriques)【7†L208-L214】【20†L24-L32】 :
|
||||
- **Lune croissante** (montante) – *moment d’action au-dessus du sol* : semis de légumes-fruits, greffage, récolte. Exemple : on sème haricots/tomates le premier quartier【7†L208-L214】.
|
||||
- **Lune décroissante** (descendante) – *moment d’action sur racines/sol* : plantations, repiquages, binage, taille. Ex.: planter pommes de terre en lune descendante【7†L208-L214】.
|
||||
- **Jour “Racine”** (signe de Terre) : planter légumes-racines (carottes, betteraves)【24†L44-L53】.
|
||||
- **Jour “Feuille”** (signe d’Eau) : semer feuilles et aromatiques (laitues, épinards)【24†L44-L53】.
|
||||
- **Jour “Fleur”** (signe d’Air) : greffer et soigner fleurs/ornementales (brocolis, roses)【24†L50-L53】.
|
||||
- **Jour “Fruit”** (signe de Feu) : semer/planter légumes-fruits (tomates, courgettes, arbres fruitiers)【24†L52-L55】.
|
||||
|
||||
Un tableau synthétique :
|
||||
|
||||
| **Phase / Jour lunaire** | **Action jardin** | **Exemple** |
|
||||
|-------------------------------|-----------------------------------|-----------------------------------------|
|
||||
| Croissante (Nouvelle→Pleine) | Développement aérien – semis/greffe| Semer tomates au premier quartier【7†L208-L214】 |
|
||||
| Décroissante (Pleine→Nouvelle)| Consolidation – planter, tailler | Planter pommes de terre en lune descendante【7†L208-L214】 |
|
||||
| Lune montante | Semis/engrais/ récoltes | Récolter herbes aromatiques【7†L208-L214】 |
|
||||
| Lune descendante | Planter/tailler/travailler le sol | Repiquer laitues, tailler rosiers【7†L208-L214】 |
|
||||
| Jour **Racine** (Terre) | Légumes-racines (oignons, navets) | Semer carottes (Lune en Taureau)【24†L44-L53】 |
|
||||
| Jour **Feuille** (Eau) | Laitues, choux, épinards | Semer épinards (Lune en Cancer)【24†L44-L53】 |
|
||||
| Jour **Fleur** (Air) | Fleurs, brocolis, vivaces | Planter choux-fleurs (Lune en Balance)【24†L50-L53】 |
|
||||
| Jour **Fruit** (Feu) | Tomates, haricots, pois | Semer tomates (Lune en Lion)【24†L52-L55】 |
|
||||
| Nœuds lunaires | *Éviter tout travail* | (période d’éclipse, jours “perturbés”)【24†L66-L73】 |
|
||||
| Périgée / Apogée | *Éviter/attention* | Récoltes précoces, éviter tailes risquées【24†L66-L73】 |
|
||||
|
||||
En pratique, on imprime souvent un calendrier lunaire annuel (papier ou appli mobile) pour suivre ces repères【24†L54-L60】【22†L66-L73】. À titre d’exemple, voici la correspondance **signes zodiacaux → type de jour**, sous forme tabulaire :
|
||||
|
||||
| Signe zodiacal | Élément | Type de jour | Exemples de cultures |
|
||||
|-----------------------------|---------|--------------|--------------------------------------|
|
||||
| Bélier, Lion, Sagittaire | Feu | Fruit | Tomates, poivrons, arbres fruitiers |
|
||||
| Taureau, Vierge, Capricorne | Terre | Racine | Carottes, pommes de terre, oignons |
|
||||
| Gémeaux, Balance, Verseau | Air | Fleur | Fleurs, choux-fleurs, aromatiques |
|
||||
| Cancer, Scorpion, Poissons | Eau | Feuille | Laitues, épinards, choux, salades |
|
||||
|
||||
Ces associations sont présentées par exemple dans Semencemag【24†L44-L53】.
|
||||
|
||||
## Le script Python : description technique
|
||||
|
||||
Le script `lunar_calendar.py` (Python 3.9+) génère un calendrier lunaire sur une période donnée. Points clés du fonctionnement :
|
||||
|
||||
- **Dépendances** : `skyfield` (pour l’astronomie), `pytz` (timezones), `numpy`. `de421.bsp` est téléchargé automatiquement (éphéméride NASA).
|
||||
- **Période de calcul** : par défaut un an (Jan→Déc). On peut modifier `start` et `end` dans la section `__main__`.
|
||||
- **Fuseau horaire** : Europe/Paris. On prend l’heure locale *midi* pour éviter les transitions de date, puis on convertit en UTC pour Skyfield :
|
||||
```python
|
||||
TZ = pytz.timezone("Europe/Paris")
|
||||
local_noon = TZ.localize(datetime(year,month,day,12))
|
||||
t = ts.utc(local_noon.astimezone(pytz.utc))
|
||||
```
|
||||
Ceci garantit que chaque date du calendrier correspond bien au jour solaire local.
|
||||
|
||||
- **Phases exactes** :
|
||||
```python
|
||||
f_phase = almanac.moon_phases(eph)
|
||||
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
|
||||
phase_by_day = {}
|
||||
for t, ev in zip(phase_times, phase_events):
|
||||
local_day = t.utc_datetime().astimezone(TZ).date()
|
||||
phase_by_day[local_day] = ["Nouvelle Lune","Premier Quartier","Pleine Lune","Dernier Quartier"][int(ev)]
|
||||
```
|
||||
On récupère ainsi les jours (par date locale) où surviennent exactement la NL, PQ, PL, DQ. Ces étiquettes sont stockées dans `phase_by_day`.
|
||||
|
||||
- **Illumination et montante/descendante** : Pour chaque jour `d`, on calcule :
|
||||
```python
|
||||
e = earth.at(t) # position de la Terre à midi UTC
|
||||
v_sun = e.observe(sun).apparent()
|
||||
v_moon = e.observe(moon).apparent()
|
||||
sep = v_sun.separation_from(v_moon).radians
|
||||
illum = (1 - math.cos(sep)) / 2 # fraction (0..1) éclairée
|
||||
```
|
||||
Puis pour le lendemain, même calcul (`illum2`, `dec2`). On définit :
|
||||
```python
|
||||
croissante = "Croissante" if illum2 >= illum else "Décroissante"
|
||||
dec = v_moon.radec()[1].degrees
|
||||
dec2 = v_moon2.radec()[1].degrees
|
||||
montante = "Montante" if dec2 >= dec else "Descendante"
|
||||
```
|
||||
C’est-à-dire la Lune est “montante” si sa déclinaison augmente.
|
||||
|
||||
- **Signe zodiacal → type de jour** : Toujours à midi, on récupère la longitude écliptique :
|
||||
```python
|
||||
lat, lon, dist = v_moon.ecliptic_latlon()
|
||||
signe = SIGN_NAMES[int(lon.degrees // 30) % 12]
|
||||
type_jour = SIGN_TO_TYPE[signe]
|
||||
```
|
||||
Ainsi on remplit `signe` (ex. “Taureau”) et `type_jour` (“Racine”, etc) pour chaque date.
|
||||
|
||||
- **Périgée/Apogée manuel** : Après avoir construit une liste quotidienne de distances (voir ci-dessus), on parcourt les valeurs : si `dist[i] < dist[i-1]` et `< dist[i+1]`, c’est un **périgée** (jour local minimal). Inversement pour un **apogée**. Ce repérage simple identifie un périgée et un apogée par lunaison. Exemple :
|
||||
```python
|
||||
if distances[i] < distances[i-1] and distances[i] < distances[i+1]:
|
||||
perigee_days.add(all_days[i])
|
||||
if distances[i] > distances[i-1] and distances[i] > distances[i+1]:
|
||||
apogee_days.add(all_days[i])
|
||||
```
|
||||
Ces jours sont marqués dans l’export pour information.
|
||||
|
||||
- **Nœuds lunaires** : On utilise directement `almanac.moon_nodes(eph)` et `find_discrete` entre `t0` et `t1`. On convertit chaque instant en date locale pour obtenir `node_days`.
|
||||
|
||||
**Limitations et précision** :
|
||||
- Le calcul se fait au pas d’un jour (midi). Il n’est donc pas d’une précision horaire au-delà du jour (assez pour un calendrier de plantation).
|
||||
- Le passage entre heures d’été/hiver est géré par `pytz`.
|
||||
- Les assignations *racine/feuille/fleur/fruit* reposent sur des conventions astrologiques. Elles sont cohérentes avec la littérature francophone (Semencemag【24†L44-L53】, Rustica, etc.) mais non scientifiques.
|
||||
- Les algorithmes Skyfield sont précautionneusement utilisés pour donner des résultats très fiables sur plusieurs décennies.
|
||||
|
||||
## Installation et tests
|
||||
|
||||
1. **Python 3.9+** : Vérifier (`python3 --version`).
|
||||
2. **Environnement virtuel** (optionnel mais recommandé) :
|
||||
```bash
|
||||
cd /path/to/projet
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # prompt indique (.venv)
|
||||
```
|
||||
3. **Installer les dépendances** :
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install skyfield numpy pytz
|
||||
```
|
||||
Vérifier : `pip list` doit lister `skyfield`, `numpy`, `pytz`.
|
||||
4. **Lancer le script** (`lunar_calendar.py`) :
|
||||
```bash
|
||||
python lunar_calendar.py
|
||||
```
|
||||
– Au premier lancement, `skyfield` télécharge automatiquement `de421.bsp`.
|
||||
– Un message “Calendrier lunaire généré” doit s’afficher. Le fichier `calendrier_lunaire_2026.json` (ou défini dans le script) est créé.
|
||||
5. **Vérifier le résultat** :
|
||||
```bash
|
||||
head -n 5 calendrier_lunaire_2026.json
|
||||
```
|
||||
ou
|
||||
```bash
|
||||
cat calendrier_lunaire_2026.json | jq .
|
||||
```
|
||||
Exemple de ligne JSON produite :
|
||||
```json
|
||||
{
|
||||
"date": "2026-03-14",
|
||||
"phase": "",
|
||||
"illumination": 67.34,
|
||||
"croissante_decroissante": "Croissante",
|
||||
"montante_descendante": "Montante",
|
||||
"signe": "Taureau",
|
||||
"type_jour": "Racine",
|
||||
"perigee": false,
|
||||
"apogee": false,
|
||||
"noeud_lunaire": false
|
||||
}
|
||||
```
|
||||
Cette ligne indique qu’au 14/03/2026, la Lune est en *Taureau* (Jour Racine), croissante et montante, sans phase particulière ni événement spécial.
|
||||
|
||||
**Commandes utiles** :
|
||||
- Tester l’import Skyfield : `python -c "from skyfield.api import load; print('Skyfield OK')"`
|
||||
- Debug : ajouter `print` pour les valeurs (illumination, décli, etc.) si nécessaire.
|
||||
|
||||
## Exemples d’usage et sorties
|
||||
|
||||
Le script génère un **JSON** (tableau d’objets quotidiens). On peut facilement adapter pour un **CSV**. Par exemple, le module Python `csv` est prêt à l’emploi (démontré dans le code source). Les champs exportés sont : date, phase, « croissante/décroissante », « montante/descendante », signe, type de jour, booleans périgée/apogée/nœud.
|
||||
|
||||
Un exemple de format CSV (séparateur `;`) :
|
||||
```csv
|
||||
date;phase_exacte;croissante_décroissante;montante_descendante;signe;type_jour;perigee;apogee;noeud
|
||||
2026-03-14;;Croissante;Montante;Taureau;Racine;0;0;0
|
||||
2026-03-15;Dernier Quartier;Décroissante;Descendante;Bélier;Fruit;0;0;0
|
||||
...
|
||||
```
|
||||
Ce fichier peut être importé en base de données (SQLite) ou servi via une API (FastAPI) pour alimenter une interface web/mobile.
|
||||
|
||||
## Améliorations possibles et pièges à éviter
|
||||
|
||||
- **Arguments en ligne de commande** : utiliser `argparse` pour accepter `--start`, `--end`, `--format` (JSON/CSV).
|
||||
- **FastAPI ou Flask** : créer un endpoint `/api/lune/{year}` qui lit le JSON pré-calculé ou exécute dynamiquement le calcul. Attention à la latence du calcul si fait à la volée (mieux pré-calculer).
|
||||
- **Base de données** : pré-calculer plusieurs années (5–10 ans) et stocker en SQLite avec une table indexée sur date. Permet d’interroger rapidement pour une date donnée.
|
||||
- **Timezones** : toujours utiliser `pytz` et `ASTimezone` pour éviter les décalages DST erronés. Tester en hiver/été.
|
||||
- **Précision** : le calcul d’événements précis (phase à l’heure près) est assuré par Skyfield. Pour le quotidien, on s’en tient au repère “jour où l’événement tombe (UTC→local)”.
|
||||
- **Interface graphique** : ajouter un calendrier réactif (HTML/CSS/JS) coloré par type de jour (ex. : orange racine, vert feuille, violet fleur, jaune fruit), marquer les événements spéciaux (noeuds en rouge discret, périgée/apogée en gris). Gruvbox ou autre thème sombre/contrasté pour développeurs.
|
||||
- **Stockage des données** : suggestions JSON/CSV ci-dessus, ou génération de JSON à partir de SQLite. Ex:
|
||||
```sql
|
||||
CREATE TABLE lune (
|
||||
date TEXT PRIMARY KEY,
|
||||
phase TEXT, lumiere REAL,
|
||||
croiss_dec TEXT, mont_dec TEXT,
|
||||
signe TEXT, type_jour TEXT,
|
||||
perigee INTEGER, apogee INTEGER, noeud INTEGER
|
||||
);
|
||||
```
|
||||
- **Documentation** : ajouter des tests unitaires, du logging, et un README (vous êtes ici !).
|
||||
|
||||
## Dictons et proverbes populaires du jardinage
|
||||
|
||||
La tradition française regorge de **dictons et proverbes** relatifs aux saisons et au jardinage. En voici quelques exemples :
|
||||
|
||||
- « À chaque plante son temps, à chaque temps sa plante » – on plante/sème selon la saison appropriée【32†L118-L121】.
|
||||
- « Tel est le jardinier, tel est le jardin » – l’état du potager reflète les soins du jardinier【32†L105-L109】.
|
||||
- « En avril, ne te découvre pas d’un fil » – prudence contre les dernières gelées tardives.
|
||||
- « Jamais trop tôt pour semer, jamais trop tard pour récolter » – planter semis précoces et récolter tardivement.
|
||||
- « La patience est la mère des jardiniers » – la réussite vient avec l’observation et le temps【32†L158-L161】.
|
||||
|
||||
Ces dictons reflètent l’observation empirique. Aucune librairie Python spécifique aux proverbes français n’est connue. On peut les stocker dans un fichier JSON ou CSV pour usage interne. Par exemple, un format JSON possible :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dicton": "En avril, ne te découvre pas d'un fil",
|
||||
"signification": "Ne pas ôter les protections trop tôt car les gelées peuvent revenir tardivement.",
|
||||
"source": "Proverbe populaire"
|
||||
},
|
||||
{
|
||||
"dicton": "À chaque plante son temps, à chaque temps sa plante",
|
||||
"signification": "Chaque semis/plantation doit se faire en fonction de la saison appropriée.",
|
||||
"source": "Santamaria Motoculture【32†L118-L121】"
|
||||
}
|
||||
]
|
||||
```
|
||||
On ajoutera «source» ou «conseil associé» selon besoins. Si besoin de proverbes automatiques, on utilisera plutôt une API publique de citations (ex. «Proverbes français» non automatique) plutôt qu’une librairie locale.
|
||||
|
||||
## Calendrier des saints de glace (France)
|
||||
|
||||
En France, de nombreux dictons s’appuient sur le **calendrier des saints**. Les plus célèbres pour le jardinage sont les **Saints de Glace** (traditionnellement 11, 12, 13 mai – Mamert, Pancrace, Servais) et les saints qui les prolongent (Yves 19/5, Urbain 25/5). Ces dates marquent la fin présumée des gelées printanières. Exemples de dictons associés【39†L155-L164】【42†L209-L212】 :
|
||||
|
||||
- **11 mai (St Mamert), 12 mai (St Pancrace), 13 mai (St Servais)** : *« Avant Saint-Servais, point d’été ; après Saint-Servais, plus de gelée. »*【39†L155-L164】 conseille d’attendre la mi-mai.
|
||||
- **Saint-Urbain (25 mai)** : *« Quand la Saint-Urbain est passée, le vigneron est rassuré. »*【42†L209-L212】 (fin définitive du risque de gel).
|
||||
- Variante : *« Mamert, Pancrace, Servais sont trois saints de glace, mais Saint-Urbain les tient tous dans sa main. »*【42†L209-L212】.
|
||||
- **Saint-Pancrace (12/5), St-Servais (13/5), St-Boniface (14/5)** : *« Saints Pancrace, Servais et Boniface apportent souvent de la glace. »*【42†L209-L212】.
|
||||
|
||||
Le *calendrier des saints* est large : on trouve par région d’autres saints réputés «glaçants» en avril (Georges 23/4, Marc 25/4, etc.). Mais pour la France métropolitaine, c’est la période mi-mai qui domine ces dictons. En résumé : mieux vaut repousser l’installation des cultures sensibles au froid (tomates, etc.) jusqu’à fin mai【39†L155-L164】【40†L81-L88】.
|
||||
|
||||
## Références et lectures suggérées
|
||||
|
||||
- **Skyfield API** – Exemples de calculs astronomiques (phases, positions)【14†L123-L131】.
|
||||
- **Documentation SO/USNO** – Éphémérides officielles pour la Lune (phases, illumination)【18†L108-L112】.
|
||||
- **Journaux et blogs FR** – Articles de vulgarisation : Semencemag (juin 2025) sur l’usage du calendrier lunaire【24†L44-L53】, Rustica, Gerbeaud.
|
||||
- **Sources historiques** – Dictons et fêtes des saints : «Les saints de glace» sur le Potager Permacole【42†L209-L212】, revue Science et Vie (SNHF) pour le scepticisme scientifique【6†L373-L381】.
|
||||
- **Ressources additionnelles** : RFC et documentation FastAPI, tutoriels Skyfield (rhodesmill.org), bases de données open (p. ex. base de dictons BotAccess).
|
||||
|
||||
Ce README est prêt à être sauvegardé comme document `README.md`. Il offre un point de départ complet pour un projet de **webapp jardinage** incorporant un calendrier lunaire.
|
||||
5
calendrier_lunaire/list_command
Normal file
5
calendrier_lunaire/list_command
Normal file
@@ -0,0 +1,5 @@
|
||||
source .venv/bin/activate
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install skyfield numpy pytz
|
||||
|
||||
396
calendrier_lunaire/lunar_calendar.py
Normal file
396
calendrier_lunaire/lunar_calendar.py
Normal file
@@ -0,0 +1,396 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import date, datetime, timedelta
|
||||
import math
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytz
|
||||
from skyfield.api import load, wgs84, load_constellation_map
|
||||
from skyfield import almanac
|
||||
|
||||
TZ = pytz.timezone("Europe/Paris")
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
LATITUDE = 48.8566
|
||||
LONGITUDE = 2.3522
|
||||
|
||||
# --- Mapping "jour racine/feuille/fleur/fruit" ---
|
||||
# We align with a sidereal approach using the Moon's constellation.
|
||||
CONSTELLATION_TO_SIGN = {
|
||||
"Ari": "Bélier",
|
||||
"Tau": "Taureau",
|
||||
"Gem": "Gémeaux",
|
||||
"Cnc": "Cancer",
|
||||
"Leo": "Lion",
|
||||
"Vir": "Vierge",
|
||||
"Lib": "Balance",
|
||||
"Sco": "Scorpion",
|
||||
"Sgr": "Sagittaire",
|
||||
"Cap": "Capricorne",
|
||||
"Aqr": "Verseau",
|
||||
"Psc": "Poissons",
|
||||
# The Moon can cross Ophiuchus in official IAU boundaries.
|
||||
# We map it to Scorpion for gardening day continuity.
|
||||
"Oph": "Scorpion",
|
||||
}
|
||||
|
||||
SIGN_TO_TYPE = {
|
||||
"Taureau": "Racine", "Vierge": "Racine", "Capricorne": "Racine",
|
||||
"Cancer": "Feuille", "Scorpion": "Feuille", "Poissons": "Feuille",
|
||||
"Gémeaux": "Fleur", "Balance": "Fleur", "Verseau": "Fleur",
|
||||
"Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DayInfo:
|
||||
date: str
|
||||
phase: str
|
||||
illumination: float
|
||||
croissante_decroissante: str
|
||||
montante_descendante: str
|
||||
signe: str
|
||||
type_jour: str
|
||||
soleil_lever: str
|
||||
soleil_coucher: str
|
||||
duree_jour: str
|
||||
lune_lever: str
|
||||
lune_coucher: str
|
||||
duree_presence_lune: str
|
||||
saint_du_jour: str
|
||||
saint_de_glace: bool
|
||||
perigee: bool
|
||||
apogee: bool
|
||||
noeud_lunaire: bool
|
||||
transitions_type_jour: list[dict[str, str]] = field(default_factory=list)
|
||||
transitions_montante_descendante: list[dict[str, str]] = field(default_factory=list)
|
||||
|
||||
|
||||
def _zodiac_sign_from_constellation(constellation_at, position) -> str:
|
||||
abbr = constellation_at(position)
|
||||
return CONSTELLATION_TO_SIGN.get(abbr, "Scorpion")
|
||||
|
||||
|
||||
def _local_noon(d: date) -> datetime:
|
||||
return TZ.localize(datetime(d.year, d.month, d.day, 12, 0, 0))
|
||||
|
||||
|
||||
def _default_saints_france() -> dict[str, str]:
|
||||
# Core gardening references in France; full calendar can be provided via saints_france.json.
|
||||
return {
|
||||
"04-23": "Saint Georges",
|
||||
"04-25": "Saint Marc",
|
||||
"05-11": "Saint Mamert",
|
||||
"05-12": "Saint Pancrace",
|
||||
"05-13": "Saint Servais",
|
||||
"05-14": "Saint Boniface",
|
||||
"05-19": "Saint Yves",
|
||||
"05-25": "Saint Urbain",
|
||||
}
|
||||
|
||||
|
||||
def _load_saints_france() -> dict[str, str]:
|
||||
path = SCRIPT_DIR / "saints_dictons" / "saints_france.json"
|
||||
if not path.exists():
|
||||
return _default_saints_france()
|
||||
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
saints: dict[str, str] = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(key, str) and isinstance(value, str):
|
||||
saints[key] = value.strip()
|
||||
return saints
|
||||
|
||||
|
||||
def _compute_perigee_apogee_days(ts, earth, moon, start: date, end: date) -> tuple[set[date], set[date]]:
|
||||
# Hourly sampling + one-day padding on each side gives stable local extrema detection.
|
||||
sample_start = datetime.combine(start - timedelta(days=1), datetime.min.time())
|
||||
sample_end = datetime.combine(end + timedelta(days=1), datetime.max.time().replace(microsecond=0))
|
||||
|
||||
samples: list[tuple[date, float]] = []
|
||||
current = TZ.localize(sample_start)
|
||||
end_local = TZ.localize(sample_end)
|
||||
step = timedelta(hours=1)
|
||||
|
||||
while current <= end_local:
|
||||
t = ts.utc(current.astimezone(pytz.utc))
|
||||
dist_km = earth.at(t).observe(moon).distance().km
|
||||
samples.append((current.date(), dist_km))
|
||||
current += step
|
||||
|
||||
perigee_days: set[date] = set()
|
||||
apogee_days: set[date] = set()
|
||||
|
||||
for i in range(1, len(samples) - 1):
|
||||
day, dist = samples[i]
|
||||
if not (start <= day <= end):
|
||||
continue
|
||||
|
||||
prev_dist = samples[i - 1][1]
|
||||
next_dist = samples[i + 1][1]
|
||||
if dist < prev_dist and dist < next_dist:
|
||||
perigee_days.add(day)
|
||||
if dist > prev_dist and dist > next_dist:
|
||||
apogee_days.add(day)
|
||||
|
||||
return perigee_days, apogee_days
|
||||
|
||||
|
||||
def _to_local_dt(t) -> datetime:
|
||||
return t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ)
|
||||
|
||||
|
||||
def _pick_first_event_within_window(
|
||||
ts,
|
||||
observer,
|
||||
target,
|
||||
start_local: datetime,
|
||||
end_local: datetime,
|
||||
event_kind: str,
|
||||
) -> tuple[datetime | None, int | None]:
|
||||
if event_kind == "rise":
|
||||
event_func = almanac.find_risings
|
||||
else:
|
||||
event_func = almanac.find_settings
|
||||
|
||||
t0 = ts.utc(start_local.astimezone(pytz.utc))
|
||||
t1 = ts.utc(end_local.astimezone(pytz.utc))
|
||||
times, flags = event_func(observer, target, t0, t1)
|
||||
|
||||
for t, ok in zip(times, flags):
|
||||
if not ok:
|
||||
continue
|
||||
dt_local = _to_local_dt(t)
|
||||
if start_local <= dt_local < end_local:
|
||||
day_offset = (dt_local.date() - start_local.date()).days
|
||||
return dt_local, day_offset
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _format_time(dt_local: datetime | None, day_offset: int | None) -> str:
|
||||
if dt_local is None:
|
||||
return ""
|
||||
base = dt_local.strftime("%H:%M")
|
||||
if day_offset and day_offset > 0:
|
||||
return f"{base} (+{day_offset}j)"
|
||||
return base
|
||||
|
||||
|
||||
def _format_duration(start_dt: datetime | None, end_dt: datetime | None) -> str:
|
||||
if start_dt is None or end_dt is None:
|
||||
return ""
|
||||
delta = end_dt - start_dt
|
||||
if delta.total_seconds() < 0:
|
||||
return ""
|
||||
total_minutes = int(round(delta.total_seconds() / 60))
|
||||
hours, minutes = divmod(total_minutes, 60)
|
||||
return f"{hours:02d}h{minutes:02d}"
|
||||
|
||||
|
||||
def _moon_type_jour_at(ts, earth, moon, constellation_at, local_dt: datetime) -> str:
|
||||
t = ts.utc(local_dt.astimezone(pytz.utc))
|
||||
v_moon = earth.at(t).observe(moon).apparent()
|
||||
signe = _zodiac_sign_from_constellation(constellation_at, v_moon)
|
||||
return SIGN_TO_TYPE[signe]
|
||||
|
||||
|
||||
def _moon_montante_descendante_at(ts, earth, moon, local_dt: datetime) -> str:
|
||||
t = ts.utc(local_dt.astimezone(pytz.utc))
|
||||
t2 = ts.utc((local_dt + timedelta(minutes=30)).astimezone(pytz.utc))
|
||||
v_moon = earth.at(t).observe(moon).apparent()
|
||||
v_moon2 = earth.at(t2).observe(moon).apparent()
|
||||
dec = v_moon.radec()[1].degrees
|
||||
dec2 = v_moon2.radec()[1].degrees
|
||||
return "Montante" if dec2 >= dec else "Descendante"
|
||||
|
||||
|
||||
def _find_transition_time(
|
||||
value_at,
|
||||
left_dt: datetime,
|
||||
right_dt: datetime,
|
||||
left_value: str,
|
||||
) -> datetime:
|
||||
# Binary search at minute precision for the first instant where value changes.
|
||||
while (right_dt - left_dt) > timedelta(minutes=1):
|
||||
mid = left_dt + (right_dt - left_dt) / 2
|
||||
if value_at(mid) == left_value:
|
||||
left_dt = mid
|
||||
else:
|
||||
right_dt = mid
|
||||
return right_dt.replace(second=0, microsecond=0)
|
||||
|
||||
|
||||
def _compute_daily_transitions(
|
||||
value_at,
|
||||
day_start: datetime,
|
||||
day_end: datetime,
|
||||
step_minutes: int = 20,
|
||||
) -> list[dict[str, str]]:
|
||||
transitions: list[dict[str, str]] = []
|
||||
step = timedelta(minutes=step_minutes)
|
||||
|
||||
t = day_start
|
||||
current_value = value_at(t)
|
||||
|
||||
while t < day_end:
|
||||
probe = min(t + step, day_end)
|
||||
probe_value = value_at(probe)
|
||||
if probe_value != current_value:
|
||||
transition_dt = _find_transition_time(value_at, t, probe, current_value)
|
||||
transitions.append(
|
||||
{
|
||||
"heure": transition_dt.strftime("%H:%M"),
|
||||
"avant": current_value,
|
||||
"apres": probe_value,
|
||||
}
|
||||
)
|
||||
current_value = probe_value
|
||||
t = probe
|
||||
|
||||
return transitions
|
||||
|
||||
|
||||
def build_calendar(start: date, end: date) -> list[DayInfo]:
|
||||
if end < start:
|
||||
raise ValueError(f"Invalid date range: start ({start}) is after end ({end}).")
|
||||
|
||||
ts = load.timescale()
|
||||
eph = load("de421.bsp")
|
||||
constellation_at = load_constellation_map()
|
||||
saints_by_mmdd = _load_saints_france()
|
||||
saints_de_glace = {"05-11", "05-12", "05-13", "05-14", "05-25"}
|
||||
|
||||
earth, moon, sun = eph["earth"], eph["moon"], eph["sun"]
|
||||
observer = earth + wgs84.latlon(LATITUDE, LONGITUDE)
|
||||
|
||||
t0 = ts.utc(start.year, start.month, start.day)
|
||||
t1 = ts.utc(end.year, end.month, end.day + 1)
|
||||
|
||||
# --- Phases exactes ---
|
||||
f_phase = almanac.moon_phases(eph)
|
||||
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
|
||||
|
||||
phase_by_day: dict[date, str] = {}
|
||||
for t, ev in zip(phase_times, phase_events):
|
||||
local_day = t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
|
||||
phase_by_day[local_day] = ["Nouvelle Lune", "Premier Quartier",
|
||||
"Pleine Lune", "Dernier Quartier"][int(ev)]
|
||||
|
||||
# --- Nœuds lunaires (instants) ---
|
||||
f_nodes = almanac.moon_nodes(eph)
|
||||
node_times, _ = almanac.find_discrete(t0, t1, f_nodes)
|
||||
|
||||
node_days: set[date] = set()
|
||||
for t in node_times:
|
||||
local_day = t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
|
||||
node_days.add(local_day)
|
||||
|
||||
# --- Périgée / apogée : calcul manuel via distance Terre->Lune (min/max locaux) ---
|
||||
perigee_days, apogee_days = _compute_perigee_apogee_days(ts, earth, moon, start, end)
|
||||
|
||||
# --- Boucle jour par jour ---
|
||||
result: list[DayInfo] = []
|
||||
d = start
|
||||
|
||||
while d <= end:
|
||||
# midi local : stabilise signe du jour + évite bascules UTC
|
||||
local_noon = _local_noon(d)
|
||||
local_day_start = TZ.localize(datetime(d.year, d.month, d.day, 0, 0, 0))
|
||||
local_day_end = local_day_start + timedelta(days=1)
|
||||
local_moon_window_end = local_day_start + timedelta(days=2)
|
||||
t = ts.utc(local_noon.astimezone(pytz.utc))
|
||||
|
||||
e = earth.at(t)
|
||||
v_sun = e.observe(sun).apparent()
|
||||
v_moon = e.observe(moon).apparent()
|
||||
|
||||
# illumination (0..1) via séparation soleil-lune
|
||||
sep = v_sun.separation_from(v_moon).radians
|
||||
illum = (1 - math.cos(sep)) / 2
|
||||
|
||||
# lendemain (pour croissante/décroissante + montante/descendante)
|
||||
d2 = d + timedelta(days=1)
|
||||
local_noon2 = _local_noon(d2)
|
||||
t2 = ts.utc(local_noon2.astimezone(pytz.utc))
|
||||
|
||||
e2 = earth.at(t2)
|
||||
v_sun2 = e2.observe(sun).apparent()
|
||||
v_moon2 = e2.observe(moon).apparent()
|
||||
|
||||
sep2 = v_sun2.separation_from(v_moon2).radians
|
||||
illum2 = (1 - math.cos(sep2)) / 2
|
||||
|
||||
croissante = "Croissante" if illum2 >= illum else "Décroissante"
|
||||
|
||||
dec = v_moon.radec()[1].degrees
|
||||
dec2 = v_moon2.radec()[1].degrees
|
||||
montante = "Montante" if dec2 >= dec else "Descendante"
|
||||
|
||||
# sidereal sign via Moon constellation
|
||||
signe = _zodiac_sign_from_constellation(constellation_at, v_moon)
|
||||
type_jour = SIGN_TO_TYPE[signe]
|
||||
mmdd = f"{d.month:02d}-{d.day:02d}"
|
||||
|
||||
sun_rise_dt, sun_rise_offset = _pick_first_event_within_window(
|
||||
ts, observer, sun, local_day_start, local_day_end, "rise"
|
||||
)
|
||||
sun_set_dt, sun_set_offset = _pick_first_event_within_window(
|
||||
ts, observer, sun, local_day_start, local_day_end, "set"
|
||||
)
|
||||
moon_rise_dt, moon_rise_offset = _pick_first_event_within_window(
|
||||
ts, observer, moon, local_day_start, local_moon_window_end, "rise"
|
||||
)
|
||||
moon_set_dt, moon_set_offset = _pick_first_event_within_window(
|
||||
ts, observer, moon, local_day_start, local_moon_window_end, "set"
|
||||
)
|
||||
transitions_type_jour = _compute_daily_transitions(
|
||||
lambda dt: _moon_type_jour_at(ts, earth, moon, constellation_at, dt),
|
||||
local_day_start,
|
||||
local_day_end,
|
||||
)
|
||||
transitions_montante_descendante = _compute_daily_transitions(
|
||||
lambda dt: _moon_montante_descendante_at(ts, earth, moon, dt),
|
||||
local_day_start,
|
||||
local_day_end,
|
||||
)
|
||||
|
||||
result.append(DayInfo(
|
||||
date=d.isoformat(),
|
||||
phase=phase_by_day.get(d, ""),
|
||||
illumination=round(illum * 100.0, 2), # %
|
||||
croissante_decroissante=croissante,
|
||||
montante_descendante=montante,
|
||||
signe=signe,
|
||||
type_jour=type_jour,
|
||||
soleil_lever=_format_time(sun_rise_dt, sun_rise_offset),
|
||||
soleil_coucher=_format_time(sun_set_dt, sun_set_offset),
|
||||
duree_jour=_format_duration(sun_rise_dt, sun_set_dt),
|
||||
lune_lever=_format_time(moon_rise_dt, moon_rise_offset),
|
||||
lune_coucher=_format_time(moon_set_dt, moon_set_offset),
|
||||
duree_presence_lune=_format_duration(moon_rise_dt, moon_set_dt),
|
||||
transitions_type_jour=transitions_type_jour,
|
||||
transitions_montante_descendante=transitions_montante_descendante,
|
||||
saint_du_jour=saints_by_mmdd.get(mmdd, ""),
|
||||
saint_de_glace=(mmdd in saints_de_glace),
|
||||
perigee=(d in perigee_days),
|
||||
apogee=(d in apogee_days),
|
||||
noeud_lunaire=(d in node_days),
|
||||
))
|
||||
|
||||
d += timedelta(days=1)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = build_calendar(date(2026, 1, 1), date(2026, 12, 31))
|
||||
out_path = Path(__file__).with_name("calendrier_lunaire_2026.json")
|
||||
|
||||
with out_path.open("w", encoding="utf-8") as f:
|
||||
json.dump([asdict(x) for x in data], f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Calendrier lunaire généré : {out_path}")
|
||||
@@ -0,0 +1,266 @@
|
||||
Voici des éléments structurés essentiels pour écrire un tutoriel de scraping Python à partir du site saint-dicton.com, en particulier pour la page d’un jour précis (par exemple https://www.saint-dicton.com/0222.html) :
|
||||
|
||||
📌 Structure observée du site Saint-Dicton
|
||||
|
||||
Un exemple de page date contient :
|
||||
|
||||
Liste des saints fêtés ce jour
|
||||
Exemples : “St-Sulpice Sévère”, “St-Valère”, etc.
|
||||
Ce bloc est présenté sous forme de texte HTML listé en paragraphes ou sections.
|
||||
|
||||
Phase de la lune
|
||||
Peut être présente mais n’est pas structurée pour le scraping des saints.
|
||||
|
||||
Dicton du jour
|
||||
Un bloc intitulé “## Dicton du jour” suivi d’un ou plusieurs dictons.
|
||||
|
||||
Prénoms à fêter
|
||||
Peut contenir une liste ou être vide.
|
||||
|
||||
🧠 Analyse de la pagination annuelle
|
||||
|
||||
Le site propose également une navigation jour par jour :
|
||||
|
||||
L’URL pour une date donnée prend la forme https://www.saint-dicton.com/MMDD.html où MM est le mois sur deux chiffres et DD le jour sur deux chiffres (par exemple 0222 pour 22 février).
|
||||
|
||||
Il existe une page par mois avec tous les saints par ordre alphabétique (ex. SaintsA.html).
|
||||
|
||||
Ceci permet de parcourir toutes les dates de l’année systématiquement.
|
||||
|
||||
📘 Sources externes utiles
|
||||
|
||||
Pour enrichir ton scraping :
|
||||
|
||||
Nominis propose une base de plusieurs milliers de saints et fêtes liturgiques (utile pour comparer).
|
||||
|
||||
Des sites comme Éphéméride ou Wiki des proverbes donnent des dictons et proverbes météo/agricoles si besoin de compléter la base. (source non spécifique, généraliste).
|
||||
|
||||
🧪 Extrait de page (exemple réel)
|
||||
|
||||
Dans la page 0222.html, on observe :
|
||||
|
||||
## Dicton du jour
|
||||
|
||||
S'il gèle à la Saint-Sulpice,
|
||||
Le printemps sera propice
|
||||
|
||||
Ici :
|
||||
|
||||
“Dicton du jour” est un titre h2
|
||||
|
||||
Le dicton est une ligne ou plusieurs lignes de texte juste après ce titre.
|
||||
|
||||
📌 Tutoriel ciblé pour écrire scrap_saint.md
|
||||
|
||||
Dans ce fichier .md, tu pourras expliquer :
|
||||
|
||||
Objectif du scraping
|
||||
Extraire pour chaque date :
|
||||
|
||||
date (MM-DD)
|
||||
|
||||
nom du saint (ou saints)
|
||||
|
||||
dictons du jour
|
||||
|
||||
éventuellement prénoms associés
|
||||
|
||||
Analyse HTML du site
|
||||
|
||||
Les pages individuelles des dates
|
||||
|
||||
Les sections à scraper
|
||||
|
||||
Comment repérer les titres HTML des dictons
|
||||
|
||||
Étapes du script Python
|
||||
|
||||
itérer sur toutes les dates de l’année
|
||||
|
||||
fetch des pages MMDD.html
|
||||
|
||||
parser l’HTML avec BeautifulSoup
|
||||
|
||||
extraire listes de saints
|
||||
|
||||
extraire bloc dictons
|
||||
|
||||
stocker dans une structure CSV ou JSON
|
||||
|
||||
Exemple de structure de sortie
|
||||
|
||||
{
|
||||
"date": "0222",
|
||||
"saints": ["St-Sulpice Sévère", "St-Valère"],
|
||||
"dictons": ["S'il gèle à la Saint-Sulpice, …"]
|
||||
}
|
||||
|
||||
Conseils pour éviter les pièges
|
||||
|
||||
certaines dates ont plusieurs dictons
|
||||
|
||||
structure HTML parfois variable
|
||||
|
||||
gérer les accents et encodage UTF-8
|
||||
|
||||
Si tu veux, je peux te générer directement le contenu complet de ce fichier scrap_saint.md avec un script Python détaillé (requests + BeautifulSoup) prêt à copier.
|
||||
Dis-moi si tu veux aussi inclure le scraping du contenu de la page alphabétique SaintsA.html pour récupérer la liste complète des noms de saints.
|
||||
|
||||
genere scrap.md
|
||||
|
||||
Voici un tutoriel complet scrap_saint.md pour t’aider à scraper les pages de saints et leurs dictons à partir du site https://www.saint-dicton.com/0222.html
|
||||
et des pages annuelles correspondantes. Ce fichier explique l’analyse de la structure, un exemple concret de script Python, et des conseils pour boucler sur toute l’année.
|
||||
|
||||
# scrap_saint.md
|
||||
|
||||
## Objectif
|
||||
|
||||
Ce tutoriel explique comment *scraper automatiquement* les informations suivantes pour chaque jour de l’année à partir du site **saint-dicton.com** :
|
||||
|
||||
- la **date** format MMDD (ex : *0222*)
|
||||
- le(s) **nom(s) du saint** fêté(s)
|
||||
- le ou les **dictons associés au jour**
|
||||
|
||||
L’objectif est de générer une base de données annuelle (CSV/JSON) des saints et des dictons, utilisable ensuite dans un projet Python par exemple.
|
||||
|
||||
---
|
||||
|
||||
## Analyse de la structure du site
|
||||
|
||||
Le site propose une **page par date** selon le format d’URL :
|
||||
|
||||
|
||||
https://www.saint-dicton.com/MMDD.html
|
||||
|
||||
|
||||
Exemples :
|
||||
- https://www.saint-dicton.com/0222.html → *Saint-Sulpice Sévère* + dicton du jour :contentReference[oaicite:0]{index=0}
|
||||
- https://www.saint-dicton.com/0208.html → *dicton de la Saint-Jean* :contentReference[oaicite:1]{index=1}
|
||||
|
||||
Chaque page contient typiquement :
|
||||
|
||||
1. **Fêtes du jour**
|
||||
Une section listant un ou plusieurs saints (ex : *St-Sulpice Sévère*, *St-Valère*) :contentReference[oaicite:2]{index=2}
|
||||
|
||||
2. **Bloc Dicton du jour**
|
||||
Titre suivi du texte du dicton (souvent une ou plusieurs lignes) :contentReference[oaicite:3]{index=3}
|
||||
|
||||
3. **(Optionnel) Prénoms à fêter**
|
||||
Liste de prénoms associés à la date :contentReference[oaicite:4]{index=4}
|
||||
|
||||
> Le site propose aussi une page d’accueil et des pages alphabétiques, mais pour extraire une *base annuelle*, le patron `MMDD.html` est utile pour itérer sur chaque jour de l’année. :contentReference[oaicite:5]{index=5}
|
||||
|
||||
---
|
||||
|
||||
## Pré-requis
|
||||
|
||||
Installer les dépendances Python nécessaires :
|
||||
|
||||
```bash
|
||||
pip install requests beautifulsoup4
|
||||
|
||||
Optionnel mais recommandé :
|
||||
|
||||
pip install lxml
|
||||
Exemple de script Python (scraper)
|
||||
|
||||
Ce script parcourt toutes les dates de l’année, récupère chaque page, analyse l’HTML et enregistre les données dans un fichier CSV.
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import csv
|
||||
import time
|
||||
|
||||
BASE_URL = "https://www.saint-dicton.com/{:02d}{:02d}.html"
|
||||
|
||||
def scrape_day(month: int, day: int):
|
||||
url = BASE_URL.format(month, day)
|
||||
resp = requests.get(url)
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
|
||||
soup = BeautifulSoup(resp.text, "lxml")
|
||||
|
||||
# Extraire les noms de saints
|
||||
saints = []
|
||||
h1 = soup.find("h1")
|
||||
if h1:
|
||||
# souvent la liste des saints est en texte dans la partie principale
|
||||
for line in h1.text.split("\n"):
|
||||
if line.strip():
|
||||
saints.append(line.strip())
|
||||
|
||||
# trouver le div ou section contenant "Dicton du jour"
|
||||
dicton_data = ""
|
||||
target = soup.find(text="Dicton du jour")
|
||||
if target:
|
||||
parent = target.find_parent()
|
||||
if parent:
|
||||
# juste après ce bloc, prendre les lignes de dicton
|
||||
for p in parent.find_next_siblings():
|
||||
text = p.get_text(strip=True)
|
||||
if text:
|
||||
dicton_data += text + " "
|
||||
|
||||
return {
|
||||
"month": month,
|
||||
"day": day,
|
||||
"saints": saints,
|
||||
"dicton": dicton_data.strip()
|
||||
}
|
||||
|
||||
# Boucle sur toute l'année
|
||||
with open("saints_dictons.csv", "w", newline="", encoding="utf-8") as csvfile:
|
||||
fieldnames = ["month", "day", "saints", "dicton"]
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=";")
|
||||
writer.writeheader()
|
||||
|
||||
for m in range(1, 13):
|
||||
for d in range(1, 32):
|
||||
data = scrape_day(m, d)
|
||||
if data:
|
||||
writer.writerow({
|
||||
"month": data["month"],
|
||||
"day": data["day"],
|
||||
"saints": "|".join(data["saints"]),
|
||||
"dicton": data["dicton"]
|
||||
})
|
||||
print(f"Scraped {m:02d}-{d:02d}")
|
||||
time.sleep(1) # pause pour éviter surcharge
|
||||
Bonnes pratiques & conseils
|
||||
💡 Respect et performance
|
||||
|
||||
Respecte les règles d’utilisation du site. Le scraping est légitime sur du contenu public, mais il faut éviter une surcharge du serveur.
|
||||
|
||||
Met une pause (time.sleep(1)) entre les requêtes pour ne pas déclencher de blocage ou DDoS.
|
||||
|
||||
🧠 Inspection du HTML
|
||||
|
||||
Avant d’extraire le texte, inspecte la page dans un navigateur (clic droit → Inspecter) pour confirmer la structure des balises.
|
||||
|
||||
Le site n’utilise pas d’API JSON, donc BeautifulSoup est adapté.
|
||||
|
||||
⚠️ Variations possibles
|
||||
|
||||
La structure peut varier légèrement d’une date à l’autre (par ex., plusieurs saints listés en texte, parfois des paragraphes additionnels).
|
||||
|
||||
Adaptation des sélecteurs (find, find_all, classes ou IDs) peut être nécessaire selon les pages.
|
||||
|
||||
Sorties possibles
|
||||
|
||||
À partir du script ci-dessus, tu peux générer :
|
||||
|
||||
un CSV (comme ci-dessous)
|
||||
|
||||
un JSON structuré
|
||||
|
||||
un SQLite pour intégration dans ton application jardinage
|
||||
|
||||
Exemple d’enregistrement CSV :
|
||||
|
||||
month;day;saints;dicton
|
||||
2;22;St-Sulpice Sévère|St-Valère;"S'il gèle à la Saint-Sulpice, Le printemps sera propice"
|
||||
Conclusion
|
||||
|
||||
En parcourant les URL de MMDD.html et en analysant l’HTML, tu peux extraire automatiquement pour toute l’année la liste des saints et leurs dictons associés.
|
||||
Ce fichier scrap_saint.md te fournit une base de départ claire avec un script Python prêt à l’emploi.
|
||||
191
calendrier_lunaire/saints_dictons/parse_saints_dictons.py
Normal file
191
calendrier_lunaire/saints_dictons/parse_saints_dictons.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DATE_HEADING_RE = re.compile(
|
||||
r"^\s{0,3}(?:#{1,6}\s*)?((?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01])|(?:0[1-9]|[12][0-9]|3[01])[-/](?:0[1-9]|1[0-2])|(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))\s*$"
|
||||
)
|
||||
INLINE_DATE_DIC_RE = re.compile(
|
||||
r"^\s*((?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01])|(?:0[1-9]|[12][0-9]|3[01])[-/](?:0[1-9]|1[0-2])|(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))\s*[:\-]\s*(.+)$"
|
||||
)
|
||||
|
||||
|
||||
def _normalize_mmdd(token: str) -> str | None:
|
||||
token = token.strip()
|
||||
if re.fullmatch(r"(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])", token):
|
||||
month = token[:2]
|
||||
day = token[2:]
|
||||
elif re.fullmatch(r"(0[1-9]|1[0-2])[-/](0[1-9]|[12][0-9]|3[01])", token):
|
||||
month, day = re.split(r"[-/]", token)
|
||||
elif re.fullmatch(r"(0[1-9]|[12][0-9]|3[01])[-/](0[1-9]|1[0-2])", token):
|
||||
day, month = re.split(r"[-/]", token)
|
||||
else:
|
||||
return None
|
||||
return f"{month}-{day}"
|
||||
|
||||
|
||||
def _unique(values: list[str]) -> list[str]:
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in values:
|
||||
v = value.strip()
|
||||
if not v:
|
||||
continue
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
|
||||
def _split_saints(text: str) -> list[str]:
|
||||
# Normalize separators and preserve saint labels.
|
||||
cleaned = text.strip().strip(".")
|
||||
cleaned = re.sub(r"^(saints?\s*[:\-]\s*)", "", cleaned, flags=re.I).strip()
|
||||
if not cleaned:
|
||||
return []
|
||||
parts = re.split(r"\s*(?:,|;|\||\set\s)\s*", cleaned, flags=re.I)
|
||||
return _unique(parts)
|
||||
|
||||
|
||||
def _split_dictons(text: str) -> list[str]:
|
||||
cleaned = text.strip()
|
||||
cleaned = re.sub(r"^(dictons?\s*[:\-]\s*)", "", cleaned, flags=re.I).strip()
|
||||
if not cleaned:
|
||||
return []
|
||||
# Keep sentences readable; split on explicit separators first.
|
||||
if "|" in cleaned or ";" in cleaned:
|
||||
parts = re.split(r"\s*(?:\||;)\s*", cleaned)
|
||||
else:
|
||||
parts = [cleaned]
|
||||
return _unique(parts)
|
||||
|
||||
|
||||
def load_saints(path: Path) -> dict[str, list[str]]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
out: dict[str, list[str]] = {}
|
||||
for mmdd, saint_value in raw.items():
|
||||
key = _normalize_mmdd(mmdd)
|
||||
if key is None:
|
||||
continue
|
||||
if isinstance(saint_value, list):
|
||||
saints = [str(x).strip() for x in saint_value]
|
||||
else:
|
||||
saints = _split_saints(str(saint_value))
|
||||
out[key] = _unique(saints)
|
||||
return out
|
||||
|
||||
|
||||
def parse_dictons_text(path: Path) -> dict[str, list[str]]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
out: dict[str, list[str]] = {}
|
||||
current_date: str | None = None
|
||||
|
||||
for raw in lines:
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Date heading block
|
||||
m_head = DATE_HEADING_RE.match(line)
|
||||
if m_head:
|
||||
current_date = _normalize_mmdd(m_head.group(1))
|
||||
if current_date and current_date not in out:
|
||||
out[current_date] = []
|
||||
continue
|
||||
|
||||
# Inline date + dicton
|
||||
m_inline = INLINE_DATE_DIC_RE.match(line)
|
||||
if m_inline:
|
||||
mmdd = _normalize_mmdd(m_inline.group(1))
|
||||
if mmdd:
|
||||
out.setdefault(mmdd, []).extend(_split_dictons(m_inline.group(2)))
|
||||
current_date = mmdd
|
||||
continue
|
||||
|
||||
# Bullets or plain lines inside current date block
|
||||
if current_date:
|
||||
line = re.sub(r"^\s*[-*]\s*", "", line).strip()
|
||||
if not line:
|
||||
continue
|
||||
if re.match(r"^saints?\s*[:\-]", line, flags=re.I):
|
||||
# Saints line is ignored here; saints come from saints_json.
|
||||
continue
|
||||
out.setdefault(current_date, []).extend(_split_dictons(line))
|
||||
|
||||
return {k: _unique(v) for k, v in out.items()}
|
||||
|
||||
|
||||
def _as_iso(year: int, mmdd: str) -> str:
|
||||
month, day = mmdd.split("-")
|
||||
return date(year, int(month), int(day)).isoformat()
|
||||
|
||||
|
||||
def build_output(
|
||||
saints_by_date: dict[str, list[str]],
|
||||
dictons_by_date: dict[str, list[str]],
|
||||
year: int | None,
|
||||
) -> list[dict]:
|
||||
all_dates = sorted(set(saints_by_date) | set(dictons_by_date))
|
||||
rows: list[dict] = []
|
||||
for mmdd in all_dates:
|
||||
row = {
|
||||
"date": _as_iso(year, mmdd) if year else mmdd,
|
||||
"saints": saints_by_date.get(mmdd, []),
|
||||
"dictons": dictons_by_date.get(mmdd, []),
|
||||
}
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Génère un JSON saints+dictons: date, saints[], dictons[]"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--saints-json",
|
||||
default="calendrier_lunaire/saints_dictons/saints_france.json",
|
||||
help="Fichier JSON des saints (clé MM-DD)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dictons-file",
|
||||
required=True,
|
||||
help="Fichier texte/markdown des dictons (avec dates MM-DD, DD/MM ou MMDD)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="calendrier_lunaire/saints_dictons/saints_dictons.json",
|
||||
help="Fichier JSON de sortie",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--year",
|
||||
type=int,
|
||||
help="Optionnel: convertit MM-DD en YYYY-MM-DD",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
saints_path = Path(args.saints_json)
|
||||
dictons_path = Path(args.dictons_file)
|
||||
output_path = Path(args.output)
|
||||
|
||||
saints_by_date = load_saints(saints_path)
|
||||
dictons_by_date = parse_dictons_text(dictons_path)
|
||||
rows = build_output(saints_by_date, dictons_by_date, args.year)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"JSON généré: {output_path} ({len(rows)} dates)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
171
calendrier_lunaire/saints_dictons/saint_dicton_year_scraper.py
Normal file
171
calendrier_lunaire/saints_dictons/saint_dicton_year_scraper.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import date, timedelta
|
||||
from html import unescape
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
MONTHS_FR = {
|
||||
1: "janvier", 2: "février", 3: "mars", 4: "avril", 5: "mai", 6: "juin",
|
||||
7: "juillet", 8: "août", 9: "septembre", 10: "octobre", 11: "novembre", 12: "décembre",
|
||||
}
|
||||
|
||||
|
||||
def fetch_html(url: str) -> str:
|
||||
req = Request(url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"})
|
||||
with urlopen(req, timeout=25) as resp:
|
||||
raw = resp.read()
|
||||
charset = (resp.headers.get_content_charset() or "utf-8").lower()
|
||||
try:
|
||||
return raw.decode(charset, errors="replace")
|
||||
except Exception:
|
||||
return raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def clean_html_text(s: str) -> str:
|
||||
s = re.sub(r"<br\s*/?>", " ", s, flags=re.I)
|
||||
s = re.sub(r"<[^>]+>", "", s)
|
||||
s = unescape(s)
|
||||
s = s.replace("\xa0", " ")
|
||||
return re.sub(r"\s+", " ", s).strip()
|
||||
|
||||
|
||||
def parse_saints(html: str) -> list[str]:
|
||||
rows = re.findall(r'<p[^>]*class="sd-name"[^>]*>(.*?)</p>', html, flags=re.I | re.S)
|
||||
out, seen = [], set()
|
||||
for row in rows:
|
||||
txt = clean_html_text(row)
|
||||
if txt and txt not in seen:
|
||||
out.append(txt)
|
||||
seen.add(txt)
|
||||
return out
|
||||
|
||||
|
||||
def parse_dictons(html: str) -> list[str]:
|
||||
rows = re.findall(r'<p[^>]*class="dict"[^>]*>(.*?)</p>', html, flags=re.I | re.S)
|
||||
out = []
|
||||
for row in rows:
|
||||
txt = clean_html_text(row)
|
||||
if txt:
|
||||
out.append(txt)
|
||||
return out
|
||||
|
||||
|
||||
def parse_prenoms(html: str) -> list[str]:
|
||||
block = re.search(
|
||||
r'<h2[^>]*>[^<]*Pr[^<]*noms[^<]*f[^<]*ter[^<]*</h2>.*?<ul[^>]*>(.*?)</ul>',
|
||||
html,
|
||||
flags=re.I | re.S,
|
||||
)
|
||||
target = block.group(1) if block else ""
|
||||
rows = re.findall(r'<li[^>]*>(.*?)</li>', target, flags=re.I | re.S)
|
||||
out, seen = [], set()
|
||||
for row in rows:
|
||||
txt = clean_html_text(row)
|
||||
if txt and txt not in seen:
|
||||
out.append(txt)
|
||||
seen.add(txt)
|
||||
return out
|
||||
|
||||
|
||||
def iter_mmdd_full_year(year: int):
|
||||
d = date(year, 1, 1)
|
||||
end = date(year, 12, 31)
|
||||
while d <= end:
|
||||
yield d.strftime("%m%d"), d
|
||||
d += timedelta(days=1)
|
||||
# assure 29 février même année non bissextile
|
||||
if year % 4 != 0 or (year % 100 == 0 and year % 400 != 0):
|
||||
yield "0229", None
|
||||
|
||||
|
||||
def scrape_day(base_url: str, mmdd: str, d: date | None) -> dict:
|
||||
url = f"{base_url.rstrip('/')}/{mmdd}.html"
|
||||
html = fetch_html(url)
|
||||
if d:
|
||||
label = f"{d.day:02d} {MONTHS_FR[d.month]}"
|
||||
iso = d.isoformat()
|
||||
else:
|
||||
label = "29 février"
|
||||
iso = None
|
||||
|
||||
return {
|
||||
"date": label,
|
||||
"date_iso": iso,
|
||||
"mmdd": mmdd,
|
||||
"saints": parse_saints(html),
|
||||
"dictons": parse_dictons(html),
|
||||
"prenoms_a_feter": parse_prenoms(html),
|
||||
"source_url": url,
|
||||
}
|
||||
|
||||
|
||||
def _ts() -> str:
|
||||
return datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
|
||||
def _log(message: str, enabled: bool) -> None:
|
||||
if enabled:
|
||||
print(f"[{_ts()}] {message}", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Scrape saints/dictons pour toute une année (inclut 29 février)")
|
||||
ap.add_argument("--year", type=int, default=date.today().year)
|
||||
ap.add_argument("--base", default="https://www.saint-dicton.com")
|
||||
ap.add_argument("--sleep-ms", type=int, default=150, help="Pause entre requêtes")
|
||||
ap.add_argument("--limit", type=int, default=0, help="Limiter le nb de jours (test rapide)")
|
||||
ap.add_argument("--out", default="", help="Fichier de sortie JSON (sinon stdout)")
|
||||
ap.add_argument("--log-every", type=int, default=10, help="Affiche un log de progression tous les N jours")
|
||||
ap.add_argument("--quiet", action="store_true", help="Réduit les logs")
|
||||
args = ap.parse_args()
|
||||
|
||||
results = []
|
||||
count = 0
|
||||
verbose = not args.quiet
|
||||
log_every = max(1, args.log_every)
|
||||
_log(f"Démarrage scrape année={args.year}, base={args.base}", verbose)
|
||||
for mmdd, d in iter_mmdd_full_year(args.year):
|
||||
url = f"{args.base.rstrip('/')}/{mmdd}.html"
|
||||
_log(f"[{count + 1}] fetch {mmdd} -> {url}", verbose)
|
||||
try:
|
||||
results.append(scrape_day(args.base, mmdd, d))
|
||||
_log(f"[{count + 1}] ok {mmdd}", verbose and ((count + 1) % log_every == 0 or count == 0))
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"mmdd": mmdd,
|
||||
"date_iso": d.isoformat() if d else None,
|
||||
"error": str(e),
|
||||
"source_url": url,
|
||||
})
|
||||
_log(f"[{count + 1}] erreur {mmdd}: {e}", True)
|
||||
count += 1
|
||||
if args.limit and count >= args.limit:
|
||||
_log(f"Arrêt par --limit={args.limit}", verbose)
|
||||
break
|
||||
if args.sleep_ms > 0:
|
||||
time.sleep(args.sleep_ms / 1000)
|
||||
|
||||
payload = {
|
||||
"year": args.year,
|
||||
"count": len(results),
|
||||
"includes_feb29": any(r.get("mmdd") == "0229" for r in results),
|
||||
"data": results,
|
||||
}
|
||||
|
||||
txt = json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
if args.out:
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
f.write(txt)
|
||||
_log(f"Fichier écrit: {args.out}", True)
|
||||
else:
|
||||
print(txt)
|
||||
_log(f"Terminé: {len(results)} jours", verbose)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
10282
calendrier_lunaire/saints_dictons/saints_2026.json
Normal file
10282
calendrier_lunaire/saints_dictons/saints_2026.json
Normal file
File diff suppressed because it is too large
Load Diff
10
calendrier_lunaire/saints_dictons/saints_france.json
Normal file
10
calendrier_lunaire/saints_dictons/saints_france.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"04-23": "Saint Georges",
|
||||
"04-25": "Saint Marc",
|
||||
"05-11": "Saint Mamert",
|
||||
"05-12": "Saint Pancrace",
|
||||
"05-13": "Saint Servais",
|
||||
"05-14": "Saint Boniface",
|
||||
"05-19": "Saint Yves",
|
||||
"05-25": "Saint Urbain"
|
||||
}
|
||||
65
calendrier_lunaire/test_lunar_calendar.py
Normal file
65
calendrier_lunaire/test_lunar_calendar.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from lunar_calendar import DayInfo, build_calendar
|
||||
|
||||
|
||||
def test_build_calendar_rejects_invalid_range() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
build_calendar(date(2026, 1, 10), date(2026, 1, 1))
|
||||
|
||||
|
||||
def test_build_calendar_returns_expected_day_count() -> None:
|
||||
rows = build_calendar(date(2026, 1, 1), date(2026, 1, 3))
|
||||
assert len(rows) == 3
|
||||
assert rows[0].date == "2026-01-01"
|
||||
assert rows[-1].date == "2026-01-03"
|
||||
|
||||
|
||||
def test_dayinfo_fields_are_well_typed() -> None:
|
||||
row = build_calendar(date(2026, 1, 1), date(2026, 1, 1))[0]
|
||||
assert isinstance(row, DayInfo)
|
||||
assert isinstance(row.date, str)
|
||||
assert isinstance(row.phase, str)
|
||||
assert isinstance(row.illumination, float)
|
||||
assert isinstance(row.croissante_decroissante, str)
|
||||
assert isinstance(row.montante_descendante, str)
|
||||
assert isinstance(row.signe, str)
|
||||
assert isinstance(row.type_jour, str)
|
||||
assert isinstance(row.soleil_lever, str)
|
||||
assert isinstance(row.soleil_coucher, str)
|
||||
assert isinstance(row.duree_jour, str)
|
||||
assert isinstance(row.lune_lever, str)
|
||||
assert isinstance(row.lune_coucher, str)
|
||||
assert isinstance(row.duree_presence_lune, str)
|
||||
assert isinstance(row.saint_du_jour, str)
|
||||
assert isinstance(row.saint_de_glace, bool)
|
||||
assert isinstance(row.perigee, bool)
|
||||
assert isinstance(row.apogee, bool)
|
||||
assert isinstance(row.noeud_lunaire, bool)
|
||||
assert isinstance(row.transitions_type_jour, list)
|
||||
assert isinstance(row.transitions_montante_descendante, list)
|
||||
|
||||
|
||||
def test_saints_de_glace_are_exposed() -> None:
|
||||
rows = build_calendar(date(2026, 5, 11), date(2026, 5, 13))
|
||||
assert rows[0].saint_du_jour == "Saint Mamert"
|
||||
assert rows[1].saint_du_jour == "Saint Pancrace"
|
||||
assert rows[2].saint_du_jour == "Saint Servais"
|
||||
assert all(r.saint_de_glace for r in rows)
|
||||
|
||||
|
||||
def test_rise_set_fields_are_present() -> None:
|
||||
row = build_calendar(date(2026, 2, 22), date(2026, 2, 22))[0]
|
||||
assert row.soleil_lever != ""
|
||||
assert row.soleil_coucher != ""
|
||||
assert row.duree_jour != ""
|
||||
assert row.lune_lever != ""
|
||||
assert row.lune_coucher != ""
|
||||
|
||||
|
||||
def test_transition_items_have_expected_shape() -> None:
|
||||
row = build_calendar(date(2026, 2, 22), date(2026, 2, 22))[0]
|
||||
for item in row.transitions_type_jour + row.transitions_montante_descendante:
|
||||
assert set(item.keys()) == {"heure", "avant", "apres"}
|
||||
Reference in New Issue
Block a user