职位数据解析器
| 6 | |
| 7 | |
| 8 | class JobParser: |
| 9 | """职位数据解析器""" |
| 10 | |
| 11 | POSITION_TYPES = {"ft": "Full-time", "fr": "Freelance", "pt": "Part-time"} |
| 12 | |
| 13 | EXPERIENCE_MAP = { |
| 14 | "ENTRY_LEVEL": "Entry Level", |
| 15 | "MID_LEVEL": "Mid Level", |
| 16 | "SENIOR_LEVEL": "Senior Level", |
| 17 | "EXECUTIVE": "Executive", |
| 18 | } |
| 19 | |
| 20 | def parse(self, raw_job: dict) -> dict: |
| 21 | """解析单条职位数据""" |
| 22 | source = raw_job["_source"] |
| 23 | |
| 24 | return { |
| 25 | "id": source.get("id"), |
| 26 | "title": source.get("title", "N/A"), |
| 27 | "company": source.get("company", "N/A"), |
| 28 | "category": source.get("category_name", "N/A"), |
| 29 | "location": self._format_location(source.get("locations")), |
| 30 | "position_type": self.POSITION_TYPES.get( |
| 31 | source.get("position_type", "ft"), "Full-time" |
| 32 | ), |
| 33 | "salary": self._format_salary(source), |
| 34 | "experience": self.EXPERIENCE_MAP.get( |
| 35 | source.get("experience_level", ""), "Not specified" |
| 36 | ), |
| 37 | "tags": source.get("tags", []) or [], |
| 38 | "description": self._clean_html(source.get("description", "")), |
| 39 | "apply_url": source.get("apply_url", ""), |
| 40 | "pub_date": self._format_date(source.get("pub_date")), |
| 41 | } |
| 42 | |
| 43 | def _format_location(self, locations: list) -> str: |
| 44 | """格式化地点""" |
| 45 | if not locations: |
| 46 | return "Remote" |
| 47 | return ", ".join(locations[:3]) |
| 48 | |
| 49 | def _format_salary(self, source: dict) -> str: |
| 50 | """格式化薪资信息""" |
| 51 | annual = source.get("annual_salary_usd") |
| 52 | range_str = source.get("salary_range_short") or source.get("salary_range") |
| 53 | |
| 54 | if annual: |
| 55 | return f"${annual:,}/year" |
| 56 | elif range_str: |
| 57 | return range_str |
| 58 | return "Not specified" |
| 59 | |
| 60 | def _clean_html(self, html: str) -> str: |
| 61 | """清理 HTML 标签""" |
| 62 | if not html: |
| 63 | return "" |
| 64 | |
| 65 | text = re.sub(r"<[^>]+>", "", html) |