Initial commit
This commit is contained in:
198
extractor.py
Normal file
198
extractor.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user