diff --git a/gimme_job/adapters/linkedin.py b/gimme_job/adapters/linkedin.py index f92a6e8..8a0cd89 100644 --- a/gimme_job/adapters/linkedin.py +++ b/gimme_job/adapters/linkedin.py @@ -8,6 +8,30 @@ from gimme_job.adapters.registry import register from gimme_job.models.dto import JobPostingCandidate, RawJobCard +def _deduplicate_title(title: str) -> str: + """Remove LinkedIn's accessibility text duplication. + + LinkedIn renders job titles as: + Title + Title with verification + inner_text() returns both, so we get "Title Title with verification". + Strategy: if the title starts with a repeated prefix word-by-word, keep the shorter part. + """ + words = title.split() + n = len(words) + # Try splitting at each midpoint from 1 to n//2 + for split in range(1, n // 2 + 1): + prefix = words[:split] + rest = words[split:] + # Exact full duplication: "Foo Bar Foo Bar" + if rest == prefix: + return " ".join(prefix) + # Prefix repeats at start of rest: "Foo Foo with verification" + if rest[:split] == prefix: + return " ".join(prefix) + return title + + @register("linkedin") class LinkedInAdapter(ManifestDrivenAdapter): @@ -17,6 +41,16 @@ class LinkedInAdapter(ManifestDrivenAdapter): return super().collect_cards(page, manifest) def normalize(self, raw: RawJobCard) -> JobPostingCandidate: + # Fix relative URLs → absolute + if raw.url and raw.url.startswith("/"): + raw = raw.model_copy(update={"url": "https://www.linkedin.com" + raw.url}) + + # Fix duplicated titles caused by LinkedIn's accessibility span + # e.g. "Orthodontist Orthodontist with verification" → "Orthodontist" + # e.g. "LOCUM Dentist ... LOCUM Dentist ..." → "LOCUM Dentist ..." + if raw.title: + raw = raw.model_copy(update={"title": _deduplicate_title(raw.title)}) + return super().normalize(raw) @staticmethod