Improve browser CV field extraction
Browse files
app.js
CHANGED
|
@@ -38,10 +38,10 @@ function cleanList(value) {
|
|
| 38 |
}
|
| 39 |
|
| 40 |
const headerRules = [
|
| 41 |
-
["summary", /^(?:professional\s+)?summary
|
| 42 |
-
["experience", /^(?:professional\s+)?(?:work\s+)?experience
|
| 43 |
-
["education", /^education(?:al\s+background)?
|
| 44 |
-
["skills", /^(?:technical\s+|core\s+|key\s+|professional\s+)?skills?
|
| 45 |
];
|
| 46 |
|
| 47 |
function headerName(line) {
|
|
@@ -86,6 +86,12 @@ function sectionItems(section = [], limit = 1200) {
|
|
| 86 |
return section.filter(Boolean).join("; ").slice(0, limit);
|
| 87 |
}
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
function extractCandidateFields(text) {
|
| 90 |
const sections = parseSections(text);
|
| 91 |
const allText = text.replace(/\s+/g, " ");
|
|
@@ -94,10 +100,9 @@ function extractCandidateFields(text) {
|
|
| 94 |
let role = "";
|
| 95 |
for (const line of (sections.other ?? []).slice(0, 8)) {
|
| 96 |
if (line === firstLine) continue;
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
}
|
| 101 |
}
|
| 102 |
role ||= firstLine;
|
| 103 |
|
|
@@ -105,18 +110,22 @@ function extractCandidateFields(text) {
|
|
| 105 |
const seniority = seniorityMatch ? seniorityMatch[1].replace("-", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()) : "";
|
| 106 |
const yearsMatch = allText.match(/\b(\d+(?:\.\d+)?)\+?\s*(?:years?|yrs?)\b/i);
|
| 107 |
const industryTerms = ["FinTech", "Banking", "Finance", "Software", "Information Technology", "Healthcare", "Education", "Marketing", "Tourism", "Retail", "Logistics", "Construction", "Manufacturing", "Telecommunications", "Insurance", "Hospitality", "Government", "E-commerce"];
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
return {
|
| 112 |
candidateRole: role,
|
| 113 |
candidateSeniority: seniority,
|
| 114 |
yearsExperience: yearsMatch?.[1] ?? "",
|
| 115 |
candidateIndustry: industry,
|
| 116 |
-
education
|
| 117 |
-
candidateSkills:
|
| 118 |
summary,
|
| 119 |
-
experienceBullets:
|
| 120 |
};
|
| 121 |
}
|
| 122 |
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
const headerRules = [
|
| 41 |
+
["summary", /^(?:(?:professional\s+)?summary|profile|about\s+me|objective|career\s+objective)$/i],
|
| 42 |
+
["experience", /^(?:(?:professional\s+)?(?:work\s+)?experience|employment\s+history|career\s+history)$/i],
|
| 43 |
+
["education", /^(?:education(?:al\s+background)?|academic\s+qualifications?)$/i],
|
| 44 |
+
["skills", /^(?:(?:technical\s+|core\s+|key\s+|professional\s+)?skills?|competenc(?:y|ies)|technologies)$/i],
|
| 45 |
];
|
| 46 |
|
| 47 |
function headerName(line) {
|
|
|
|
| 86 |
return section.filter(Boolean).join("; ").slice(0, limit);
|
| 87 |
}
|
| 88 |
|
| 89 |
+
function inlineSection(text, labels, nextLabels, limit) {
|
| 90 |
+
const flat = text.replace(/\s+/g, " ");
|
| 91 |
+
const match = flat.match(new RegExp(`\\b(?:${labels})\\b\\s*[:\\-]\\s*(.*?)(?=\\s+\\b(?:${nextLabels})\\b\\s*[:\\-]\\s*|$)`, "i"));
|
| 92 |
+
return match ? match[1].trim().slice(0, limit) : "";
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
function extractCandidateFields(text) {
|
| 96 |
const sections = parseSections(text);
|
| 97 |
const allText = text.replace(/\s+/g, " ");
|
|
|
|
| 100 |
let role = "";
|
| 101 |
for (const line of (sections.other ?? []).slice(0, 8)) {
|
| 102 |
if (line === firstLine) continue;
|
| 103 |
+
const parts = line.split(/\s*(?:\||鈥路)\s*|\s{2,}/);
|
| 104 |
+
role = parts.find((part) => roleKeywords.some((keyword) => part.toLowerCase().includes(keyword)))?.trim() || "";
|
| 105 |
+
if (role) break;
|
|
|
|
| 106 |
}
|
| 107 |
role ||= firstLine;
|
| 108 |
|
|
|
|
| 110 |
const seniority = seniorityMatch ? seniorityMatch[1].replace("-", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()) : "";
|
| 111 |
const yearsMatch = allText.match(/\b(\d+(?:\.\d+)?)\+?\s*(?:years?|yrs?)\b/i);
|
| 112 |
const industryTerms = ["FinTech", "Banking", "Finance", "Software", "Information Technology", "Healthcare", "Education", "Marketing", "Tourism", "Retail", "Logistics", "Construction", "Manufacturing", "Telecommunications", "Insurance", "Hospitality", "Government", "E-commerce"];
|
| 113 |
+
let industry = industryTerms.find((term) => new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(allText)) ?? "";
|
| 114 |
+
industry ||= allText.match(/\bindustry\s*[:\-]\s*([^|]+)/i)?.[1]?.trim() || "";
|
| 115 |
+
const summary = sectionText(sections.summary) || inlineSection(text, "professional\\s+summary|summary|profile|objective", "experience|education|skills|certifications", 900) || sectionText((sections.other ?? []).slice(1, 5), 900);
|
| 116 |
+
const education = sectionItems(sections.education, 600) || inlineSection(text, "education|academic\\s+qualifications?", "experience|skills|summary|certifications", 600);
|
| 117 |
+
const skills = sectionItems(sections.skills) || inlineSection(text, "technical\\s+skills|core\\s+skills|skills|competenc(?:y|ies)|technologies", "experience|education|summary|certifications", 1200);
|
| 118 |
+
const experience = sectionItems(sections.experience, 1400) || inlineSection(text, "professional\\s+experience|work\\s+experience|experience|employment\\s+history", "education|skills|summary|certifications", 1400);
|
| 119 |
|
| 120 |
return {
|
| 121 |
candidateRole: role,
|
| 122 |
candidateSeniority: seniority,
|
| 123 |
yearsExperience: yearsMatch?.[1] ?? "",
|
| 124 |
candidateIndustry: industry,
|
| 125 |
+
education,
|
| 126 |
+
candidateSkills: skills,
|
| 127 |
summary,
|
| 128 |
+
experienceBullets: experience,
|
| 129 |
};
|
| 130 |
}
|
| 131 |
|