Files
CareerNexus/app/match/resume_docx.py
T
RoyirinRosmontis a10442d78c CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)
- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径
- WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径
- 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API)
- 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
2026-08-03 13:15:35 +08:00

134 lines
4.9 KiB
Python
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.
"""规范简历 DOCX 导出:照片 + 基本信息表 + 分节正文(关键词高亮)+ 可选匹配诊断。
完全离线,仅依赖 python-docx。返回 bytes,由 API 层以文件流下发。
"""
from __future__ import annotations
import base64
import io
import re
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
from app.models import ResumeProfile
_GREEN = RGBColor(0x00, 0xB4, 0x2A) # 命中必备
_BLUE = RGBColor(0x2F, 0x54, 0xEB) # 命中加分
_GRAY = RGBColor(0x4E, 0x59, 0x69) # 其他技能
_DARK = RGBColor(0x1D, 0x21, 0x29)
def _set_cn_font(doc: Document) -> None:
st = doc.styles["Normal"]
st.font.name = "Times New Roman"
st.font.size = Pt(10.5)
st.element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
def _highlight_runs(paragraph, text: str, cls_of: dict[str, RGBColor]) -> None:
"""按技能清单把 text 切段写入段落,命中技能加粗上色。"""
if not cls_of:
paragraph.add_run(text)
return
names = sorted(cls_of, key=len, reverse=True)
pat = re.compile("|".join(re.escape(n) for n in names), re.IGNORECASE)
pos = 0
for m in pat.finditer(text):
if m.start() > pos:
paragraph.add_run(text[pos:m.start()])
hit = m.group(0)
run = paragraph.add_run(hit)
run.bold = True
run.font.color.rgb = cls_of.get(hit.lower(), _DARK)
pos = m.end()
if pos < len(text):
paragraph.add_run(text[pos:])
def build_resume_docx(profile: ResumeProfile, match: dict | None = None) -> bytes:
"""把脱敏简历画像渲染为规范 DOCX,返回文件 bytes。"""
doc = Document()
_set_cn_font(doc)
title = doc.add_paragraph()
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
trun = title.add_run("个 人 简 历")
trun.bold = True
trun.font.size = Pt(20)
sub = doc.add_paragraph()
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
srun = sub.add_run("(CareerNexus 职联 · 结构化解析导出,已自动脱敏)")
srun.font.size = Pt(9)
srun.font.color.rgb = _GRAY
# ---- 头部:左照片 + 右基本信息表 ----
head = doc.add_table(rows=1, cols=2)
head.style = "Table Grid"
head.columns[0].width = Cm(3.4)
head.columns[1].width = Cm(13.4)
photo_cell, info_cell = head.cell(0, 0), head.cell(0, 1)
if profile.photo_b64:
try:
raw = base64.b64decode(profile.photo_b64.split(",", 1)[1])
photo_cell.paragraphs[0].add_run().add_picture(io.BytesIO(raw), width=Cm(3.0))
except Exception:
photo_cell.paragraphs[0].add_run("(照片解析失败)")
else:
photo_cell.paragraphs[0].add_run("(未提供照片)")
info_items = [
("姓名", profile.name or "—"), ("性别", profile.gender or "—"),
("学历", profile.education or "—"), ("工作年限", f"{profile.years:g} 年" if profile.years else "—"),
("手机", profile.phone or "—"), ("邮箱", profile.email or "—"),
("家庭住址", profile.address or "—"),
]
info_table = info_cell.add_table(rows=(len(info_items) + 1) // 2, cols=2)
for i, (k, v) in enumerate(info_items):
cell = info_table.cell(i // 2, i % 2)
p = cell.paragraphs[0]
kr = p.add_run(f"{k}:")
kr.bold = True
p.add_run(v)
# 技能高亮类别(小写键)
cls_of: dict[str, RGBColor] = {}
if match:
for s in match.get("matched_required", []):
cls_of[s.lower()] = _GREEN
for s in match.get("matched_preferred", []):
cls_of[s.lower()] = _BLUE
for s in profile.skills:
cls_of.setdefault(s.lower(), _GRAY)
# ---- 分节正文(关键词标注) ----
for sec in profile.sections or [{"title": "简历全文", "text": profile.raw_masked_text}]:
h = doc.add_paragraph()
hrun = h.add_run(f"▍{sec['title']}")
hrun.bold = True
hrun.font.size = Pt(13)
hrun.font.color.rgb = _BLUE
_highlight_runs(doc.add_paragraph(), sec["text"], cls_of)
# ---- 匹配诊断摘要(可选) ----
if match and match.get("job_title"):
h = doc.add_paragraph()
hrun = h.add_run("▍人岗匹配诊断(CareerNexus)")
hrun.bold = True
hrun.font.size = Pt(13)
hrun.font.color.rgb = _BLUE
for line in [
f"目标岗位:{match['job_title']} 综合匹配分:{match.get('total_score', 0)}",
f"命中必备技能:{'、'.join(match.get('matched_required', [])) or '无'}",
f"命中加分技能:{'、'.join(match.get('matched_preferred', [])) or '无'}",
f"缺失技能:{'、'.join((match.get('missing_required', [])
+ match.get('missing_preferred', []))) or '无'}",
]:
doc.add_paragraph(line)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()