179 lines
6.5 KiB
Python
Executable File
179 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
star_story_builder.py — 从简历文本里抽出工作 / 项目经历,生成"故事矩阵"骨架
|
||
|
||
用法:
|
||
python star_story_builder.py --resume resume.md --out stories.md
|
||
|
||
输出一份 markdown,包含:
|
||
- 检测到的 N 个经历(按时间倒序)
|
||
- 每个经历的 STAR 骨架占位(让用户 / 模型补全)
|
||
- 每个故事可以回答的行为题清单
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
|
||
COMMON_BEHAVIORAL_QUESTIONS = {
|
||
"ownership": [
|
||
"讲一次你 ownership 体现得最好的经历",
|
||
"你做过的最有成就感的项目",
|
||
"讲一次你主动推动事情",
|
||
],
|
||
"collab": [
|
||
"讲一次你跨部门 / 跨团队推动事情",
|
||
"讲一次你和同事意见不合,怎么解决",
|
||
"讲一次你说服别人改变想法",
|
||
],
|
||
"challenge": [
|
||
"讲一次你在资源 / 时间不足下完成目标",
|
||
"讲一次你做艰难决策的经历",
|
||
"讲一次你打破常规 / 创新的经历",
|
||
],
|
||
"failure": [
|
||
"讲一次你失败 / 没达成目标的经历",
|
||
"讲一次你犯过的最大错误",
|
||
"讲一次接到负面反馈,怎么应对",
|
||
],
|
||
"learning": [
|
||
"讲一次你快速学会全新领域的经历",
|
||
"你最近学到的最重要的事是什么",
|
||
],
|
||
}
|
||
|
||
|
||
def load_resume_text(path: Path) -> str:
|
||
suffix = path.suffix.lower()
|
||
if suffix in {".md", ".txt"}:
|
||
return path.read_text(encoding="utf-8")
|
||
if suffix == ".docx":
|
||
try:
|
||
from docx import Document
|
||
except ImportError:
|
||
print("✗ 缺少 python-docx", file=sys.stderr)
|
||
sys.exit(1)
|
||
return "\n".join(p.text for p in Document(str(path)).paragraphs)
|
||
print(f"✗ 暂不支持 {suffix}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def extract_experiences(text: str) -> list[dict]:
|
||
"""
|
||
简易抽取:找形如 "公司 | 岗位 | 时间" 或 "项目名 | ..." 的行作为锚点,
|
||
然后取该行后面、下一个锚点之前的内容作为经历内容。
|
||
"""
|
||
lines = text.splitlines()
|
||
experiences: list[dict] = []
|
||
current = None
|
||
|
||
# 匹配锚点(含 | 或 |,且包含日期/年份)
|
||
anchor_re = re.compile(
|
||
r"^(?P<title>[^\n]+?[||][^\n]+?[||][^\n]+)$"
|
||
)
|
||
|
||
for line in lines:
|
||
if anchor_re.match(line.strip()):
|
||
if current and current["bullets"]:
|
||
experiences.append(current)
|
||
current = {"title": line.strip(), "bullets": []}
|
||
else:
|
||
stripped = line.strip()
|
||
if current and stripped.startswith(("-", "*", "•")):
|
||
current["bullets"].append(stripped.lstrip("-*• "))
|
||
if current and current["bullets"]:
|
||
experiences.append(current)
|
||
|
||
return experiences
|
||
|
||
|
||
def categorize_story(bullets: list[str]) -> list[str]:
|
||
"""根据 bullet 关键词,判断这个故事最适合回答哪一类行为题。"""
|
||
text = " ".join(bullets).lower()
|
||
cats = []
|
||
if any(k in text for k in ["主导", "owner", "推动", "drive", "lead", "0-1"]):
|
||
cats.append("ownership")
|
||
if any(k in text for k in ["跨部门", "跨团队", "协作", "对接", "合作"]):
|
||
cats.append("collab")
|
||
if any(k in text for k in ["紧急", "时间紧", "资源", "决策", "突破"]):
|
||
cats.append("challenge")
|
||
if any(k in text for k in ["失败", "下线", "回滚", "复盘", "教训", "踩坑"]):
|
||
cats.append("failure")
|
||
if any(k in text for k in ["新", "学习", "陌生", "首次", "从零"]):
|
||
cats.append("learning")
|
||
return cats or ["ownership"]
|
||
|
||
|
||
def render(experiences: list[dict]) -> str:
|
||
if not experiences:
|
||
return (
|
||
"# 故事矩阵(未检测到经历)\n\n"
|
||
"无法从简历里自动抽取出工作 / 项目经历。可能是因为:\n"
|
||
"1. 简历格式不是 `公司 | 岗位 | 时间` 的常见结构\n"
|
||
"2. 经历用普通段落写,没有明显的锚点\n\n"
|
||
"建议:手工告诉我你最有代表性的 3~5 段经历,我来帮你做 STAR 拆解。\n"
|
||
)
|
||
|
||
out = ["# 故事矩阵(Story Matrix)", ""]
|
||
out.append(f"从简历里检测到 {len(experiences)} 段经历,按 STAR 拆解如下。")
|
||
out.append("**请补充每个 STAR 段落里 `[占位]` 的内容**,准备好后这些故事可以覆盖 80% 的行为面问题。")
|
||
out.append("")
|
||
|
||
for idx, exp in enumerate(experiences[:8], start=1):
|
||
cats = categorize_story(exp["bullets"])
|
||
out.append(f"## 故事 {idx}:{exp['title']}")
|
||
out.append("")
|
||
out.append("**简历原始 bullet:**")
|
||
for b in exp["bullets"][:5]:
|
||
out.append(f"- {b}")
|
||
out.append("")
|
||
out.append("**STAR 拆解(请补全):**")
|
||
out.append("- **S(背景)**:[占位 - 一句话点明背景 / 痛点]")
|
||
out.append("- **T(任务)**:[占位 - 你的具体任务和目标]")
|
||
out.append("- **A(动作)**:[占位 - 分 2~4 步,每步带动词 + 决策依据]")
|
||
out.append("- **R(结果)**:[占位 - 量化结果 + 一句反思]")
|
||
out.append("")
|
||
|
||
question_pool = []
|
||
for cat in cats:
|
||
question_pool.extend(COMMON_BEHAVIORAL_QUESTIONS.get(cat, []))
|
||
out.append(f"**最适合回答的行为题({', '.join(cats)}):**")
|
||
for q in question_pool[:4]:
|
||
out.append(f"- {q}")
|
||
out.append("")
|
||
out.append("---")
|
||
out.append("")
|
||
|
||
out.append("## 使用建议")
|
||
out.append("")
|
||
out.append("- 把每个故事的 STAR 段落填好,每段控制在 30~90 秒讲完")
|
||
out.append("- 面试时灵活组合:同一个故事可以从不同角度回答不同题")
|
||
out.append("- 至少准备 **3 个完整故事**(成功 + 失败 + 协作 各一个),覆盖 80% 行为题")
|
||
out.append('- 每个故事里强调「我」做了什么,避免大量「我们」')
|
||
return "\n".join(out)
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--resume", required=True)
|
||
parser.add_argument("--out")
|
||
args = parser.parse_args()
|
||
|
||
resume_text = load_resume_text(Path(args.resume).expanduser())
|
||
experiences = extract_experiences(resume_text)
|
||
report = render(experiences)
|
||
|
||
if args.out:
|
||
Path(args.out).write_text(report, encoding="utf-8")
|
||
print(f"✓ 故事矩阵已生成:{args.out}")
|
||
else:
|
||
print(report)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|