从零打造赛博朋克风格的AI简历解析桌面应用:PyWebView + Flask 全栈开发实战

Cyber Resume Parser 截图

开篇:当 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 + FlaskWeb 技术做 UI、Python 生态相对小众、文档较少

最终我选择了 PyWebView + Flask 的组合,原因很简单:

  1. Python 生态:AI 解析、PDF 处理、Excel 生成都有成熟的库
  2. Web UI 的灵活性:用 HTML/CSS/JS 可以轻松实现炫酷的赛博朋克界面
  3. 轻量级:不需要打包整个 Chromium
  4. 快速开发: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 桌面应用开发有了更深的理解:

  1. PyWebView + Flask 是个好选择:Web 技术做 UI 真的很灵活,Python 生态做后端真的很省心

  2. 跨平台打包是个大坑:每个平台都有自己的"特色"问题,需要耐心调试

  3. 用户体验细节很重要:从启动速度到授权提示,每个细节都影响使用体验

  4. 先做 MVP,再迭代优化:不要一开始就追求完美,先让核心功能跑起来

如果你也有类似的工具类需求,不妨试试 PyWebView + Flask 的组合。代码已开源在 GitHub,欢迎 Star 和 PR!


Written in code, meant for you! 💝

项目地址https://github.com/Polly2014/Hr_Resume_Translator

留言与讨论