简历解析加固:标签字间空格容错(姓 名 :X)、在读学历识别(硕士在读/大二→本科在读)、docx 表格文本按文档顺序提取(修复技能区整块丢失)、词表源补 LLM/大语言模型/Transformers/Prompt Engineering/智能体开发等真实简历高频别名(generate_dataset.py 为词表唯一源,data/ 不入库)、在读/应届年限显示,新增回归测试 2 例,real_jd_500 F1 99.83% 无回退

This commit is contained in:
FinStab
2026-08-31 20:44:57 +08:00
parent a4db91bf33
commit 896ac10f12
5 changed files with 80 additions and 13 deletions
+47 -7
View File
@@ -18,13 +18,17 @@ logger = logging.getLogger(__name__)
_PHONE_RE = re.compile(r"1[3-9]\d{9}")
_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
_NAME_RE = re.compile(r"姓名[::]\s*([一-龥]{2,4})")
# 标签字间可含空格/全角空格(PDF/Word 排版常见:"姓 名 :孙千越")
_NAME_RE = re.compile(r"姓\s*名\s*[::]\s*([一-龥]{2,4})")
# 简历常见写法:首行裸姓名(无"姓名:"前缀),如" 王伟航 "
_BARE_NAME_RE = re.compile(r"^\s*([一-龥]{2,4})\s*$", re.MULTILINE)
_YEARS_RE = re.compile(r"(\d+(?:\.\d+)?)\s*年(?:以上)?[^\n,。;]{0,6}?经验")
_GENDER_RE = re.compile(r"性别[::]?\s*(男|女)")
_GENDER_RE = re.compile(r"性\s*别\s*[::]?\s*(男|女)")
_EDU_RE = re.compile(r"(博士后|博士|专硕|学硕|硕士|研究生|MBA|本科|大专|专科)")
_EDU_MAP = {"专硕": "硕士", "学硕": "硕士", "研究生": "硕士"}
# 在读学历:「硕士在读」或「大二/大三」这类年级写法 → 本科(在读)
_EDU_READING_RE = re.compile(r"(本科|硕士|博士|研究生|MBA)\s*在读")
_GRADE_RE = re.compile(r"大[一二三四]")
_ADDR_RE = re.compile(r"(?:家庭住址|现居(?:住)?(?:地|址)?|联系地址|通讯地址|籍贯)[::]\s*([^\n,。;]{2,30})")
# 常见简历分节标题(整行匹配,可带冒号)
@@ -48,6 +52,17 @@ def _extract_name(text: str) -> str:
return ""
def _extract_education(text: str) -> str:
"""学历:优先识别在读("硕士在读"/"大二"年级写法),再匹配学位词。"""
rm = _EDU_READING_RE.search(text)
if rm:
return f"{_EDU_MAP.get(rm.group(1), rm.group(1))}(在读)"
if _GRADE_RE.search(text):
return "本科(在读)"
m = _EDU_RE.search(text)
return _EDU_MAP.get(m.group(1), m.group(1)) if m else ""
def mask_text(text: str) -> str:
"""脱敏:手机、邮箱、姓名(含首行裸姓名)、住址打码。"""
text = _PHONE_RE.sub(lambda m: m.group(0)[:3] + "****" + m.group(0)[-2:], text)
@@ -110,6 +125,34 @@ def extract_photo_b64(path: str | Path) -> str:
return ""
def _docx_text(path: Path) -> str:
"""按文档顺序提取 docx 文本:段落 + 表格(含嵌套表格单元格)。
简历模板普遍用无边框表格排版个人信息/技能区,只读 paragraphs 会整块丢失。
"""
from docx import Document # python-docx,requirements 已声明
from docx.table import Table
from docx.text.paragraph import Paragraph
doc = Document(str(path))
parts: list[str] = []
def walk_table(tbl) -> None:
for row in tbl.rows:
for cell in row.cells:
if cell.text.strip():
parts.append(cell.text)
for nested in cell.tables:
walk_table(nested)
for child in doc.element.body.iterchildren():
if child.tag.endswith("}p"):
parts.append(Paragraph(child, doc).text)
elif child.tag.endswith("}tbl"):
walk_table(Table(child, doc))
return "\n".join(parts)
def read_resume_file(path: str | Path) -> str:
"""按扩展名读取简历文件文本。"""
path = Path(path)
@@ -117,9 +160,7 @@ def read_resume_file(path: str | Path) -> str:
if suffix == ".txt":
return path.read_text(encoding="utf-8")
if suffix == ".docx":
from docx import Document # python-docx,requirements 已声明
doc = Document(str(path))
return "\n".join(p.text for p in doc.paragraphs)
return _docx_text(path)
if suffix == ".pdf":
try:
from pypdf import PdfReader
@@ -147,7 +188,6 @@ def parse_resume(text: str, matcher: SkillMatcher | None = None,
email_m = _EMAIL_RE.search(text)
years_m = _YEARS_RE.search(text)
gender_m = _GENDER_RE.search(text)
edu_m = _EDU_RE.search(text)
addr_m = _ADDR_RE.search(text)
masked = mask_text(text)
return ResumeProfile(
@@ -159,7 +199,7 @@ def parse_resume(text: str, matcher: SkillMatcher | None = None,
scenarios=matcher.find_scenarios(text),
raw_masked_text=masked,
gender=gender_m.group(1) if gender_m else "",
education=_EDU_MAP.get(edu_m.group(1), edu_m.group(1)) if edu_m else "",
education=_extract_education(text),
address=_mask_addr(addr_m.group(1).strip()) if addr_m else "",
photo_b64=photo_b64,
sections=split_sections(masked),