fix: relax cv experience parsing
Reject narrative verb-led bullets, allow short tech titles, and switch company prose checks to phrase-level matching so real employers like Made in Design are preserved.
This commit is contained in:
parent
49cc4a9959
commit
042feab4fd
@ -35,6 +35,21 @@ EXPERIENCE_TITLE_STOPWORDS = {
|
|||||||
"because",
|
"because",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EXPERIENCE_TITLE_ACTION_STARTS = {
|
||||||
|
"built",
|
||||||
|
"created",
|
||||||
|
"delivered",
|
||||||
|
"designed",
|
||||||
|
"developed",
|
||||||
|
"deployed",
|
||||||
|
"implemented",
|
||||||
|
"improved",
|
||||||
|
"managed",
|
||||||
|
"migrated",
|
||||||
|
"maintained",
|
||||||
|
"worked",
|
||||||
|
}
|
||||||
|
|
||||||
EXPERIENCE_TITLE_LABELS = {
|
EXPERIENCE_TITLE_LABELS = {
|
||||||
"education",
|
"education",
|
||||||
"experience",
|
"experience",
|
||||||
@ -79,6 +94,16 @@ EXPERIENCE_PROSE_MARKERS = {
|
|||||||
"worked",
|
"worked",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EXPERIENCE_PROSE_COMPANY_PATTERNS = (
|
||||||
|
re.compile(r"\bau\s+sein\s+de\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\b(before|after|during|while|since|because)\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\b(joined|joining|moved|moving|relocated|relocating|worked|working)\b", re.IGNORECASE),
|
||||||
|
re.compile(
|
||||||
|
r"\bin\s+(?:paris|london|lyon|france|berlin|amsterdam|madrid|rome|marseille|bordeaux|toulouse|nantes|lille|grenoble|strasbourg|nice|rennes|montpellier|remote)\b",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
EDUCATION_TITLE_KEYWORDS = {
|
EDUCATION_TITLE_KEYWORDS = {
|
||||||
"bachelor",
|
"bachelor",
|
||||||
"degree",
|
"degree",
|
||||||
@ -293,9 +318,6 @@ def _looks_like_experience_title(title: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
first_word = title_words[0]
|
first_word = title_words[0]
|
||||||
if len(first_word) < 2:
|
|
||||||
return False
|
|
||||||
|
|
||||||
normalized_title = " ".join(title_words).casefold()
|
normalized_title = " ".join(title_words).casefold()
|
||||||
if normalized_title in EXPERIENCE_TITLE_LABELS:
|
if normalized_title in EXPERIENCE_TITLE_LABELS:
|
||||||
return False
|
return False
|
||||||
@ -303,16 +325,16 @@ def _looks_like_experience_title(title: str) -> bool:
|
|||||||
if first_word.casefold() in EXPERIENCE_TITLE_STOPWORDS:
|
if first_word.casefold() in EXPERIENCE_TITLE_STOPWORDS:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if first_word.casefold() in EXPERIENCE_TITLE_ACTION_STARTS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(title_words) == 1 and len(first_word) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
return not any(
|
return not any(
|
||||||
word.casefold() in EXPERIENCE_PROSE_MARKERS for word in title_words if word
|
word.casefold() in EXPERIENCE_PROSE_MARKERS for word in title_words if word
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _looks_like_prose_company(company: str) -> bool:
|
def _looks_like_prose_company(company: str) -> bool:
|
||||||
company_words = [word.strip(".,;:!?()[]{}") for word in company.split()]
|
return any(pattern.search(company) for pattern in EXPERIENCE_PROSE_COMPANY_PATTERNS)
|
||||||
return any(
|
|
||||||
word
|
|
||||||
and word.lower() in EXPERIENCE_PROSE_MARKERS
|
|
||||||
and not (word.isupper() and len(word) <= 3)
|
|
||||||
for word in company_words
|
|
||||||
)
|
|
||||||
|
|||||||
@ -169,7 +169,7 @@ def test_extract_cv_signals_parses_clear_titles_with_french_and_english_connecto
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() -> None:
|
def test_extract_cv_signals_accepts_lowercase_short_and_real_company_titles() -> None:
|
||||||
text = dedent(
|
text = dedent(
|
||||||
"""
|
"""
|
||||||
Tonio
|
Tonio
|
||||||
@ -177,6 +177,8 @@ def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() ->
|
|||||||
data engineer at Company A
|
data engineer at Company A
|
||||||
iOS Engineer at Company A
|
iOS Engineer at Company A
|
||||||
3D Artist at Studio
|
3D Artist at Studio
|
||||||
|
R Developer at Company A
|
||||||
|
Data Engineer at Made in Design
|
||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
@ -186,6 +188,8 @@ def test_extract_cv_signals_accepts_lowercase_mixed_case_and_numeric_titles() ->
|
|||||||
{"title": "data engineer", "company": "Company A"},
|
{"title": "data engineer", "company": "Company A"},
|
||||||
{"title": "iOS Engineer", "company": "Company A"},
|
{"title": "iOS Engineer", "company": "Company A"},
|
||||||
{"title": "3D Artist", "company": "Studio"},
|
{"title": "3D Artist", "company": "Studio"},
|
||||||
|
{"title": "R Developer", "company": "Company A"},
|
||||||
|
{"title": "Data Engineer", "company": "Made in Design"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -219,6 +223,21 @@ def test_extract_cv_signals_rejects_label_like_lines_without_colons() -> None:
|
|||||||
assert extracted["experience_entries"] == []
|
assert extracted["experience_entries"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_cv_signals_rejects_narrative_bullet_experience_lines() -> None:
|
||||||
|
text = dedent(
|
||||||
|
"""
|
||||||
|
Tonio
|
||||||
|
Location: France
|
||||||
|
Implemented data pipelines at Airbnb
|
||||||
|
Designed dashboards at Company A
|
||||||
|
"""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
extracted = extract_cv_signals(text)
|
||||||
|
|
||||||
|
assert extracted["experience_entries"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_extract_cv_signals_ignores_in_paris_prose_tail() -> None:
|
def test_extract_cv_signals_ignores_in_paris_prose_tail() -> None:
|
||||||
text = dedent(
|
text = dedent(
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user