
开篇:当 HR 遇上 AI
"团队每天要处理上百份简历,手动录入 Excel 不仅慢,还时不时出错——姓名填错列、电话漏了位、学历写串行... 我既要盯着她们的工作严格把关,忙的时候还得自己上手帮忙录,真的心累。"
这是朋友的真实吐槽。作为一名 HR 总监,她管理着一个几人的招聘团队。每到招聘旺季,简历像雪片一样飞来,团队成员机械性地从 PDF 里抠信息、往 Excel 里填数据,效率低不说,错误率还居高不下。她一边要抽查质量,一边还得亲自下场救火,根本腾不出手做更有价值的人才筛选工作。
作为程序员的我,本能立刻被激活了——这不就是一个典型的可以用 AI 解决的问题吗?
于是,Cyber Resume Parser 诞生了。一款融合了赛博朋克美学的 AI 简历解析工具,能够一键将 PDF/Word 简历转换为标准化的 Excel 模板,彻底告别手动录入的时代。本文将完整记录这款应用从零到发布的全过程,包括我踩过的那些坑。
技术架构选型:为什么是 PyWebView + Flask?
在开始动手之前,我需要做一个关键决策:用什么技术栈?
备选方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Electron | 生态成熟、跨平台 | 包体积大(100MB+)、内存占用高 |
| Tauri | 轻量、Rust 性能 | 需要学习 Rust、生态较新 |
| PyQt/Tkinter | 纯 Python、原生控件 | UI 开发繁琐、美观度有限 |
| PyWebView + Flask | Web 技术做 UI、Python 生态 | 相对小众、文档较少 |
最终我选择了 PyWebView + Flask 的组合,原因很简单:
- Python 生态:AI 解析、PDF 处理、Excel 生成都有成熟的库
- Web UI 的灵活性:用 HTML/CSS/JS 可以轻松实现炫酷的赛博朋克界面
- 轻量级:不需要打包整个 Chromium
- 快速开发:Flask 提供了熟悉的后端开发体验
架构设计
graph TB
subgraph PyWebView["🖥️ PyWebView 窗口"]
subgraph Frontend["前端层"]
UI["HTML/CSS/JS<br/>赛博朋克风格 UI"]
end
UI <-->|HTTP| Backend
subgraph Backend["Flask 后端服务"]
Parser["📄 简历解析<br/>PyMuPDF"]
Excel["📊 Excel生成<br/>openpyxl"]
License["🔐 授权管理<br/>Supabase"]
end
end
Parser -->|调用| AI["🤖 DeepSeek API"]
License -->|验证| Cloud["☁️ Supabase 云端"]
style PyWebView fill:#f5f5f5,stroke:#999,stroke-width:2px
style Frontend fill:#e8e8e8,stroke:#888
style Backend fill:#e8e8e8,stroke:#888
style AI fill:#fafafa,stroke:#999
style Cloud fill:#fafafa,stroke:#999
核心模块实现
1. 简历解析:AI 的魔法
简历解析的核心是调用 DeepSeek 大模型,将非结构化的简历文本转换为结构化的 JSON 数据。
def parse_resume_with_llm(text: str) -> dict:
"""使用 LLM 解析简历文本"""
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL")
)
prompt = """请从以下简历中提取信息,返回 JSON 格式:
{
"name": "姓名",
"phone": "电话",
"education": [{"school": "学校", "major": "专业", "degree": "学历"}],
"work_experience": [{"company": "公司", "position": "职位"}],
...
}
"""
response = client.chat.completions.create(
model=os.getenv("AI_MODEL", "deepseek-chat"),
messages=[{"role": "user", "content": prompt + text}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
这里有个小技巧:使用 response_format={"type": "json_object"} 可以确保模型返回有效的 JSON,避免后续解析错误。
2. 赛博朋克 UI:让工具也有灵魂
既然是给朋友做的,那必须得有点仪式感。我选择了赛博朋克风格——深紫色背景、霓虹色渐变、粒子动画。
:root {
--bg-primary: #0f0f1a;
--accent-purple: #a855f7;
--accent-cyan: #06b6d4;
--accent-pink: #ec4899;
}
.main-title {
background: linear-gradient(90deg, var(--accent-purple), var(--accent-cyan), var(--accent-pink));
background-size: 200% 100%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 4s ease infinite;
}
粒子背景用纯 CSS 实现,不需要额外的库:
.particle {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
opacity: 0.5;
animation: float 20s infinite linear;
}
@keyframes float {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 0.5; transform: scale(1); }
100% { transform: translateY(-100vh) translateX(50px) scale(0); opacity: 0; }
}
3. 授权系统:Supabase 云端验证
为了控制使用配额(毕竟 API 调用是有成本的),我集成了 Supabase 作为授权后端。
class LicenseManager:
def validate_license(self, license_code: str) -> dict:
# 查询授权码
endpoint = f"licenses?code=eq.{license_code}&is_active=eq.true"
result = self._supabase_request("GET", endpoint)
if not result.get("data"):
return {"valid": False, "message": "无效的授权码"}
license_info = result["data"][0]
# 检查配额
if not license_info.get("is_unlimited"):
used = license_info.get("used_quota", 0)
total = license_info.get("total_quota", 0)
if used >= total:
return {"valid": False, "message": f"配额已用完 ({used}/{total})"}
return {"valid": True, "license_info": license_info}
当然,给朋友的授权码是无限配额的 VIP 待遇 😎
跨平台打包:踩坑实录
这部分是整个项目中最"刺激"的环节。PyInstaller 打包看起来简单,实际上每个平台都有自己的坑。
坑 1:macOS 端口冲突
首次运行时,Flask 默认的 5000 端口竟然被占用了。一查才发现是 macOS Monterey 的 AirPlay Receiver 占用了这个端口。
# 默认端口(5000 被 macOS AirPlay Receiver 占用)
DEFAULT_PORT = 5050
坑 2:Windows 编码问题
Windows 控制台默认使用 cp1252 编码,而我的代码里有中文和 emoji...
UnicodeEncodeError: 'charmap' codec can't encode characters
解决方案:在程序启动时强制设置 UTF-8 编码:
if sys.platform == 'win32':
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
os.environ['PYTHONIOENCODING'] = 'utf-8'
坑 3:--onefile vs --onedir 的抉择
PyInstaller 提供两种打包模式:
| 模式 | 启动速度 | 文件结构 | 稳定性 |
|---|---|---|---|
--onefile | 慢(10-20秒解压) | 单个 exe | 更稳定 |
--onedir | 快(3-5秒) | 文件夹 | 可能有路径问题 |
我原本倾向于 --onedir 以获得更快的启动速度,但在 Windows 上遇到了 pythonnet 加载失败的问题。经过反复测试,最终还是选择了 --onefile 模式——虽然首次启动慢一点,但"丝滑"无报错。
坑 4:PyWebView 的 GUI 后端
PyWebView 在 Windows 上可以使用多种渲染后端:
- EdgeChromium(需要 pythonnet):现代渲染,效果最好
- MSHTML(IE 内核):兼容性强,但渲染效果一般
- CEF:需要额外安装
最终配置:
if platform.system() == 'Windows':
webview.start(gui='edgechromium') # 使用 Edge 渲染
else:
webview.start() # macOS 使用默认的 cocoa
GitHub Actions 自动构建
为了自动化发布流程,我配置了 GitHub Actions:
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build-macos:
runs-on: macos-latest
steps:
- name: Build Desktop
run: |
pyinstaller --name="Cyber Resume Parser" \
--onefile \
--windowed \
--icon=icon.jpg \
--add-data="static:static" \
desktop_app.py
build-windows:
runs-on: windows-latest
steps:
- name: Install pythonnet
run: pip install pythonnet clr-loader
- name: Build Desktop
run: |
pyinstaller --name="Cyber Resume Parser" `
--onefile `
--windowed `
--collect-all=pythonnet `
--collect-all=webview `
desktop_app.py
现在只需要推送一个 tag,就能自动构建 macOS 和 Windows 双平台的安装包。
并行处理:批量解析的性能优化
当朋友告诉我她有时候需要一次性处理几十份简历时,我意识到串行处理是不可接受的。于是引入了并行处理:
MAX_PARALLEL_WORKERS = 3 # 避免 API 限流
def process_all_parallel():
with ThreadPoolExecutor(max_workers=MAX_PARALLEL_WORKERS) as executor:
future_to_task = {
executor.submit(process_single_task, task): task
for task in pending_tasks
}
for future in as_completed(future_to_task):
task = future_to_task[future]
try:
future.result()
print(f"[OK] {task.filename} done")
except Exception as e:
print(f"[ERR] {task.filename} failed: {e}")
这里有个重要细节:配额扣减必须在并行处理之前批量完成,否则多个线程同时扣减会导致 Supabase 连接竞争问题。
云端同步:构建人才库
既然每份简历都会被解析成结构化数据,为什么不顺便存到云端呢?这样就能逐步构建一个可搜索的人才库。
def log_resume_result(license_code, filename, result_json, status):
"""异步上传解析结果到云端"""
def _upload():
log_data = {
"license_code": license_code,
"filename": filename,
"result_json": result_json, # 完整的解析结果
"status": status,
"app_version": APP_VERSION,
"client_info": _get_client_info()
}
self._supabase_request("POST", "usage_logs", log_data)
# 后台线程异步上传,不阻塞主流程
thread = threading.Thread(target=_upload, daemon=True)
thread.start()
使用 daemon=True 确保上传线程不会阻塞程序退出。
最终成果
经过两天的开发和调试,Cyber Resume Parser v2.0.0 终于发布了:
- ✅ 支持 PDF/Word 简历智能解析
- ✅ 赛博朋克风格炫酷 UI
- ✅ 批量处理 + 并行加速
- ✅ 在线授权 + 配额管理
- ✅ 云端同步构建人才库
- ✅ macOS/Windows 双平台支持
- ✅ GitHub Actions 自动发布
总结与反思
这个项目让我对 Python 桌面应用开发有了更深的理解:
PyWebView + Flask 是个好选择:Web 技术做 UI 真的很灵活,Python 生态做后端真的很省心
跨平台打包是个大坑:每个平台都有自己的"特色"问题,需要耐心调试
用户体验细节很重要:从启动速度到授权提示,每个细节都影响使用体验
先做 MVP,再迭代优化:不要一开始就追求完美,先让核心功能跑起来
如果你也有类似的工具类需求,不妨试试 PyWebView + Flask 的组合。代码已开源在 GitHub,欢迎 Star 和 PR!
Written in code, meant for you! 💝
