Skill: wuritu/scrapling-boss-recruitment (version 0.1.0)

--- name: scrapling-boss-recruitment description: "招聘数据爬取Skill，基于Scrapling框架在Boss直聘上执行招聘操作。当需要以下场景时使用：在Boss直聘搜索候选人、批量发送招呼、获取简历信息、解析结构化数据。Trigger phrases: boss直聘、招聘爬取、搜索候选人、获取简历、发送招呼、boss recruitment、zhipin" --- # Scrapling Boss Recruitment 基于 Scrapling 的 Boss直聘 招聘辅助工具，支持候选人搜索、简历获取、自动打招呼。 ## 核心能力 | 功能 | 方法 | 说明 | |------|------|------| | 搜索候选人 | `search_candidates()` | 按关键词/条件搜索 | | 获取简历 | `get_resume()` | 获取候选人详细信息 | | 批量打招呼 | `batch_greet()` | 批量发送招呼 | | 解析简历 | `parse_resume()` | 结构化提取简历字段 | ## 快速开始 ### 1. 安装依赖 ```bash pip install scrapling ``` ### 2. 配置Cookie 用户需提供Boss直聘登录Cookie： ```python COOKIES = { '__zp_stoken__': '用户提供的stoken', '__zp_phoenix_id': '用户提供的phoenix_id', # ... 其他Cookie } ``` ### 3. 搜索候选人 ```python from scripts.boss_scraper import BossRecruiter recruiter = BossRecruiter(cookies=COOKIES) results = recruiter.search_candidates( keyword='Python后端', city='北京', experience='3-5年', degree='本科' ) ``` ### 4. 发送招呼 ```python recruiter.batch_greet( candidate_ids=['候选人ID列表'], greeting_template='您好，{name}，我们正在招聘{position}，您有兴趣吗？' ) ``` ## 工作流程 ``` 用户输入招聘需求 ↓ Agent解析需求 → search_candidates() ↓ 获取候选人列表 → 展示给用户 ↓ 用户选择候选人 → batch_greet() ↓ 获取简历 → get_resume() ↓ 存入人才库 ``` ## 关键文件 | 文件 | 用途 | |------|------| | `scripts/boss_scraper.py` | 主爬虫类 | | `scripts/parser.py` | 简历解析器 | | `references/selectors.md` | Boss直聘 CSS选择器 | | `references/config.md` | 配置说明 | ## 反爬策略 1. **随机延迟**：3-10秒随机间隔 2. **请求限流**：每日搜索≤200次，打招呼≤500次 3. **Cookie刷新**：Token过期后提示用户更新 4. **失败重试**：自动重试3次，间隔递增 ## 注意事项 - 仅供用户本人账号使用，禁止批量操作他人账号 - 遵守Boss直聘robots.txt和服务条款 - 定期检查Cookie有效性

path: references/config.md
content: # Boss直聘爬虫配置指南

## Cookie获取

### 步骤

