fix: tighten cv extraction heuristics

This commit is contained in:
Antoine 2026-05-28 14:34:37 +02:00
parent df140c3c77
commit 544b5892fa
2 changed files with 91 additions and 2 deletions

View File

@ -5,9 +5,32 @@ from pathlib import Path
from pypdf import PdfReader
EXPERIENCE_TITLE_ENDINGS = {
"administrator",
"analyst",
"architect",
"consultant",
"data engineer",
"data scientist",
"developer",
"director",
"engineer",
"lead",
"manager",
"researcher",
"scientist",
"specialist",
}
def extract_pdf_text(path: Path) -> str:
reader = PdfReader(str(path))
return "\n".join(page.extract_text() or "" for page in reader.pages)
page_texts = [
text.strip()
for text in (page.extract_text() or "" for page in reader.pages)
if text.strip()
]
return "\n".join(page_texts)
def extract_cv_signals(text: str) -> dict[str, object]:
@ -31,7 +54,7 @@ def extract_cv_signals(text: str) -> dict[str, object]:
if lowered.startswith("skills:"):
skills = _parse_csv_field(line)
continue
if " at " in line:
if _looks_like_experience_line(line):
title, company = line.split(" at ", 1)
experience_entries.append(
{
@ -53,3 +76,21 @@ def extract_cv_signals(text: str) -> dict[str, object]:
def _parse_csv_field(line: str) -> list[str]:
_, value = line.split(":", 1)
return [item.strip() for item in value.split(",") if item.strip()]
def _looks_like_experience_line(line: str) -> bool:
if line.count(" at ") != 1:
return False
title, company = (part.strip() for part in line.split(" at ", 1))
if not title or not company:
return False
title_words = title.split()
if len(title_words) < 2:
return False
if title_words[-1].lower() not in EXPERIENCE_TITLE_ENDINGS:
return False
return any(character.isupper() for character in company)

View File

@ -1,6 +1,8 @@
from textwrap import dedent
from pathlib import Path
from job_research.profile.cv_extractor import extract_cv_signals
from job_research.profile.cv_extractor import extract_pdf_text
def test_extract_cv_signals_reads_basic_fields_from_text() -> None:
@ -23,3 +25,49 @@ def test_extract_cv_signals_reads_basic_fields_from_text() -> None:
assert extracted["skills"] == ["Python", "SQL", "Terraform", "GCP", "BigQuery"]
assert extracted["experience_entries"][0]["title"] == "Data Engineer"
assert len(extracted["experience_entries"]) == 2
def test_extract_cv_signals_ignores_prose_that_mentions_at() -> None:
text = dedent(
"""
Tonio
Location: France
Worked at a startup before joining the team.
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_pdf_text_skips_blank_pages(monkeypatch) -> None:
class FakePage:
def __init__(self, text: str | None) -> None:
self._text = text
def extract_text(self) -> str | None:
return self._text
class FakePdfReader:
def __init__(self, path: str) -> None:
self.path = path
self.pages = [
FakePage(None),
FakePage("Tonio"),
FakePage(""),
FakePage("Data Engineer"),
]
seen_paths: list[str] = []
def fake_pdf_reader(path: str) -> FakePdfReader:
seen_paths.append(path)
return FakePdfReader(path)
monkeypatch.setattr("job_research.profile.cv_extractor.PdfReader", fake_pdf_reader)
extracted = extract_pdf_text(Path("cv.pdf"))
assert extracted == "Tonio\nData Engineer"
assert seen_paths == ["cv.pdf"]