Spaces:
Paused
Paused
File size: 11,580 Bytes
3161850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | """
jd_parser.py — JD 文本解析器(规则版)
将自由文本 JD 转为标准化 dict,与 jobs.json 结构兼容。
"""
from __future__ import annotations
import re
from typing import Optional
# 方向映射(JD 关键词 → direction 标签)
DIRECTION_KEYWORDS = {
"大模型应用": "大模型应用算法", "LLM": "大模型应用算法", "Agent": "大模型应用算法",
"RAG": "大模型应用算法", "大模型算法": "大模型应用算法",
"推荐算法": "推荐算法", "推荐系统": "推荐算法", "推荐排序": "推荐算法",
"搜索算法": "大模型应用算法", "检索": "大模型应用算法",
"自然语言处理": "大模型应用算法", "NLP": "大模型应用算法",
"计算机视觉": "大模型应用算法", "CV": "大模型应用算法", "YOLO": "大模型应用算法",
"后端研发": "后端研发", "后端开发": "后端研发", "Go": "后端研发",
"数据分析": "数据分析", "SQL": "数据分析",
"产品经理": "产品经理",
"匹配算法": "大模型应用算法", "人岗匹配": "大模型应用算法",
}
SKILL_PATTERNS = [
"Python", "PyTorch", "TensorFlow", "Transformer", "BERT", "GPT", "LLM",
"RAG", "Agent", "Embedding", "LangChain", "FAISS", "Faiss",
"推荐系统", "召回", "排序", "NDCG", "A/B Test", "ABTest", "A/B 测试",
"Prompt", "Prompt Engineering", "Prompt 工程",
"语义检索", "向量检索", "搜索", "重排", "rerank",
"Java", "Go", "C\\+\\+", "Rust", "Scala",
"Docker", "Kubernetes", "K8s", "gRPC", "微服务",
"MySQL", "Redis", "Kafka", "MongoDB", "Elasticsearch",
"SQL", "Spark", "Hadoop", "数据仓库",
"模型微调", "LoRA", "QLoRA", "SFT", "RLHF",
"OpenCV", "YOLO", "目标检测", "图像分类",
"自然语言处理", "NLP", "命名实体识别", "NER", "文本分类", "Seq2Seq", "摘要生成",
"Beam Search", "Attention",
"数据清洗", "特征工程", "模型部署",
]
def parse_jd(jd_text: str) -> dict:
"""解析单个 JD 文本,返回标准化 dict。"""
text = jd_text.strip()
lines = text.split("\n")
title = _extract_title(lines, text)
company = _extract_company(lines, text)
city = _extract_city(lines, text)
stage = _extract_stage(lines, text)
direction = _infer_direction(title, text)
skills = _extract_skills(text)
project_signals = _extract_project_signals(text, skills)
hard_requirements = _extract_hard_requirements(text)
bonus_requirements = _extract_bonus_requirements(text)
risk_flags = _extract_risk_flags(text)
interview_themes = _infer_interview_themes(skills, text)
return {
"id": f"user_{_hash_title(title)}",
"title": title,
"company": company,
"city": city,
"direction": direction,
"stage": stage,
"skills": skills,
"project_signals": project_signals,
"jd": text[:500],
"hard_requirements": hard_requirements,
"bonus_requirements": bonus_requirements,
"risk_flags": risk_flags,
"interview_themes": interview_themes,
"source": "user_pasted",
"raw_text": text,
}
def _hash_title(title: str) -> str:
h = 0
for c in title:
h = (h * 31 + ord(c)) & 0xFFFFFFFF
return hex(h)[2:]
def _extract_title(lines: list[str], text: str) -> str:
# 优先从第一行提取
first = lines[0].strip().lstrip("#").strip()
# 去掉常见前缀
for prefix in ["岗位:", "职位:", "岗位名称:", "标题:", "Title:"]:
if first.startswith(prefix):
first = first[len(prefix):].strip()
if first and len(first) < 50:
if any(kw in first for kw in ["算法", "实习", "工程师", "开发", "产品", "数据"]):
return first
for line in lines:
s = line.strip().lstrip("#").strip()
for prefix in ["岗位:", "职位:"]:
if s.startswith(prefix):
s = s[len(prefix):].strip()
if any(kw in s for kw in ["实习生", "工程师", "岗位"]) and len(s) < 60:
return s
return "算法岗位"
def _extract_company(lines: list[str], text: str) -> str:
pat = re.compile(r"(公司|部门|事业群|企业)[::]\s*(.+)")
m = pat.search(text)
if m:
return m.group(2).strip()[:30]
for kw in ["腾讯", "字节", "阿里", "百度", "美团", "快手", "小红书", "B站", "微软", "Google"]:
if kw in text:
return kw
return "未知公司"
def _extract_city(lines: list[str], text: str) -> str:
cities = ["北京", "上海", "深圳", "广州", "杭州", "成都", "武汉", "南京", "苏州", "西安"]
for c in cities:
if c in text:
return c
pat = re.compile(r"(城市|地点|工作地点)[::]\s*(.+)")
m = pat.search(text)
if m:
return m.group(2).strip()[:20]
return "不限"
def _extract_stage(lines: list[str], text: str) -> str:
if "实习" in text:
return "实习"
if "校招" in text or "应届" in text or "毕业" in text:
return "校招"
if "社招" in text or "经验" in text or "年以" in text:
return "社招"
return "不限"
def _infer_direction(title: str, text: str) -> str:
combined = title + " " + text
# 按优先级匹配
for kw, direction in DIRECTION_KEYWORDS.items():
if kw.lower() in combined.lower():
return direction
# 从 title 推断
if "算法" in title:
return "大模型应用算法"
if "后端" in title or "研发" in title:
return "后端研发"
return "通用"
def _extract_skills(text: str) -> list[str]:
found = []
text_lower = text.lower()
for skill in SKILL_PATTERNS:
if skill.lower() in text_lower:
found.append(skill)
# 去重 + 保持顺序
seen = set()
result = []
for s in found:
if s.lower() not in seen:
result.append(s)
seen.add(s.lower())
return result[:20]
def _extract_project_signals(text: str, skills: list[str]) -> list[str]:
signals = []
# 从 skills 中选最具项目信号价值的
signal_keywords = {
"RAG", "Agent", "Embedding", "Transformer", "微服务", "推荐系统",
"召回", "排序", "重排", "检测", "识别", "分类", "搜索",
"LLM", "Prompt", "模型部署", "模型微调", "LoRA",
}
for s in skills:
if s in signal_keywords:
signals.append(s)
return signals[:10]
def _extract_hard_requirements(text: str) -> list[str]:
reqs = []
if "Python" in text: reqs.append("熟练使用 Python")
if "PyTorch" in text or "TensorFlow" in text: reqs.append("熟悉 PyTorch / TensorFlow")
if "硕士" in text or "研究生" in text: reqs.append("硕士及以上学历")
if "本科" in text: reqs.append("本科及以上学历")
if any(kw in text for kw in ["3 年", "3年", "三年", "5 年", "5年"]): reqs.append("相关经验")
return reqs or ["具备基本编程能力"]
def _extract_bonus_requirements(text: str) -> list[str]:
bonus = []
for kw in ["LLM", "大模型", "Agent", "RAG", "论文", "开源", "Kaggle"]:
if kw in text:
bonus.append(f"有 {kw} 经验者优先")
return bonus[:5]
def _extract_risk_flags(text: str) -> list[str]:
flags = []
if "5 年" in text or "5年" in text or "资深" in text:
flags.append("要求较高经验")
if "985" in text or "211" in text or "一本" in text:
flags.append("学历门槛")
return flags
def _infer_interview_themes(skills: list[str], text: str) -> list[str]:
themes = []
skill_set = {s.lower() for s in skills}
if "rag" in skill_set or "检索" in skill_set: themes.append("RAG 检索增强")
if "agent" in skill_set: themes.append("Agent 工具调用")
if "transformer" in skill_set: themes.append("Transformer 原理")
if "推荐系统" in skill_set or "召回" in skill_set: themes.append("推荐系统与召回排序")
if "embedding" in skill_set: themes.append("Embedding 与向量检索")
if "模型微调" in skill_set or "lora" in skill_set: themes.append("模型微调技术")
if not themes: themes = ["算法基础", "项目深挖", "系统设计"]
return themes[:4]
# ---------------------------------------------------------------------------
# LLM 增强解析(可选,失败自动 fallback 到规则版)
# ---------------------------------------------------------------------------
JD_LLM_SCHEMA = """{
"title": "岗位名称",
"company": "公司/部门",
"city": "城市",
"direction": "技术方向(大模型应用算法/推荐算法/后端研发/数据分析/产品经理)",
"stage": "阶段(实习/校招/社招)",
"skills": ["要求的技能列表"],
"project_signals": ["项目信号词"],
"hard_requirements": ["硬性要求"],
"bonus_requirements": ["加分项"],
"risk_flags": ["潜在风险提示"],
"interview_themes": ["面试主题"]
}"""
JD_LLM_PROMPT = """你是一个岗位 JD 解析器。请从以下 JD 文本中提取结构化信息,严格按 JSON Schema 输出。
Schema:
{schema}
要求:
1. 只输出 JSON,不要输出任何其他文字
2. skills 只提取 JD 中明确提到或强烈暗示的技术名称
3. direction 从 大模型应用算法/推荐算法/后端研发/数据分析/产品经理 中选择最匹配的
4. stage 从 实习/校招/社招 中选择
5. 如果某字段无法确定,填写空列表或"不限"
JD 文本:
{jd_text}"""
def parse_jd_with_llm(jd_text: str, llm_client=None) -> dict:
"""LLM 增强 JD 解析,失败自动 fallback 到 parse_jd。"""
if llm_client is None:
return parse_jd(jd_text)
try:
from src.llm_client import LLMClient
if not isinstance(llm_client, LLMClient) or not llm_client.available:
return parse_jd(jd_text)
except ImportError:
return parse_jd(jd_text)
prompt = JD_LLM_PROMPT.format(schema=JD_LLM_SCHEMA, jd_text=jd_text[:3000])
result = llm_client.chat_json("你是精确的岗位 JD 解析器。", prompt)
if result is None:
return parse_jd(jd_text) # LLM 失败 → fallback
# 校验 + 补全。LLM 输出只作为结构化候选,类型不可信时退回规则值。
fallback = parse_jd(jd_text)
scalar_keys = ["title", "company", "city", "direction", "stage"]
list_keys = [
"skills",
"project_signals",
"hard_requirements",
"bonus_requirements",
"risk_flags",
"interview_themes",
]
for key in scalar_keys:
if not isinstance(result.get(key), str) or not result.get(key, "").strip():
result[key] = fallback.get(key, "不限")
for key in list_keys:
if not isinstance(result.get(key), list):
result[key] = fallback.get(key, [])
else:
result[key] = [str(item).strip() for item in result[key] if str(item).strip()]
if not result["skills"]:
result["skills"] = fallback.get("skills", [])
if not result["project_signals"]:
result["project_signals"] = fallback.get("project_signals", [])
if not result["interview_themes"]:
result["interview_themes"] = fallback.get("interview_themes", ["项目深挖"])
result["id"] = f"llm_{_hash_title(result.get('title', ''))}"
result["jd"] = jd_text[:500]
result["source"] = "user_pasted"
result["raw_text"] = jd_text
return result
|