59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""LinkedIn profile-link extraction and normalization.
|
|
|
|
One shared vocabulary for "the same person" across the two places a LinkedIn
|
|
identity appears: sourced talent profiles (a normalized URL from the Apify
|
|
actor) and CV text (a link the candidate wrote, often mangled by PDF
|
|
extraction). The match key is the lowercase public slug from /in/<slug>.
|
|
|
|
Top-level module on purpose: talent/, inbox/ and job/ all need it, and any
|
|
package-local home would invite an import cycle.
|
|
"""
|
|
|
|
import re
|
|
from urllib.parse import unquote
|
|
|
|
# CV text arrives from PDF extraction: URLs may carry percent-escapes, no
|
|
# scheme ("linkedin.com/in/jane-doe"), or trailing sentence punctuation glued
|
|
# on by layout. /pub/ is the legacy public-profile path some older CVs still
|
|
# carry.
|
|
_SLUG_RE = re.compile(r"linkedin\.com/(?:in|pub)/([A-Za-z0-9\-_.%]+)", re.IGNORECASE)
|
|
|
|
# Sentinel stored on application rows: NULL means "never scanned", the empty
|
|
# string means "scanned, no link found". The distinction is what lets the lazy
|
|
# backfill converge instead of rescanning every CV on every request.
|
|
NO_SLUG = ""
|
|
|
|
|
|
def normalize_slug(raw) -> str | None:
|
|
"""Lowercase, percent-decoded, stripped of trailing sentence punctuation."""
|
|
if not raw:
|
|
return None
|
|
slug = unquote(str(raw)).strip().lower().rstrip(".")
|
|
return slug or None
|
|
|
|
|
|
def slug_from_url(url) -> str | None:
|
|
"""Slug from an already-normalized profile URL (talent_profiles.linkedin_url)."""
|
|
if not url:
|
|
return None
|
|
match = _SLUG_RE.search(str(url))
|
|
return normalize_slug(match.group(1)) if match else None
|
|
|
|
|
|
def slugs_from_text(text) -> list[str]:
|
|
"""Every distinct slug mentioned in a CV, in order of first appearance."""
|
|
if not text:
|
|
return []
|
|
found: list[str] = []
|
|
for match in _SLUG_RE.finditer(text):
|
|
slug = normalize_slug(match.group(1))
|
|
if slug and slug not in found:
|
|
found.append(slug)
|
|
return found
|
|
|
|
|
|
def primary_slug_from_text(text) -> str:
|
|
"""The slug to persist on an application row; NO_SLUG when the CV has none."""
|
|
slugs = slugs_from_text(text)
|
|
return slugs[0] if slugs else NO_SLUG
|