#!/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
[^\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()