1. 在Chrome浏览器登录 [Boss直聘](https://www.zhipin.com)
2. 按 `F12` 打开开发者工具
3. 切换到 `Application` 标签
4. 左侧选择 `Cookies` → `https://www.zhipin.com`
5. 复制以下关键Cookie的值：

### 必需Cookie

| Cookie名称 | 说明 | 重要性 |
|-----------|------|--------|
| `__zp_stoken__` | 认证Token，核心参数 | ⭐⭐⭐ 必需 |
| `__zp_phoenix_id` | 浏览器会话ID | ⭐⭐⭐ 必需 |
| `Hm_lvt_xxx` | Session标记 | ⭐⭐ 重要 |
| `Hm_lpvt_xxx` | 最后活跃时间 | ⭐⭐ 重要 |
| `wt` | 访问令牌 | ⭐ 辅助 |
| `l` | 用户ID标记 | ⭐ 辅助 |
| `wmda_uuid` | 设备指纹 | ⭐ 辅助 |

### 注意事项

- Cookie有效期通常为 **几个小时到几天**
- 过期后需要用户重新登录获取
- 建议同时提供所有可见的Cookie以提高成功率

### Python Cookie格式

```python
COOKIES = {
    '__zp_stoken__': 'your_stoken_here',
    '__zp_phoenix_id': 'your_phoenix_id_here',
    'Hm_lvt_194a310...': 'timestamp',
    'Hm_lpvt_194a310...': 'timestamp',
    'wt': 'your_wt_here',
    'l': 'your_l_here',
    'wmda_uuid': 'your_uuid_here',
    'wmda_new_uuid': 'your_new_uuid_here',
}
```

## 请求频率配置

### 限制参数

```python
# 每日限制
DAILY_SEARCH_LIMIT = 200   # 搜索次数
DAILY_GREET_LIMIT = 500    # 打招呼次数

# 请求间隔
MIN_DELAY = 3   # 最小延迟（秒）
MAX_DELAY = 10  # 最大延迟（秒）

# 失败重试
MAX_RETRIES = 3           # 最大重试次数
RETRY_DELAY = 5           # 重试间隔（秒）
```

### 安全建议

| 建议 | 说明 |
|------|------|
| 限制日操作量 | 搜索≤200/天，打招呼≤500/天 |
| 随机延迟 | 每次请求间隔3-10秒随机 |
| 分散操作时间 | 避免集中在短时间大量请求 |
| 监控成功率 | 成功率低于80%时应降低频率 |

## 代理配置（可选）

如需使用代理：

```python
PROXY = {
    'http': 'http://username:[REDACTED_SECRET]:8080',
    'https': 'http://username:[REDACTED_SECRET]:8080',
}

# 使用代理
with DynamicSession(proxy=PROXY) as session:
    page = session.fetch(url)
```

## 错误处理

### 常见错误

| 错误类型 | 原因 | 处理方式 |
|---------|------|----------|
| `403 Forbidden` | Cookie无效或被禁止 | 提示用户更新Cookie |
| `429 Too Many Requests` | 请求过于频繁 | 降低频率，等待后重试 |
| `Captcha Required` | 触发验证码 | 暂停操作，提示用户 |
| `Network Error` | 网络问题 | 自动重试3次 |

### 重试装饰器

```python
from functools import wraps
import time

def retry_on_failure(max_retries=3, delay=5):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise
                    logger.warning(f"尝试 {attempt+1} 失败: {e}")
                    time.sleep(delay * (attempt + 1))
        return wrapper
    return decorator

@retry_on_failure(max_retries=3, delay=5)
def fetch_page(url):
    # 获取页面的逻辑
    pass
```

## 环境变量配置

```bash
# .env 文件
BOSSBOT_COOKIE_STOKEN=your_stoken
BOSSBOT_COOKIE_PHOENIX=your_phoenix
BOSSBOT_DAILY_SEARCH_LIMIT=200
BOSSBOT_DAILY_GREET_LIMIT=500
BOSSBOT_MIN_DELAY=3
BOSSBOT_MAX_DELAY=10
```

```python
import os
from dotenv import load_dotenv

load_dotenv()

COOKIES = {
    '__zp_stoken__': os.getenv('BOSSBOT_COOKIE_STOKEN'),
    '__zp_phoenix_id': os.getenv('BOSSBOT_COOKIE_PHOENIX'),
}
```


path: references/selectors.md
content: # Boss直聘 CSS选择器参考

> ⚠️ 注意：Boss直聘页面结构可能更新，请定期验证选择器有效性

## 候选人列表页 (搜索结果)

| 元素 | CSS选择器 | 说明 |
|------|-----------|------|
| 候选人卡片 | `.job-card-box` | 单个候选人卡片容器 |
| 候选人ID | `@data-lid` 或 `@data-jobid` | 候选人唯一标识 |
| 姓名 | `.name::text` | 候选人姓名 |
| 职位 | `.job-title::text` | 应聘职位 |
| 公司名 | `.company-name::text` | 当前/曾任职公司 |
| 城市 | `.city::text` | 所在城市 |
| 经验 | `.experience::text` | 工作年限 |
| 学历 | `.degree::text` | 最高学历 |
| 薪资 | `.salary::text` | 薪资范围 |
| 技能标签 | `.skill-tag::text` | 技能关键词列表 |
| 更新时间 | `.update-time::text` | 简历更新时间 |
| 头像 | `img.avatar@src` | 头像URL |
| 立即沟通 | `.btn-start-chat` | 打招呼按钮 |

## 简历详情页

| 元素 | CSS选择器 | 说明 |
|------|-----------|------|
| 姓名 | `.name::text` | 候选人姓名 |
| 性别 | `.gender::text` | 性别 |
| 年龄 | `.age::text` | 年龄 |
| 期望职位 | `.job-title::text` | 期望职位 |
| 期望城市 | `.city::text` | 期望城市 |
| 工作经验 | `.experience::text` | 工作年限 |
| 学历 | `.degree::text` | 最高学历 |
| 期望薪资 | `.salary-expectation::text` | 期望薪资 |
| 当前公司 | `.current-company::text` | 当前公司 |
| 当前职位 | `.current-position::text` | 当前职位 |
| 技能标签 | `.skill-tag::text` | 技能列表 |
| 自我介绍 | `.self-description::text` | 自我介绍 |

## 工作经历

| 元素 | CSS选择器 | 说明 |
|------|-----------|------|
| 容器 | `.work-history-item` | 单个工作经历 |
| 公司名 | `.company::text` | 公司名称 |
| 职位 | `.position::text` | 职位名称 |
| 时间 | `.duration::text` | 在职时间 |
| 描述 | `.description::text` | 工作描述 |

## 教育经历

| 元素 | CSS选择器 | 说明 |
|------|-----------|------|
| 容器 | `.education-item` | 单个教育经历 |
| 学校 | `.school::text` | 学校名称 |
| 专业 | `.major::text` | 所学专业 |
| 学历 | `.degree::text` | 学历 |
| 时间 | `.duration::text` | 就读时间 |

## 搜索参数

### URL参数说明

```
https://www.zhipin.com/web/candidate/search?
    query={关键词}           # 搜索关键词
    &city={城市代码}          # 城市，如101010100(北京)
    &experience={经验代码}    # 经验要求，如102（3-5年）
    &degree={学历代码}        # 学历要求，如205（本科）
    &salary={薪资代码}        # 薪资范围，如3（15K-25K）
    &page={页码}             # 页码
    &pageSize={每页数量}     # 每页数量，默认30
```

### 常用参数值

| 参数 | 值 | 说明 |
|------|-----|------|
| 经验 | 101 | 1年以下 |
| 经验 | 102 | 1-3年 |
| 经验 | 103 | 3-5年 |
| 经验 | 104 | 5-10年 |
| 经验 | 105 | 10年以上 |
| 学历 | 204 | 大专 |
| 学历 | 205 | 本科 |
| 学历 | 206 | 硕士 |
| 学历 | 207 | 博士 |

## 动态加载内容

Boss直聘使用Ajax动态加载内容，需要：

1. 使用 `DynamicFetcher` 或 `StealthyFetcher`
2. 设置 `network_idle=True` 等待请求完成
3. 或使用 `wait_for_selector` 等待特定元素出现

```python
from scrapling.fetchers import DynamicSession

with DynamicSession(headless=True, network_idle=True) as session:
    page = session.fetch(url)
    # 等待动态内容加载
    page.wait_for_selector('.job-card-box', timeout=10000)
```

## XPath备选方案

如CSS选择器失效，可使用XPath：

```python
# XPath示例
items = page.xpath('//div[@class="job-card-box"]')
name = page.xpath('//span[@class="name"]/text()').get()
```


path: scripts/boss_scraper.py
content: """
Boss直聘 招聘爬虫
基于 Scrapling 框架
"""

import time
import random
import logging
from typing import Optional
from dataclasses import dataclass
from scrapling.fetchers import DynamicSession, DynamicFetcher

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@dataclass
class Candidate:
    """候选人数据结构"""
    id: str
    name: str
    position: str
    company: str
    city: str
    experience: str
    degree: str
    salary: str
    skills: list
    update_time: str
    avatar: Optional[str] = None
    description: Optional[str] = None


@dataclass
class Resume:
    """简历数据结构"""
    candidate_id: str
    name: str
    gender: Optional[str] = None
    age: Optional[str] = None
    position: Optional[str] = None
    company: Optional[str] = None
    city: Optional[str] = None
    experience_years: Optional[str] = None
    degree: Optional[str] = None
    salary_expectation: Optional[str] = None
    skills: Optional[list] = None
    work_history: Optional[list] = None
    education: Optional[list] = None
    self_description: Optional[str] = None


class BossRecruiter:
    """Boss直聘招聘助手"""
    
    BASE_URL = "https://www.zhipin.com"
    SEARCH_URL = "/web/candidate/search"
    RESUME_URL = "/web/resume/detail"
    GREET_URL = "/web/chat/add"
    
    # 请求限制配置
    DAILY_SEARCH_LIMIT = 200
    DAILY_GREET_LIMIT = 500
    MIN_DELAY = 3  # 秒
    MAX_DELAY = 10  # 秒
    
    def __init__(self, cookies: dict):
        self.cookies = cookies
        self.search_count = 0
        self.greet_count = 0
        
    def _random_delay(self):
        """随机延迟，模拟真人操作"""
        delay = random.uniform(self.MIN_DELAY, self.MAX_DELAY)
        logger.info(f"延迟 {delay:.1f} 秒")
        time.sleep(delay)
    
    def _check_limits(self, action: str = 'search'):
        """检查操作限制"""
        if action == 'search' and self.search_count >= self.DAILY_SEARCH_LIMIT:
            raise Exception(f"今日搜索次数已达上限 ({self.DAILY_SEARCH_LIMIT}次)")
        if action == 'greet' and self.greet_count >= self.DAILY_GREET_LIMIT:
            raise Exception(f"今日打招呼次数已达上限 ({self.DAILY_GREET_LIMIT}次)")
    
    def search_candidates(
        self,
        keyword: str,
        city: str = None,
        experience: str = None,
        degree: str = None,
        salary: str = None,
        page: int = 1,
        page_size: int = 30
    ) -> list[Candidate]:
        """
        搜索候选人
        
        Args:
            keyword: 搜索关键词（如：Python后端）
            city: 城市（如：北京）
            experience: 经验要求（如：3-5年）
            degree: 学历要求（如：本科）
            salary: 薪资范围
            page: 页码
            page_size: 每页数量
        
        Returns:
            候选人列表
        """
        self._check_limits('search')
        
        # 构建搜索URL
        params = f"?query={keyword}&page={page}&pageSize={page_size}"
        if city:
            params += f"&city={city}"
        if experience:
            params += f"&experience={experience}"
        if degree:
            params += f"&degree={degree}"
        if salary:
            params += f"&salary={salary}"
        
        url = f"{self.BASE_URL}{self.SEARCH_URL}{params}"
        
        logger.info(f"搜索候选人: {url}")
        
        with DynamicSession(headless=True, network_idle=True) as session:
            page = session.fetch(url, cookies=self.cookies)
            
            # 解析候选人列表
            candidates = []
            items = page.css('.job-card-box')
            
            for item in items:
                try:
                    candidate = Candidate(
                        id=item.css('@data-lid').get() or item.css('@data-jobid').get() or '',
                        name=item.css('.name::text').get() or '未知',
                        position=item.css('.job-title::text').get() or '',
                        company=item.css('.company-name::text').get() or '',
                        city=item.css('.city::text').get() or '',
                        experience=item.css('.experience::text').get() or '',
                        degree=item.css('.degree::text').get() or '',
                        salary=item.css('.salary::text').get() or '',
                        skills=item.css('.skill-tag::text').getall() or [],
                        update_time=item.css('.update-time::text').get() or '',
                        avatar=item.css('img.avatar@src').get()
                    )
                    candidates.append(candidate)
                except Exception as e:
                    logger.warning(f"解析候选人失败: {e}")
                    continue
            
            self.search_count += 1
            self._random_delay()
            
            return candidates
    
    def get_resume(self, candidate_id: str) -> Resume:
        """
        获取候选人简历详情
        
        Args:
            candidate_id: 候选人ID
        
        Returns:
            简历对象
        """
        url = f"{self.BASE_URL}{self.RESUME_URL}?lid={candidate_id}"
        
        logger.info(f"获取简历: {candidate_id}")
        
        with DynamicSession(headless=True, network_idle=True) as session:
            page = session.fetch(url, cookies=self.cookies)
            
            # 解析简历详情
            resume = Resume(
                candidate_id=candidate_id,
                name=page.css('.name::text').get() or '未知',
                position=page.css('.job-title::text').get() or '',
                company=page.css('.current-company::text').get() or '',
                city=page.css('.city::text').get() or '',
                experience_years=page.css('.experience::text').get() or '',
                degree=page.css('.degree::text').get() or '',
                salary_expectation=page.css('.salary-expectation::text').get() or '',
                skills=page.css('.skill-tag::text').getall(),
                work_history=self._parse_work_history(page),
                education=self._parse_education(page),
                self_description=page.css('.self-description::text').get()
            )
            
            self._random_delay()
            return resume
    
    def _parse_work_history(self, page) -> list:
        """解析工作经历"""
        history = []
        items = page.css('.work-history-item')
        for item in items:
            history.append({
                'company': item.css('.company::text').get() or '',
                'position': item.css('.position::text').get() or '',
                'duration': item.css('.duration::text').get() or '',
                'description': item.css('.description::text').get() or ''
            })
        return history
    
    def _parse_education(self, page) -> list:
        """解析教育经历"""
        education = []
        items = page.css('.education-item')
        for item in items:
            education.append({
                'school': item.css('.school::text').get() or '',
                'major': item.css('.major::text').get() or '',
                'degree': item.css('.degree::text').get() or '',
                'duration': item.css('.duration::text').get() or ''
            })
        return education
    
    def batch_greet(
        self,
        candidate_ids: list[str],
        greeting_template: str = "您好，{name}，看到您对{position}岗位很感兴趣，我们团队正在招聘，您有兴趣聊聊吗？"
    ) -> dict:
        """
        批量发送招呼
        
        Args:
            candidate_ids: 候选人ID列表
            greeting_template: 招呼语模板
        
        Returns:
            发送结果统计
        """
        self._check_limits('greet')
        
        results = {'success': 0, 'failed': 0, 'failed_ids': []}
        
        logger.info(f"开始批量打招呼，共 {len(candidate_ids)} 人")
        
        for candidate_id in candidate_ids:
            try:
                # 先获取候选人信息用于个性化
                candidate = self.search_candidates(keyword='', page=1)
                # 这里应该根据candidate_id获取详情，简化处理
                
                # 发送招呼
                greeting = greeting_template.format(
                    name='候选人',  # 实际应从候选人详情获取
                    position='相关岗位'
                )
                
                # 实际发送逻辑（需要对接Boss直聘API）
                # with DynamicSession(...) as session:
                #     session.post(self.GREET_URL, data={'lid': candidate_id, 'msg': greeting})
                
                results['success'] += 1
                self.greet_count += 1
                
            except Exception as e:
                logger.warning(f"打招呼失败 {candidate_id}: {e}")
                results['failed'] += 1
                results['failed_ids'].append(candidate_id)
            
            self._random_delay()
        
        return results
    
    def get_candidate_detail(self, candidate_id: str) -> dict:
        """
        获取候选人简要信息（用于打招呼前的详情确认）
        """
        # 简化实现，实际应调用详情API
        return {
            'id': candidate_id,
            'name': '待获取',
            'position': '待获取',
            'company': '待获取'
        }


path: scripts/parser.py
content: """
简历解析器
使用LLM + 正则 提取结构化简历信息
"""

import re
import json
import logging
from typing import Optional
from dataclasses import dataclass, asdict

logger = logging.getLogger(__name__)


@dataclass
class ParsedResume:
    """结构化简历数据"""
    raw_text: str
    name: Optional[str] = None
    gender: Optional[str] = None
    age: Optional[str] = None
    phone: Optional[str] = None
    email: Optional[str] = None
    position: Optional[str] = None
    city: Optional[str] = None
    experience_years: Optional[int] = None
    degree: Optional[str] = None
    current_company: Optional[str] = None
    current_position: Optional[str] = None
    salary_current: Optional[str] = None
    salary_expectation: Optional[str] = None
    skills: list = None
    work_history: list = None
    education: list = None
    
    def __post_init__(self):
        if self.skills is None:
            self.skills = []
        if self.work_history is None:
            self.work_history = []
        if self.education is None:
            self.education = []
    
    def to_dict(self) -> dict:
        return asdict(self)
    
    def to_json(self) -> str:
        return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)


