Files
mantle-ai-trader/skills/jd-resume-tailor/scripts/parse_jd.py
2026-06-06 05:21:10 +00:00

190 lines
6.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
parse_jd.py — 解析 JD 文本,抽取 must-have / nice-to-have / 职责 / 特殊要求
用法:
python parse_jd.py --jd-file jd.txt --out jd_parsed.json
python parse_jd.py --jd-text "..." --out jd_parsed.json
输出 JSON 结构供下一步的 jd_gap.py 使用,也可以直接给用户看。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
MUST_PATTERNS = [
r"必须", r"必备", r"必要条件", r"硬性要求", r"应当", r"需要", r"需具备",
r"至少\s*\d+\s*年", r"\d+\+?\s*年以上",
r"required", r"must\s*have", r"mandatory", r"essential",
r"minimum\s+\d+\s+years",
]
NICE_PATTERNS = [
r"加分", r"优先", r"加分项", r"熟悉.+者优先", r"有.+经验者优先",
r"preferred", r"nice\s*to\s*have", r"plus", r"bonus", r"desirable",
]
ACTION_VERBS = [
"负责", "主导", "推动", "设计", "搭建", "构建", "优化", "规划", "迭代",
"孵化", "复盘", "运营", "管理", "协调", "执行", "驱动", "落地", "重构",
"lead", "drive", "build", "design", "architect", "develop", "implement",
"optimize", "manage", "coordinate", "execute", "own",
]
def split_sentences(text: str) -> list[str]:
# 中文按 。!?; 拆,英文按 . ; 拆,并保留 bullet 行
raw = re.split(r"[。!?!?;\n]+", text)
return [s.strip(" \t-•·*") for s in raw if s.strip()]
def classify_sentences(sentences: list[str]) -> dict:
must, nice, resp, others = [], [], [], []
for s in sentences:
s_low = s.lower()
if any(re.search(p, s_low) for p in NICE_PATTERNS):
nice.append(s)
elif any(re.search(p, s_low) for p in MUST_PATTERNS):
must.append(s)
elif any(v in s_low for v in ACTION_VERBS):
resp.append(s)
else:
others.append(s)
return {"must": must, "nice": nice, "responsibilities": resp, "others": others}
def extract_special(text: str) -> dict:
out: dict[str, str] = {}
# 学历
edu = re.search(r"(本科|硕士|博士|大专)(?:及以上|以上)?", text)
if edu:
out["education"] = edu.group(0)
# 工作年限
years = re.search(r"(\d+)\s*[-~–到至]\s*(\d+)\s*年|(\d+)\s*\+?\s*年(以上|及以上)?", text)
if years:
out["years"] = years.group(0)
# 语言
lang_pat = re.search(
r"(英语\s*(口语)?\s*(流利|熟练|母语)|CET[-\s]?[46]|雅思\s*\d(\.\d)?|托福\s*\d{2,3}|母语水平|business\s*english)",
text,
flags=re.IGNORECASE,
)
if lang_pat:
out["language"] = lang_pat.group(0)
# 城市
cities = re.findall(
r"(北京|上海|广州|深圳|杭州|南京|苏州|成都|武汉|西安|香港|新加坡|remote|hybrid|远程|海外)",
text,
flags=re.IGNORECASE,
)
if cities:
out["location"] = "/".join(sorted(set(c.lower() for c in cities)))
# 出差 / 加班信号
travel = re.search(r"(出差|派驻|常驻|项目制|加班|999|996|大小周)", text)
if travel:
out["working_style"] = travel.group(0)
# 证书
certs = re.findall(
r"(CFA(?:\s*Level\s*[I123]+)?|CPA|FRM|ACCA|PMP|AWS\s*[\w\s]*认证|Azure\s*[\w]*|GCP\s*[\w]*)",
text,
flags=re.IGNORECASE,
)
if certs:
out["certificates"] = "/".join(sorted(set(c.strip() for c in certs)))
return out
def extract_skills(sentences: list[str]) -> list[str]:
"""从所有句子里抽取技能词候选(短词优先,避免抽出整句)。"""
text = " ".join(sentences)
# 英文技能CamelCase 或大写开头的词、含 . 或 +/- 的标识)
en = re.findall(r"\b[A-Za-z][A-Za-z0-9+/.\-_#]{1,20}\b", text)
# 中文 2~5 字常见技能词
zh = re.findall(r"[一-龥]{2,5}", text)
raw = en + zh
stop = {
# 中文虚词 / 通用动词
"公司", "我们", "你将", "团队", "需要", "能够", "具备", "熟悉", "了解",
"良好", "优秀", "经验", "能力", "岗位", "职责", "要求", "以上", "相关",
"进行", "完成", "负责", "推动", "实现", "提升", "并且", "包括", "以下",
"工作", "项目", "业务", "及其", "或者", "", "", "", "", "",
"", "", "", "", "", "等等", "通过", "", "", "", "",
"至上", "本科", "硕士", "博士", "者优先", "根据", "进行", "支持", "参与",
"主导", "提供", "建立", "搭建", "设计", "驱动", "决策", "分析", "推动",
"迭代", "规划", "运营", "协作", "跨部门", "跨团队", "高级", "资深",
"若干", "多种", "多元", "多类",
# 英文虚词
"and", "the", "with", "for", "of", "or", "to", "be", "as", "an", "is",
"are", "in", "on", "at", "by", "all", "you", "we", "us", "our", "your",
"a", "an", "this", "that", "these", "those", "it", "its",
}
# 含数字的"X年""X个"也过滤
digit_only = re.compile(r"^\d+$")
seen = set()
out = []
for token in raw:
key = token.lower()
if key in seen or token in stop or len(token) < 2 or digit_only.match(token):
continue
# 中文 token 不允许全是 stop 词的子串
seen.add(key)
out.append(token)
return out[:60]
def main() -> None:
parser = argparse.ArgumentParser()
src = parser.add_mutually_exclusive_group(required=True)
src.add_argument("--jd-file", help="JD 文本文件路径")
src.add_argument("--jd-text", help="直接传 JD 文本")
parser.add_argument("--out", help="输出 JSON 路径,缺省打印")
args = parser.parse_args()
if args.jd_file:
path = Path(args.jd_file).expanduser()
if not path.exists():
print(f"✗ JD 文件不存在:{path}", file=sys.stderr)
sys.exit(1)
text = path.read_text(encoding="utf-8")
else:
text = args.jd_text
sentences = split_sentences(text)
classified = classify_sentences(sentences)
special = extract_special(text)
skills = extract_skills(classified["must"] + classified["responsibilities"])
result = {
"must_have": classified["must"],
"nice_to_have": classified["nice"],
"responsibilities": classified["responsibilities"],
"skills_extracted": skills,
"special_requirements": special,
"raw_sentence_count": len(sentences),
}
payload = json.dumps(result, ensure_ascii=False, indent=2)
if args.out:
out_path = Path(args.out).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(payload, encoding="utf-8")
print(f"✓ JD 解析结果已保存:{out_path}")
else:
print(payload)
if __name__ == "__main__":
main()