Initial: Script de recherche médiathèques Plaine Commune
- Recherche par mot-clé, auteur, titre, sujet - Utilise Selenium avec Firefox headless - Affiche titre, auteur, type, disponibilité et résumé - Fonctionne sur le mesh KAWA
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.log
|
||||||
|
.geckodriver/
|
||||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Médiathèques Plaine Commune - Recherche de livres
|
||||||
|
|
||||||
|
Script Python pour rechercher des livres dans le catalogue des médiathèques de Plaine Commune (Saint-Denis, Aubervilliers, etc.).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer un venv
|
||||||
|
python3 -m venv ~/mediatheque-venv
|
||||||
|
source ~/mediatheque-venv/bin/activate
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
pip install selenium webdriver-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- **Firefox** doit être installé (le script utilise GeckoDriver)
|
||||||
|
- Python 3.8+
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Recherche simple
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mediatheque_search.py "Carl Jung"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recherche par auteur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mediatheque_search.py --author "Jung"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recherche par titre
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mediatheque_search.py --title "rêves"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recherche par sujet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mediatheque_search.py --subject "psychologie"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `--max N` : Nombre maximum de résultats (défaut: 20)
|
||||||
|
- `--no-headless` : Afficher le navigateur Firefox (pour debug)
|
||||||
|
|
||||||
|
## Exemple de sortie
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
RECHERCHE: Carl Jung
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
📚 1. Sur l'interprétation des rêves
|
||||||
|
Auteur: / Carl Gustav Jung
|
||||||
|
Type: Livre
|
||||||
|
Disponibilité: Au moins un exemplaire disponible sur le réseau
|
||||||
|
Résumé: Ouvrage issu d'un séminaire dans lequel le psychiatre recense les grands systèmes...
|
||||||
|
|
||||||
|
📚 2. Jung
|
||||||
|
Auteur: / Carole, Sédillot
|
||||||
|
Type: Livre
|
||||||
|
Disponibilité: Au moins un exemplaire disponible sur le réseau
|
||||||
|
|
||||||
|
📄 Page 1 de 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intégration KAWA
|
||||||
|
|
||||||
|
Ce script est conçu pour fonctionner sur les nœuds du mesh KAWA. Le venv peut être partagé via Syncthing ou installé localement sur chaque nœud.
|
||||||
|
|
||||||
|
### Dépendances
|
||||||
|
|
||||||
|
- selenium >= 4.0
|
||||||
|
- webdriver-manager >= 4.0
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT
|
||||||
234
mediatheque_search.py
Normal file
234
mediatheque_search.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Médiathèques Plaine Commune - Recherche de livres
|
||||||
|
|
||||||
|
Script de recherche dans le catalogue des médiathèques de Plaine Commune
|
||||||
|
Utilise Selenium pour interagir avec l'interface web (JavaScript requis).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python mediatheque_search.py "Carl Jung"
|
||||||
|
python mediatheque_search.py --author "Jung"
|
||||||
|
python mediatheque_search.py --title "rêves"
|
||||||
|
python mediatheque_search.py --subject "psychologie"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.firefox.options import Options
|
||||||
|
from selenium.webdriver.firefox.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from webdriver_manager.firefox import GeckoDriverManager
|
||||||
|
|
||||||
|
|
||||||
|
class MediathequeSearch:
|
||||||
|
"""Recherche dans le catalogue des médiathèques de Plaine Commune"""
|
||||||
|
|
||||||
|
BASE_URL = "https://mediatheques-plainecommune.fr/iguana/www.main.cls"
|
||||||
|
SEARCH_URL = "https://mediatheques-plainecommune.fr/iguana/www.main.cls?surl=search&searchProfile=Recherche"
|
||||||
|
|
||||||
|
def __init__(self, headless=True):
|
||||||
|
"""Initialise le driver Selenium"""
|
||||||
|
options = Options()
|
||||||
|
if headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
options.add_argument("--width=1920")
|
||||||
|
options.add_argument("--height=1080")
|
||||||
|
|
||||||
|
service = Service(GeckoDriverManager().install())
|
||||||
|
self.driver = webdriver.Firefox(service=service, options=options)
|
||||||
|
self.driver.implicitly_wait(5)
|
||||||
|
|
||||||
|
def _reject_cookies(self):
|
||||||
|
"""Refuse la bannière cookies RGPD"""
|
||||||
|
try:
|
||||||
|
refuse_btn = WebDriverWait(self.driver, 3).until(
|
||||||
|
EC.element_to_be_clickable((By.ID, "gdpr-cookie-refuse"))
|
||||||
|
)
|
||||||
|
refuse_btn.click()
|
||||||
|
time.sleep(0.5)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_search_page(self):
|
||||||
|
"""Charge la page de recherche"""
|
||||||
|
self.driver.get(self.BASE_URL + "?surl=axiell")
|
||||||
|
time.sleep(2)
|
||||||
|
self._reject_cookies()
|
||||||
|
|
||||||
|
self.driver.get(self.SEARCH_URL)
|
||||||
|
time.sleep(3)
|
||||||
|
self._reject_cookies()
|
||||||
|
|
||||||
|
def search(self, query, max_results=20):
|
||||||
|
"""
|
||||||
|
Effectue une recherche dans le catalogue
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Terme de recherche
|
||||||
|
max_results: Nombre maximum de résultats à retourner
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Liste de dictionnaires contenant les résultats
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._load_search_page()
|
||||||
|
|
||||||
|
# Entrer la requête
|
||||||
|
search_input = self.driver.find_element(By.CSS_SELECTOR, "input[id*='inputWords']")
|
||||||
|
search_input.clear()
|
||||||
|
search_input.send_keys(query)
|
||||||
|
search_input.send_keys(Keys.RETURN)
|
||||||
|
|
||||||
|
# Attendre les résultats
|
||||||
|
time.sleep(5)
|
||||||
|
self._reject_cookies()
|
||||||
|
|
||||||
|
# Extraire les résultats
|
||||||
|
results = []
|
||||||
|
items = self.driver.find_elements(By.CSS_SELECTOR, "li.listItem")
|
||||||
|
|
||||||
|
for item in items[:max_results]:
|
||||||
|
try:
|
||||||
|
result = self._extract_result(item)
|
||||||
|
if result:
|
||||||
|
results.append(result)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Infos de pagination
|
||||||
|
try:
|
||||||
|
page_info = self.driver.find_element(By.CSS_SELECTOR, ".pageInfo").text
|
||||||
|
except:
|
||||||
|
page_info = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
"page_info": page_info,
|
||||||
|
"total_found": len(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.driver.quit()
|
||||||
|
|
||||||
|
def _extract_result(self, item):
|
||||||
|
"""Extrait les informations d'un résultat"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Titre
|
||||||
|
try:
|
||||||
|
result["title"] = item.find_element(By.CSS_SELECTOR, ".briefMainTitle").text.strip()
|
||||||
|
except:
|
||||||
|
result["title"] = "N/A"
|
||||||
|
|
||||||
|
# Auteur
|
||||||
|
try:
|
||||||
|
result["author"] = item.find_element(By.CSS_SELECTOR, ".BriefAuthorpco").text.strip()
|
||||||
|
except:
|
||||||
|
result["author"] = "N/A"
|
||||||
|
|
||||||
|
# Type de document
|
||||||
|
try:
|
||||||
|
result["type"] = item.find_element(By.CSS_SELECTOR, ".briefTypeDoc").text.strip()
|
||||||
|
except:
|
||||||
|
result["type"] = "N/A"
|
||||||
|
|
||||||
|
# Disponibilité
|
||||||
|
try:
|
||||||
|
result["availability"] = item.find_element(By.CSS_SELECTOR, ".briefAvailability span").text.strip()
|
||||||
|
except:
|
||||||
|
result["availability"] = "N/A"
|
||||||
|
|
||||||
|
# Résumé
|
||||||
|
try:
|
||||||
|
summary = item.find_element(By.CSS_SELECTOR, ".briefResume").text.strip()
|
||||||
|
result["summary"] = summary[:200] + "..." if len(summary) > 200 else summary
|
||||||
|
except:
|
||||||
|
result["summary"] = ""
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def search_author(self, author, max_results=20):
|
||||||
|
"""Recherche par auteur"""
|
||||||
|
return self.search(f"auteur: {author}", max_results)
|
||||||
|
|
||||||
|
def search_title(self, title, max_results=20):
|
||||||
|
"""Recherche par titre"""
|
||||||
|
return self.search(f"titre: {title}", max_results)
|
||||||
|
|
||||||
|
def search_subject(self, subject, max_results=20):
|
||||||
|
"""Recherche par sujet"""
|
||||||
|
return self.search(f"sujet: {subject}", max_results)
|
||||||
|
|
||||||
|
|
||||||
|
def print_results(data):
|
||||||
|
"""Affiche les résultats formatés"""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"RECHERCHE: {data['query']}")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
for i, result in enumerate(data["results"], 1):
|
||||||
|
print(f"📚 {i}. {result['title']}")
|
||||||
|
print(f" Auteur: {result['author']}")
|
||||||
|
print(f" Type: {result['type']}")
|
||||||
|
print(f" Disponibilité: {result['availability']}")
|
||||||
|
if result['summary']:
|
||||||
|
print(f" Résumé: {result['summary']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if data['page_info']:
|
||||||
|
print(f"📄 {data['page_info']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Recherche dans le catalogue des médiathèques de Plaine Commune",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Exemples:
|
||||||
|
%(prog)s "Carl Jung"
|
||||||
|
%(prog)s --author "Jung"
|
||||||
|
%(prog)s --title "rêves"
|
||||||
|
%(prog)s --subject "psychologie" --max 50
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("query", nargs="?", help="Terme de recherche")
|
||||||
|
parser.add_argument("--author", "-a", help="Rechercher par auteur")
|
||||||
|
parser.add_argument("--title", "-t", help="Rechercher par titre")
|
||||||
|
parser.add_argument("--subject", "-s", help="Rechercher par sujet")
|
||||||
|
parser.add_argument("--max", "-m", type=int, default=20, help="Nombre max de résultats (défaut: 20)")
|
||||||
|
parser.add_argument("--no-headless", action="store_true", help="Afficher le navigateur")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Déterminer le type de recherche
|
||||||
|
if args.author:
|
||||||
|
searcher = MediathequeSearch(headless=not args.no_headless)
|
||||||
|
results = searcher.search_author(args.author, args.max)
|
||||||
|
elif args.title:
|
||||||
|
searcher = MediathequeSearch(headless=not args.no_headless)
|
||||||
|
results = searcher.search_title(args.title, args.max)
|
||||||
|
elif args.subject:
|
||||||
|
searcher = MediathequeSearch(headless=not args.no_headless)
|
||||||
|
results = searcher.search_subject(args.subject, args.max)
|
||||||
|
elif args.query:
|
||||||
|
searcher = MediathequeSearch(headless=not args.no_headless)
|
||||||
|
results = searcher.search(args.query, args.max)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print_results(results)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
selenium>=4.0.0
|
||||||
|
webdriver-manager>=4.0.0
|
||||||
Reference in New Issue
Block a user