class ResumeParser:
    """简历解析器"""
    
    # 正则模式
    PHONE_PATTERN = re.compile(r'1[3-9]\d{9}')
    EMAIL_PATTERN = re.compile(r'[\w.-]+@[\w.-]+\.\w+')
    AGE_PATTERN = re.compile(r'(\d{2})岁')
    DEGREE_PATTERN = re.compile(r'(博士|硕士|本科|大专|高中|中专|初中)')
    EXPERIENCE_PATTERN = re.compile(r'(\d+)年.*经验')
    SALARY_PATTERN = re.compile(r'(\d+(?:\.\d+)?)[kK]?-(\d+(?:\.\d+)?)[kK]?')
    
    def __init__(self, llm_client=None):
        """
        初始化解析器
        
        Args:
            llm_client: LLM客户端（可选，用于高级解析）
        """
        self.llm_client = llm_client
    
    def parse(self, text: str) -> ParsedResume:
        """
        解析简历文本
        
        Args:
            text: 原始简历文本
        
        Returns:
            结构化简历对象
        """
        resume = ParsedResume(raw_text=text)
        
        # 基础信息提取
        resume.phone = self._extract_phone(text)
        resume.email = self._extract_email(text)
        resume.age = self._extract_age(text)
        resume.degree = self._extract_degree(text)
        resume.experience_years = self._extract_experience(text)
        
        # 薪资提取
        salary_info = self._extract_salary(text)
        resume.salary_current = salary_info.get('current')
        resume.salary_expectation = salary_info.get('expectation')
        
        # 技能提取
        resume.skills = self._extract_skills(text)
        
        # 工作经历解析
        resume.work_history = self._parse_work_history(text)
        
        # 教育经历解析
        resume.education = self._parse_education(text)
        
        return resume
    
    def _extract_phone(self, text: str) -> Optional[str]:
        match = self.PHONE_PATTERN.search(text)
        return match.group(0) if match else None
    
    def _extract_email(self, text: str) -> Optional[str]:
        match = self.EMAIL_PATTERN.search(text)
        return match.group(0) if match else None
    
    def _extract_age(self, text: str) -> Optional[str]:
        match = self.AGE_PATTERN.search(text)
        return match.group(1) if match else None
    
    def _extract_degree(self, text: str) -> Optional[str]:
        match = self.DEGREE_PATTERN.search(text)
        return match.group(1) if match else None
    
    def _extract_experience(self, text: str) -> Optional[int]:
        match = self.EXPERIENCE_PATTERN.search(text)
        return int(match.group(1)) if match else None
    
    def _extract_salary(self, text: str) -> dict:
        """提取薪资信息"""
        result = {'current': None, 'expectation': None}
        
        # 当前薪资
        current_match = re.search(r'目前薪资.*?(\d+(?:\.\d+)?)[kK]?', text)
        if current_match:
            result['current'] = current_match.group(0)
        
        # 期望薪资
        expect_match = re.search(r'期望薪资.*?(\d+(?:\.\d+)?)[kK]?', text)
        if expect_match:
            result['expectation'] = expect_match.group(0)
        
        return result
    
    def _extract_skills(self, text: str) -> list:
        """提取技能标签"""
        # 常见技能关键词
        skill_keywords = [
            'Python', 'Java', 'JavaScript', 'Go', 'Rust', 'C++', 'C#',
            'React', 'Vue', 'Angular', 'Node.js',
            'MySQL', 'PostgreSQL', 'MongoDB', 'Redis',
            'AWS', 'Azure', 'GCP', 'Docker', 'Kubernetes',
            'Machine Learning', 'Deep Learning', 'NLP',
            'Git', 'Linux', 'Nginx', 'Kafka'
        ]
        
        found_skills = []
        text_upper = text.upper()
        for skill in skill_keywords:
            if skill.upper() in text_upper:
                found_skills.append(skill)
        
        return found_skills
    
    def _parse_work_history(self, text: str) -> list:
        """解析工作经历"""
        history = []
        
        # 简单实现：按换行分割，查找公司/职位模式
        lines = text.split('\n')
        current_item = {}
        
        for line in lines:
            line = line.strip()
            if not line:
                continue
            
            # 检测公司名模式（如：xx公司、xx科技）
            if re.search(r'(公司|科技|集团|有限|企业)', line) and len(line) < 30:
                if current_item:
                    history.append(current_item)
                current_item = {'company': line}
            
            # 检测职位模式
            elif re.search(r'(工程师|经理|总监|主管|架构师|开发者)', line):
                current_item['position'] = line
            
            # 检测时间范围
            elif re.search(r'\d{4}.\d{2}', line):
                current_item['duration'] = line
            
            # 检测描述
            elif current_item and 'position' in current_item:
                current_item['description'] = current_item.get('description', '') + line
        
        if current_item:
            history.append(current_item)
        
        return history
    
    def _parse_education(self, text: str) -> list:
        """解析教育经历"""
        education = []
        
        # 常见学校/大学关键词
        school_pattern = re.compile(r'(大学|学院|学校|研究院)')
        
        lines = text.split('\n')
        for line in lines:
            if school_pattern.search(line):
                edu_item = {'school': line.strip()}
                
                # 提取学历
                degree_match = self.DEGREE_PATTERN.search(line)
                if degree_match:
                    edu_item['degree'] = degree_match.group(1)
                
                # 提取专业
                major_match = re.search(r'(计算机|软件|电子|机械|化学|物理|数学|经济|管理)', line)
                if major_match:
                    edu_item['major'] = major_match.group(0)
                
                education.append(edu_item)
        
        return education
    
    def parse_with_llm(self, text: str, llm_prompt: str = None) -> ParsedResume:
        """
        使用LLM增强解析
        
        Args:
            text: 简历文本
            llm_prompt: 自定义LLM提示词
        
        Returns:
            结构化简历对象
        """
        if not self.llm_client:
            logger.warning("未提供LLM客户端，使用基础解析")
            return self.parse(text)
        
        default_prompt = """
        请从以下简历文本中提取结构化信息，返回JSON格式：
        {
            "name": "姓名",
            "gender": "性别",
            "age": "年龄",
            "position": "当前职位",
            "city": "所在城市",
            "experience_years": 工作年限（数字）,
            "degree": "最高学历",
            "current_company": "当前公司",
            "skills": ["技能1", "技能2"],
            "work_history": [{"company": "公司", "position": "职位", "duration": "时间"}],
            "education": [{"school": "学校", "major": "专业", "degree": "学历"}]
        }
        
        简历文本：
        {text}
        """
        
        prompt = llm_prompt or default_prompt.format(text=text)
        
        try:
            response = self.llm_client.generate(prompt)
            # 解析LLM返回的JSON
            data = json.loads(response)
            
            # 合并基础解析和LLM解析
            base_resume = self.parse(text)
            for key, value in data.items():
                if value and getattr(base_resume, key) is None:
                    setattr(base_resume, key, value)
            
            return base_resume
            
        except Exception as e:
            logger.error(f"LLM解析失败: {e}")
            return self.parse(text)


# 使用示例
if __name__ == '__main__':
    sample_resume = """
    张三
    男 | 28岁 | 13800138000 | [REDACTED_SECRET]
    
    Python高级工程师
    北京 | 5年经验 | 本科
    
    目前薪资：30K
    期望薪资：40K-50K
    
    技能：Python, Django, FastAPI, MySQL, Redis, Docker, AWS, Git
    
    工作经历：
    2020.03 - 至今    字节跳动    后端工程师
    负责短视频推荐系统后端开发，使用Python/Django构建高性能API服务。
    优化数据库查询性能，QPS提升50%。
    
    2018.07 - 2020.02    阿里巴巴    Java开发
    参与电商平台订单系统开发。
    
    教育经历：
    2014.09 - 2018.06    北京大学    计算机科学与技术    本科
    """
    
    parser = ResumeParser()
    resume = parser.parse(sample_resume)
    print(resume.to_json())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)