Files
AutoMood/extractor.py
2026-06-13 13:32:38 +02:00

199 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Extraction par règles : texte brut d'une page Facebook → champs du prospect."""
import re
import unicodedata
from datetime import date
from departements import DEPARTEMENTS, departement_depuis_cp
RE_TELEPHONE = re.compile(r"(?:\+33\s?[1-9]|0[1-9])(?:[\s.\-]?\d{2}){4}")
RE_EMAIL = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
RE_CP_VILLE = re.compile(r"(?<!\d)(\d{5})(?!\d)[ \t,]+([A-Za-zÀ-ÖØ-öø-ÿ'\- ]{2,40})")
RE_CP_SEUL = re.compile(r"(?<!\d)(\d{5})(?!\d)")
# « à Saint-Omer-de-Blain (44) » : ville + numéro de département entre parenthèses
RE_VILLE_DEPT = re.compile(r"([A-Za-zÀ-ÖØ-öø-ÿ'\-]{3,40})?\s*\(\s*(\d{2,3}|2[AB])\s*\)", re.I)
# « Blain, France, 44130 » ou « Blain, 44130 » : ville avant le code postal
RE_VILLE_CP = re.compile(
r"([A-Za-zÀ-ÖØ-öø-ÿ'\- ]{2,40})\s*,\s*(?:France\s*,?\s*)?(\d{5})(?!\d)", re.I)
VOIES = (r"rue|avenue|av\.|boulevard|bd\.?|chemin|route|impasse|place|quai|allée|cours"
r"|esplanade|promenade|lieu-dit|hameau|zone|za|zac|zi")
# Ligne ressemblant à une voie : « 12 Quai du Port », « 6, rue du moulin »...
RE_ADRESSE = re.compile(
rf"(?:^|\n)\s*((?:\d{{1,4}}\s?(?:bis|ter)?\s*,?\s+)?(?:{VOIES})\s[^\n;]{{2,60}})", re.I)
# Mots d'interface Facebook à ne pas prendre pour une ville devant « (NN) »
MOTS_INTERFACE = {
"photos", "avis", "reels", "followers", "publications", "posts",
"mentions", "tout", "plus", "voir", "vidéos", "amis",
}
# (mot-clé, type) — ordre = priorité, le premier trouvé gagne
TYPES = [
("camping", "Camping"),
("guinguette", "Guinguette"),
("festival", "Festival"),
("restaurant", "Restaurant"),
("pizzeria", "Restaurant"),
("creperie", "Restaurant"),
("brasserie", "Restaurant"),
("bistrot", "Restaurant"),
("bistro", "Restaurant"),
("pub", "Bar"),
("bar", "Bar"),
("cafe", "Bar"),
("cave", "Bar"),
("salle", "Salle"),
("mjc", "Salle"),
("hotel", "Hôtel"),
("domaine", "Domaine"),
]
INFOS_LIEU = [
"jardin", "terrasse", "plage", "bord de mer", "port", "piscine",
"lac", "riviere", "rooftop", "patio", "plein air", "scene", "guinguette",
]
def _sans_accents(texte):
return unicodedata.normalize("NFKD", texte).encode("ascii", "ignore").decode()
def _chercher_mot(mot, texte_normalise):
return re.search(rf"(?<![a-z]){re.escape(mot)}(?![a-z])", texte_normalise) is not None
def _telephone(texte):
m = RE_TELEPHONE.search(texte)
if not m:
return ""
chiffres = re.sub(r"\D", "", m.group(0))
if chiffres.startswith("33"):
chiffres = "0" + chiffres[2:]
if len(chiffres) != 10:
return ""
return " ".join(chiffres[i:i + 2] for i in range(0, 10, 2))
def _email(texte):
for m in RE_EMAIL.finditer(texte):
adresse = m.group(0)
if not adresse.lower().endswith(("@facebook.com", "@fb.com")):
return adresse
return ""
def _cp_ville(texte):
for m in RE_CP_VILLE.finditer(texte):
cp, ville = m.group(1), m.group(2)
if not ("01" <= cp[:2] <= "98"):
continue
ville = ville.split("\n")[0].strip(" -'")
ville = re.sub(r"\bfrance\b", "", ville, flags=re.IGNORECASE).strip(" ,-")
return cp, ville.title()
# « Blain, France, 44130 » : ville avant le code postal
for m in RE_VILLE_CP.finditer(texte):
cp, ville = m.group(2), m.group(1).split("\n")[-1].strip(" -'")
if not ("01" <= cp[:2] <= "98") or ville.lower() == "france":
continue
return cp, ville.title() if ville.islower() else ville
m = RE_CP_SEUL.search(texte)
if m and "01" <= m.group(1)[:2] <= "98":
return m.group(1), ""
return "", ""
def _nettoyer_adresse(brut):
# couper avant le code postal s'il suit sur la même ligne
brut = re.split(r",?\s*\d{5}\b", brut)[0]
# « 6, rue du moulin , Blain, France » : ne garder que jusqu'au segment de voie,
# la ville et le pays ont leurs propres colonnes
segments = [s.strip() for s in brut.split(",") if s.strip()]
dernier_voie = -1
for i, seg in enumerate(segments):
if re.search(rf"\b(?:{VOIES})\b", seg, re.I):
dernier_voie = i
if dernier_voie >= 0:
segments = segments[:dernier_voie + 1]
else:
segments = [s for s in segments if s.lower() != "france"]
return ", ".join(segments).strip(" ,-")
def _adresse(texte):
lignes = [l.strip() for l in texte.split("\n")]
# libellé « Adresse » de la section Coordonnées (mais pas « Adresse e-mail »)
for i, ligne in enumerate(lignes):
if ligne.lower() in ("adresse", "address"):
for suite in lignes[i + 1:i + 3]:
if suite and not re.match(r"^(adresse|address|e-?mail|téléphone|phone|site|web|messenger)", suite, re.I):
return _nettoyer_adresse(suite)
m = RE_ADRESSE.search(texte)
return _nettoyer_adresse(m.group(1)) if m else ""
def _ville_dept_parentheses(texte):
"""Repli quand il n'y a pas d'adresse postale : « Saint-Omer-de-Blain (44) »."""
for m in RE_VILLE_DEPT.finditer(texte):
cle = m.group(2).upper()
nom = DEPARTEMENTS.get(cle)
if not nom:
continue
ville = (m.group(1) or "").strip("-' ")
if len(ville) < 3 or ville.lower() in MOTS_INTERFACE or ville.lower() == "france":
ville = ""
return ville, f"{cle} - {nom}"
return "", ""
def _type_lieu(titre, texte):
# La catégorie de la page apparaît en général dans les premiers caractères du texte
for zone in (titre, texte[:500], texte):
zone_norm = _sans_accents(zone.lower())
for mot, type_lieu in TYPES:
if _chercher_mot(mot, zone_norm):
return type_lieu
return ""
def _infos_lieu(texte):
texte_norm = _sans_accents(texte.lower())
trouves = [mot for mot in INFOS_LIEU if _chercher_mot(mot, texte_norm)]
return ", ".join(trouves[:5])
def extraire(titre, texte, url):
cp, ville = _cp_ville(texte)
departement = departement_depuis_cp(cp)
if not cp:
ville_repli, departement = _ville_dept_parentheses(texte)
ville = ville or ville_repli
return {
"Nom du prospect": titre.strip(),
"Département": departement,
"Ville": ville,
"Code Postal": cp,
"Adresse": _adresse(texte),
"Date d'ajout": date.today().strftime("%d/%m/%Y"),
"Date de contact": "",
"Nom de contact": "",
"Téléphone": _telephone(texte),
"Email": _email(texte),
"Infos du lieu": _infos_lieu(texte),
"Type": _type_lieu(titre, texte),
"Lien Facebook": url,
}
if __name__ == "__main__":
exemple = """Le Vieux Gréement
Bar · Restaurant
12 Quai du Port, 29200 Brest, France
06 12 34 56 78
contact@vieuxgreement.fr
Grande terrasse avec vue sur le port, concerts en plein air l'été dans le jardin.
"""
import json
champs = extraire("Le Vieux Gréement", exemple, "https://www.facebook.com/vieuxgreement")
print(json.dumps(champs, ensure_ascii=False, indent=2))