From 45762101bc6cbdbb78088d643f927207c9123bc4 Mon Sep 17 00:00:00 2001 From: kgod Date: Thu, 28 May 2026 00:04:05 +0800 Subject: [PATCH] chore: remove legacy code, keep only crawl directory --- README.md | 3 - docs/PROJECT.md | 936 ------------------ docs/crawler_design.md | 356 ------- docs/playwright_guide.md | 307 ------ outputs/samsung_jobs.csv | 729 -------------- outputs/samsung_jobs.html | 25 - outputs/samsung_jobs.json | 187 ---- requirements.txt | 32 - .../manual_step5_crawl.cpython-312.pyc | Bin 8409 -> 0 bytes scripts/manual_step5_crawl.py | 173 ---- service.sh | 79 -- src/__init__.py | 1 - src/__pycache__/__init__.cpython-312.pyc | Bin 168 -> 0 bytes src/__pycache__/bash_model.cpython-312.pyc | Bin 2169 -> 0 bytes src/bash_model.py | 91 -- src/browser/__init__.py | 126 --- .../__pycache__/__init__.cpython-312.pyc | Bin 6048 -> 0 bytes src/company_generator/__init__.py | 3 - .../__pycache__/__init__.cpython-312.pyc | Bin 266 -> 0 bytes .../__pycache__/generator.cpython-312.pyc | Bin 5239 -> 0 bytes .../__pycache__/prompts.cpython-312.pyc | Bin 1522 -> 0 bytes src/company_generator/generator.py | 134 --- src/company_generator/prompts.py | 35 - src/company_importer/__init__.py | 3 - src/company_importer/__main__.py | 15 - src/company_importer/importer.py | 155 --- src/company_importer/url_validator.py | 88 -- src/config/__init__.py | 3 - .../__pycache__/__init__.cpython-312.pyc | Bin 263 -> 0 bytes src/config/__pycache__/config.cpython-312.pyc | Bin 728 -> 0 bytes .../__pycache__/development.cpython-312.pyc | Bin 1008 -> 0 bytes .../__pycache__/production.cpython-312.pyc | Bin 1006 -> 0 bytes .../__pycache__/settings.cpython-312.pyc | Bin 2089 -> 0 bytes src/config/config.py | 19 - src/config/development.py | 19 - src/config/production.py | 19 - src/config/settings.py | 47 - src/crawler/__init__.py | 11 - .../__pycache__/__init__.cpython-312.pyc | Bin 425 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 1232 -> 0 bytes .../__pycache__/crawler.cpython-312.pyc | Bin 12459 -> 0 bytes src/crawler/__pycache__/main.cpython-312.pyc | Bin 2839 -> 0 bytes src/crawler/__pycache__/utils.cpython-312.pyc | Bin 8429 -> 0 bytes src/crawler/config.py | 32 - src/crawler/crawler.py | 326 ------ src/crawler/main.py | 54 - src/crawler/utils.py | 200 ---- src/database/__init__.py | 25 - .../__pycache__/__init__.cpython-312.pyc | Bin 584 -> 0 bytes src/database/__pycache__/base.cpython-312.pyc | Bin 454 -> 0 bytes .../__pycache__/connection.cpython-312.pyc | Bin 927 -> 0 bytes src/database/base.py | 7 - src/database/connection.py | 26 - src/database/models/__init__.py | 17 - .../__pycache__/__init__.cpython-312.pyc | Bin 693 -> 0 bytes .../__pycache__/crawl_task.cpython-312.pyc | Bin 1743 -> 0 bytes .../__pycache__/job_data.cpython-312.pyc | Bin 2956 -> 0 bytes .../__pycache__/task_crawl.cpython-312.pyc | Bin 2017 -> 0 bytes .../__pycache__/task_detail.cpython-312.pyc | Bin 2388 -> 0 bytes .../__pycache__/task_next.cpython-312.pyc | Bin 2269 -> 0 bytes .../__pycache__/task_page.cpython-312.pyc | Bin 2269 -> 0 bytes .../__pycache__/task_search.cpython-312.pyc | Bin 2214 -> 0 bytes src/database/models/crawl_task.py | 27 - src/database/models/job_data.py | 41 - src/database/models/task_crawl.py | 30 - src/database/models/task_detail.py | 33 - src/database/models/task_next.py | 32 - src/database/models/task_page.py | 32 - src/database/models/task_search.py | 29 - src/detail_analysis_graph/__init__.py | 5 - .../__pycache__/__init__.cpython-312.pyc | Bin 337 -> 0 bytes .../__pycache__/graph.cpython-312.pyc | Bin 2531 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 5270 -> 0 bytes .../__pycache__/nodes.cpython-312.pyc | Bin 14445 -> 0 bytes .../__pycache__/prompts.cpython-312.pyc | Bin 3416 -> 0 bytes .../__pycache__/state.cpython-312.pyc | Bin 971 -> 0 bytes src/detail_analysis_graph/graph.py | 89 -- src/detail_analysis_graph/main.py | 108 -- src/detail_analysis_graph/nodes.py | 331 ------- src/detail_analysis_graph/prompts.py | 107 -- src/detail_analysis_graph/state.py | 34 - src/main.py | 43 - src/next_page_graph/__init__.py | 5 - .../__pycache__/__init__.cpython-312.pyc | Bin 334 -> 0 bytes .../__pycache__/graph.cpython-312.pyc | Bin 2514 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 3989 -> 0 bytes .../__pycache__/nodes.cpython-312.pyc | Bin 8687 -> 0 bytes .../__pycache__/prompts.cpython-312.pyc | Bin 2199 -> 0 bytes .../__pycache__/state.cpython-312.pyc | Bin 922 -> 0 bytes src/next_page_graph/graph.py | 90 -- src/next_page_graph/main.py | 85 -- src/next_page_graph/nodes.py | 227 ----- src/next_page_graph/prompts.py | 80 -- src/next_page_graph/state.py | 33 - src/page_analysis_graph/__init__.py | 5 - .../__pycache__/__init__.cpython-312.pyc | Bin 337 -> 0 bytes .../__pycache__/graph.cpython-312.pyc | Bin 2545 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 4118 -> 0 bytes .../__pycache__/nodes.cpython-312.pyc | Bin 12687 -> 0 bytes .../__pycache__/prompts.cpython-312.pyc | Bin 2759 -> 0 bytes .../__pycache__/state.cpython-312.pyc | Bin 909 -> 0 bytes src/page_analysis_graph/graph.py | 82 -- src/page_analysis_graph/main.py | 87 -- src/page_analysis_graph/nodes.py | 327 ------ src/page_analysis_graph/prompts.py | 108 -- src/page_analysis_graph/state.py | 33 - src/scheduler/__init__.py | 3 - .../__pycache__/__init__.cpython-312.pyc | Bin 293 -> 0 bytes .../__pycache__/jobs.cpython-312.pyc | Bin 39311 -> 0 bytes .../__pycache__/scheduler.cpython-312.pyc | Bin 2853 -> 0 bytes src/scheduler/jobs.py | 762 -------------- src/scheduler/scheduler.py | 117 --- src/search_company_graph/__init__.py | 4 - .../__pycache__/__init__.cpython-312.pyc | Bin 301 -> 0 bytes .../__pycache__/graph.cpython-312.pyc | Bin 3041 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 3924 -> 0 bytes .../__pycache__/nodes.cpython-312.pyc | Bin 18448 -> 0 bytes .../__pycache__/prompts.cpython-312.pyc | Bin 3474 -> 0 bytes .../__pycache__/state.cpython-312.pyc | Bin 969 -> 0 bytes src/search_company_graph/graph.py | 105 -- src/search_company_graph/main.py | 88 -- src/search_company_graph/nodes.py | 457 --------- src/search_company_graph/prompts.py | 119 --- src/search_company_graph/state.py | 31 - test_crawler.py | 62 -- 125 files changed, 8134 deletions(-) delete mode 100644 README.md delete mode 100644 docs/PROJECT.md delete mode 100644 docs/crawler_design.md delete mode 100644 docs/playwright_guide.md delete mode 100644 outputs/samsung_jobs.csv delete mode 100644 outputs/samsung_jobs.html delete mode 100644 outputs/samsung_jobs.json delete mode 100644 requirements.txt delete mode 100644 scripts/__pycache__/manual_step5_crawl.cpython-312.pyc delete mode 100644 scripts/manual_step5_crawl.py delete mode 100644 service.sh delete mode 100644 src/__init__.py delete mode 100644 src/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/__pycache__/bash_model.cpython-312.pyc delete mode 100644 src/bash_model.py delete mode 100644 src/browser/__init__.py delete mode 100644 src/browser/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/company_generator/__init__.py delete mode 100644 src/company_generator/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/company_generator/__pycache__/generator.cpython-312.pyc delete mode 100644 src/company_generator/__pycache__/prompts.cpython-312.pyc delete mode 100644 src/company_generator/generator.py delete mode 100644 src/company_generator/prompts.py delete mode 100644 src/company_importer/__init__.py delete mode 100644 src/company_importer/__main__.py delete mode 100644 src/company_importer/importer.py delete mode 100644 src/company_importer/url_validator.py delete mode 100644 src/config/__init__.py delete mode 100644 src/config/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/config/__pycache__/config.cpython-312.pyc delete mode 100644 src/config/__pycache__/development.cpython-312.pyc delete mode 100644 src/config/__pycache__/production.cpython-312.pyc delete mode 100644 src/config/__pycache__/settings.cpython-312.pyc delete mode 100644 src/config/config.py delete mode 100644 src/config/development.py delete mode 100644 src/config/production.py delete mode 100644 src/config/settings.py delete mode 100644 src/crawler/__init__.py delete mode 100644 src/crawler/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/crawler/__pycache__/config.cpython-312.pyc delete mode 100644 src/crawler/__pycache__/crawler.cpython-312.pyc delete mode 100644 src/crawler/__pycache__/main.cpython-312.pyc delete mode 100644 src/crawler/__pycache__/utils.cpython-312.pyc delete mode 100644 src/crawler/config.py delete mode 100644 src/crawler/crawler.py delete mode 100644 src/crawler/main.py delete mode 100644 src/crawler/utils.py delete mode 100644 src/database/__init__.py delete mode 100644 src/database/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/database/__pycache__/base.cpython-312.pyc delete mode 100644 src/database/__pycache__/connection.cpython-312.pyc delete mode 100644 src/database/base.py delete mode 100644 src/database/connection.py delete mode 100644 src/database/models/__init__.py delete mode 100644 src/database/models/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/crawl_task.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/job_data.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/task_crawl.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/task_detail.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/task_next.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/task_page.cpython-312.pyc delete mode 100644 src/database/models/__pycache__/task_search.cpython-312.pyc delete mode 100644 src/database/models/crawl_task.py delete mode 100644 src/database/models/job_data.py delete mode 100644 src/database/models/task_crawl.py delete mode 100644 src/database/models/task_detail.py delete mode 100644 src/database/models/task_next.py delete mode 100644 src/database/models/task_page.py delete mode 100644 src/database/models/task_search.py delete mode 100644 src/detail_analysis_graph/__init__.py delete mode 100644 src/detail_analysis_graph/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/__pycache__/graph.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/__pycache__/main.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/__pycache__/nodes.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/__pycache__/prompts.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/__pycache__/state.cpython-312.pyc delete mode 100644 src/detail_analysis_graph/graph.py delete mode 100644 src/detail_analysis_graph/main.py delete mode 100644 src/detail_analysis_graph/nodes.py delete mode 100644 src/detail_analysis_graph/prompts.py delete mode 100644 src/detail_analysis_graph/state.py delete mode 100644 src/main.py delete mode 100644 src/next_page_graph/__init__.py delete mode 100644 src/next_page_graph/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/next_page_graph/__pycache__/graph.cpython-312.pyc delete mode 100644 src/next_page_graph/__pycache__/main.cpython-312.pyc delete mode 100644 src/next_page_graph/__pycache__/nodes.cpython-312.pyc delete mode 100644 src/next_page_graph/__pycache__/prompts.cpython-312.pyc delete mode 100644 src/next_page_graph/__pycache__/state.cpython-312.pyc delete mode 100644 src/next_page_graph/graph.py delete mode 100644 src/next_page_graph/main.py delete mode 100644 src/next_page_graph/nodes.py delete mode 100644 src/next_page_graph/prompts.py delete mode 100644 src/next_page_graph/state.py delete mode 100644 src/page_analysis_graph/__init__.py delete mode 100644 src/page_analysis_graph/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/page_analysis_graph/__pycache__/graph.cpython-312.pyc delete mode 100644 src/page_analysis_graph/__pycache__/main.cpython-312.pyc delete mode 100644 src/page_analysis_graph/__pycache__/nodes.cpython-312.pyc delete mode 100644 src/page_analysis_graph/__pycache__/prompts.cpython-312.pyc delete mode 100644 src/page_analysis_graph/__pycache__/state.cpython-312.pyc delete mode 100644 src/page_analysis_graph/graph.py delete mode 100644 src/page_analysis_graph/main.py delete mode 100644 src/page_analysis_graph/nodes.py delete mode 100644 src/page_analysis_graph/prompts.py delete mode 100644 src/page_analysis_graph/state.py delete mode 100644 src/scheduler/__init__.py delete mode 100644 src/scheduler/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/scheduler/__pycache__/jobs.cpython-312.pyc delete mode 100644 src/scheduler/__pycache__/scheduler.cpython-312.pyc delete mode 100644 src/scheduler/jobs.py delete mode 100644 src/scheduler/scheduler.py delete mode 100644 src/search_company_graph/__init__.py delete mode 100644 src/search_company_graph/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/search_company_graph/__pycache__/graph.cpython-312.pyc delete mode 100644 src/search_company_graph/__pycache__/main.cpython-312.pyc delete mode 100644 src/search_company_graph/__pycache__/nodes.cpython-312.pyc delete mode 100644 src/search_company_graph/__pycache__/prompts.cpython-312.pyc delete mode 100644 src/search_company_graph/__pycache__/state.cpython-312.pyc delete mode 100644 src/search_company_graph/graph.py delete mode 100644 src/search_company_graph/main.py delete mode 100644 src/search_company_graph/nodes.py delete mode 100644 src/search_company_graph/prompts.py delete mode 100644 src/search_company_graph/state.py delete mode 100644 test_crawler.py diff --git a/README.md b/README.md deleted file mode 100644 index 06211095..00000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ai-review-template - -Template repository with Gitea Actions AI pull request review workflow. \ No newline at end of file diff --git a/docs/PROJECT.md b/docs/PROJECT.md deleted file mode 100644 index 5067bb83..00000000 --- a/docs/PROJECT.md +++ /dev/null @@ -1,936 +0,0 @@ -# AI 招聘网站智能爬虫系统 - -## 项目目标 - -用户输入**公司名称**,系统自动完成: -1. 通过搜索引擎找到该公司的官网招聘页面 -2. AI 分析页面结构,提取爬虫所需的 CSS 选择器配置 -3. 使用配置自动爬取所有岗位数据 - -**核心理念**:让 AI Agent 像人一样「看懂」网页,自动生成爬虫配置,无需针对每个网站手写选择器。 - ---- - -## 整体流程 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 用户输入: 公司名称 │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Step 1: search_company │ -│ 功能: 搜索引擎查找公司官网招聘页面 │ -│ 输出: 招聘列表页 URL │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Step 2: page_analysis_graph │ -│ 功能: 分析列表页,找到岗位项的 CSS 选择器 │ -│ 输出: job_item_selector, change_type │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Step 3: next_page_graph │ -│ 功能: 分析分页组件,找到「下一页」按钮的选择器 │ -│ 输出: next_page_selector, change_type │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Step 4: detail_analysis_graph │ -│ 功能: 进入详情页,提取各字段(标题/薪资/要求等)的选择器 │ -│ 输出: field_selectors │ -└─────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Step 5: crawler (待开发) │ -│ 功能: 使用上述选择器配置,自动翻页爬取所有岗位数据 │ -│ 输出: 结构化岗位数据列表 │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 模块说明 - -### 1. search_company_graph - 搜索招聘页面 - -**目的**: 根据公司名,通过搜索引擎找到其官网招聘列表页 - -**技术方案**: LangGraph 状态机 + Playwright + LLM 结构化输出 - -**流程**: -``` -Node 0: route_entry (入口路由) - 有已知 candidate_urls → 跳过搜索,直接 Node 3 - 无 → 进入 Node 1 搜索 - │ - ▼ -Node 1: search_sogou - 搜狗搜索 "{company_name} 招聘 官网" - 提取结构化搜索结果(标题、URL、描述) - │ - ▼ -Node 2: extract_links - LLM 分析搜索结果 - 提取最多 3 个候选 URL(按可能性排序) - │ - ▼ -Node 3: visit_and_verify - 访问候选 URL - LLM 判断是否为招聘列表页 - 判断标准: - - 包含多个岗位信息(职位名称) - - 页面有分页控件 - - 或有岗位搜索/筛选功能 - - 或有明确的岗位分类导航 - │ - ✔️ 是 → 返回 URL ✅ - ❌ 不是 → 进入 Node 4 - │ - ▼ -Node 4: navigate_to_recruitment - LLM 找页面上的招聘入口(避开已点击过的) - 点击入口 → 回 Node 3 验证 - 循环最多 10 次,超过则换下一个候选 URL -``` - -**输入/输出**: -```python -from src.search_company_graph import search_company_recruitment - -# 输入 -company_name: str # 公司名称,如 "联想" -input_url: str # 已知招聘地址(可选),有值时跳过搜索直接验证 -headless: bool # 是否无头模式,默认 False - -# 输出 (成功) -["https://jobs.lenovo.com.cn/..."] # 招聘页面 URL 列表 - -# 输出 (失败) -[] # 空列表,表示未找到 -``` - -**文件结构**: -``` -src/search_company_graph/ -├── __init__.py # 导出 search_company_recruitment -├── state.py # SearchState TypedDict -├── prompts.py # LLM 提示词模板 -├── nodes.py # 4 个节点实现 -├── graph.py # LangGraph 流程定义 -└── main.py # 入口函数 -``` - ---- - -### 2. page_analysis_graph - 岗位列表分析 - -**目的**: 分析招聘列表页,找到可点击进入岗位详情的元素 CSS 选择器 - -**流程**: 获取HTML → LLM找列表区域 → LLM找可点击元素 → 点击验证能否进入详情 - -**选择器优先级**: 岗位名称链接 > 申请/查看按钮 > 详情图标 > 整个岗位卡片 - -**输入/输出**: -```python -# 输入 -url: str # 招聘列表页URL -headless: bool # 是否无头模式 - -# 输出 (成功) -{ - "status": "success", - "selector": ".job-list a.title", # 可点击元素选择器 - "item_count": 20, # 匹配到的岗位数 - "change_type": "redirect" # 点击后的变化类型 -} - -# change_type: redirect(跳转) / new_tab(新标签) / in_page(页内变化) - -# 输出 (失败) -{ - "status": "failed", - "reason": "未找到招聘区域", # 失败原因 - "tried_selectors": ["...", ...] # 尝试过的选择器 -} - -# 特殊情况: item_count=0 时标记为失败,不触发下一步 -``` - ---- - -### 3. next_page_graph - 分页分析 - -**目的**: 找到「下一页」按钮的 CSS 选择器,用于翻页爬取 - -**流程**: 获取HTML → LLM找分页区域 → LLM找下一页按钮 → 点击验证页面变化 - -**输入/输出**: -```python -# 输入 -url: str # 招聘列表页URL -headless: bool - -# 输出 (成功 - 有分页) -{ - "status": "success", - "selector": ".pagination .next", # 下一页按钮选择器 - "change_type": "url_change", # 点击后的变化类型 - "has_pagination": 1 # 有分页 -} - -# 输出 (成功 - 无分页) -{ - "status": "success", - "selector": null, - "change_type": null, - "has_pagination": 0 # 无分页,一页展示完 -} - -# change_type: url_change(URL变化) / content_change(内容变化) / new_tab - -# 输出 (失败) -{ - "status": "failed", - "reason": "点击后无变化", - "tried_selectors": ["...", ...] -} -``` - ---- - -### 4. detail_analysis_graph - 详情页分析 - -**目的**: 从列表进入详情页,提取岗位各字段的 CSS 选择器 - -**流程**: -``` -open_list_page → click_first_job → find_detail_area(仅in_page) → extract_field_selectors → validate_data -``` - -**字段提取原则**: -- `job_title` 和 `description` 是核心字段,必须提取 -- `description` 包含完整岗位信息(职责、要求、公司介绍等都放这里) -- 其他字段(salary/location/company/experience/education)只有在页面上有**独立、明确的元素**时才提取,否则为 null - -**输入/输出**: -```python -# 输入 -url: str # 招聘列表页URL -job_item_selector: str # 来自 page_analysis_graph -change_type: str # 来自 page_analysis_graph (redirect/new_tab/in_page) -headless: bool - -# 输出 (成功) -{ - "status": "success", - "detail_area_selector": ".job-detail", # 详情区域选择器(仅in_page有) - "fields": { - "job_title": {"selector": "h1.title", "sample": "Python工程师"}, - "description": {"selector": [".desc", ".req"], "sample": "负责...\n要求..."}, - "salary": {"selector": ".salary", "sample": "25-40K"}, # 或 null - "location": {"selector": ".loc", "sample": "北京"}, # 或 null - "company": {"selector": ".company", "sample": "XX公司"}, # 或 null - "experience": {"selector": ".exp", "sample": "3-5年"}, # 或 null - "education": {"selector": ".edu", "sample": "本科"} # 或 null - } -} -``` - -**支持字段**: job_title, description, salary, location, company, experience, education, detail_url - -```python -# 输出 (失败) -{ - "status": "failed", - "error": "验证失败,无法提取有效字段" # 失败原因 -} -``` - ---- - -### 5. crawler - 数据爬取 (待开发) - -**目的**: 使用分析得到的选择器配置,自动爬取所有岗位数据 - -**预期流程**: -1. 打开列表页 -2. 使用 job_item_selector 获取当前页所有岗位 -3. 逐个点击进入详情页,用 field_selectors 提取数据 -4. 使用 next_page_selector 翻页 -5. 重复直到无下一页 - -**预期输入/输出**: -```python -# 输入 -config: { - "url": "https://...", - "job_item_selector": "...", - "next_page_selector": "...", - "change_type": "...", - "field_selectors": {...} -} - -# 输出 -[ - {"job_title": "...", "salary": "...", "location": "...", ...}, - {"job_title": "...", "salary": "...", "location": "...", ...}, - ... -] -``` - ---- - -## 技术栈 - -- **LangGraph**: AI Agent 工作流编排 -- **Playwright**: 浏览器自动化 -- **LangChain + Pydantic**: LLM 调用与结构化输出 -- **doubao-1-5-pro-32k**: 主力 LLM 模型 - ---- - -## 开发状态 - -| 模块 | 状态 | 说明 | -|-----------------------|------|------------| -| search_company | ✅ 已完成 | 基于MCP的搜索方案 | -| page_analysis_graph | ✅ 已完成 | 岗位选择器提取 | -| next_page_graph | ✅ 已完成 | 分页选择器提取 | -| detail_analysis_graph | ✅ 已完成 | 详情字段选择器提取 | -| crawler | ✅ 已完成 | 数据爬取模块 | -| company_generator | ✅ 已完成 | AI生成公司名 | -| company_importer | ✅ 已完成 | Excel表格批量导入公司 | - - ---- - -### 7. company_importer - Excel 公司名单批量导入 - -**目的**: 从 Excel 表格批量导入公司名称及已知招聘地址,自动创建爬虫任务 - -**模块位置**: `src/company_importer/` - -**核心逻辑**: -1. 读取 Excel(支持超链接提取真实 URL) -2. URL 验证与清洗(过滤邮箱、微信公众号、无效地址) -3. 内存去重 + 数据库唯一约束兜底 -4. 逐行插入 `app_crawl_task` + `app_task_search`(带 `input_url`) - -**URL 过滤规则**: -- 邮箱地址(含 `@`)→ 跳过 -- 微信公众号(`mp.weixin.qq.com`)→ 跳过 -- 无协议头 → 自动补 `https://` -- 无域名或域名无点 → 无效 - -**输入/输出**: -```python -from src.company_importer import import_companies - -# 输入 -result = import_companies( - file_path="xxx.xlsx", # Excel 文件路径 - company_col=3, # 公司名所在列(从1开始,如 C列=3) - url_col=9, # 招聘地址所在列(如 I列=9) -) - -# 输出 -{ - "total_rows": 100, - "inserted_count": 60, - "skipped_empty_name": 5, - "skipped_empty_url": 10, - "skipped_email": 3, - "skipped_weixin": 2, - "skipped_invalid_url": 5, - "skipped_duplicate": 15, - "inserted_companies": ["公司A", "公司B", ...] -} -``` - -**与 Step1 的联动**: 导入时将已知 URL 写入 `app_task_search.input_url`,Step1 执行时检测到 `input_url` 非空,跳过搜索直接进入验证流程。 - -**文件结构**: -``` -src/company_importer/ -├── __init__.py # 导出 import_companies -├── importer.py # 导入核心逻辑 -├── url_validator.py # URL 提取、验证、清洗 -└── __main__.py # 测试入口 (python -m src.company_importer) -``` - ---- - -## 数据库表结构 - -### 主表:app_crawl_task(爬虫任务主表) - -```sql -CREATE TABLE `app_crawl_task` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `company_name` VARCHAR(255) NOT NULL COMMENT '公司名称', - - -- 配置阶段(前4步) - `config_step` INT NULL DEFAULT 1 COMMENT '【配置阶段】当前步骤: 1=搜索招聘页, 2=列表分析, 3=分页分析, 4=详情分析', - `config_status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '【配置阶段】状态: pending=待执行, running=执行中, success=成功, failed=失败', - - -- 爬取阶段(第5步) - `crawl_status` VARCHAR(32) DEFAULT NULL COMMENT '【爬取阶段】状态: pending=待执行, running=执行中, success=成功, failed=失败', - `total_crawl_times` INT NOT NULL DEFAULT 0 COMMENT '【爬取阶段】累计爬取次数', - `last_crawl_at` DATETIME DEFAULT NULL COMMENT '【爬取阶段】上次爬取时间', - - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE INDEX `uk_company_name` (`company_name`), - INDEX `idx_config_status` (`config_status`), - INDEX `idx_crawl_status` (`crawl_status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT ='爬虫任务主表'; - --- 业务规则: --- 1. 项目入口表,创建时检查 company_name 唯一索引,防止重复 --- 2. 状态初始为 pending(待执行),保存后禁止任何编辑 --- 3. 创建成功后,自动在 app_task_search 表创建一条关联任务 --- 4. 其他状态更新同步至本表 --- 5. job_periodic_crawl定时任务检查上次 crawl_status = success 且 last_crawl_at 大于15天, 更新crawl_status 为 pending,并创建app_task_crawl数据 - -``` - - - - -### Step 1:app_task_search(搜索招聘页面) - -```sql -CREATE TABLE `app_task_search` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `crawl_task_id` BIGINT NOT NULL COMMENT '关联主表ID', - `status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '任务状态: pending=待执行, running=执行中, success=成功, failed=失败', - `retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数', - `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数', - `input_company_name` VARCHAR(255) NOT NULL COMMENT '【输入】公司名称', - `input_url` VARCHAR(1024) DEFAULT NULL COMMENT '【输入】已知招聘地址(表格导入时填入,有值时Step1跳过搜索直接验证)', - `output_url` VARCHAR(512) DEFAULT NULL COMMENT '【输出】招聘页URL', - `error_message` TEXT DEFAULT NULL COMMENT '错误信息', - `started_at` DATETIME DEFAULT NULL COMMENT '任务开始执行时间', - `finished_at` DATETIME DEFAULT NULL COMMENT '任务完成时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_crawl_task_id` (`crawl_task_id`), - INDEX `idx_status` (`status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = 'Step1-搜索招聘页面任务表'; - --- 业务规则: --- 1. app_crawl_task新建数据成功时默认创建 input_company_name 来自主表 --- 2. 若由 company_importer 导入,input_url 存储已知招聘地址 --- 3. 定时任务扫描 status='pending' 的记录执行搜索,修改app_crawl_task 的 config_step=1, config_status='running' --- 4. input_url 非空时,跳过搜索引擎搜索,直接将 input_url 作为候选链接验证 --- 5. 执行成功后 status='success',output_url 存储搜索到的招聘页URL --- 6. 执行失败 retry_count+1,达到 max_retry 后 status='failed',修改app_crawl_task 的 config_status='failed' --- 7. 成功后自动创建 app_task_page_analysis 任务 -``` - - - -### Step 2:app_task_page_analysis(岗位列表分析) - -```sql -CREATE TABLE `app_task_page_analysis` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `crawl_task_id` BIGINT NOT NULL COMMENT '关联主表ID', - `status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '任务状态: pending=待执行, running=执行中, success=成功, failed=失败', - `retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数', - `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数', - `input_url` VARCHAR(1024) NOT NULL COMMENT '【输入】招聘页URL', - `output_selector` VARCHAR(512) DEFAULT NULL COMMENT '【输出】岗位项CSS选择器', - `output_change_type` VARCHAR(32) DEFAULT NULL COMMENT '【输出】点击变化类型: redirect=跳转, new_tab=新标签, in_page=页内变化', - `output_item_count` INT DEFAULT NULL COMMENT '【输出】匹配到的岗位数量', - `error_message` TEXT DEFAULT NULL COMMENT '错误信息', - `started_at` DATETIME DEFAULT NULL COMMENT '任务开始执行时间', - `finished_at` DATETIME DEFAULT NULL COMMENT '任务完成时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_crawl_task_id` (`crawl_task_id`), - INDEX `idx_status` (`status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = 'Step2-岗位列表分析任务表'; - --- 业务规则: --- 1. 由 Step1 成功后自动创建,input_url 来自 app_task_search.output_url --- 2. 定时任务扫描 status='pending' 的记录执行列表页分析,修改 app_crawl_task.config_step=2 --- 3. 执行成功后 status='success',输出岗位选择器、点击变化类型、匹配数量 app_crawl_task.config_status=success --- 4. 执行失败 retry_count+1,达到 max_retry 后 status='failed',修改 app_crawl_task.config_status='failed' --- 5. 成功后自动创建 app_task_next_page 任务(Step3 -``` - - - - -### Step 3:app_task_next_page(分页分析) - -```sql -CREATE TABLE `app_task_next_page` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `crawl_task_id` BIGINT NOT NULL COMMENT '关联主表ID', - `status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '任务状态: pending=待执行, running=执行中, success=成功, failed=失败', - `retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数', - `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数', - `input_url` VARCHAR(1024) NOT NULL COMMENT '【输入】招聘页URL', - `output_selector` VARCHAR(512) DEFAULT NULL COMMENT '【输出】下一页按钮选择器', - `output_change_type` VARCHAR(32) DEFAULT NULL COMMENT '【输出】翻页变化类型: url_change=URL变化, content_change=内容变化, new_tab=新页面', - `has_pagination` TINYINT(1) DEFAULT NULL COMMENT '是否有分页: 0=仅一页, 1=有分页', - `error_message` TEXT DEFAULT NULL COMMENT '错误信息', - `started_at` DATETIME DEFAULT NULL COMMENT '任务开始执行时间', - `finished_at` DATETIME DEFAULT NULL COMMENT '任务完成时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_crawl_task_id` (`crawl_task_id`), - INDEX `idx_status` (`status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = 'Step3-分页分析任务表'; - --- 业务规则: --- 1. 由 Step2 成功后自动创建,input_url 来自 app_task_page_analysis.input_url --- 2. 定时任务扫描 status='pending' 的记录执行分页分析,修改 app_crawl_task.config_step=3 --- 3. 执行成功后 status='success',输出下一页选择器、翻页变化类型、是否有分页 --- 4. 执行失败 retry_count+1,达到 max_retry 后 status='failed',修改 app_crawl_task.config_status='failed' --- 5. 成功后自动创建 app_task_detail_analysis 任务(Step4) -``` - - - - -### Step 4:app_task_detail_analysis(详情页分析) - -```sql -CREATE TABLE `app_task_detail_analysis` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `crawl_task_id` BIGINT NOT NULL COMMENT '关联主表ID', - `status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '任务状态: pending=待执行, running=执行中, success=成功, failed=失败', - `retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数', - `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数', - `input_url` VARCHAR(1024) NOT NULL COMMENT '【输入】招聘页URL', - `input_job_selector` VARCHAR(512) NOT NULL COMMENT '【输入】岗位项选择器(来自Step2)', - `input_change_type` VARCHAR(32) NOT NULL COMMENT '【输入】点击变化类型(来自Step2): redirect=跳转, new_tab=新标签, in_page=页内变化', - `output_detail_selector` VARCHAR(512) DEFAULT NULL COMMENT '【输出】详情区域选择器(仅in_page时有值)', - `output_fields` JSON DEFAULT NULL COMMENT '【输出】字段选择器配置JSON, 结构: {"字段名": {"selector": "CSS选择器" 或 ["选择器1","选择器2"], "sample": "示例值"} | null}, 字段: job_title=职位名称, description=岗位描述(数组), salary=薪资, location=地点, company=公司, experience=经验, education=学历', - `error_message` TEXT DEFAULT NULL COMMENT '错误信息', - `started_at` DATETIME DEFAULT NULL COMMENT '任务开始执行时间', - `finished_at` DATETIME DEFAULT NULL COMMENT '任务完成时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_crawl_task_id` (`crawl_task_id`), - INDEX `idx_status` (`status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = 'Step4-详情页分析任务表'; - --- 业务规则: --- 1. 由 Step3 成功后自动创建,input_url/input_job_selector/input_change_type 来自 Step2/Step3 --- 2. 定时任务扫描 status='pending' 的记录执行详情页分析,修改 app_crawl_task.config_step=4 --- 3. 执行成功后 status='success',输出详情区域选择器、字段选择器配置 --- 4. 执行失败 retry_count+1,达到 max_retry 后 status='failed',修改 app_crawl_task.config_status='failed' --- 5. 成功后修改 app_crawl_task.config_status='success',自动创建 app_task_crawl 任务(Step5) -``` - -### Step 5:app_task_crawl(数据爬取) - -```sql -CREATE TABLE `app_task_crawl` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `crawl_task_id` BIGINT NOT NULL COMMENT '关联主表ID', - `status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '任务状态: pending=待执行, running=执行中, success=成功, failed=失败', - `retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数', - `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数', - `input_config` JSON NOT NULL COMMENT '【输入】完整爬取配置JSON, 包含: url=招聘页URL, job_item_selector=岗位项选择器, job_change_type=点击变化类型, next_page_selector=下一页选择器, next_change_type=翻页变化类型, detail_area_selector=详情区域选择器, fields=字段选择器配置', - `crawled_count` INT DEFAULT NULL COMMENT '【输出】本次爬取数量', - `error_message` TEXT DEFAULT NULL COMMENT '错误信息', - `started_at` DATETIME DEFAULT NULL COMMENT '任务开始执行时间', - `finished_at` DATETIME DEFAULT NULL COMMENT '任务完成时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_crawl_task_id` (`crawl_task_id`), - INDEX `idx_status` (`status`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = 'Step5-数据爬取任务表'; - --- 业务规则: --- 1. 由 Step4 成功后自动创建 / 或者job_periodic_crawl定时任务创建,input_config 汇总 Step1-4 的所有输出配置 --- 2. 定时任务扫描 status='pending' 的记录执行数据爬取,修改 app_crawl_task.crawl_status='running' --- 3. 执行成功后 status='success',爬取的岗位数据存入 app_job_data 表 --- 4. 执行失败 retry_count+1,达到 max_retry 后 status='failed',修改 app_crawl_task.crawl_status='failed' --- 5. 成功后修改 app_crawl_task: crawl_status='success', total_crawl_times+1, last_crawl_at=当前时间 --- 6. 周期任务:当 last_crawl_at 超过15天时,自动创建新的爬取任务 -``` - - - - - -### 岗位数据表:app_job_data - -```sql -CREATE TABLE `app_job_data` -( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `task_crawl_id` BIGINT NOT NULL COMMENT '关联爬取任务ID', - `job_title` VARCHAR(255) DEFAULT NULL COMMENT '职位名称', - `salary` VARCHAR(128) DEFAULT NULL COMMENT '薪资', - `location` VARCHAR(128) DEFAULT NULL COMMENT '工作地点', - `company` VARCHAR(255) DEFAULT NULL COMMENT '公司名称', - `experience` VARCHAR(64) DEFAULT NULL COMMENT '经验要求', - `education` VARCHAR(64) DEFAULT NULL COMMENT '学历要求', - `description` TEXT DEFAULT NULL COMMENT '岗位详情(职责+要求+介绍)', - `detail_url` VARCHAR(1024) DEFAULT NULL COMMENT '详情页URL', - `content_hash` VARCHAR(64) NOT NULL COMMENT '内容哈希值: job_title+salary+location+company+experience+education+description+detail_url 的MD5,用于查重', - `sources` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '数据来源: 0=官网, 1=平台', - `is_independent_url` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否独立URL: 0=页内展示, 1=独立页面', - `is_valid` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有效: 0=无效, 1=有效', - `expire_at` DATETIME DEFAULT NULL COMMENT '有效期', - `check_status` VARCHAR(32) DEFAULT 'pending' COMMENT '验证状态: pending=待验证, checking=验证中, checked=已验证', - `last_check_at` DATETIME DEFAULT NULL COMMENT '上次验证时间', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - INDEX `idx_task_crawl_id` (`task_crawl_id`), - INDEX `idx_content_hash` (`content_hash`), - INDEX `idx_job_title` (`job_title`), - INDEX `idx_is_valid` (`is_valid`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT ='岗位数据表'; - --- 业务规则: --- 1. 由 Step5 爬取成功后批量创建,每条岗位数据关联到 task_crawl_id --- 2. 插入前查询:相同 content_hash 且 is_valid=1 的记录存在则跳过,否则插入 --- 3. is_independent_url 根据 Step2 的 change_type 决定:redirect/new_tab=1, in_page=0 --- 4. 定时任务检查有效性:expire_at 已过期 → is_valid=0 --- 5. 定时任务检查有效性:访问 detail_url 返回404 → is_valid=0 --- 6. 查询时默认过滤 is_valid=1 的有效数据 -``` - - - - ---- - -## 定时任务设计 - -### 任务总览 - -| 任务名 | 频率 | 并发 | 说明 | -|--------|------|------|------| -| job_step1_search | 1分钟 | 串行 | 搜索招聘页面 | -| job_step2_page | 1分钟 | 串行 | 分析岗位列表 | -| job_step3_next | 1分钟 | 串行 | 分析分页 | -| job_step4_detail | 1分钟 | 串行 | 分析详情页 | -| job_step5_crawl | 1分钟 | 串行 | 数据爬取 | -| job_periodic_crawl | 10分钟 | 串行 | 周期爬取 | -| job_check_validity | 1分钟 | 按域名并发 | 有效性检查 | -| job_generate_company | 1天 | 单实例 | AI生成公司名 | - ---- - -### job_step1_search(搜索招聘页面) - -``` -1. 查询 WHERE status='pending' LIMIT 1 -2. 若无记录 → 结束 -3. 更新 status='running', started_at=NOW() -4. 调用 search_company_graph 执行搜索 - - 若 input_url 非空:将 input_url 作为 candidate_url 传入,跳过搜索直接验证 - - 若 input_url 为空:正常执行搜索引擎搜索流程 -5. 成功: - - 更新 status='success', output_url=结果URL, finished_at=NOW() - - 更新 app_crawl_task: config_status='running', config_step=1 - - 创建 app_task_page_analysis 记录 (input_url=output_url) -6. 失败: - - retry_count += 1, error_message=错误信息, finished_at=NOW() - - 若 retry_count >= max_retry: - - status='failed' - - app_crawl_task.config_status='failed' - - 否则: status='pending' (下次继续重试) - -异常处理: status='running'卡住的任务需手动重置为'pending' -``` - ---- - -### job_step2_page(岗位列表分析) - -``` -1. 查询 WHERE status='pending' LIMIT 1 -2. 若无记录 → 结束 -3. 更新 status='running', started_at=NOW() -4. 调用 page_analysis_graph(input_url) -5. 成功: - - 更新 status='success', finished_at=NOW() - - 保存 output_selector, output_change_type, output_item_count - - 更新 app_crawl_task: config_step=2 - - 创建 app_task_next_page 记录 (input_url 来自当前任务) -6. 失败: - - retry_count += 1, error_message=错误信息, finished_at=NOW() - - 若 retry_count >= max_retry: - - status='failed' - - app_crawl_task.config_status='failed' - - 否则: status='pending' - -异常处理: status='running'卡住的任务需手动重置为'pending' -``` - ---- - -### job_step3_next(分页分析) - -``` -1. 查询 WHERE status='pending' LIMIT 1 -2. 若无记录 → 结束 -3. 更新 status='running', started_at=NOW() -4. 调用 next_page_graph(input_url) -5. 成功: - - 更新 status='success', finished_at=NOW() - - 保存 output_selector, output_change_type, has_pagination - - 更新 app_crawl_task: config_step=3 - - 通过 crawl_task_id 查询 Step2 输出 - - 创建 app_task_detail_analysis 记录: - - input_url 来自 Step2 - - input_job_selector 来自 Step2.output_selector - - input_change_type 来自 Step2.output_change_type -6. 失败: - - retry_count += 1, error_message=错误信息, finished_at=NOW() - - 若 retry_count >= max_retry: - - status='failed' - - app_crawl_task.config_status='failed' - - 否则: status='pending' - -异常处理: status='running'卡住的任务需手动重置为'pending' -``` - ---- - -### job_step4_detail(详情页分析) - -``` -1. 查询 WHERE status='pending' LIMIT 1 -2. 若无记录 → 结束 -3. 更新 status='running', started_at=NOW() -4. 调用 detail_analysis_graph(input_url, input_job_selector, input_change_type) -5. 成功: - - 更新 status='success', finished_at=NOW() - - 保存 output_detail_selector, output_fields - - 更新 app_crawl_task: config_step=4, config_status='success' - - 通过 crawl_task_id 查询 Step1-4 输出,汇总 input_config: - { - url, job_item_selector, job_change_type, - next_page_selector, next_change_type, has_pagination, - detail_area_selector, fields - } - - 创建 app_task_crawl 记录 - - 更新 app_crawl_task: crawl_status='pending' -6. 失败: - - retry_count += 1, error_message=错误信息, finished_at=NOW() - - 若 retry_count >= max_retry: - - status='failed' - - app_crawl_task.config_status='failed' - - 否则: status='pending' - -异常处理: status='running'卡住的任务需手动重置为'pending' -``` - ---- - -### job_step5_crawl(数据爬取) - -``` -1. 查询 WHERE status='pending' LIMIT 1 -2. 若无记录 → 结束 -3. 更新 status='running', started_at=NOW() -4. 更新 app_crawl_task: crawl_status='running' -5. 调用 crawler(input_config) -6. 成功: - - 更新 status='success', crawled_count=爬取数量, finished_at=NOW() - - 批量插入 app_job_data: - - 插入前查询: 相同 content_hash 且 is_valid=1 的记录存在则跳过 - - 不存在则插入, expire_at=NOW()+15天 - - 更新 app_crawl_task: - - crawl_status='success' - - total_crawl_times += 1 - - last_crawl_at = NOW() -7. 失败: - - retry_count += 1, error_message=错误信息, finished_at=NOW() - - 若 retry_count >= max_retry: - - status='failed' - - app_crawl_task.crawl_status='failed' - - 否则: status='pending' (主表 crawl_status 保持 running) - -异常处理: status='running'卡住的任务需手动重置为'pending' -``` - ---- - -### job_periodic_crawl(周期爬取) - -``` -1. 查询 app_crawl_task WHERE - crawl_status='success' - AND last_crawl_at < DATE_SUB(NOW(), INTERVAL 15 DAY) -2. 遍历每条记录: - - 更新 crawl_status='pending' - - 通过 crawl_task_id 查询 Step1-4 的输出,汇总 input_config - - 创建 app_task_crawl 记录 -``` - ---- - -### job_check_validity(有效性检查) - -``` -1. 两次查询合并(避免OR影响索引): - a. 正常待验证: WHERE is_valid=1 AND check_status='pending' AND expire_at <= NOW() LIMIT 20 - b. 超时恢复: WHERE is_valid=1 AND check_status='checking' AND last_check_at < NOW() - 2小时 LIMIT 20 - 代码层合并结果取前 20 条 -2. 若无记录 → 结束 -3. 批量更新 check_status='checking', last_check_at=NOW() -4. 分流处理: - A. is_independent_url=0 (页内展示): - - 直接过期: is_valid=0 - - 更新 check_status='pending' - - B. is_independent_url=1 (独立页面): - - 按 detail_url 域名分组 - - 不同域名并发,同域名串行验证 - - GET 访问 detail_url: - - 状态码 200 → expire_at = NOW() + 配置天数 - - 其他(超时/404/非200等) → is_valid=0 - - 每条完成后更新 check_status='pending' -``` - ---- - -### job_generate_company(AI生成公司名) - -**目的**: 使用 AI 自动生成可能有官网招聘信息的公司名称,自动创建爬取任务 - -**模块位置**: `src/company_generator/` - -**配置参数**: -- `max_company_count`: 公司总数上限,默认 5000 -- `generate_company_count`: 每次生成数量,默认 10 -- `job_generate_company_interval`: 执行间隔,默认 86400秒(1天) - -**执行流程**: -``` -1. 检查公司总数 - SELECT COUNT(*) FROM app_crawl_task - 若 >= max_company_count(5000) → 结束 - -2. 查询所有已有公司名 → 存入内存 set - -3. 随机抽样 500 个传给 LLM - - 使用 V3_2 模型 - - with_structured_output(CompanyList) 结构化输出 - - 生成 10 个公司名 - -4. 遍历生成结果 - - 长度校验(2-15字符)→ 不符则跳过 - - 内存 set 判重 → 已存在则跳过 - - INSERT app_crawl_task (company_name) - - INSERT app_task_search (crawl_task_id, input_company_name) - - 数据库唯一约束兆底 - -5. 返回结果 - { inserted_count, skipped_count, inserted_companies } -``` - -**公司名命名规范**: -- 使用公司常用简称(如"华为"而非"华为技术有限公司") -- 不加"集团"、"有限公司"、"股份有限公司"等后缀 -- 不加"(中国)"、"(北京)"等地域标注 -- 2-15个字符 - -**去重策略**: -1. Prompt 传递已有公司名(随机抽样 500 个) -2. 代码层内存 set 判重(全量) -3. 数据库唯一约束兆底 - ---- - -### 任务执行流程图 - -``` - ┌─────────────────┐ - │ 用户创建任务 │ - │ (输入公司名称) │ - └────────┬────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ job_step1_search (每1分钟) │ -│ 查询: app_task_search WHERE status='pending' │ -│ 执行: 搜索招聘页面URL │ -│ 成功: status='success', 创建Step2任务 │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ job_step2_page (每1分钟) │ -│ 查询: app_task_page_analysis WHERE status='pending' │ -│ 执行: 分析岗位列表选择器 │ -│ 成功: status='success', 创建Step3任务 │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ job_step3_next (每1分钟) │ -│ 查询: app_task_next_page WHERE status='pending' │ -│ 执行: 分析分页选择器 │ -│ 成功: status='success', 创建Step4任务 │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ job_step4_detail (每1分钟) │ -│ 查询: app_task_detail_analysis WHERE status='pending' │ -│ 执行: 分析详情页字段选择器 │ -│ 成功: config_status='success', 创建Step5任务 │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ job_step5_crawl (每1分钟) │ -│ 查询: app_task_crawl WHERE status='pending' │ -│ 执行: 爬取岗位数据,存入app_job_data │ -│ 成功: crawl_status='success', total_crawl_times+1 │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ 爬取完成 │ - └────────┬────────┘ - │ - ┌──────────────────┴──────────────────┐ - ▼ ▼ -┌──────────────────────┐ ┌──────────────────────┐ -│ job_periodic_crawl │ │ job_check_validity │ -│ (每10分钟) │ │ (每1分钟) │ -│ 检查last_crawl_at │ │ 检查expire_at已过期 │ -│ 超过N天则重新爬取 │ │ 按域名并发验证URL │ -└──────────────────────┘ └──────────────────────┘ -``` diff --git a/docs/crawler_design.md b/docs/crawler_design.md deleted file mode 100644 index 6af8ab49..00000000 --- a/docs/crawler_design.md +++ /dev/null @@ -1,356 +0,0 @@ -# Crawler 模块设计方案 - -## 状态 -✅ 已完成 - ---- - -## 一、输入参数 - -| 参数 | 来源 | 说明 | -|-----|------|-----| -| url | 初始输入 | 列表页 URL | -| job_item_selector | Step 2 | 岗位项选择器 | -| item_change_type | Step 2 | redirect / new_tab / in_page | -| next_page_selector | Step 3 | 下一页按钮选择器(None = 无分页) | -| page_change_type | Step 3 | url_change / content_change / new_tab | -| field_selectors | Step 4 | 字段选择器字典,selector 为数组格式 | -| detail_area_selector | Step 4 | 详情区域选择器(仅 in_page 有值) | - ---- - -## 二、限制条件 - -- 最大页数:5 页 -- 单个岗位失败:跳过继续 -- 超时时间:可配置(如 10 秒) - ---- - -## 三、数据提取范围 - -| item_change_type | 提取范围 | detail_url 记录 | -|-----------------|---------|---------------| -| redirect | 跳转后的详情页整页 | 详情页 URL | -| new_tab | 新标签详情页整页 | 新标签页 URL | -| in_page | detail_area_selector 内部 | 列表页 URL | - ---- - -## 四、入口逻辑 - -``` -results = [] - -IF item_change_type == "redirect": - results = 流程A_redirect() -ELSE IF item_change_type == "new_tab": - results = 流程B_new_tab() -ELSE IF item_change_type == "in_page": - results = 流程C_in_page() - -RETURN results -``` - ---- - -## 五、流程 A:redirect - -**特点**:点击岗位后整页跳转,返回后状态丢失,需重新打开并翻页恢复 - -``` -初始化: - page_index = 1 - item_index = 1 - results = [] - - 打开 url - 等待加载完成 - items_per_page = 用 job_item_selector 计算当前页岗位数 - -主循环: - WHILE True: - - # ===== 1. 检查是否结束 ===== - IF items_per_page == 0: - BREAK - IF page_index > 5: - BREAK - - # ===== 2. 处理当前岗位 ===== - TRY: - elements = 获取所有岗位元素(job_item_selector) - 点击 elements[item_index - 1] - 等待页面跳转完成(URL 变化 或 networkidle) - - data = 在整页范围用 field_selectors 提取数据 - data["detail_url"] = 当前页面 URL - results.append(data) - CATCH: - 记录失败,跳过 - - # ===== 3. 计算下一个位置 ===== - item_index += 1 - - IF item_index > items_per_page: - page_index += 1 - item_index = 1 - - IF page_index > 5: - BREAK - IF next_page_selector == None: - BREAK - - # ===== 4. 重新打开并恢复到目标页 ===== - goto(url) - 等待加载完成 - - FOR i = 1 TO (page_index - 1): - success, page = click_next_page(page) - IF not success: - BREAK 主循环 - - items_per_page = 重新计算当前页岗位数 - -RETURN results -``` - ---- - -## 六、流程 B:new_tab - -**特点**:点击后新标签打开详情,关闭新标签后原页面状态保持 - -``` -初始化: - page_index = 1 - results = [] - - 打开 url - 等待加载完成 - -主循环: - WHILE page_index <= 5: - - # ===== 1. 获取当前页岗位 ===== - items_per_page = 用 job_item_selector 计算当前页岗位数 - IF items_per_page == 0: - BREAK - - # ===== 2. 遍历当前页所有岗位 ===== - FOR item_index = 1 TO items_per_page: - TRY: - elements = 获取所有岗位元素(job_item_selector) - - 开始监听 context 的 "page" 事件 - 点击 elements[item_index - 1] - - 等待新标签页打开(超时则跳过) - new_page = 获取新打开的标签页 - 等待 new_page 加载完成 - - data = 在 new_page 整页范围用 field_selectors 提取数据 - data["detail_url"] = new_page.url - results.append(data) - - 关闭 new_page - CATCH: - 尝试关闭可能存在的新标签 - 记录失败,跳过 - - # ===== 3. 翻页 ===== - IF next_page_selector == None: - BREAK - - success, page = click_next_page(page) - IF not success: - BREAK - - page_index += 1 - -RETURN results -``` - ---- - -## 七、流程 C:in_page - -**特点**:点击后弹窗/详情区展示,通过刷新页面恢复状态,兼容性更强 - -``` -初始化: - page_index = 1 - item_index = 1 - results = [] - - 打开 url - 等待加载完成 - items_per_page = 用 job_item_selector 计算当前页岗位数 - -主循环: - WHILE True: - - # ===== 1. 检查是否结束 ===== - IF items_per_page == 0: - BREAK - IF page_index > 5: - BREAK - - # ===== 2. 处理当前岗位 ===== - TRY: - elements = 获取所有岗位元素(job_item_selector) - 点击 elements[item_index - 1] - 等待 detail_area_selector 出现(visible) - - detail_element = 获取 detail_area_selector 元素 - data = 在 detail_element 内部用 field_selectors 提取数据 - data["detail_url"] = 当前页面 URL - results.append(data) - CATCH: - 记录失败,跳过 - - # ===== 3. 计算下一个位置 ===== - item_index += 1 - - IF item_index > items_per_page: - page_index += 1 - item_index = 1 - - IF page_index > 5: - BREAK - IF next_page_selector == None: - BREAK - - # ===== 4. 刷新页面恢复状态 ===== - goto(url) - 等待加载完成 - - FOR i = 1 TO (page_index - 1): - success, page = click_next_page(page) - IF not success: - BREAK 主循环 - - items_per_page = 重新计算当前页岗位数 - -RETURN results -``` - ---- - -## 八、统一翻页函数 - -**返回值**:(success: bool, page: Page) —— new_tab 模式时 page 引用会变 - -``` -函数 click_next_page(page, next_page_selector, page_change_type): - - # ===== 1. 检查按钮是否可点 ===== - element = 查找 next_page_selector - - IF element 不存在: - RETURN (false, page) - - IF element 有 disabled 属性: - RETURN (false, page) - - IF element 的 class 包含 "disabled": - RETURN (false, page) - - IF element 不可见 (is_visible == false): - RETURN (false, page) - - # ===== 2. 根据 page_change_type 处理 ===== - - IF page_change_type == "url_change": - before_url = page.url - 点击 element - 等待 URL 变化(page.url != before_url) - 等待加载完成(networkidle) - RETURN (true, page) - - ELSE IF page_change_type == "content_change": - before_text = 获取第一个岗位的文本内容 - 点击 element - 等待第一个岗位文本变化(!= before_text) - 短暂等待确保渲染完成 - RETURN (true, page) - - ELSE IF page_change_type == "new_tab": - 开始监听 context 的 "page" 事件 - 点击 element - 等待新标签页打开 - new_page = 获取新标签页 - 等待 new_page 加载完成 - 关闭原 page - RETURN (true, new_page) # 返回新的 page 引用 -``` - ---- - -## 九、数据提取函数 - -``` -函数 extract_data(scope, field_selectors): - # scope: 整个 page 或 detail_element - - data = {} - - FOR field_name, selector_info IN field_selectors: - TRY: - selectors = selector_info.selector # 数组格式 - IF selectors 为空数组: - data[field_name] = None - CONTINUE - - # 遍历所有选择器,提取文本并拼接 - texts = [] - FOR selector IN selectors: - element = scope.query_selector(selector) - IF element: - text = element.inner_text().strip() - IF text: - texts.append(text) - - data[field_name] = "\n".join(texts) IF texts ELSE None - CATCH: - data[field_name] = None - - RETURN data -``` - ---- - -## 十、字段选择器格式 - -`field_selectors` 统一使用数组格式: - -``` -{ - "job_title": {"selector": ["h1.title"], "sample": "Python工程师"}, - "description": {"selector": [".desc", ".req"], "sample": "负责...\n要求..."}, - "salary": {"selector": [".salary"], "sample": "25-40K"}, - ... -} -``` - -- 单选择器:`["h1.title"]` -- 多选择器:`[".desc", ".req"]`,提取结果用 `\n` 拼接 - ---- - -## 十一、输出格式 - -``` -[ - { - "job_title": "...", - "salary": "...", - "location": "...", - "description": "...", - "requirements": "...", - "detail_url": "...", - ... - }, - ... -] -``` diff --git a/docs/playwright_guide.md b/docs/playwright_guide.md deleted file mode 100644 index a9eea33a..00000000 --- a/docs/playwright_guide.md +++ /dev/null @@ -1,307 +0,0 @@ -# Playwright Python 使用指南 - -本文档覆盖项目中会用到的 Playwright 核心功能。 - -## 安装 - -```bash -pip install playwright -playwright install chromium # 安装浏览器 -``` - -## 基本结构 - -```python -from playwright.async_api import async_playwright - -async def main(): - async with async_playwright() as p: - # 启动浏览器 - browser = await p.chromium.launch(headless=False) # False 可看到浏览器界面 - - # 创建页面 - page = await browser.new_page() - - # 操作页面... - - # 关闭 - await browser.close() -``` - -## 核心操作 - -### 1. 导航 - -```python -# 访问 URL -await page.goto("https://example.com") - -# 等待加载完成(可选策略) -await page.goto("https://example.com", wait_until="networkidle") # 网络空闲 -await page.goto("https://example.com", wait_until="domcontentloaded") # DOM 加载完成 - -# 获取当前 URL -current_url = page.url -``` - -### 2. 获取内容 - -```python -# 获取整个页面 HTML -html = await page.content() - -# 获取 body 内部 HTML -body_html = await page.inner_html("body") - -# 获取元素内部 HTML -area_html = await page.inner_html(".job-list") - -# 获取元素外部 HTML(包含自身标签) -outer = await page.evaluate("document.querySelector('.job-item').outerHTML") - -# 获取文本内容 -text = await page.inner_text(".title") - -# 获取页面标题 -title = await page.title() -``` - -### 3. 选择器与元素定位 - -```python -# 单个元素 -element = await page.query_selector(".job-item") # 返回 ElementHandle 或 None - -# 多个元素 -elements = await page.query_selector_all(".job-item") # 返回列表 -count = len(elements) - -# 检查元素是否存在 -if await page.query_selector(".job-item"): - print("存在") - -# Locator API(推荐,更稳定) -locator = page.locator(".job-item") -count = await locator.count() -first = locator.first -nth = locator.nth(2) # 第3个元素 -``` - -### 4. 点击与交互 - -```python -# 点击元素 -await page.click(".job-item") - -# 点击第一个匹配的元素 -await page.locator(".job-item").first.click() - -# 点击第 N 个元素 -await page.locator(".job-item").nth(0).click() - -# 带等待的点击 -await page.click(".job-item", timeout=5000) # 最多等 5 秒 - -# 输入文本 -await page.fill("input[name='search']", "关键词") - -# 按键 -await page.keyboard.press("Enter") -``` - -### 5. 等待 - -```python -import asyncio - -# 简单等待(秒) -await asyncio.sleep(2) - -# 等待选择器出现 -await page.wait_for_selector(".job-list", timeout=10000) - -# 等待选择器消失 -await page.wait_for_selector(".loading", state="hidden") - -# 等待导航完成 -async with page.expect_navigation(): - await page.click(".next-page") - -# 等待网络空闲 -await page.wait_for_load_state("networkidle") -``` - -### 6. 执行 JavaScript - -```python -# 简单表达式 -result = await page.evaluate("document.title") - -# 带参数 -selector = ".job-item" -count = await page.evaluate(f"document.querySelectorAll('{selector}').length") - -# 复杂逻辑 -result = await page.evaluate(""" - () => { - const items = document.querySelectorAll('.job-item'); - return Array.from(items).map(el => el.outerHTML); - } -""") - -# 在元素上执行 -element = await page.query_selector(".job-item") -html = await element.evaluate("el => el.outerHTML") -``` - -### 7. 截图 - -```python -# 整页截图 -await page.screenshot(path="screenshot.png") - -# 元素截图 -element = await page.query_selector(".job-list") -await element.screenshot(path="element.png") - -# 全页面(包括滚动区域) -await page.screenshot(path="full.png", full_page=True) -``` - -## 常用模式 - -### 模式1:获取多个元素的 HTML - -```python -# 方法1:evaluate -htmls = await page.evaluate(""" - selector => { - const items = document.querySelectorAll(selector); - return Array.from(items).slice(0, 3).map(el => el.outerHTML); - } -""", ".job-item") - -# 方法2:遍历 ElementHandle -elements = await page.query_selector_all(".job-item") -htmls = [] -for el in elements[:3]: - html = await el.evaluate("el => el.outerHTML") - htmls.append(html) -``` - -### 模式2:检测页面变化 - -```python -before_url = page.url - -await page.click(".job-item") -await asyncio.sleep(2) - -after_url = page.url - -if after_url != before_url: - print("发生了跳转") -``` - -### 模式3:带重试的选择器验证 - -```python -async def validate_selector(page, selector: str) -> int: - """验证选择器,返回匹配数量""" - try: - elements = await page.query_selector_all(selector) - return len(elements) - except Exception: - return 0 -``` - -### 模式4:安全获取元素内容 - -```python -async def safe_inner_html(page, selector: str) -> str | None: - """安全获取元素 innerHTML""" - element = await page.query_selector(selector) - if element: - return await element.inner_html() - return None -``` - -## 浏览器配置 - -```python -# 有头模式(可见) -browser = await p.chromium.launch(headless=False) - -# 无头模式(后台运行) -browser = await p.chromium.launch(headless=True) - -# 慢动作(调试用) -browser = await p.chromium.launch(headless=False, slow_mo=500) # 每步慢 500ms - -# 设置窗口大小 -context = await browser.new_context(viewport={"width": 1280, "height": 720}) -page = await context.new_page() - -# 设置 User-Agent -context = await browser.new_context( - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" -) -``` - -## 错误处理 - -```python -from playwright.async_api import TimeoutError as PlaywrightTimeout - -try: - await page.click(".not-exist", timeout=3000) -except PlaywrightTimeout: - print("元素未找到") -except Exception as e: - print(f"其他错误: {e}") -``` - -## 项目中的典型用法 - -```python -from playwright.async_api import async_playwright - -async def analyze_page(url: str): - async with async_playwright() as p: - browser = await p.chromium.launch(headless=False) - page = await browser.new_page() - - try: - # 1. 访问页面 - await page.goto(url, wait_until="networkidle") - - # 2. 获取 HTML - html = await page.inner_html("body") - - # 3. 验证选择器 - selector = ".job-item" - elements = await page.query_selector_all(selector) - print(f"匹配到 {len(elements)} 个元素") - - # 4. 点击验证 - if elements: - before_url = page.url - await elements[0].click() - await asyncio.sleep(2) - after_url = page.url - print(f"跳转: {before_url} -> {after_url}") - - finally: - await browser.close() -``` - -## 与 MCP 对比 - -| 操作 | MCP 方式 | 直接 Playwright | -|------|----------|-----------------| -| 导航 | `await call_tool(tools, "browser_navigate", url=url)` | `await page.goto(url)` | -| 获取 HTML | `await call_tool(tools, "browser_evaluate", function="document.body.innerHTML")` | `await page.inner_html("body")` | -| 点击 | `await call_tool(tools, "browser_evaluate", function="document.querySelector('.x').click()")` | `await page.click(".x")` | -| 获取 URL | `await call_tool(tools, "browser_evaluate", function="window.location.href")` | `page.url` | - -直接用 Playwright 代码更简洁、更 Pythonic。 diff --git a/outputs/samsung_jobs.csv b/outputs/samsung_jobs.csv deleted file mode 100644 index c17904ef..00000000 --- a/outputs/samsung_jobs.csv +++ /dev/null @@ -1,729 +0,0 @@ -job_title,description,detail_url -[社会招聘] 三星SDSC IT售前支持,"岗位职责: -任职资格: -1. 根据客户情况提供合理的解决方案, -2. 项目的整体规划,日程管理,风险管理,品质管理,验收报告等, -3. 编写项目相关文档,确保项目资料的收集、整理、建档和保存, -4. 公司运营,采购,执行等相关部门的业务协调等。 -1. 学历专业:正规院校本科及以上学历,双证齐全,专业不限; -2. 工作经验: -- 3~5年销售经验或者软件开发目管理经验 -- 有大型项目参与经验者优先 -3. 技术能力: -- CRM,SAP,RMS,WMS等相关系统项目管理经验 -- 熟悉Java、C#等主流编程语言‌者优先 -- 了解HTML、CSS、JavaScript,熟悉Vue、React等前端框架 -- 了解TCP/IP、HTTP等网络协议,熟悉Socket编程 -- 了解Python编程基础,熟悉Django、Flask等框架,了解MongoDB、Linux系统操作 -- 了解服务器,网络设备等相关内容者优先 -4. 综合素质: -- 了解韩国文化,理解韩国文化 -- 为人正直,有责任心,积极主动,逻辑清晰,具备优秀沟通、宣讲及文案能力 -- 具备良好的沟通能力和方案撰写能力,能够与客户和团队成员有效沟通‌ -5.语言:无限制,如懂韩语优先 -6.其他:取得PMP资格证优先,接受出差",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] 三星SDSC IT销售代表(韩语),"岗位职责: -任职资格: -1. 挖掘可购买公司产品及服务的企业客户与商机; -2. 挖掘可与公司开展业务合作的销售及交付合作伙伴; -3. ‌分析客户IT需求,基于公司与合作伙伴的产品及能力制定解决方案与业务提案; -4. 通过分析公司产品及能力,开发服务产品并提出销售方案等; -5.公司内部销售系统处理及管理: 订单录入,成本确认,合同处理,损益确认,计划录入等。 -1. 正规院校本科及以上学历,双证齐全; -2. 工作经验: -- 拥有IT销售经验或在IT服务行业工作过经验; -- 具有云计算相关的销售或技术工作经验,特别是AWS相关经验者优先考虑; -3. 技术能力: -- 对最新的IT技术趋势的广泛理解 -- 熟练使用MS Office软件(Excel,Word,PPT) -4. 语言要求:韩语精通(必须) -5.其他:接受短期出差",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -无线通信CUDA开发工程师,"岗位职责: -任职资格: -1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求; -2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案; -3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告; -1. 硕士及以上学历,计算机科学/通信等相关专业; -2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧; -3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力; -4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先; -5. 有基于GPU的算力利用率提升项目经验者优先; -6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -人形机器人全身运动控制算法工程师(强化学习方向),"岗位职责: -任职资格: -1. 负责基于强化学习的人形机器人全身运动控制算法研发,涵盖行走、跑跳、平衡控制、复杂地形适配、动态抗扰动、全身协同运动规划等核心场景的算法设计、迭代与优化; -2. 设计适配人形机器人高自由度特性的强化学习算法框架与深度网络架构,解决全身运动的稳定性、泛化性、实时性与安全性核心问题,持续提升机器人在复杂动态环境下的运动性能上限; -3. 负责算法的仿真验证、sim-to-real迁移优化与人形机器人真机部署调试,结合视觉、力觉、IMU等多模态传感器反馈完成闭环控制优化,完成算法性能指标的评估、迭代与落地; -4. 与感知、硬件、任务规划等跨团队紧密协作,推动算法与人形机器人本体、多模态感知系统的深度适配与集成,支撑整机运动能力的落地与产品化迭代; -5. 持续跟踪人形机器人全身运动控制、深度强化学习领域的国际前沿研究与技术动态,引入顶会顶刊的创新方法,推动核心技术的突破与技术壁垒构建; -1. 硕士及以上学历,计算机科学与技术、人工智能、自动化、控制工程、机器人工程等相关专业,具备扎实的自动控制理论、刚体动力学与机器学习理论基础; -2. 精通深度强化学习算法原理与工程落地,熟悉PPO、SAC、TD3、DQN等主流DRL算法,具备基于强化学习的机器人运动控制算法完整研发与落地经验; -3. 精通人形机器人正逆运动学、刚体动力学建模,熟悉高自由度机器人全身运动控制相关理论,具备人形机器人全身运动控制项目研发经验者优先; -4. 熟练使用PyTorch/TensorFlow等主流深度学习框架,精通Python/C++编程语言,熟悉ROS/ROS2等机器人软件框架,掌握Isaac Lab、Isaac Gym、MuJoCo、Gazebo等主流机器人仿真环境; -5. 熟悉模仿学习、逆强化学习、sim-to-real迁移、模型预测控制(MPC)、最优控制、数值优化等相关技术,具备多模态感知与运动控制融合开发经验者优先; -6. 具备良好的科研能力、工程落地能力与跨团队协作能力,在机器人、强化学习相关领域顶会顶刊(RSS、CoRL、ICRA、IROS、NeurIPS、ICML、ICLR等)发表过论文者优先。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -AI软件工程师,"岗位职责: -任职资格: -工作职责: -- 数据中心AI系统架构与系统软件设计。 -任职资格: -1. 熟练使用C/C++/Python,熟悉常用数据结构与算法; -2. 熟悉大语言模型推理,有相关开发、分析和优化经验; -3. 具备AI infrastructure分析和优化经验; -4. 对系统软件研发有强烈兴趣。 -5. 以下为加分项: -- 具备OpenCL,CUDA等开发和优化经验; -- 具备模拟器开发、系统仿真分析和优化经验。 - -其他要求: -本科/硕士学历 -CET4及以上 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 - 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 -同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -Edge AI软件工程师,"岗位职责: -任职资格: -工作职责: -- 端侧系统架构,算法及系统软件设计。 -任职资格: -1. 熟练使用C/C++/Python,熟悉常用数据结构与算法; -2. 熟悉VLM、VLA,掌握模型训练、推理、优化相关知识,并有实际开发部署经验; -3. 具备模拟器开发、系统仿真分析和优化经验; -4. 有良好的沟通和学习能力。 -5. 以下为加分项: -- 具备OpenCL,CUDA开发和优化经验; -- 具备GPU,NPU设计经验; -- 对机器人方向算法优化和系统软件研发有强烈兴趣。 -其他要求: -本科及以上学历 -CET4及以上 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 - 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 -同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -技术战略分析师,"岗位职责: -任职资格: -岗位职责: -- 技术分析:从技术视角深入研究前沿科技,重点聚焦人工智能、存储技术、高性能计算等领域,开展系统性调研并撰写高质量的技术分析报告,助力技术决策与战略布局。 -- 行业洞察:紧跟国内外技术发展趋势,研判技术演进方向与竞争态势,为集团战略决策提供坚实依据。 -- 专家访谈:积极与行业专家保持深入沟通,建立长期合作关系,获取第一手权威见解与前沿信息。 -- 论文管理:负责公司学术论文的评审与发表流程的统筹与优化,推动论文质量稳步提升,增强公司学术影响力。 -- 业务支援:参与公司中长期技术规划,协助组织并推进技术创新相关活动,提升研发效率与创新能力。 -岗位要求: -1. 人工智能、存储技术、计算机科学等相关专业背景,有在人工智能、存储系统、高性能计算等领域的工作经验者优先。 -2. 对前沿技术保持高度关注,具备独立开展技术调研的能力,能够撰写逻辑清晰、内容详实的技术分析报告。 -3. 能够熟练阅读英文技术文档与学术论文,准确把握核心思想与创新点,以及优秀的英语口语交流能力。 -4. 对新技术有热情,独立问题解决能力,海外同事合作能力 -5. 以下为加分项: -- 有韩语能力者优先,能进行基本的书面或口语交流。 -- 有技术研发经验者优先。 -- 有深度技术调研报告撰写经验者优先。 -其他要求: -- 硕士/博士学历 -- 人工智能、存储技术、计算机科学等相关专业 -- 英语六级(CET-6)或同等级英语水平及以上 - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 - 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 -同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -存储软件开发系统工程师,"岗位职责: -任职资格: -岗位职责: -- 负责三星存储产品系统软件开发及性能优化; -- 负责三星存储系统前沿技术研究。 -岗位要求: -1. 具备良好的编程基础,熟练掌握C/C++/Python至少一种编程语言; -2. 熟练掌握常用数据结构及算法; -3. 熟悉Linux系统的编程基础,熟悉Linux I/O相关协议栈。 -4. 以下为加分项: -- 具有数据库,存储引擎以及文件系统相关开发和优化经验者; -- 具备AI框架下存储相关研究经验。 -其他要求: -- 硕士学历 -- 计算机、软件、电子,通信,自动化等相关专业 -- CET4及以上 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 -请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 -《个人信息收集使用申明》 -在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -分布式系统软件工程师,"岗位职责: -任职资格: -岗位职责: -- 负责三星大规模集群系统任务调度及资源管理方案的优化与开发; -- 提升大规模集群的调度系统的整体效率和资源利用率。 -岗位要求: -1. 具有扎实的shell/C++/Python编程基础,熟悉数据结构和算法; -2. 掌握计算机操作系统,分布式系统等专业知识; -3. 熟悉多线程开发,有良好的代码阅读和开发能力。 -4. 以下为加分项: -- 具有Slurm, LSF, Kubernetes的代码经验或者部署经验。 -其他要求: -- 硕士学历 -- 计算机、人工智能等相关专业 -- CET4及以上 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 -请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 -《个人信息收集使用申明》 -在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 -同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -存储软件工程师(AI算法方向),"岗位职责: -任职资格: -岗位职责: -- 负责三星存储产品相关AI算法的设计、训练、优化和研发。 -岗位要求: -1. 精通Python,熟悉C/C++语言,熟练掌握常用的数据结构和算法; -2. 掌握机器学习/深度学习算法模型构建、训练、优化相关专业知识并有相关经验; -3. 能熟练使用PyTorch等框架进行模型的开发和优化; -4. 具有较强的动手能力和研究能力,能够快速复现论文方法,并针对特定的场景进行优化; -5. 以下为加分项: -- 具备AI框架下存储相关研究经验; -- 有顶会/顶刊论文相关成果者优先; -- 有transformer,大模型研发经验者优先。 -其他要求: -- 硕士学历 -- 计算机、软件、大数据、人工智能等相关专业 -- CET4及以上 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 -《个人信息收集使用申明》 -在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -存储系统专家,"岗位职责: -任职资格: -岗位职责: -- 作为团队技术带头人,负责存储系统软件架构设计和实现以及存储系统的应用优化; -- 负责三星存储相关新技术解决方案的研究以及学术成果转化。 -岗位要求: -1. 熟悉NVMe SSD相关知识且具备存储设备性能优化经验; -2. 熟悉Linux内核I/O协议栈,具备SPDK等用户态驱动开发和优化经验; -3. 较强的软件架构以及代码重构能力; -4. 了解数据库以及常用存储引擎相关知识以及体系架构 (Rocks DB, MySQL.),并有相应系统设计开发以及优化经验。 -加分项: -- 熟悉新的Memory技术如CXL,有相关研究经验者优先; -- 具备AI框架下存储相关研究经验者优先; -- 在该领域顶级会议或期刊发表过论文者优先。 -其他要求: -- 博士学历 -- 计算机、软件、电子、通信、自动化等相关专业 -- CET6及以上,英语可作为工作语言 - - - -感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! -三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 -若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 -《个人信息收集使用申明》 -在本次招聘活动中,我们承诺以下事项: -1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; -2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; -3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 -同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 -1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; -2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; -3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; -4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; -5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 -如您需取消投递,请自行撤销简历; -若需帮助请联系HR:srcxianhr@samsung.com。",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -大模型算法工程师,"岗位职责: -任职资格: -1. 参与基础大模型和端侧大模型的研发工作,包括架构设计、预训练、后训练等,端到端构建通用大模型; -2. 负责大模型高阶能力(Coding、Agent等)的专项提升,打造模型长版特性; -3. 持续关注大模型最新研究,探索下一代大模型的架构和训练方法,做出有影响力的成果; -1. 硕士及以上学历,计算机科学、人工智能等相关专业; -2. 熟悉深度学习框架(例如pytorch等),具备大模型和端侧大模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验; -3. 对大模型架构、训练、数据、系统优化中的某一方面有深入理解,以下符合1条以上: -- 能够提出创新性的大模型架构和端侧大模型架构,探索技术的下一跳; -- 熟练掌握强化学习(RL)和模型微调(SFT)等后训练技术,并可以提出创新的后训练方法; -- 对coding、math、agent等大模型高阶能力有深入思考; -- 熟练掌握大模型预训练的Know How,可以快速诊断并修复问题,提升模型能力; -- 对预训练数据、后训练数据的生产、合成方法有深入理解; -- 熟练模型训练/推理的系统优化方法,提升模型的实际训练、推理性能; -4. 有大模型/端侧大模型架构、训练、数据、系统优化等相关实战经验者优先,在NeurIPS/ICML/ICLR/ACL/EMNLP/CVPR/ICCV/TPAMI等AI顶会发表过相关论文者优先; - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -具身智能算法工程师,"岗位职责: -任职资格: -1. 负责研发具身智能操作算法,研发基于VLA、强化学习等AI技术在机器人操作场景中的应用; -2. 设计网络架构,分析实验数据,评估算法表现,提升机器人在精准操作、长程任务、动态响应等关键领域的表现性能; -3. 负责将VLA模型在跨机器人本体和环境进行真机部署,评估模型在真机的性能指标; -4. 持续跟进最新的具身操作方向研究工作动态,引入最前沿的方法持续提升模型表现性能; -1. 硕士及以上学历,计算机科学、人工智能、自动化、机器人技术等相关专业; -2. 熟悉深度学习框架(例如pytorch、TensorFlow),具备大规模VLA模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验; -3. 具备机器学习算法在机器人领域的开发经验,包括并不限于reinforcement learning、imitation learning、transfer learning、action-conditioned world models、representation learning、dexterous manipulation、sim-to-real transfer、vision language models、motion planning等; -4. 具备机器人真机经验(例如Aloha、Franka、Fetch等),具备良好的编程能力,熟悉Python或C++编程语言,熟悉主流机器人软件框架例如ROS,熟悉主流的仿真软件例如IsaacLab、IsaacGym、Mujoco、Bullet、Gazebo等; -5. 具备良好的科研能力,在顶会或期刊发表过文章的优先,例如机器学习方向(NeurIPS、ICML、ICLR),机器人方向(RSS、CoRL、ICRA、IROS)、计算机视觉方向(CVPR、ICCV、ECCV)等; - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -高性能并行计算开发工程师,"岗位职责: -任职资格: -1. 根据无线通信模块需要,基于CPU/GPU设计适合并行计算的数据结构,利用多线程实现高效的并行计算处理编程; -2. 具有较高的并行计算/高性能计算方面SW设计及开发能力,能通过性能分析找到CPU或GPU处理的瓶颈,并提出改善方案; -1. 硕士及以上学历,计算机科学/人工智能等相关专业; -2. 精通高性能计算/并行计算软件设计及优化思想,具有多线程并行计算开发经验; -3. 熟练使用CPU/GPU性能分析工具(Nsight等)并通过性能监测找到存储,数据传输,并行计算中的瓶颈点,并结合数据访存和并行计算能力设计最优SW方案; -4. 有GPU高性能计算/CUDA开发经验者优先; -5. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -无线通信CUDA开发工程师,"岗位职责: -任职资格: -1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求; -2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案; -3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告; -1. 硕士及以上学历,计算机科学/通信等相关专业; -2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧; -3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力; -4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先; -5. 有基于GPU的算力利用率提升项目经验者优先; -6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -SCS-技术员,"岗位职责: -任职资格: -1.负责现场设备日常维护工作; -2.协助工程师进行调试工作,进行设备日常作业; -3.根据作业标准要求进行设备的操作。 -1.学历:25~26届毕业大专生; -2.专业:机械类、电气类、自动化类、计算机类、电子类等理工科相关专业; -3.熟练使用Office办公软件; -4.能适应倒班工作(三班倒)。 -(早班:6:00-14:00;中班:14:00-22:00;晚班:22:00-06:00,上五休二,每班工作8小时)",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -知识图谱、智能体、大模型算法工程师,"岗位职责: -任职资格: -从事知识图谱,大语言模型、多智能体优化的前沿算法研究: -- 研究方向包括:知识图谱,智能体设计, 大模型RAG, 多智能体优化 -- 算法领域包括但不限于: Knowledge Graph, Graph Retrieval, AI Agent, RAG, LLM/VLM/LMM, CoT, etc. -1. 硕士及以上学历,计算机、电子、自动化、数学等相关专业; -2. 熟练掌握智能体设计,大模型微调等相关基础; -3. 对知识图谱,大模型/RAG/Agent/Retrieval 有相关开发经验; -4. 熟悉Python,PyTorch深度学习框架; -5. 对前沿技术有热情,有较强的独立工作能力(算法理解与实现,问题分析与解决); -[加分项] 在顶会NIPS,ICML等发表过高水平文章,或在权威竞赛中以主要参与者身份取得过优秀名次; - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -通信AI算法工程师,"岗位职责: -任职资格: -1. 网络优化和诊断的AI算法研究; -2. AI算法实现,优化和评估; -3. 网络性能问题研究; -1. 博士及以上学历,通信工程、电子工程、计算机等相关专业; -2. 对AI算法有深入研究; -3. 具有AI算法编码和应用经验; -4. 擅长应用问题的算法建模; -5. 有无线通信的AI算法研究经验者优先; -6. 对AI算法有广泛实用经验者优先。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -数字孪生算法工程师,"岗位职责: -任职资格: -1. 5G-A/5G+AI MAC/RLC/PDCP系统算法研究、设计工作; -2. Ray tracing/3GPP信道建模算法研究、设计工作; -3. 5G-A/5G+AI 系统级仿真分析工作; -4. 指导和协助开发 S/W实现与优化工作。 -1. 硕士及以上学历,博士优先,通信工程、计算机等相关专业; -2. 对5GA/5G协议和系统有深入了解和相应研究经历; -3. 有MAC/RLC/PDCP/PHY算法设计和研究经验; -4. 有信道建模设计和研究经验; -5. 有系统级/链路级+AI仿真经验; -6. 熟练掌握C/C++,Python编程; -7. 有良好的英语交流能力,积极进取,有良好的团队合作意识和技术创新意识。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -5G/5GA算法工程师,"岗位职责: -任职资格: -1. LTE/5G MAC、SON系统算法研究、设计工作; -2. LTE/5G系统级仿真、分析工作; -3. 5G 链路级仿真工作; -4. 指导和协助开发 S/W实现与测试工作; -5. 系统级advance算法相关研究工作。 -1. 硕士及以上学历,博士优先,通信工程、电子相关专业; -2. 对LTE/5G/5GA协议和系统有深入了解和相应研究经历; -3. 有MAC、SON算法设计和研究经验; -4. 有系统级或者链路级仿真经验,熟练掌握C/C++/matlab/python编程; -5. 有5G/5GA标准化经验者优先考虑; -6. 有丰富MIMO, SON相关研究经验者优先考虑; -7. 有良好的英文交流能力; -8. 积极进取,有良好的团队合作意识,技术创新意识。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -通信SoC软件开发工程师,"岗位职责: -任职资格: -1. 负责OMMU/ORU LPHY模块设计和开发; -2. 负责ORAN-based LPHY模块性能优化; -1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业; -2. 精通C/C++,熟悉架构设计及优化者优先; -3. 熟悉基于通信基带SoC芯片的软件开发,优化和测试过程,有Marvell通信芯片软件开发经验者优先; -4. 熟悉3GPP协议,ORAN协议,具有ORAN Opt7-2x/Opt7-3开发经验者优先; -5. 熟悉RTOS,Linux驱动开发经验者优先; -6. 具备良好的数字信号处理背景优先; -7. 良好的英文书面和口语表达。 -加分项: -- 熟悉常用AI算法模型,有广泛实用经验/模型部署移植等相关经验; -- 有DFE(digital front-end)开发经验; - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -4G/5G 物理层开发工程师,"岗位职责: -任职资格: -按照经验能力参与以下工作的一项或多项,包括: -1. 负责4G/5G物理层接收机&发送机的算法模块SW实现; -2. 负责基站侧或终端侧 Modem 架构设计及SW开发; -3. 负责AI+5G开发及优化; -4. 负责数字孪生开发和验证; -1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业; -2. 精通C/C++语言,熟悉架构设计及优化者优先; -3. 熟悉至少一种CPU结构及指令,有Intel AVX512/AMX优化经验者优先; -4. 熟悉至少一种无线通信协议,有NR、LTE、NB-IOT、2G GSM物理层研发经验优先; -5. 熟悉3GPP信道建模优化,有digital twin经验者优先; -6. 熟悉常用AI训练推理架构,熟悉常用AI算法模型者优先; -7. 熟悉linux操作系统,有内核优化经验者优先; -8. 具备良好的数字信号处理背景优先; -9. 良好的英文书面和口语表达。 - -公司介绍: -三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。 -新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入! - -※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密 -※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -战略企划,"岗位职责: -任职资格: -主要职责: -1. 销售数据分析及企划 -2. 行业市场分析 -3. 韩语翻译及部门交代的其他相关事项 -岗位要求 -- 学历:统招本科及以上 -- 经历:销售企划相关经历者优先考虑 -- 熟练掌握 M/S Office (Excel, Word, PPT) -- 韩国语能力优秀者优先考虑 (TOPIK 6级 或 近母语水平)",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory MKT|存储芯片市场,"岗位职责: -任职资格: -1. Establish and maintain the regular and close communication relationship with customers and colleague -2. Memory business Marketer -3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market -4. Working closely with our customers -5. Writing market analysis report -6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater. -1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ -2. Experience in Memroy market and product (DRAM/SSD/GFX) -3. Experience in PC martket -4. Understaing of Memory business value chain including PC Market -5. Sophisticated in PPT and Excel, with good presentation skills",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] DRAM BE|存储芯片市场营销,"岗位职责: -任职资格: -1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China; -2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers. -3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win. -4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements. -'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory DRAM FAE|存储芯片技术支持,"岗位职责: -任职资格: -- Support DRAM failure analysis with customers and ODM venders -- Prompt and professional technical on-site support for acquiring - customer's demand of DRAM. - - Make an initial judgement for the issues from customers - - Eliminate noise and ambiguity of on-site test results - - Regular technical discussion with HQ AE team and customers and -cross functional departments independently. -- Familiar with DRAM function and system operation -(Command sequence/AC parameter/…). -- Experience in DDR module on NB, desktop -(server system design/validation is a plus) - - To have analysis ability for log -(system booting log and DRAM training log, etc.) - - To have experience of tools updating BIOS (Dediprog, etc.) -- To have basic software knowledge related to PC, Notebook -(Windows, Linux, Python, etc.) -- To have experience of operating oscilloscope and logic analyzer. - - To have good comprehension and good communication skills with -ODM & customers, HQ and related departments",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] DRAM TEC|测试技术工程师,"岗位职责: -任职资格: -1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5) -2. Attend tech. meeting with DRAM BE and CS contact window; -3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support; -4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage. -- Familiar with DRAM function and system operation -(Command sequence/AC parameter/…). -- Experience in DDR module on NB, desktop -(server system design/validation is a plus) - - To have analysis ability for log -(system booting log and DRAM training log, etc.) - - To have experience of tools updating BIOS (Dediprog, etc.) -- To have basic software knowledge related to PC, Notebook -(Windows, Linux, Python, etc.) -- To have experience of operating oscilloscope and logic analyzer. - - To have good comprehension and good communication skills with -ODM & customers, HQ and related departments",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Foundry FAE|晶圆代工技术支持,"岗位职责: -任职资格: -1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C -2. Design in (T/O) the Foundry customer product in E/C -3. Build the relationship with Tier 1/2/3 customer in E/C -Education: Bachelor degree and above -Major: Electrical/Electronics engineering -Experience Range:>3 Years -Prof. Experience: Experience in SOC design -Language skills:Fluent in English read/write/speaking -Min. skills & knowledge: -1. Knowledge in Semiconductor Process manufacturing, device component -2. Experience on designing Digital IP, Analog IP, SOC Design -3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Auto TEC|测试技术工程师,"岗位职责: -任职资格: -1. Responsible as TEC engineer in Mobile/Auto TEC department; -2.Responsible for Auto DRAM product testing when needed. -3. Suppport on-site and DRAM Validation activities to nVIDIA; -4. Maintain and Improve customer's satisfaction with issue on-site support; -1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent; -2. Understanding Electronic Basic & Advanced Theory and semiconductor industry -3. At least 2 years’ experience, with Auto memory product is highly preferred; -4. Familiar with Automotive applications is highly preferred. -5. Good command of oral and written English; Good Korean is highly preferred;",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory FAE|存储芯片技术支持,"岗位职责: -任职资格: -1. Promote and Design in SSD products to PC customers in East China. -2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM -3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win. -4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements -1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ. -2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market. -3. Experience and knowledge in SSD products is preferred.",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory MKT|存储芯片市场,"岗位职责: -任职资格: -1. Establish and maintain the regular and close communication relationship with customers and colleague -2. Memory business Marketer -3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market -4. Working closely with our customers -5. Writing market analysis report -6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater. -1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ -2. Experience in Memroy market and product (DRAM/SSD/GFX) -3. Experience in PC martket -4. Understaing of Memory business value chain including PC Market -5. Sophisticated in PPT and Excel, with good presentation skills",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] DRAM BE|存储芯片市场营销,"岗位职责: -任职资格: -1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China; -2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers. -3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win. -4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements. -'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory DRAM FAE|存储芯片技术支持,"岗位职责: -任职资格: -- Support DRAM failure analysis with customers and ODM venders -- Prompt and professional technical on-site support for acquiring - customer's demand of DRAM. - - Make an initial judgement for the issues from customers - - Eliminate noise and ambiguity of on-site test results - - Regular technical discussion with HQ AE team and customers and -cross functional departments independently. -- Familiar with DRAM function and system operation -(Command sequence/AC parameter/…). -- Experience in DDR module on NB, desktop -(server system design/validation is a plus) - - To have analysis ability for log -(system booting log and DRAM training log, etc.) - - To have experience of tools updating BIOS (Dediprog, etc.) -- To have basic software knowledge related to PC, Notebook -(Windows, Linux, Python, etc.) -- To have experience of operating oscilloscope and logic analyzer. - - To have good comprehension and good communication skills with -ODM & customers, HQ and related departments",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] DRAM TEC|测试技术工程师,"岗位职责: -任职资格: -1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5) -2. Attend tech. meeting with DRAM BE and CS contact window; -3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support; -4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage. -- Familiar with DRAM function and system operation -(Command sequence/AC parameter/…). -- Experience in DDR module on NB, desktop -(server system design/validation is a plus) - - To have analysis ability for log -(system booting log and DRAM training log, etc.) - - To have experience of tools updating BIOS (Dediprog, etc.) -- To have basic software knowledge related to PC, Notebook -(Windows, Linux, Python, etc.) -- To have experience of operating oscilloscope and logic analyzer. - - To have good comprehension and good communication skills with -ODM & customers, HQ and related departments",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Foundry FAE|晶圆代工技术支持,"岗位职责: -任职资格: -1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C -2. Design in (T/O) the Foundry customer product in E/C -3. Build the relationship with Tier 1/2/3 customer in E/C -Education: Bachelor degree and above -Major: Electrical/Electronics engineering -Experience Range:>3 Years -Prof. Experience: Experience in SOC design -Language skills:Fluent in English read/write/speaking -Min. skills & knowledge: -1. Knowledge in Semiconductor Process manufacturing, device component -2. Experience on designing Digital IP, Analog IP, SOC Design -3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Auto TEC|测试技术工程师,"岗位职责: -任职资格: -1. Responsible as TEC engineer in Mobile/Auto TEC department; -2.Responsible for Auto DRAM product testing when needed. -3. Suppport on-site and DRAM Validation activities to nVIDIA; -4. Maintain and Improve customer's satisfaction with issue on-site support; -1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent; -2. Understanding Electronic Basic & Advanced Theory and semiconductor industry -3. At least 2 years’ experience, with Auto memory product is highly preferred; -4. Familiar with Automotive applications is highly preferred. -5. Good command of oral and written English; Good Korean is highly preferred;",https://dearsamsung.zhiye.com/#/samsung/pc/szzw -[社会招聘] Memory FAE|存储芯片技术支持,"岗位职责: -任职资格: -1. Promote and Design in SSD products to PC customers in East China. -2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM -3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win. -4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements -1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ. -2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market. -3. Experience and knowledge in SSD products is preferred.",https://dearsamsung.zhiye.com/#/samsung/pc/szzw diff --git a/outputs/samsung_jobs.html b/outputs/samsung_jobs.html deleted file mode 100644 index e500f072..00000000 --- a/outputs/samsung_jobs.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - -Samsung Jobs - - - -

Samsung Jobs (37)

- - - -
#??????
1[社会招聘] 三星SDSC IT售前支持岗位职责:
任职资格:
1. 根据客户情况提供合理的解决方案,
2. 项目的整体规划,日程管理,风险管理,品质管理,验收报告等,
3. 编写项目相关文档,确保项目资料的收集、整理、建档和保存,
4. 公司运营,采购,执行等相关部门的业务协调等。
1. 学历专业:正规院校本科及以上学历,双证齐全,专业不限;
2. 工作经验:
- 3~5年销售经验或者软件开发目管理经验
- 有大型项目参与经验者优先
3. 技术能力:
- CRM,SAP,RMS,WMS等相关系统项目管理经验
- 熟悉Java、C#等主流编程语言‌者优先
- 了解HTML、CSS、JavaScript,熟悉Vue、React等前端框架
- 了解TCP/IP、HTTP等网络协议,熟悉Socket编程
- 了解Python编程基础,熟悉Django、Flask等框架,了解MongoDB、Linux系统操作
- 了解服务器,网络设备等相关内容者优先
4. 综合素质:
- 了解韩国文化,理解韩国文化
- 为人正直,有责任心,积极主动,逻辑清晰,具备优秀沟通、宣讲及文案能力
- 具备良好的沟通能力和方案撰写能力,能够与客户和团队成员有效沟通‌
5.语言:无限制,如懂韩语优先
6.其他:取得PMP资格证优先,接受出差
link
2[社会招聘] 三星SDSC IT销售代表(韩语)岗位职责:
任职资格:
1. 挖掘可购买公司产品及服务的企业客户与商机;
2. 挖掘可与公司开展业务合作的销售及交付合作伙伴;
3. ‌分析客户IT需求,基于公司与合作伙伴的产品及能力制定解决方案与业务提案;
4. 通过分析公司产品及能力,开发服务产品并提出销售方案等;
5.公司内部销售系统处理及管理: 订单录入,成本确认,合同处理,损益确认,计划录入等。
1. 正规院校本科及以上学历,双证齐全;
2. 工作经验:
- 拥有IT销售经验或在IT服务行业工作过经验;
- 具有云计算相关的销售或技术工作经验,特别是AWS相关经验者优先考虑;
3. 技术能力:
- 对最新的IT技术趋势的广泛理解
- 熟练使用MS Office软件(Excel,Word,PPT)
4. 语言要求:韩语精通(必须)
5.其他:接受短期出差
link
3无线通信CUDA开发工程师岗位职责:
任职资格:
1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求;
2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案;
3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告;
1. 硕士及以上学历,计算机科学/通信等相关专业;
2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧;
3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力;
4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先;
5. 有基于GPU的算力利用率提升项目经验者优先;
6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
4人形机器人全身运动控制算法工程师(强化学习方向)岗位职责:
任职资格:
1. 负责基于强化学习的人形机器人全身运动控制算法研发,涵盖行走、跑跳、平衡控制、复杂地形适配、动态抗扰动、全身协同运动规划等核心场景的算法设计、迭代与优化;
2. 设计适配人形机器人高自由度特性的强化学习算法框架与深度网络架构,解决全身运动的稳定性、泛化性、实时性与安全性核心问题,持续提升机器人在复杂动态环境下的运动性能上限;
3. 负责算法的仿真验证、sim-to-real迁移优化与人形机器人真机部署调试,结合视觉、力觉、IMU等多模态传感器反馈完成闭环控制优化,完成算法性能指标的评估、迭代与落地;
4. 与感知、硬件、任务规划等跨团队紧密协作,推动算法与人形机器人本体、多模态感知系统的深度适配与集成,支撑整机运动能力的落地与产品化迭代;
5. 持续跟踪人形机器人全身运动控制、深度强化学习领域的国际前沿研究与技术动态,引入顶会顶刊的创新方法,推动核心技术的突破与技术壁垒构建;
1. 硕士及以上学历,计算机科学与技术、人工智能、自动化、控制工程、机器人工程等相关专业,具备扎实的自动控制理论、刚体动力学与机器学习理论基础;
2. 精通深度强化学习算法原理与工程落地,熟悉PPO、SAC、TD3、DQN等主流DRL算法,具备基于强化学习的机器人运动控制算法完整研发与落地经验;
3. 精通人形机器人正逆运动学、刚体动力学建模,熟悉高自由度机器人全身运动控制相关理论,具备人形机器人全身运动控制项目研发经验者优先;
4. 熟练使用PyTorch/TensorFlow等主流深度学习框架,精通Python/C++编程语言,熟悉ROS/ROS2等机器人软件框架,掌握Isaac Lab、Isaac Gym、MuJoCo、Gazebo等主流机器人仿真环境;
5. 熟悉模仿学习、逆强化学习、sim-to-real迁移、模型预测控制(MPC)、最优控制、数值优化等相关技术,具备多模态感知与运动控制融合开发经验者优先;
6. 具备良好的科研能力、工程落地能力与跨团队协作能力,在机器人、强化学习相关领域顶会顶刊(RSS、CoRL、ICRA、IROS、NeurIPS、ICML、ICLR等)发表过论文者优先。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
5AI软件工程师岗位职责:
任职资格:
工作职责:
- 数据中心AI系统架构与系统软件设计。
任职资格:
1. 熟练使用C/C++/Python,熟悉常用数据结构与算法;
2. 熟悉大语言模型推理,有相关开发、分析和优化经验;
3. 具备AI infrastructure分析和优化经验;
4. 对系统软件研发有强烈兴趣。
5. 以下为加分项:
- 具备OpenCL,CUDA等开发和优化经验;
- 具备模拟器开发、系统仿真分析和优化经验。

其他要求:
本科/硕士学历
CET4及以上



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。
同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
6Edge AI软件工程师岗位职责:
任职资格:
工作职责:
- 端侧系统架构,算法及系统软件设计。
任职资格:
1. 熟练使用C/C++/Python,熟悉常用数据结构与算法;
2. 熟悉VLM、VLA,掌握模型训练、推理、优化相关知识,并有实际开发部署经验;
3. 具备模拟器开发、系统仿真分析和优化经验;
4. 有良好的沟通和学习能力。
5. 以下为加分项:
- 具备OpenCL,CUDA开发和优化经验;
- 具备GPU,NPU设计经验;
- 对机器人方向算法优化和系统软件研发有强烈兴趣。
其他要求:
本科及以上学历
CET4及以上



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。
同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
7技术战略分析师岗位职责:
任职资格:
岗位职责:
- 技术分析:从技术视角深入研究前沿科技,重点聚焦人工智能、存储技术、高性能计算等领域,开展系统性调研并撰写高质量的技术分析报告,助力技术决策与战略布局。
- 行业洞察:紧跟国内外技术发展趋势,研判技术演进方向与竞争态势,为集团战略决策提供坚实依据。
- 专家访谈:积极与行业专家保持深入沟通,建立长期合作关系,获取第一手权威见解与前沿信息。
- 论文管理:负责公司学术论文的评审与发表流程的统筹与优化,推动论文质量稳步提升,增强公司学术影响力。
- 业务支援:参与公司中长期技术规划,协助组织并推进技术创新相关活动,提升研发效率与创新能力。
岗位要求:
1. 人工智能、存储技术、计算机科学等相关专业背景,有在人工智能、存储系统、高性能计算等领域的工作经验者优先。
2. 对前沿技术保持高度关注,具备独立开展技术调研的能力,能够撰写逻辑清晰、内容详实的技术分析报告。
3. 能够熟练阅读英文技术文档与学术论文,准确把握核心思想与创新点,以及优秀的英语口语交流能力。
4. 对新技术有热情,独立问题解决能力,海外同事合作能力
5. 以下为加分项:
- 有韩语能力者优先,能进行基本的书面或口语交流。
- 有技术研发经验者优先。
- 有深度技术调研报告撰写经验者优先。
其他要求:
- 硕士/博士学历
- 人工智能、存储技术、计算机科学等相关专业
- 英语六级(CET-6)或同等级英语水平及以上


感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。
同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
8存储软件开发系统工程师岗位职责:
任职资格:
岗位职责:
- 负责三星存储产品系统软件开发及性能优化;
- 负责三星存储系统前沿技术研究。
岗位要求:
1. 具备良好的编程基础,熟练掌握C/C++/Python至少一种编程语言;
2. 熟练掌握常用数据结构及算法;
3. 熟悉Linux系统的编程基础,熟悉Linux I/O相关协议栈。
4. 以下为加分项:
- 具有数据库,存储引擎以及文件系统相关开发和优化经验者;
- 具备AI框架下存储相关研究经验。
其他要求:
- 硕士学历
- 计算机、软件、电子,通信,自动化等相关专业
- CET4及以上



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。
请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》
在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
9分布式系统软件工程师岗位职责:
任职资格:
岗位职责:
- 负责三星大规模集群系统任务调度及资源管理方案的优化与开发;
- 提升大规模集群的调度系统的整体效率和资源利用率。
岗位要求:
1. 具有扎实的shell/C++/Python编程基础,熟悉数据结构和算法;
2. 掌握计算机操作系统,分布式系统等专业知识;
3. 熟悉多线程开发,有良好的代码阅读和开发能力。
4. 以下为加分项:
- 具有Slurm, LSF, Kubernetes的代码经验或者部署经验。
其他要求:
- 硕士学历
- 计算机、人工智能等相关专业
- CET4及以上



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。
请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》
在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。
同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
10存储软件工程师(AI算法方向)岗位职责:
任职资格:
岗位职责:
- 负责三星存储产品相关AI算法的设计、训练、优化和研发。
岗位要求:
1. 精通Python,熟悉C/C++语言,熟练掌握常用的数据结构和算法;
2. 掌握机器学习/深度学习算法模型构建、训练、优化相关专业知识并有相关经验;
3. 能熟练使用PyTorch等框架进行模型的开发和优化;
4. 具有较强的动手能力和研究能力,能够快速复现论文方法,并针对特定的场景进行优化;
5. 以下为加分项:
- 具备AI框架下存储相关研究经验;
- 有顶会/顶刊论文相关成果者优先;
- 有transformer,大模型研发经验者优先。
其他要求:
- 硕士学历
- 计算机、软件、大数据、人工智能等相关专业
- CET4及以上



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》
在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
11存储系统专家岗位职责:
任职资格:
岗位职责:
- 作为团队技术带头人,负责存储系统软件架构设计和实现以及存储系统的应用优化;
- 负责三星存储相关新技术解决方案的研究以及学术成果转化。
岗位要求:
1. 熟悉NVMe SSD相关知识且具备存储设备性能优化经验;
2. 熟悉Linux内核I/O协议栈,具备SPDK等用户态驱动开发和优化经验;
3. 较强的软件架构以及代码重构能力;
4. 了解数据库以及常用存储引擎相关知识以及体系架构 (Rocks DB, MySQL.),并有相应系统设计开发以及优化经验。
加分项:
- 熟悉新的Memory技术如CXL,有相关研究经验者优先;
- 具备AI框架下存储相关研究经验者优先;
- 在该领域顶级会议或期刊发表过论文者优先。
其他要求:
- 博士学历
- 计算机、软件、电子、通信、自动化等相关专业
- CET6及以上,英语可作为工作语言



感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!
三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。
若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。
《个人信息收集使用申明》
在本次招聘活动中,我们承诺以下事项:
1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;
2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果;
3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。
同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。
1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用;
2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;
3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;
4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;
5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。
如您需取消投递,请自行撤销简历;
若需帮助请联系HR:srcxianhr@samsung.com。
link
12大模型算法工程师岗位职责:
任职资格:
1. 参与基础大模型和端侧大模型的研发工作,包括架构设计、预训练、后训练等,端到端构建通用大模型;
2. 负责大模型高阶能力(Coding、Agent等)的专项提升,打造模型长版特性;
3. 持续关注大模型最新研究,探索下一代大模型的架构和训练方法,做出有影响力的成果;
1. 硕士及以上学历,计算机科学、人工智能等相关专业;
2. 熟悉深度学习框架(例如pytorch等),具备大模型和端侧大模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验;
3. 对大模型架构、训练、数据、系统优化中的某一方面有深入理解,以下符合1条以上:
- 能够提出创新性的大模型架构和端侧大模型架构,探索技术的下一跳;
- 熟练掌握强化学习(RL)和模型微调(SFT)等后训练技术,并可以提出创新的后训练方法;
- 对coding、math、agent等大模型高阶能力有深入思考;
- 熟练掌握大模型预训练的Know How,可以快速诊断并修复问题,提升模型能力;
- 对预训练数据、后训练数据的生产、合成方法有深入理解;
- 熟练模型训练/推理的系统优化方法,提升模型的实际训练、推理性能;
4. 有大模型/端侧大模型架构、训练、数据、系统优化等相关实战经验者优先,在NeurIPS/ICML/ICLR/ACL/EMNLP/CVPR/ICCV/TPAMI等AI顶会发表过相关论文者优先;

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
13具身智能算法工程师岗位职责:
任职资格:
1. 负责研发具身智能操作算法,研发基于VLA、强化学习等AI技术在机器人操作场景中的应用;
2. 设计网络架构,分析实验数据,评估算法表现,提升机器人在精准操作、长程任务、动态响应等关键领域的表现性能;
3. 负责将VLA模型在跨机器人本体和环境进行真机部署,评估模型在真机的性能指标;
4. 持续跟进最新的具身操作方向研究工作动态,引入最前沿的方法持续提升模型表现性能;
1. 硕士及以上学历,计算机科学、人工智能、自动化、机器人技术等相关专业;
2. 熟悉深度学习框架(例如pytorch、TensorFlow),具备大规模VLA模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验;
3. 具备机器学习算法在机器人领域的开发经验,包括并不限于reinforcement learning、imitation learning、transfer learning、action-conditioned world models、representation learning、dexterous manipulation、sim-to-real transfer、vision language models、motion planning等;
4. 具备机器人真机经验(例如Aloha、Franka、Fetch等),具备良好的编程能力,熟悉Python或C++编程语言,熟悉主流机器人软件框架例如ROS,熟悉主流的仿真软件例如IsaacLab、IsaacGym、Mujoco、Bullet、Gazebo等;
5. 具备良好的科研能力,在顶会或期刊发表过文章的优先,例如机器学习方向(NeurIPS、ICML、ICLR),机器人方向(RSS、CoRL、ICRA、IROS)、计算机视觉方向(CVPR、ICCV、ECCV)等;

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
14高性能并行计算开发工程师岗位职责:
任职资格:
1. 根据无线通信模块需要,基于CPU/GPU设计适合并行计算的数据结构,利用多线程实现高效的并行计算处理编程;
2. 具有较高的并行计算/高性能计算方面SW设计及开发能力,能通过性能分析找到CPU或GPU处理的瓶颈,并提出改善方案;
1. 硕士及以上学历,计算机科学/人工智能等相关专业;
2. 精通高性能计算/并行计算软件设计及优化思想,具有多线程并行计算开发经验;
3. 熟练使用CPU/GPU性能分析工具(Nsight等)并通过性能监测找到存储,数据传输,并行计算中的瓶颈点,并结合数据访存和并行计算能力设计最优SW方案;
4. 有GPU高性能计算/CUDA开发经验者优先;
5. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
15无线通信CUDA开发工程师岗位职责:
任职资格:
1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求;
2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案;
3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告;
1. 硕士及以上学历,计算机科学/通信等相关专业;
2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧;
3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力;
4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先;
5. 有基于GPU的算力利用率提升项目经验者优先;
6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
16SCS-技术员岗位职责:
任职资格:
1.负责现场设备日常维护工作;
2.协助工程师进行调试工作,进行设备日常作业;
3.根据作业标准要求进行设备的操作。
1.学历:25~26届毕业大专生;
2.专业:机械类、电气类、自动化类、计算机类、电子类等理工科相关专业;
3.熟练使用Office办公软件;
4.能适应倒班工作(三班倒)。
(早班:6:00-14:00;中班:14:00-22:00;晚班:22:00-06:00,上五休二,每班工作8小时)
link
17知识图谱、智能体、大模型算法工程师岗位职责:
任职资格:
从事知识图谱,大语言模型、多智能体优化的前沿算法研究:
- 研究方向包括:知识图谱,智能体设计, 大模型RAG, 多智能体优化
- 算法领域包括但不限于: Knowledge Graph, Graph Retrieval, AI Agent, RAG, LLM/VLM/LMM, CoT, etc.
1. 硕士及以上学历,计算机、电子、自动化、数学等相关专业;
2. 熟练掌握智能体设计,大模型微调等相关基础;
3. 对知识图谱,大模型/RAG/Agent/Retrieval 有相关开发经验;
4. 熟悉Python,PyTorch深度学习框架;
5. 对前沿技术有热情,有较强的独立工作能力(算法理解与实现,问题分析与解决);
[加分项] 在顶会NIPS,ICML等发表过高水平文章,或在权威竞赛中以主要参与者身份取得过优秀名次;

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
18通信AI算法工程师岗位职责:
任职资格:
1. 网络优化和诊断的AI算法研究;
2. AI算法实现,优化和评估;
3. 网络性能问题研究;
1. 博士及以上学历,通信工程、电子工程、计算机等相关专业;
2. 对AI算法有深入研究;
3. 具有AI算法编码和应用经验;
4. 擅长应用问题的算法建模;
5. 有无线通信的AI算法研究经验者优先;
6. 对AI算法有广泛实用经验者优先。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
19数字孪生算法工程师岗位职责:
任职资格:
1. 5G-A/5G+AI MAC/RLC/PDCP系统算法研究、设计工作;
2. Ray tracing/3GPP信道建模算法研究、设计工作;
3. 5G-A/5G+AI 系统级仿真分析工作;
4. 指导和协助开发 S/W实现与优化工作。
1. 硕士及以上学历,博士优先,通信工程、计算机等相关专业;
2. 对5GA/5G协议和系统有深入了解和相应研究经历;
3. 有MAC/RLC/PDCP/PHY算法设计和研究经验;
4. 有信道建模设计和研究经验;
5. 有系统级/链路级+AI仿真经验;
6. 熟练掌握C/C++,Python编程;
7. 有良好的英语交流能力,积极进取,有良好的团队合作意识和技术创新意识。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
205G/5GA算法工程师岗位职责:
任职资格:
1. LTE/5G MAC、SON系统算法研究、设计工作;
2. LTE/5G系统级仿真、分析工作;
3. 5G 链路级仿真工作;
4. 指导和协助开发 S/W实现与测试工作;
5. 系统级advance算法相关研究工作。
1. 硕士及以上学历,博士优先,通信工程、电子相关专业;
2. 对LTE/5G/5GA协议和系统有深入了解和相应研究经历;
3. 有MAC、SON算法设计和研究经验;
4. 有系统级或者链路级仿真经验,熟练掌握C/C++/matlab/python编程;
5. 有5G/5GA标准化经验者优先考虑;
6. 有丰富MIMO, SON相关研究经验者优先考虑;
7. 有良好的英文交流能力;
8. 积极进取,有良好的团队合作意识,技术创新意识。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
21通信SoC软件开发工程师岗位职责:
任职资格:
1. 负责OMMU/ORU LPHY模块设计和开发;
2. 负责ORAN-based LPHY模块性能优化;
1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业;
2. 精通C/C++,熟悉架构设计及优化者优先;
3. 熟悉基于通信基带SoC芯片的软件开发,优化和测试过程,有Marvell通信芯片软件开发经验者优先;
4. 熟悉3GPP协议,ORAN协议,具有ORAN Opt7-2x/Opt7-3开发经验者优先;
5. 熟悉RTOS,Linux驱动开发经验者优先;
6. 具备良好的数字信号处理背景优先;
7. 良好的英文书面和口语表达。
加分项:
- 熟悉常用AI算法模型,有广泛实用经验/模型部署移植等相关经验;
- 有DFE(digital front-end)开发经验;

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
224G/5G 物理层开发工程师岗位职责:
任职资格:
按照经验能力参与以下工作的一项或多项,包括:
1. 负责4G/5G物理层接收机&发送机的算法模块SW实现;
2. 负责基站侧或终端侧 Modem 架构设计及SW开发;
3. 负责AI+5G开发及优化;
4. 负责数字孪生开发和验证;
1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业;
2. 精通C/C++语言,熟悉架构设计及优化者优先;
3. 熟悉至少一种CPU结构及指令,有Intel AVX512/AMX优化经验者优先;
4. 熟悉至少一种无线通信协议,有NR、LTE、NB-IOT、2G GSM物理层研发经验优先;
5. 熟悉3GPP信道建模优化,有digital twin经验者优先;
6. 熟悉常用AI训练推理架构,熟悉常用AI算法模型者优先;
7. 熟悉linux操作系统,有内核优化经验者优先;
8. 具备良好的数字信号处理背景优先;
9. 良好的英文书面和口语表达。

公司介绍:
三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。
新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!

※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密
※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密
link
23战略企划岗位职责:
任职资格:
主要职责:
1. 销售数据分析及企划
2. 行业市场分析
3. 韩语翻译及部门交代的其他相关事项
岗位要求
- 学历:统招本科及以上
- 经历:销售企划相关经历者优先考虑
- 熟练掌握 M/S Office (Excel, Word, PPT)
- 韩国语能力优秀者优先考虑 (TOPIK 6级 或 近母语水平)
link
24[社会招聘] Memory MKT|存储芯片市场岗位职责:
任职资格:
1. Establish and maintain the regular and close communication relationship with customers and colleague
2. Memory business Marketer
3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market
4. Working closely with our customers
5. Writing market analysis report
6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater.
1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ
2. Experience in Memroy market and product (DRAM/SSD/GFX)
3. Experience in PC martket
4. Understaing of Memory business value chain including PC Market
5. Sophisticated in PPT and Excel, with good presentation skills
link
25[社会招聘] DRAM BE|存储芯片市场营销岗位职责:
任职资格:
1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China;
2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers.
3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win.
4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements.
'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered
link
26[社会招聘] Memory DRAM FAE|存储芯片技术支持岗位职责:
任职资格:
- Support DRAM failure analysis with customers and ODM venders
- Prompt and professional technical on-site support for acquiring
customer's demand of DRAM.
- Make an initial judgement for the issues from customers
- Eliminate noise and ambiguity of on-site test results
- Regular technical discussion with HQ AE team and customers and
cross functional departments independently.
- Familiar with DRAM function and system operation
(Command sequence/AC parameter/…).
- Experience in DDR module on NB, desktop
(server system design/validation is a plus)
- To have analysis ability for log
(system booting log and DRAM training log, etc.)
- To have experience of tools updating BIOS (Dediprog, etc.)
- To have basic software knowledge related to PC, Notebook
(Windows, Linux, Python, etc.)
- To have experience of operating oscilloscope and logic analyzer.
- To have good comprehension and good communication skills with
ODM & customers, HQ and related departments
link
27[社会招聘] DRAM TEC|测试技术工程师岗位职责:
任职资格:
1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5)
2. Attend tech. meeting with DRAM BE and CS contact window;
3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support;
4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage.
- Familiar with DRAM function and system operation
(Command sequence/AC parameter/…).
- Experience in DDR module on NB, desktop
(server system design/validation is a plus)
- To have analysis ability for log
(system booting log and DRAM training log, etc.)
- To have experience of tools updating BIOS (Dediprog, etc.)
- To have basic software knowledge related to PC, Notebook
(Windows, Linux, Python, etc.)
- To have experience of operating oscilloscope and logic analyzer.
- To have good comprehension and good communication skills with
ODM & customers, HQ and related departments
link
28[社会招聘] Foundry FAE|晶圆代工技术支持岗位职责:
任职资格:
1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C
2. Design in (T/O) the Foundry customer product in E/C
3. Build the relationship with Tier 1/2/3 customer in E/C
Education: Bachelor degree and above
Major: Electrical/Electronics engineering
Experience Range:>3 Years
Prof. Experience: Experience in SOC design
Language skills:Fluent in English read/write/speaking
Min. skills & knowledge:
1. Knowledge in Semiconductor Process manufacturing, device component
2. Experience on designing Digital IP, Analog IP, SOC Design
3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc
link
29[社会招聘] Auto TEC|测试技术工程师岗位职责:
任职资格:
1. Responsible as TEC engineer in Mobile/Auto TEC department;
2.Responsible for Auto DRAM product testing when needed.
3. Suppport on-site and DRAM Validation activities to nVIDIA;
4. Maintain and Improve customer's satisfaction with issue on-site support;
1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent;
2. Understanding Electronic Basic & Advanced Theory and semiconductor industry
3. At least 2 years’ experience, with Auto memory product is highly preferred;
4. Familiar with Automotive applications is highly preferred.
5. Good command of oral and written English; Good Korean is highly preferred;
link
30[社会招聘] Memory FAE|存储芯片技术支持岗位职责:
任职资格:
1. Promote and Design in SSD products to PC customers in East China.
2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM
3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win.
4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements
1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ.
2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market.
3. Experience and knowledge in SSD products is preferred.
link
31[社会招聘] Memory MKT|存储芯片市场岗位职责:
任职资格:
1. Establish and maintain the regular and close communication relationship with customers and colleague
2. Memory business Marketer
3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market
4. Working closely with our customers
5. Writing market analysis report
6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater.
1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ
2. Experience in Memroy market and product (DRAM/SSD/GFX)
3. Experience in PC martket
4. Understaing of Memory business value chain including PC Market
5. Sophisticated in PPT and Excel, with good presentation skills
link
32[社会招聘] DRAM BE|存储芯片市场营销岗位职责:
任职资格:
1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China;
2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers.
3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win.
4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements.
'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered
link
33[社会招聘] Memory DRAM FAE|存储芯片技术支持岗位职责:
任职资格:
- Support DRAM failure analysis with customers and ODM venders
- Prompt and professional technical on-site support for acquiring
customer's demand of DRAM.
- Make an initial judgement for the issues from customers
- Eliminate noise and ambiguity of on-site test results
- Regular technical discussion with HQ AE team and customers and
cross functional departments independently.
- Familiar with DRAM function and system operation
(Command sequence/AC parameter/…).
- Experience in DDR module on NB, desktop
(server system design/validation is a plus)
- To have analysis ability for log
(system booting log and DRAM training log, etc.)
- To have experience of tools updating BIOS (Dediprog, etc.)
- To have basic software knowledge related to PC, Notebook
(Windows, Linux, Python, etc.)
- To have experience of operating oscilloscope and logic analyzer.
- To have good comprehension and good communication skills with
ODM & customers, HQ and related departments
link
34[社会招聘] DRAM TEC|测试技术工程师岗位职责:
任职资格:
1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5)
2. Attend tech. meeting with DRAM BE and CS contact window;
3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support;
4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage.
- Familiar with DRAM function and system operation
(Command sequence/AC parameter/…).
- Experience in DDR module on NB, desktop
(server system design/validation is a plus)
- To have analysis ability for log
(system booting log and DRAM training log, etc.)
- To have experience of tools updating BIOS (Dediprog, etc.)
- To have basic software knowledge related to PC, Notebook
(Windows, Linux, Python, etc.)
- To have experience of operating oscilloscope and logic analyzer.
- To have good comprehension and good communication skills with
ODM & customers, HQ and related departments
link
35[社会招聘] Foundry FAE|晶圆代工技术支持岗位职责:
任职资格:
1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C
2. Design in (T/O) the Foundry customer product in E/C
3. Build the relationship with Tier 1/2/3 customer in E/C
Education: Bachelor degree and above
Major: Electrical/Electronics engineering
Experience Range:>3 Years
Prof. Experience: Experience in SOC design
Language skills:Fluent in English read/write/speaking
Min. skills & knowledge:
1. Knowledge in Semiconductor Process manufacturing, device component
2. Experience on designing Digital IP, Analog IP, SOC Design
3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc
link
36[社会招聘] Auto TEC|测试技术工程师岗位职责:
任职资格:
1. Responsible as TEC engineer in Mobile/Auto TEC department;
2.Responsible for Auto DRAM product testing when needed.
3. Suppport on-site and DRAM Validation activities to nVIDIA;
4. Maintain and Improve customer's satisfaction with issue on-site support;
1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent;
2. Understanding Electronic Basic & Advanced Theory and semiconductor industry
3. At least 2 years’ experience, with Auto memory product is highly preferred;
4. Familiar with Automotive applications is highly preferred.
5. Good command of oral and written English; Good Korean is highly preferred;
link
37[社会招聘] Memory FAE|存储芯片技术支持岗位职责:
任职资格:
1. Promote and Design in SSD products to PC customers in East China.
2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM
3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win.
4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements
1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ.
2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market.
3. Experience and knowledge in SSD products is preferred.
link
- - \ No newline at end of file diff --git a/outputs/samsung_jobs.json b/outputs/samsung_jobs.json deleted file mode 100644 index 0d93de19..00000000 --- a/outputs/samsung_jobs.json +++ /dev/null @@ -1,187 +0,0 @@ -[ - { - "job_title": "[社会招聘] 三星SDSC IT售前支持", - "description": "岗位职责:\n任职资格:\n1. 根据客户情况提供合理的解决方案,\n2. 项目的整体规划,日程管理,风险管理,品质管理,验收报告等,\n3. 编写项目相关文档,确保项目资料的收集、整理、建档和保存,\n4. 公司运营,采购,执行等相关部门的业务协调等。\n1. 学历专业:正规院校本科及以上学历,双证齐全,专业不限;\n2. 工作经验:\n- 3~5年销售经验或者软件开发目管理经验\n- 有大型项目参与经验者优先\n3. 技术能力:\n- CRM,SAP,RMS,WMS等相关系统项目管理经验\n- 熟悉Java、C#等主流编程语言‌者优先\n- 了解HTML、CSS、JavaScript,熟悉Vue、React等前端框架\n- 了解TCP/IP、HTTP等网络协议,熟悉Socket编程\n- 了解Python编程基础,熟悉Django、Flask等框架,了解MongoDB、Linux系统操作\n- 了解服务器,网络设备等相关内容者优先\n4. 综合素质:\n- 了解韩国文化,理解韩国文化\n- 为人正直,有责任心,积极主动,逻辑清晰,具备优秀沟通、宣讲及文案能力\n- 具备良好的沟通能力和方案撰写能力,能够与客户和团队成员有效沟通‌\n5.语言:无限制,如懂韩语优先\n6.其他:取得PMP资格证优先,接受出差", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] 三星SDSC IT销售代表(韩语)", - "description": "岗位职责:\n任职资格:\n1. 挖掘可购买公司产品及服务的企业客户与商机;\n2. 挖掘可与公司开展业务合作的销售及交付合作伙伴;\n3. ‌分析客户IT需求,基于公司与合作伙伴的产品及能力制定解决方案与业务提案;\n4. 通过分析公司产品及能力,开发服务产品并提出销售方案等;\n5.公司内部销售系统处理及管理: 订单录入,成本确认,合同处理,损益确认,计划录入等。\n1. 正规院校本科及以上学历,双证齐全;\n2. 工作经验:\n- 拥有IT销售经验或在IT服务行业工作过经验;\n- 具有云计算相关的销售或技术工作经验,特别是AWS相关经验者优先考虑;\n3. 技术能力:\n- 对最新的IT技术趋势的广泛理解\n- 熟练使用MS Office软件(Excel,Word,PPT)\n4. 语言要求:韩语精通(必须)\n5.其他:接受短期出差", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "无线通信CUDA开发工程师", - "description": "岗位职责:\n任职资格:\n1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求;\n2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案;\n3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告;\n1. 硕士及以上学历,计算机科学/通信等相关专业;\n2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧;\n3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力;\n4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先;\n5. 有基于GPU的算力利用率提升项目经验者优先;\n6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "人形机器人全身运动控制算法工程师(强化学习方向)", - "description": "岗位职责:\n任职资格:\n1. 负责基于强化学习的人形机器人全身运动控制算法研发,涵盖行走、跑跳、平衡控制、复杂地形适配、动态抗扰动、全身协同运动规划等核心场景的算法设计、迭代与优化;\n2. 设计适配人形机器人高自由度特性的强化学习算法框架与深度网络架构,解决全身运动的稳定性、泛化性、实时性与安全性核心问题,持续提升机器人在复杂动态环境下的运动性能上限;\n3. 负责算法的仿真验证、sim-to-real迁移优化与人形机器人真机部署调试,结合视觉、力觉、IMU等多模态传感器反馈完成闭环控制优化,完成算法性能指标的评估、迭代与落地;\n4. 与感知、硬件、任务规划等跨团队紧密协作,推动算法与人形机器人本体、多模态感知系统的深度适配与集成,支撑整机运动能力的落地与产品化迭代;\n5. 持续跟踪人形机器人全身运动控制、深度强化学习领域的国际前沿研究与技术动态,引入顶会顶刊的创新方法,推动核心技术的突破与技术壁垒构建;\n1. 硕士及以上学历,计算机科学与技术、人工智能、自动化、控制工程、机器人工程等相关专业,具备扎实的自动控制理论、刚体动力学与机器学习理论基础;\n2. 精通深度强化学习算法原理与工程落地,熟悉PPO、SAC、TD3、DQN等主流DRL算法,具备基于强化学习的机器人运动控制算法完整研发与落地经验;\n3. 精通人形机器人正逆运动学、刚体动力学建模,熟悉高自由度机器人全身运动控制相关理论,具备人形机器人全身运动控制项目研发经验者优先;\n4. 熟练使用PyTorch/TensorFlow等主流深度学习框架,精通Python/C++编程语言,熟悉ROS/ROS2等机器人软件框架,掌握Isaac Lab、Isaac Gym、MuJoCo、Gazebo等主流机器人仿真环境;\n5. 熟悉模仿学习、逆强化学习、sim-to-real迁移、模型预测控制(MPC)、最优控制、数值优化等相关技术,具备多模态感知与运动控制融合开发经验者优先;\n6. 具备良好的科研能力、工程落地能力与跨团队协作能力,在机器人、强化学习相关领域顶会顶刊(RSS、CoRL、ICRA、IROS、NeurIPS、ICML、ICLR等)发表过论文者优先。\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "AI软件工程师", - "description": "岗位职责:\n任职资格:\n工作职责:\n- 数据中心AI系统架构与系统软件设计。\n任职资格:\n1. 熟练使用C/C++/Python,熟悉常用数据结构与算法;\n2. 熟悉大语言模型推理,有相关开发、分析和优化经验;\n3. 具备AI infrastructure分析和优化经验;\n4. 对系统软件研发有强烈兴趣。\n5. 以下为加分项:\n- 具备OpenCL,CUDA等开发和优化经验;\n- 具备模拟器开发、系统仿真分析和优化经验。\n\n其他要求:\n本科/硕士学历\nCET4及以上\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。\n 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: \n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 \n同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; \n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; \n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "Edge AI软件工程师", - "description": "岗位职责:\n任职资格:\n工作职责:\n- 端侧系统架构,算法及系统软件设计。\n任职资格:\n1. 熟练使用C/C++/Python,熟悉常用数据结构与算法;\n2. 熟悉VLM、VLA,掌握模型训练、推理、优化相关知识,并有实际开发部署经验;\n3. 具备模拟器开发、系统仿真分析和优化经验;\n4. 有良好的沟通和学习能力。\n5. 以下为加分项:\n- 具备OpenCL,CUDA开发和优化经验;\n- 具备GPU,NPU设计经验;\n- 对机器人方向算法优化和系统软件研发有强烈兴趣。\n其他要求:\n本科及以上学历\nCET4及以上\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。\n 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: \n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 \n同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; \n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; \n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "技术战略分析师", - "description": "岗位职责:\n任职资格:\n岗位职责:\n- 技术分析:从技术视角深入研究前沿科技,重点聚焦人工智能、存储技术、高性能计算等领域,开展系统性调研并撰写高质量的技术分析报告,助力技术决策与战略布局。\n- 行业洞察:紧跟国内外技术发展趋势,研判技术演进方向与竞争态势,为集团战略决策提供坚实依据。\n- 专家访谈:积极与行业专家保持深入沟通,建立长期合作关系,获取第一手权威见解与前沿信息。\n- 论文管理:负责公司学术论文的评审与发表流程的统筹与优化,推动论文质量稳步提升,增强公司学术影响力。\n- 业务支援:参与公司中长期技术规划,协助组织并推进技术创新相关活动,提升研发效率与创新能力。\n岗位要求:\n1. 人工智能、存储技术、计算机科学等相关专业背景,有在人工智能、存储系统、高性能计算等领域的工作经验者优先。\n2. 对前沿技术保持高度关注,具备独立开展技术调研的能力,能够撰写逻辑清晰、内容详实的技术分析报告。\n3. 能够熟练阅读英文技术文档与学术论文,准确把握核心思想与创新点,以及优秀的英语口语交流能力。\n4. 对新技术有热情,独立问题解决能力,海外同事合作能力\n5. 以下为加分项:\n- 有韩语能力者优先,能进行基本的书面或口语交流。\n- 有技术研发经验者优先。\n- 有深度技术调研报告撰写经验者优先。\n其他要求:\n- 硕士/博士学历\n- 人工智能、存储技术、计算机科学等相关专业\n- 英语六级(CET-6)或同等级英语水平及以上\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。\n 《个人信息收集使用申明》 在本次招聘活动中,我们承诺以下事项: \n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 \n同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; \n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; \n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "存储软件开发系统工程师", - "description": "岗位职责:\n任职资格:\n岗位职责:\n- 负责三星存储产品系统软件开发及性能优化;\n- 负责三星存储系统前沿技术研究。\n岗位要求:\n1. 具备良好的编程基础,熟练掌握C/C++/Python至少一种编程语言;\n2. 熟练掌握常用数据结构及算法;\n3. 熟悉Linux系统的编程基础,熟悉Linux I/O相关协议栈。\n4. 以下为加分项:\n- 具有数据库,存储引擎以及文件系统相关开发和优化经验者;\n- 具备AI框架下存储相关研究经验。\n其他要求:\n- 硕士学历\n- 计算机、软件、电子,通信,自动化等相关专业\n- CET4及以上\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 \n请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 \n《个人信息收集使用申明》 \n在本次招聘活动中,我们承诺以下事项:\n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息;\n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; \n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;\n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "分布式系统软件工程师", - "description": "岗位职责:\n任职资格:\n岗位职责:\n- 负责三星大规模集群系统任务调度及资源管理方案的优化与开发;\n- 提升大规模集群的调度系统的整体效率和资源利用率。\n岗位要求:\n1. 具有扎实的shell/C++/Python编程基础,熟悉数据结构和算法;\n2. 掌握计算机操作系统,分布式系统等专业知识;\n3. 熟悉多线程开发,有良好的代码阅读和开发能力。\n4. 以下为加分项:\n- 具有Slurm, LSF, Kubernetes的代码经验或者部署经验。\n其他要求:\n- 硕士学历\n- 计算机、人工智能等相关专业\n- CET4及以上\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 \n请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。\n《个人信息收集使用申明》 \n在本次招聘活动中,我们承诺以下事项:\n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 \n同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。\n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;\n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查;\n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密;\n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "存储软件工程师(AI算法方向)", - "description": "岗位职责:\n任职资格:\n岗位职责:\n- 负责三星存储产品相关AI算法的设计、训练、优化和研发。\n岗位要求:\n1. 精通Python,熟悉C/C++语言,熟练掌握常用的数据结构和算法;\n2. 掌握机器学习/深度学习算法模型构建、训练、优化相关专业知识并有相关经验;\n3. 能熟练使用PyTorch等框架进行模型的开发和优化;\n4. 具有较强的动手能力和研究能力,能够快速复现论文方法,并针对特定的场景进行优化;\n5. 以下为加分项:\n- 具备AI框架下存储相关研究经验;\n- 有顶会/顶刊论文相关成果者优先;\n- 有transformer,大模型研发经验者优先。\n其他要求:\n- 硕士学历\n- 计算机、软件、大数据、人工智能等相关专业\n- CET4及以上\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注! \n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。\n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 \n《个人信息收集使用申明》 \n在本次招聘活动中,我们承诺以下事项: \n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为;\n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; \n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "存储系统专家", - "description": "岗位职责:\n任职资格:\n岗位职责:\n- 作为团队技术带头人,负责存储系统软件架构设计和实现以及存储系统的应用优化; \n- 负责三星存储相关新技术解决方案的研究以及学术成果转化。\n岗位要求:\n1. 熟悉NVMe SSD相关知识且具备存储设备性能优化经验;\n2. 熟悉Linux内核I/O协议栈,具备SPDK等用户态驱动开发和优化经验;\n3. 较强的软件架构以及代码重构能力;\n4. 了解数据库以及常用存储引擎相关知识以及体系架构 (Rocks DB, MySQL.),并有相应系统设计开发以及优化经验。\n加分项: \n- 熟悉新的Memory技术如CXL,有相关研究经验者优先;\n- 具备AI框架下存储相关研究经验者优先; \n- 在该领域顶级会议或期刊发表过论文者优先。\n其他要求:\n- 博士学历\n- 计算机、软件、电子、通信、自动化等相关专业\n- CET6及以上,英语可作为工作语言\n\n\n\n感谢您对三星(中国)半导体有限公司下属西安三星电子研究所的关注!\n三星(中国)半导体有限公司高度重视用户的个人信息,并严格遵守《中华人民共和国个人信息保护法》及相关法律法规进行个人信息的收集、使用、安全管理。 请您在投递简历前认真认真阅读以下内容,确认后完成简历投递。 \n若您已经成功投递简历则表示您已经同意授权个人信息至西安三星电子研究所招聘活动并承诺以下申明。 \n《个人信息收集使用申明》 \n在本次招聘活动中,我们承诺以下事项: \n1.我们收到的个人信息仅用于本次您所投递三星(中国)半导体有限公司下属西安三星电子研究所的招聘岗位及可能录取后的招聘活动管理中,并保证未经本人同意不向第三方披露; 如果您没有被录取,我们将在招聘结束后销毁文本和电子信息; 个人信息及包括但不限于以下信息:姓名、性别、出生日期、籍贯、现居住城市、电话、邮箱、教育背景(学校、学历学位、专业、在学时间)、工作履历(公司名称、职位、在职时间、离职原因、税前工资)等一般个人信息;个人照片等敏感个人信息; \n2.我们不会在招聘中询问您现在或以前工作单位的任何商业秘密,并保证不会因此影响招聘结果; \n3.在招聘中如果有任何您认为不适当的行为或询问,您都有权拒绝;我们保证不会因此影响招聘结果。 \n同时也请您阅读以下事项,并保证以下内容真实性;如果内容虚假,本人愿意接受因此为三星(中国)半导体有限公司下属西安三星电子研究所带来的任何不良后果,并承担由此给公司造成的任何损失。 \n1.我在自愿的情况下同意投递三星(中国)半导体有限公司下属西安三星电子研究所招聘岗位收集以上信息,并允许公司在招聘和可能录取后的招聘活动管理中使用; \n2.我保证在本次招聘中提供的个人信息真实,并认可公司对虚假个人信息的处理行为; \n3.我允许招聘公司对我提供的个人信息在法律许可的范围内进行背景调查; \n4.我在此次招聘或将来入职后,将对现公司、前公司的商业秘密进行保密; \n5.若本人还存在与原公司在有效期内的竞业限制协议,将在招聘过程中如实告知公司。 \n如您需取消投递,请自行撤销简历;\n若需帮助请联系HR:srcxianhr@samsung.com。", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "大模型算法工程师", - "description": "岗位职责:\n任职资格:\n1. 参与基础大模型和端侧大模型的研发工作,包括架构设计、预训练、后训练等,端到端构建通用大模型;\n2. 负责大模型高阶能力(Coding、Agent等)的专项提升,打造模型长版特性;\n3. 持续关注大模型最新研究,探索下一代大模型的架构和训练方法,做出有影响力的成果;\n1. 硕士及以上学历,计算机科学、人工智能等相关专业;\n2. 熟悉深度学习框架(例如pytorch等),具备大模型和端侧大模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验;\n3. 对大模型架构、训练、数据、系统优化中的某一方面有深入理解,以下符合1条以上:\n- 能够提出创新性的大模型架构和端侧大模型架构,探索技术的下一跳;\n- 熟练掌握强化学习(RL)和模型微调(SFT)等后训练技术,并可以提出创新的后训练方法;\n- 对coding、math、agent等大模型高阶能力有深入思考;\n- 熟练掌握大模型预训练的Know How,可以快速诊断并修复问题,提升模型能力;\n- 对预训练数据、后训练数据的生产、合成方法有深入理解;\n- 熟练模型训练/推理的系统优化方法,提升模型的实际训练、推理性能;\n4. 有大模型/端侧大模型架构、训练、数据、系统优化等相关实战经验者优先,在NeurIPS/ICML/ICLR/ACL/EMNLP/CVPR/ICCV/TPAMI等AI顶会发表过相关论文者优先;\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "具身智能算法工程师", - "description": "岗位职责:\n任职资格:\n1. 负责研发具身智能操作算法,研发基于VLA、强化学习等AI技术在机器人操作场景中的应用;\n2. 设计网络架构,分析实验数据,评估算法表现,提升机器人在精准操作、长程任务、动态响应等关键领域的表现性能;\n3. 负责将VLA模型在跨机器人本体和环境进行真机部署,评估模型在真机的性能指标;\n4. 持续跟进最新的具身操作方向研究工作动态,引入最前沿的方法持续提升模型表现性能;\n1. 硕士及以上学历,计算机科学、人工智能、自动化、机器人技术等相关专业;\n2. 熟悉深度学习框架(例如pytorch、TensorFlow),具备大规模VLA模型的算法开发经验,具备数据处理、模型架构设计、大规模训练等经验;\n3. 具备机器学习算法在机器人领域的开发经验,包括并不限于reinforcement learning、imitation learning、transfer learning、action-conditioned world models、representation learning、dexterous manipulation、sim-to-real transfer、vision language models、motion planning等;\n4. 具备机器人真机经验(例如Aloha、Franka、Fetch等),具备良好的编程能力,熟悉Python或C++编程语言,熟悉主流机器人软件框架例如ROS,熟悉主流的仿真软件例如IsaacLab、IsaacGym、Mujoco、Bullet、Gazebo等;\n5. 具备良好的科研能力,在顶会或期刊发表过文章的优先,例如机器学习方向(NeurIPS、ICML、ICLR),机器人方向(RSS、CoRL、ICRA、IROS)、计算机视觉方向(CVPR、ICCV、ECCV)等;\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "高性能并行计算开发工程师", - "description": "岗位职责:\n任职资格:\n1. 根据无线通信模块需要,基于CPU/GPU设计适合并行计算的数据结构,利用多线程实现高效的并行计算处理编程;\n2. 具有较高的并行计算/高性能计算方面SW设计及开发能力,能通过性能分析找到CPU或GPU处理的瓶颈,并提出改善方案;\n1. 硕士及以上学历,计算机科学/人工智能等相关专业;\n2. 精通高性能计算/并行计算软件设计及优化思想,具有多线程并行计算开发经验;\n3. 熟练使用CPU/GPU性能分析工具(Nsight等)并通过性能监测找到存储,数据传输,并行计算中的瓶颈点,并结合数据访存和并行计算能力设计最优SW方案;\n4. 有GPU高性能计算/CUDA开发经验者优先;\n5. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "无线通信CUDA开发工程师", - "description": "岗位职责:\n任职资格:\n1 基于Nvidia GPU架构进行无线通信物理层核心算法的CUDA加速实现,包括OFDM调制,信道估计,检测均衡等模块,满足5G/6G商用化部署实时性要求;\n2 针对无线通信中密集计算模块优化CUDA内存代码,实现基于GPU的高效并行计算和内存访问,提供有竞争力的CUDA SW设计方案;\n3 能联合无线通信基带算法团队,完成CUDA加速模块开发;能支持与CPU等其他处理器联调测试及优化,输出相关的优化报告;\n1. 硕士及以上学历,计算机科学/通信等相关专业;\n2. 精通CUDA C/C++编程模型,深入理解GPU内存层次(全局,共享,常量,寄存器等)、线程调度机制、掌握CUDA core/Tensor Core并行加速技巧;\n3. 熟练使用Nvidia性能分析工具(Nsight/nvprof等)具备复杂算法的并行设计与调优能力;\n4. 熟悉异构架构(GPU+CPU),有CPU/GPU混合编程优化经验者优先;\n5. 有基于GPU的算力利用率提升项目经验者优先;\n6. 出色的英文表达能力,以及在快节奏环境中独立和协作工作的能力。\n\n公司介绍:\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\n\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "SCS-技术员", - "description": "岗位职责:\n任职资格:\n1.负责现场设备日常维护工作;\n2.协助工程师进行调试工作,进行设备日常作业;\n3.根据作业标准要求进行设备的操作。\n1.学历:25~26届毕业大专生;\n2.专业:机械类、电气类、自动化类、计算机类、电子类等理工科相关专业;\n3.熟练使用Office办公软件;\n4.能适应倒班工作(三班倒)。\n(早班:6:00-14:00;中班:14:00-22:00;晚班:22:00-06:00,上五休二,每班工作8小时)", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "知识图谱、智能体、大模型算法工程师", - "description": "岗位职责:\n任职资格:\n从事知识图谱,大语言模型、多智能体优化的前沿算法研究:\r\n- 研究方向包括:知识图谱,智能体设计, 大模型RAG, 多智能体优化\r\n- 算法领域包括但不限于: Knowledge Graph, Graph Retrieval, AI Agent, RAG, LLM/VLM/LMM, CoT, etc.\n1. 硕士及以上学历,计算机、电子、自动化、数学等相关专业;\r\n2. 熟练掌握智能体设计,大模型微调等相关基础;\r\n3. 对知识图谱,大模型/RAG/Agent/Retrieval 有相关开发经验;\r\n4. 熟悉Python,PyTorch深度学习框架;\r\n5. 对前沿技术有热情,有较强的独立工作能力(算法理解与实现,问题分析与解决);\r\n[加分项] 在顶会NIPS,ICML等发表过高水平文章,或在权威竞赛中以主要参与者身份取得过优秀名次;\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "通信AI算法工程师", - "description": "岗位职责:\n任职资格:\n1. 网络优化和诊断的AI算法研究; \r\n2. AI算法实现,优化和评估; \r\n3. 网络性能问题研究;\n1. 博士及以上学历,通信工程、电子工程、计算机等相关专业;\r\n2. 对AI算法有深入研究; \r\n3. 具有AI算法编码和应用经验; \r\n4. 擅长应用问题的算法建模; \r\n5. 有无线通信的AI算法研究经验者优先; \r\n6. 对AI算法有广泛实用经验者优先。\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "数字孪生算法工程师", - "description": "岗位职责:\n任职资格:\n1. 5G-A/5G+AI MAC/RLC/PDCP系统算法研究、设计工作;\r\n2. Ray tracing/3GPP信道建模算法研究、设计工作;\r\n3. 5G-A/5G+AI 系统级仿真分析工作;\r\n4. 指导和协助开发 S/W实现与优化工作。\n1. 硕士及以上学历,博士优先,通信工程、计算机等相关专业;\r\n2. 对5GA/5G协议和系统有深入了解和相应研究经历;\r\n3. 有MAC/RLC/PDCP/PHY算法设计和研究经验;\r\n4. 有信道建模设计和研究经验;\r\n5. 有系统级/链路级+AI仿真经验;\r\n6. 熟练掌握C/C++,Python编程;\r\n7. 有良好的英语交流能力,积极进取,有良好的团队合作意识和技术创新意识。\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "5G/5GA算法工程师", - "description": "岗位职责:\n任职资格:\n1. LTE/5G MAC、SON系统算法研究、设计工作;\r\n2. LTE/5G系统级仿真、分析工作;\r\n3. 5G 链路级仿真工作;\r\n4. 指导和协助开发 S/W实现与测试工作;\r\n5. 系统级advance算法相关研究工作。\n1. 硕士及以上学历,博士优先,通信工程、电子相关专业;\r\n2. 对LTE/5G/5GA协议和系统有深入了解和相应研究经历;\r\n3. 有MAC、SON算法设计和研究经验;\r\n4. 有系统级或者链路级仿真经验,熟练掌握C/C++/matlab/python编程;\r\n5. 有5G/5GA标准化经验者优先考虑;\r\n6. 有丰富MIMO, SON相关研究经验者优先考虑;\r\n7. 有良好的英文交流能力;\r\n8. 积极进取,有良好的团队合作意识,技术创新意识。\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "通信SoC软件开发工程师", - "description": "岗位职责:\n任职资格:\n1. 负责OMMU/ORU LPHY模块设计和开发;\r\n2. 负责ORAN-based LPHY模块性能优化;\n1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业;\r\n2. 精通C/C++,熟悉架构设计及优化者优先;\r\n3. 熟悉基于通信基带SoC芯片的软件开发,优化和测试过程,有Marvell通信芯片软件开发经验者优先;\r\n4. 熟悉3GPP协议,ORAN协议,具有ORAN Opt7-2x/Opt7-3开发经验者优先;\r\n5. 熟悉RTOS,Linux驱动开发经验者优先;\r\n6. 具备良好的数字信号处理背景优先; \r\n7. 良好的英文书面和口语表达。\r\n加分项:\r\n- 熟悉常用AI算法模型,有广泛实用经验/模型部署移植等相关经验;\r\n- 有DFE(digital front-end)开发经验;\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "4G/5G 物理层开发工程师", - "description": "岗位职责:\n任职资格:\n按照经验能力参与以下工作的一项或多项,包括:\r\n1. 负责4G/5G物理层接收机&发送机的算法模块SW实现;\r\n2. 负责基站侧或终端侧 Modem 架构设计及SW开发;\r\n3. 负责AI+5G开发及优化;\r\n4. 负责数字孪生开发和验证;\n1. 硕士及以上学历,通信、信号处理、计算机、电子及相关专业;\r\n2. 精通C/C++语言,熟悉架构设计及优化者优先;\r\n3. 熟悉至少一种CPU结构及指令,有Intel AVX512/AMX优化经验者优先;\r\n4. 熟悉至少一种无线通信协议,有NR、LTE、NB-IOT、2G GSM物理层研发经验优先;\r\n5. 熟悉3GPP信道建模优化,有digital twin经验者优先;\r\n6. 熟悉常用AI训练推理架构,熟悉常用AI算法模型者优先;\r\n7. 熟悉linux操作系统,有内核优化经验者优先;\r\n8. 具备良好的数字信号处理背景优先; \r\n9. 良好的英文书面和口语表达。\r\n\r\n公司介绍:\r\n三星电子中国研究院是三星电子在华投资设立的具有独立法人资格的研发机构,是具备博士后工作站运营资质、聚集了600人研发团队,并由国家批准认定的软件企业。研究院专注于人工智能、5G/6G通信技术研究和标准化等前沿技术,也积极进行商用化的开发、推动先进技术在三星产品中的商用化落地,提升三星电子产品的竞争力。\r\n新时代,新机遇。三星电子中国研究院希望把握人工智能、5G/6G与IoT技术发展的时代机遇,凝聚海内外计算机、电子及通信领域的优秀人才,坚守“做中国人民喜爱的企业,贡献于中国社会的企业”的承诺,与您一起携手共赢、创造未来! 真诚欢迎您的加入!\r\n\r\n※ 请应聘者在应聘过程中对现公司、 前公司的商业秘密进行保密\r\n※ 请应聘者确认您投递的简历不包含现公司、前公司的商业秘密", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "战略企划", - "description": "岗位职责:\n任职资格:\n主要职责:\n1. 销售数据分析及企划\n2. 行业市场分析\n3. 韩语翻译及部门交代的其他相关事项\n岗位要求 \n- 学历:统招本科及以上 \n- 经历:销售企划相关经历者优先考虑 \n- 熟练掌握 M/S Office (Excel, Word, PPT)\n- 韩国语能力优秀者优先考虑 (TOPIK 6级 或 近母语水平)", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory MKT|存储芯片市场", - "description": "岗位职责:\n任职资格:\n1. Establish and maintain the regular and close communication relationship with customers and colleague\r\n2. Memory business Marketer\r\n3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market\r\n4. Working closely with our customers \r\n5. Writing market analysis report\r\n6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater.\n1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ\r\n2. Experience in Memroy market and product (DRAM/SSD/GFX)\r\n3. Experience in PC martket\r\n4. Understaing of Memory business value chain including PC Market\r\n5. Sophisticated in PPT and Excel, with good presentation skills", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] DRAM BE|存储芯片市场营销", - "description": "岗位职责:\n任职资格:\n1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China;\r\n2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers.\r\n3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win.\r\n4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements.\n'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory DRAM FAE|存储芯片技术支持", - "description": "岗位职责:\n任职资格:\n- Support DRAM failure analysis with customers and ODM venders\r\n- Prompt and professional technical on-site support for acquiring\r\n customer's demand of DRAM.\r\n - Make an initial judgement for the issues from customers\r\n - Eliminate noise and ambiguity of on-site test results\r\n - Regular technical discussion with HQ AE team and customers and \r\ncross functional departments independently.\n- Familiar with DRAM function and system operation \r\n(Command sequence/AC parameter/…).\r\n- Experience in DDR module on NB, desktop\r\n(server system design/validation is a plus)\r\n - To have analysis ability for log \r\n(system booting log and DRAM training log, etc.)\r\n - To have experience of tools updating BIOS (Dediprog, etc.)\r\n- To have basic software knowledge related to PC, Notebook\r\n(Windows, Linux, Python, etc.)\r\n- To have experience of operating oscilloscope and logic analyzer.\r\n - To have good comprehension and good communication skills with\r\nODM & customers, HQ and related departments", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] DRAM TEC|测试技术工程师", - "description": "岗位职责:\n任职资格:\n1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5)\r\n2. Attend tech. meeting with DRAM BE and CS contact window;\r\n3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support;\r\n4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage.\n- Familiar with DRAM function and system operation \r\n(Command sequence/AC parameter/…).\r\n- Experience in DDR module on NB, desktop\r\n(server system design/validation is a plus)\r\n - To have analysis ability for log \r\n(system booting log and DRAM training log, etc.)\r\n - To have experience of tools updating BIOS (Dediprog, etc.)\r\n- To have basic software knowledge related to PC, Notebook\r\n(Windows, Linux, Python, etc.)\r\n- To have experience of operating oscilloscope and logic analyzer.\r\n - To have good comprehension and good communication skills with\r\nODM & customers, HQ and related departments", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Foundry FAE|晶圆代工技术支持", - "description": "岗位职责:\n任职资格:\n1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C\r\n2. Design in (T/O) the Foundry customer product in E/C\r\n3. Build the relationship with Tier 1/2/3 customer in E/C\nEducation: Bachelor degree and above\r\nMajor: Electrical/Electronics engineering \r\nExperience Range:>3 Years \r\nProf. Experience: Experience in SOC design \r\nLanguage skills:Fluent in English read/write/speaking \r\nMin. skills & knowledge: \r\n1. Knowledge in Semiconductor Process manufacturing, device component\r\n2. Experience on designing Digital IP, Analog IP, SOC Design\r\n3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Auto TEC|测试技术工程师", - "description": "岗位职责:\n任职资格:\n1. Responsible as TEC engineer in Mobile/Auto TEC department;\r\n2.Responsible for Auto DRAM product testing when needed.\r\n3. Suppport on-site and DRAM Validation activities to nVIDIA;\r\n4. Maintain and Improve customer's satisfaction with issue on-site support;\n1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent;\r\n2. Understanding Electronic Basic & Advanced Theory and semiconductor industry\r\n3. At least 2 years’ experience, with Auto memory product is highly preferred;\r\n4. Familiar with Automotive applications is highly preferred.\r\n5. Good command of oral and written English; Good Korean is highly preferred;", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory FAE|存储芯片技术支持", - "description": "岗位职责:\n任职资格:\n1. Promote and Design in SSD products to PC customers in East China.\r\n2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM\r\n3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win.\r\n4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements\n1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ.\r\n2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market.\r\n3. Experience and knowledge in SSD products is preferred.", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory MKT|存储芯片市场", - "description": "岗位职责:\n任职资格:\n1. Establish and maintain the regular and close communication relationship with customers and colleague\r\n2. Memory business Marketer\r\n3. Understanding Memory trend, perform PC market sensing, gather information of main players in PC market\r\n4. Working closely with our customers \r\n5. Writing market analysis report\r\n6. Contact closely with investiative companies such as IDC, Gartner, and Egdewater.\n1. Chinese standard language and fluent English literacy for smooth communication with customers and Korea HQ\r\n2. Experience in Memroy market and product (DRAM/SSD/GFX)\r\n3. Experience in PC martket\r\n4. Understaing of Memory business value chain including PC Market\r\n5. Sophisticated in PPT and Excel, with good presentation skills", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] DRAM BE|存储芯片市场营销", - "description": "岗位职责:\n任职资格:\n1. Promote and Design DRAM in GPU/CPU products(HBM/GDDR/SoCAMM..etc) to GPU & CPU customers in China;\r\n2. On-site Technical support to GPU/CPU Customers and manage qualification issues of customers.\r\n3. Build contact between Samsung HQ and customers, CO-work with Sales/MKT team for make products design-win.\r\n4. Regularly meet with customers to introduce Samsung memory, undertand customers' requirements.\n'1. Chinese standard language and fluent English literacy for smooth communication with customers and Samsung HQ. 2. Understanding Electronics basis, memory theory and Semiconductor industry, GPU/CPU/Server market. 3. Experience in DRAM analysis and knowledge in memory products is prefered", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory DRAM FAE|存储芯片技术支持", - "description": "岗位职责:\n任职资格:\n- Support DRAM failure analysis with customers and ODM venders\r\n- Prompt and professional technical on-site support for acquiring\r\n customer's demand of DRAM.\r\n - Make an initial judgement for the issues from customers\r\n - Eliminate noise and ambiguity of on-site test results\r\n - Regular technical discussion with HQ AE team and customers and \r\ncross functional departments independently.\n- Familiar with DRAM function and system operation \r\n(Command sequence/AC parameter/…).\r\n- Experience in DDR module on NB, desktop\r\n(server system design/validation is a plus)\r\n - To have analysis ability for log \r\n(system booting log and DRAM training log, etc.)\r\n - To have experience of tools updating BIOS (Dediprog, etc.)\r\n- To have basic software knowledge related to PC, Notebook\r\n(Windows, Linux, Python, etc.)\r\n- To have experience of operating oscilloscope and logic analyzer.\r\n - To have good comprehension and good communication skills with\r\nODM & customers, HQ and related departments", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] DRAM TEC|测试技术工程师", - "description": "岗位职责:\n任职资格:\n1. New DRAM product tech. support to customers(SOCAMM, LP5x, DDR5)\r\n2. Attend tech. meeting with DRAM BE and CS contact window;\r\n3. Quick FA support to S/C customers,improve customer's satisfaction with issue on-site support;\r\n4. To setup open lab with Main CPU vendor to minimize DRAM issue in early stage.\n- Familiar with DRAM function and system operation \r\n(Command sequence/AC parameter/…).\r\n- Experience in DDR module on NB, desktop\r\n(server system design/validation is a plus)\r\n - To have analysis ability for log \r\n(system booting log and DRAM training log, etc.)\r\n - To have experience of tools updating BIOS (Dediprog, etc.)\r\n- To have basic software knowledge related to PC, Notebook\r\n(Windows, Linux, Python, etc.)\r\n- To have experience of operating oscilloscope and logic analyzer.\r\n - To have good comprehension and good communication skills with\r\nODM & customers, HQ and related departments", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Foundry FAE|晶圆代工技术支持", - "description": "岗位职责:\n任职资格:\n1. Develop and Design Award (D/A) Tier 1/2/3 Foundry customer in E/C\r\n2. Design in (T/O) the Foundry customer product in E/C\r\n3. Build the relationship with Tier 1/2/3 customer in E/C\nEducation: Bachelor degree and above\r\nMajor: Electrical/Electronics engineering \r\nExperience Range:>3 Years \r\nProf. Experience: Experience in SOC design \r\nLanguage skills:Fluent in English read/write/speaking \r\nMin. skills & knowledge: \r\n1. Knowledge in Semiconductor Process manufacturing, device component\r\n2. Experience on designing Digital IP, Analog IP, SOC Design\r\n3. Experience on standard cell characterization, timing sign off (STA), synthesis, P&R, etc", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Auto TEC|测试技术工程师", - "description": "岗位职责:\n任职资格:\n1. Responsible as TEC engineer in Mobile/Auto TEC department;\r\n2.Responsible for Auto DRAM product testing when needed.\r\n3. Suppport on-site and DRAM Validation activities to nVIDIA;\r\n4. Maintain and Improve customer's satisfaction with issue on-site support;\n1. Electronic Engineering, Electronic Automation, Computer Science, Telecommunication or equivalent;\r\n2. Understanding Electronic Basic & Advanced Theory and semiconductor industry\r\n3. At least 2 years’ experience, with Auto memory product is highly preferred;\r\n4. Familiar with Automotive applications is highly preferred.\r\n5. Good command of oral and written English; Good Korean is highly preferred;", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - }, - { - "job_title": "[社会招聘] Memory FAE|存储芯片技术支持", - "description": "岗位职责:\n任职资格:\n1. Promote and Design in SSD products to PC customers in East China.\r\n2. Technical support to PC Customers and manage SSD qualification process&status&issue with customer and its ODM\r\n3. Build contact between Samsung HQ and customers, co-work with Sales/MKT team for making products design-win.\r\n4. Regularly meet with customers and Samsung HQ to introduce Samsung SSD product, understand customers' requirements\n1. Chinese standard language and fluent English literacy for smooth communication with customers and HQ.\r\n2. Understanding Electronics basis, memory theory and Semiconductor industry, Smartphone/Consumer electronics market.\r\n3. Experience and knowledge in SSD products is preferred.", - "detail_url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - } -] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1a8d23a4..00000000 --- a/requirements.txt +++ /dev/null @@ -1,32 +0,0 @@ -# LangChain -langchain>=0.3.0 -langchain-core>=0.3.48 -langchain-openai>=0.3.0 -langchain-deepseek>=0.1.0 -langgraph>=0.2.0 - -# MCP -mcp>=1.0.0 -langchain-mcp-adapters>=0.1.0,<=0.1.14 - -# Browser -playwright>=1.40.0 -playwright-stealth>=1.0.0 - -# Database -sqlalchemy>=2.0.0 -pymysql>=1.1.0 - -# Scheduler -apscheduler>=3.10.0 - -# HTTP Client -httpx>=0.27.0 - -# Excel -openpyxl>=3.1.0 - -# Web Framework -fastapi>=0.115.0 -uvicorn>=0.30.0 - diff --git a/scripts/__pycache__/manual_step5_crawl.cpython-312.pyc b/scripts/__pycache__/manual_step5_crawl.cpython-312.pyc deleted file mode 100644 index e2b2d3a881533130342cde5f1f3a5fdbcc2f6787..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8409 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 tuple[int, int]: - db = SessionLocal() - try: - crawl_task = db.query(CrawlTask).filter(CrawlTask.company_name == company_name).first() - if crawl_task is None: - crawl_task = CrawlTask( - company_name=company_name, - config_step=4, - config_status="success", - crawl_status="pending", - ) - db.add(crawl_task) - db.flush() - else: - crawl_task.config_step = 4 - crawl_task.config_status = "success" - crawl_task.crawl_status = "pending" - - task_crawl = TaskCrawl( - crawl_task_id=crawl_task.id, - status="pending", - max_retry=1, - input_config=SAMSUNG_CONFIG, - ) - db.add(task_crawl) - db.commit() - return crawl_task.id, task_crawl.id - finally: - db.close() - - -def print_result(crawl_task_id: int, task_crawl_id: int) -> None: - db = SessionLocal() - try: - task = db.query(TaskCrawl).filter(TaskCrawl.id == task_crawl_id).first() - inserted_count = db.query(JobData).filter(JobData.task_crawl_id == task_crawl_id).count() - - print() - print("Manual Step5 crawl finished") - print(f"crawl_task_id: {crawl_task_id}") - print(f"task_crawl_id: {task_crawl_id}") - print(f"status: {task.status if task else 'missing'}") - print(f"crawled_count: {task.crawled_count if task else None}") - print(f"inserted_count: {inserted_count}") - if task and task.error_message: - print(f"error_message: {task.error_message}") - - print() - print("Check task status:") - print( - "SELECT id,status,crawled_count,error_message,started_at,finished_at " - "FROM table_comple.app_task_crawl " - f"WHERE id = {task_crawl_id};" - ) - print() - print("Check saved jobs:") - print( - "SELECT id,job_title,company,recruit_category,created_at " - "FROM table_comple.app_job_data " - f"WHERE task_crawl_id = {task_crawl_id} " - "ORDER BY id LIMIT 50;" - ) - finally: - db.close() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Create one formal Step5 crawl task and run job_step5_crawl()." - ) - parser.add_argument( - "--company", - default=f"manual_samsung_test_{datetime.now():%Y%m%d_%H%M%S}", - help="Company name saved into app_crawl_task/app_job_data.company.", - ) - parser.add_argument( - "--headless", - action="store_true", - help="Run Chromium in headless mode. Default opens a visible browser.", - ) - return parser.parse_args() - - -async def main() -> None: - args = parse_args() - settings.browser_headless = bool(args.headless) - - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)s %(name)s: %(message)s", - ) - - crawl_task_id, task_crawl_id = create_manual_task(args.company) - print(f"Created formal Step5 task: crawl_task_id={crawl_task_id}, task_crawl_id={task_crawl_id}") - - db = SessionLocal() - try: - with temporarily_pause_other_pending_tasks(db, task_crawl_id) as paused_ids: - if paused_ids: - print(f"Temporarily paused other pending Step5 tasks: {paused_ids}") - await job_step5_crawl() - finally: - db.close() - - print_result(crawl_task_id, task_crawl_id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/service.sh b/service.sh deleted file mode 100644 index 73f3f5ec..00000000 --- a/service.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -# 配置 -APP_NAME="crawler" -APP_DIR="/opt/app/crawler" -VENV_DIR="$APP_DIR/venv" -LOG_FILE="$APP_DIR/app.log" -PID_FILE="$APP_DIR/app.pid" -DISPLAY_NUM=":1" - -# 启动 -start() { - if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then - echo "$APP_NAME 已经在运行 (PID: $(cat $PID_FILE))" - return 1 - fi - - echo "启动 $APP_NAME..." - cd "$APP_DIR" - source "$VENV_DIR/bin/activate" - nohup env DISPLAY=$DISPLAY_NUM python -m uvicorn src.main:app --host 0.0.0.0 --port 8000 > "$LOG_FILE" 2>&1 & - echo $! > "$PID_FILE" - echo "$APP_NAME 已启动 (PID: $!)" -} - -# 停止 -stop() { - if [ ! -f "$PID_FILE" ]; then - echo "$APP_NAME 未运行" - return 1 - fi - - PID=$(cat "$PID_FILE") - if kill -0 "$PID" 2>/dev/null; then - echo "停止 $APP_NAME (PID: $PID)..." - kill "$PID" - rm -f "$PID_FILE" - echo "$APP_NAME 已停止" - else - echo "$APP_NAME 未运行,清理 PID 文件" - rm -f "$PID_FILE" - fi -} - -# 重启 -restart() { - stop - sleep 2 - start -} - -# 状态 -status() { - if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then - echo "$APP_NAME 运行中 (PID: $(cat $PID_FILE))" - else - echo "$APP_NAME 未运行" - fi -} - -# 查看日志 -logs() { - tail -f "$LOG_FILE" -} - -# 使用说明 -usage() { - echo "用法: $0 {start|stop|restart|status|logs}" -} - -# 主逻辑 -case "$1" in - start) start ;; - stop) stop ;; - restart) restart ;; - status) status ;; - logs) logs ;; - *) usage ;; -esac diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 8d5a8134..00000000 --- a/src/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""src 包""" diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0349316e463f9c6904605768037f249ba1bdedcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmX@j%ge<81YI4xS^PlyF^B^Lj8MjB4j^MXLkdF_LnWgoQz<);TU?Z^@U*AZPm}Q$ zdwhIKesX;LN`}uM4Zrl9tztrpQ;UjYvh(v&V_Z^;vrF;|V)E0{Qi}=_Gh={SW8&j8 s^D;}~_l1oWK+-i+?v4KWuB|-vfO9)Z|YAvJH;vM7les*^a z*e4%J)hfsKm}4vW9li7dX(gPIB1MXN=*=jcdg`pPu_F*F?H+cXnR(}V-{+Z+-&mFg z`1@{`Ex)Azcw#Q=03?6}43xti zif$|W@pz=$?;t!H?ci=J2jrl9O(7IwpV|wYj^{#`u!pX~zEAJ@+nlt=DKm0dW-qR! zCziP+f8;9pFH{%4guU;7v$L15N3X)(<45EY&i?<%+2?V}1M;98!weUkjGp{a7|Qj- zaBct&%0ucM0PF?4+J{%i_J{Y#_72Bjto(8JWOY!P>!5DC*M{X(cdUaRfb|#vxj4=+ z3ghKWXT00)5vP~{-w*pib)-6yjQ`~gB?&~&mn8SG1&z5ca4&d9qhF7mro3nHfMn1*O>lwrY51`xF&;fRW@p@446 zP(#-wOA%FAMT`q|3)+%fu^|e#CPj5yvT?#u*{p0xCdYGG&azEzN>FjuG|y*~e%rhH zw)ZmFjQ8{IbM1P`buH)aiW*X)&Z2Z0g6l{0A zrwP%HUBKoVMP<|W-ao`hig>ooH12e^ID&ESF8kM}L;(d>Y;#IL)M9I2ge)Sz5|B5= zBjyP?oW#3?~}V_)&D?Fgno~7=KaxSwFaS@1Xy)6DCNJb^mb`ga&(--8#SQBoqjd z^#D!~9&WJ72K_-JJa56}2{o;TF!PYW*|9o!I_K5`&ql(jkWKu~jvcYL|6p(L c60azBS9&f#lYd~2eIPQ?WPty=jbCc;4}X36!vFvP diff --git a/src/bash_model.py b/src/bash_model.py deleted file mode 100644 index dc869c15..00000000 --- a/src/bash_model.py +++ /dev/null @@ -1,91 +0,0 @@ -from langchain_openai import ChatOpenAI -from openai import OpenAI - -from src.config import settings - -################################ -##### 基础模型 -################################ - - -V3_2 = ChatOpenAI( - base_url=settings.ai_base_url, - model="deepseek-v3-2-251201", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature, - model_kwargs={"response_format": {"type": "json_object"}} # 强制 JSON 模式 -) - -Pro32 = ChatOpenAI( - base_url=settings.ai_base_url, - model="doubao-1-5-pro-32k-250115", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature -) - -Gemini25 = ChatOpenAI( - base_url="https://api-i.xykjy.com/v1", - model="gemini-2.5-flash", - api_key="sk-8NxoLe7ZTJveGSmtPENBm4NwN9ai4YLGw8y6fqueZrPTo4Uu", - temperature=settings.ai_temperature -) - -K2 = ChatOpenAI( - base_url=settings.ai_base_url, - model="kimi-k2-thinking-251104", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature, - model_kwargs={ - "response_format": {"type": "json_object"}, - } # 强制 JSON 模式 -) - -V3_1 = ChatOpenAI( - base_url=settings.ai_base_url, - model="deepseek-v3-1-terminus", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature, - model_kwargs={ - "response_format": {"type": "json_object"} - } # 强制 JSON 模式 -) - -Seed_2_Code = ChatOpenAI( - base_url=settings.ai_base_url, - model="doubao-seed-2-0-code-preview-260215", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature -) - - -Seed_Code = ChatOpenAI( - base_url=settings.ai_base_url, - model="doubao-seed-code-preview-251028", - api_key=settings.ai_api_key, - temperature=settings.ai_temperature -) - -Opus = ChatOpenAI( - base_url="https://ai.hhhl.cc/v1", - model="claude-opus-4-5-20251101", - api_key="sk-txcM6uEk0LBGFUmw70sCdeeVoVrP7LxX", - temperature=settings.ai_temperature -) - - - - - -# 通用模型 -GeneralLlm = V3_2 - -# 分析主要模型 -AnalyseLlm = K2 - -if __name__ == "__main__": - client = OpenAI( - base_url="https://ai.hhhl.cc/v1", - api_key="sk-txcM6uEk0LBGFUmw70sCdeeVoVrP7LxX" - ) - for m in client.models.list().data: - print(m.id) diff --git a/src/browser/__init__.py b/src/browser/__init__.py deleted file mode 100644 index e46926d7..00000000 --- a/src/browser/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -"""全局浏览器管理""" -import random -from playwright.async_api import async_playwright, Browser, BrowserContext, Playwright -from playwright_stealth import Stealth -from src.config import settings - -_playwright: Playwright | None = None -_browser: Browser | None = None - -# 指纹配置池 -VIEWPORTS = [ - {"width": 1920, "height": 1080}, - {"width": 1366, "height": 768}, - {"width": 1536, "height": 864}, - {"width": 1440, "height": 900}, - {"width": 1280, "height": 720}, - {"width": 1600, "height": 900}, - {"width": 1680, "height": 1050}, - {"width": 1280, "height": 800}, - {"width": 1024, "height": 768}, - {"width": 1920, "height": 1200}, - {"width": 2560, "height": 1440}, - {"width": 1360, "height": 768}, - {"width": 1280, "height": 1024}, - {"width": 1600, "height": 1200}, - {"width": 1440, "height": 960}, -] - -USER_AGENTS = [ - # Chrome Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", - # Chrome Mac - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - # Firefox Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:119.0) Gecko/20100101 Firefox/119.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", - # Firefox Mac - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0", - # Safari Mac - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", - # Edge Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0", - # Chrome Linux - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", - # Firefox Linux - "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0", - "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", -] - -LOCALES = [ - "zh-CN", "zh-TW", "zh-HK", - "en-US", "en-GB", "en-AU", "en-CA", - "ja-JP", "ko-KR", -] - -TIMEZONES = [ - "Asia/Shanghai", "Asia/Hong_Kong", "Asia/Taipei", "Asia/Tokyo", "Asia/Seoul", - "America/New_York", "America/Los_Angeles", "America/Chicago", - "Europe/London", "Europe/Paris", "Europe/Berlin", - "Australia/Sydney", -] - - -async def get_browser() -> Browser: - """获取全局浏览器实例""" - global _playwright, _browser - - if _browser is None: - _playwright = await async_playwright().start() - _browser = await _playwright.chromium.launch( - headless=settings.browser_headless, - args=[ - "--disable-blink-features=AutomationControlled", # 隐藏自动化标志 - ] - ) - - return _browser - - -async def create_stealth_context() -> BrowserContext: - """创建带随机指纹的隐身 context""" - browser = await get_browser() - - # 随机选择指纹配置 - context = await browser.new_context( - viewport=random.choice(VIEWPORTS), - user_agent=random.choice(USER_AGENTS), - locale=random.choice(LOCALES), - timezone_id=random.choice(TIMEZONES), - ) - - # 应用 stealth - stealth = Stealth() - await stealth.apply_stealth_async(context) - - return context - - -async def close_browser(): - """关闭全局浏览器""" - global _playwright, _browser - - if _browser: - await _browser.close() - _browser = None - - if _playwright: - await _playwright.stop() - _playwright = None diff --git a/src/browser/__pycache__/__init__.cpython-312.pyc b/src/browser/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b755f720258527bdecf590335f1c3a8d1068b35d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6048 zcmc&2TWlN0agXnZNJ^p}(fcGniI|iq>*2&nTS`%6M-Qu({3s#ddgh(TlgK;a?r2$8 z3m{@Qk(;2hQ6O?%qi_N@P2nVHQ50=mps4-Z9}E1TaaB+Qjr{l{EEhHW)!8GDvJ^{} zQ`=qXW@cx0XSj1Sv$y+qm&<{m{bbfPE>WenJPQ<=wv+KN7akEZtqgtMqhlf_#+@SpICB@^f2k|fWN{)6P( zoXCy|C!pehRC!j7P@y)^2-0WOh+pdZ8nN_QHMT-htgjJEm626`+$cc;!S+r2^4n;A zxif(5i1c`UK^rm<|4a%oaA;!fL;LGV3ay-NQ4h zf^dUmeoobc>|vrS-x<5r-gq^eu3($f^VFK;eF`AFYXijl!-F*TdrTPFUPOU#ebCWEm zIC33fVp_=4!>o{uE5)G%D{>LKd61o=ejtbwidX0K3lbILqimd&6pzmAj=|ulptyRH zqADY6UZfDDk=R3LKDd`-Z*8e)}-$L?3p+Q}l&#itw3|H8A1sPye_y z_tBaI;?kGD{Q9pS3@U!VpW!6>Se*4A0|!p{$5J^4Uauh!?TTRI?=0qAc1mZHwTe6qih>mN=c{p@)5;7Uxfkl&pe7i)K=YXh0uwuRcZbZtkbqGQ&%ShY1%oVnmh4R3y;_qgShEjLdjH=t0k-6IOA7n0b%-SBfQBhgyK(+)`c0SA+ zfcHph1WWHWgBQU7YupdsrvwR$`HuBL2RIaTT zRWs-$8plpv)8s;Aju)bCWr$pezbknQPZ_UW$l&>#9AAjrO^z=_w?6)J!xfy3(ALKF z3?b{6KuGt>jNznV+?yN!I6O({`qpY8&`CneiccUbUWLb@Wri`i9p*31{$lAbSC{_u z>Aeecw=Z73{o(s}uKxMXh12&goV|PX3K_{=iQo$q+i{ManiNF1A4x)xQgoE%Wr%Dc zLdRLvDRT){fv}}G#^=zYP<8T9fxAQd7pfs3%J{qX*tBSQxcjtr~LL&H4> zsZejvAatz#2f9Q3J;SPfWdA@<_`qP#un4y*NcGZie=$u-x!V{@yUy_$Rck&YRI6^u znja$JSRqMLti;nRA|j+UTbQ38E&4!6!z0ZB(DLQ$vWA}xWZaFZ-mC?c*JjFgER^k7 zEUBEYJ6CscI$h#h^j6KkdG5{2HEFMZ(NjJ*b7tn^k+f&qVp;Y4z`23TzI55HtR0o_ zgv2SgK;n2Tson?0$W!qFm+HOgD9JdsEjYGail!Zbj45!#6u9LqO$}!2kf-Tk)&j#H zW=oN|`H|EHzv*3f*K5RCqHDM5TxbuF*X=}CyXm^43DdiXu1?c+zZKK%1csd$u1YrS zhEwn?e+po2sTb>4%Knp*OAotm1{FilNX`c?RKJE5dK|y6HElyZhG}A&fWX{U2u?%( zM(_9c=D%1|wjma^daIfvaRCBRE43yGDy4{T0r52|%Dr-zHlR=${UE@LmEHlMz3G4B z%DCvRnB&jzX?K0bR)51*f74qzYg@9GF1o8S?)n9HecDZCY~&3ad9$MWj1wxN&6~9% zd&9%54cQy><*zr0n;%ZuJ;a&inm@$!SPg?-!XdH(Tr?%FcH#BS2ZQnDAcW Pml2@l?GBpf?#e^2878S>2=jWxyxTF?mm*f}3=g761|-m>C%v?=sju;FiC@rO?P;#10e$ E0G=jGb^rhX diff --git a/src/company_generator/__pycache__/generator.cpython-312.pyc b/src/company_generator/__pycache__/generator.cpython-312.pyc deleted file mode 100644 index 650635c8a2bacff76a1ba9da62d0c9451040e399..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5239 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 dict: - """ - 生成公司名称并插入数据库 - - Args: - count: 生成数量,默认使用配置值 - - Returns: - {"inserted_count": N, "skipped_count": N, "inserted_companies": [...]} - """ - if count is None: - count = settings.generate_company_count - - db = SessionLocal() - - try: - # 1. 检查公司总数 - total = db.query(CrawlTask).count() - if total >= settings.max_company_count: - logger.info(f"公司总数已达上限 {total}/{settings.max_company_count},跳过生成") - return { - "inserted_count": 0, - "skipped_count": 0, - "inserted_companies": [], - "reason": "达到上限" - } - - # 2. 查询所有已有公司名 - existing_rows = db.query(CrawlTask.company_name).all() - existing_set = {row[0] for row in existing_rows} - existing_list = list(existing_set) - - # 3. 随机抽样传给 LLM(最多2000个,不足则全部) - sample_size = min(2000, len(existing_list)) - sample_companies = random.sample(existing_list, sample_size) if existing_list else [] - - # 4. 调用 LLM - prompt = USER_PROMPT.format( - count=count, - existing_companies="、".join(sample_companies) if sample_companies else "无" - ) - - chain = K2 | PydanticOutputParser(pydantic_object=CompanyList) - - result = chain.invoke([ - {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": prompt} - ]) - - # 5. 遍历插入 - inserted = [] - skipped = 0 - - for company_name in result.companies: - company_name = company_name.strip() - - # 长度校验 - if not (2 <= len(company_name) <= 30): - skipped += 1 - logger.debug(f"跳过(长度不符): {company_name}") - continue - - # 内存去重 - if company_name in existing_set: - skipped += 1 - logger.debug(f"跳过(已存在): {company_name}") - continue - - savepoint = None - try: - # 使用 savepoint 避免单条失败影响整个事务 - savepoint = db.begin_nested() - - # 插入主表 - crawl_task = CrawlTask(company_name=company_name) - db.add(crawl_task) - db.flush() # 获取 ID - - # 插入 Step1 任务 - task_search = TaskSearch( - crawl_task_id=crawl_task.id, - input_company_name=company_name - ) - db.add(task_search) - - savepoint.commit() - inserted.append(company_name) - existing_set.add(company_name) # 防止本批次重复 - - except Exception as e: - # 唯一约束冲突或其他错误,回滚 savepoint - if savepoint: - savepoint.rollback() - skipped += 1 - logger.warning(f"插入失败 {company_name}: {e}") - continue - - db.commit() - - logger.info(f"生成完成: 插入 {len(inserted)} 个,跳过 {skipped} 个") - return { - "inserted_count": len(inserted), - "skipped_count": skipped, - "inserted_companies": inserted - } - - except Exception as e: - logger.error(f"生成异常: {e}") - db.rollback() - raise - finally: - db.close() diff --git a/src/company_generator/prompts.py b/src/company_generator/prompts.py deleted file mode 100644 index f766477c..00000000 --- a/src/company_generator/prompts.py +++ /dev/null @@ -1,35 +0,0 @@ -"""公司生成提示词""" - -SYSTEM_PROMPT = "你是一个企业招聘信息专家,熟悉中国各行业的知名企业。" - -USER_PROMPT = """ -请生成 {count} 个中国企业名称。 - -## 要求 -1. 企业必须真实存在 -2. 企业有自己的官方网站(不是仅在招聘平台) -3. 官网上有校园招聘或社会招聘入口 -4. 行业不限:互联网、金融、制造、零售、医疗、教育、能源等均可 -5. 规模不限:大型企业、独角兽、上市公司、知名民企均可 - -## 命名规范 - - 使用公司最常用的简称 - - 不加"集团"、"有限公司"、"股份有限公司"、"科技"、"网络"等后缀 - - 不加"(中国)"、"(北京)"、"(上海)"等地域标注 - - 2-15个字符 - - 正确示例:华为、比亚迪、宁德时代、海尔、格力、中国平安、招商银行、顺丰、海底捞、蔚来 - 错误示例:华为技术有限公司、比亚迪股份有限公司、北京字节跳动科技有限公司 - -## 下面是已有公司,需要排除 (如果下方显示“无”,则表示没有需要排除的公司) - -{existing_companies} - - -## 输出格式 -**严格只输出有效的 JSON 对象,不要有任何额外文字、解释或标记。** - -输出格式示例: -{{"companies": ["公司名1", "公司名2", "公司名3"]}} - -""" diff --git a/src/company_importer/__init__.py b/src/company_importer/__init__.py deleted file mode 100644 index b2d1ccb7..00000000 --- a/src/company_importer/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.company_importer.importer import import_companies - -__all__ = ["import_companies"] diff --git a/src/company_importer/__main__.py b/src/company_importer/__main__.py deleted file mode 100644 index 7eab6589..00000000 --- a/src/company_importer/__main__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""测试入口: python -m src.company_importer""" -import json -import logging - -from src.company_importer import import_companies - -logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s %(message)s") - -if __name__ == "__main__": - result = import_companies( - file_path=r"C:\Users\sim18\Downloads\见识星球-校招投递链接(1.13).xlsx", - company_col=3, # C 列 - url_col=9, # I 列 - ) - print(json.dumps(result, ensure_ascii=False, indent=2, default=str)) diff --git a/src/company_importer/importer.py b/src/company_importer/importer.py deleted file mode 100644 index 5f5bddd0..00000000 --- a/src/company_importer/importer.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Excel 公司名单导入""" -import logging -from pathlib import Path - -from openpyxl import load_workbook - -from src.database import SessionLocal, CrawlTask, TaskSearch -from src.company_importer.url_validator import extract_url, validate_url, clean_url - -logger = logging.getLogger(__name__) - - -def import_companies( - file_path: str, - company_col: int = 1, - url_col: int = 2, -) -> dict: - """ - 从 Excel 导入公司名单并创建爬虫任务 - - Args: - file_path: Excel 文件完整路径(.xlsx) - company_col: 公司名所在列索引(从1开始) - url_col: 招聘地址所在列索引(从1开始) - - Returns: - 导入统计结果 - """ - # 1. 验证文件 - path = Path(file_path) - if not path.exists(): - raise FileNotFoundError(f"文件不存在: {file_path}") - if path.suffix.lower() != ".xlsx": - raise ValueError(f"仅支持 .xlsx 格式,当前文件: {path.suffix}") - - # 2. 读取 Excel - wb = load_workbook(file_path, read_only=False) # read_only=False 以读取 hyperlink - ws = wb.active - - # 验证列索引 - max_col = ws.max_column or 0 - if company_col > max_col or url_col > max_col: - wb.close() - raise ValueError(f"列索引超出范围,表格共{max_col}列,company_col={company_col}, url_col={url_col}") - - # 统计计数 - stats = { - "total_rows": 0, - "inserted_count": 0, - "skipped_empty_name": 0, - "skipped_empty_url": 0, - "skipped_email": 0, - "skipped_weixin": 0, - "skipped_invalid_url": 0, - "skipped_duplicate": 0, - "inserted_companies": [], - } - - # 3. 查询已有公司名(内存去重) - db = SessionLocal() - try: - existing_rows = db.query(CrawlTask.company_name).all() - existing_set = {row[0] for row in existing_rows} - - # 4. 逐行处理(跳过表头) - for row_idx, row in enumerate(ws.iter_rows(min_row=2), start=2): - stats["total_rows"] += 1 - - # 4.1 读取公司名 - company_cell = row[company_col - 1] - company_name = str(company_cell.value).strip() if company_cell.value else "" - - if not company_name: - stats["skipped_empty_name"] += 1 - logger.debug(f"第{row_idx}行: 公司名为空,跳过") - continue - - # 4.2 读取并提取 URL - url_cell = row[url_col - 1] - raw_url = extract_url(url_cell) - - if not raw_url: - stats["skipped_empty_url"] += 1 - logger.debug(f"第{row_idx}行: {company_name} 地址为空,跳过") - continue - - # 4.3 验证 URL - is_valid, reason = validate_url(raw_url) - if not is_valid: - if reason == "email": - stats["skipped_email"] += 1 - elif reason == "weixin": - stats["skipped_weixin"] += 1 - elif reason == "empty_url": - stats["skipped_empty_url"] += 1 - else: - stats["skipped_invalid_url"] += 1 - logger.debug(f"第{row_idx}行: {company_name} 地址无效({reason}): {raw_url}") - continue - - # 4.4 清理 URL - url = clean_url(raw_url) - - # 4.5 内存去重 - if company_name in existing_set: - stats["skipped_duplicate"] += 1 - logger.debug(f"第{row_idx}行: {company_name} 已存在,跳过") - continue - - # 4.6 单条事务入库 - try: - # 插入主表 - crawl_task = CrawlTask(company_name=company_name) - db.add(crawl_task) - db.flush() - - # 插入 Step1 任务(带 input_url) - task_search = TaskSearch( - crawl_task_id=crawl_task.id, - input_company_name=company_name, - input_url=url, - ) - db.add(task_search) - - db.commit() - stats["inserted_count"] += 1 - stats["inserted_companies"].append(company_name) - existing_set.add(company_name) - - except Exception as e: - db.rollback() - stats["skipped_duplicate"] += 1 - logger.warning(f"第{row_idx}行: {company_name} 插入失败: {e}") - continue - - logger.info( - f"导入完成: 总{stats['total_rows']}行, " - f"成功{stats['inserted_count']}条, " - f"空名{stats['skipped_empty_name']}, " - f"空地址{stats['skipped_empty_url']}, " - f"邮箱{stats['skipped_email']}, " - f"微信{stats['skipped_weixin']}, " - f"无效{stats['skipped_invalid_url']}, " - f"重复{stats['skipped_duplicate']}" - ) - - return stats - - except Exception as e: - logger.error(f"导入异常: {e}") - db.rollback() - raise - finally: - db.close() - wb.close() diff --git a/src/company_importer/url_validator.py b/src/company_importer/url_validator.py deleted file mode 100644 index 28de2966..00000000 --- a/src/company_importer/url_validator.py +++ /dev/null @@ -1,88 +0,0 @@ -"""URL 清洗与验证""" -from urllib.parse import urlparse - - -# 需要过滤的域名 -BLOCKED_DOMAINS = [ - "mp.weixin.qq.com", -] - - -def extract_url(cell) -> str | None: - """ - 从 Excel 单元格提取真实 URL - 优先取 hyperlink 地址,其次取文本值 - - Args: - cell: openpyxl 单元格对象 - - Returns: - 提取到的 URL 字符串,无效则返回 None - """ - url = None - - # 优先取超链接地址 - if cell.hyperlink and cell.hyperlink.target: - url = str(cell.hyperlink.target).strip() - - # 其次取单元格文本值 - if not url and cell.value: - url = str(cell.value).strip() - - return url if url else None - - -def validate_url(url: str) -> tuple[bool, str]: - """ - 验证 URL 是否合法 - - Args: - url: 待验证的 URL - - Returns: - (是否合法, 不合法的原因) - """ - if not url: - return False, "empty_url" - - # 过滤邮箱 - if "@" in url: - return False, "email" - - # 补全协议头 - if not url.startswith(("http://", "https://")): - url = "https://" + url - - # 解析 URL - try: - parsed = urlparse(url) - except Exception: - return False, "invalid_url" - - # 检查基本结构:有协议、有域名、域名含点(如 example.com) - if not parsed.scheme or not parsed.netloc or "." not in parsed.netloc: - return False, "invalid_url" - - # 过滤黑名单域名 - domain = parsed.netloc.lower() - for blocked in BLOCKED_DOMAINS: - if blocked in domain: - return False, "weixin" - - return True, "" - - -def clean_url(url: str) -> str: - """ - 清理并补全 URL - - Args: - url: 原始 URL - - Returns: - 清理后的 URL - """ - url = url.strip() - if not url.startswith(("http://", "https://")): - url = "https://" + url - return url diff --git a/src/config/__init__.py b/src/config/__init__.py deleted file mode 100644 index 80eacd57..00000000 --- a/src/config/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.config.config import settings, get_settings - -__all__ = ["settings", "get_settings"] diff --git a/src/config/__pycache__/__init__.cpython-312.pyc b/src/config/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 053f5bbd4bdfeefcd715c7cf0460de163c719d68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 263 zcmX@j%ge<81YI4xSqVV;F^B^LOi;#W86aaiLkdF*V-7;Lkd$0b2?KL zb0v!=>r0SQO{QBM#i=DFnR)5Ow|LT1OX6X?B4(g;5ety;(`30NSX`8>mzV15Rmx5%*e=ipTXn-x9Ehr3tVcA J>_r?vVE`*=Mcx1a diff --git a/src/config/__pycache__/config.cpython-312.pyc b/src/config/__pycache__/config.cpython-312.pyc deleted file mode 100644 index fb1dac546425d5f74784ffc0d66f71e6d1326eb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 728 zcmZWm&ubG=5T2K}n{2vC3vH?*A_-A32h)SagGw*92QNh^3JnWO$-b1BOJ#lRx-BWr7QGDp-kN z+Fadee5tyt@7?F`nqE6aoS|_x$4p)>vXVW;Y&&0?wXi&RM{uK(we!5xt7d(`&Lg3x z__YmmTM+INf+AT00*gqaa2Y`K*`dxfx*nY=u*3h?l3;s?9lHQRMj>j7s`c%U?)ImyoTIOs?#oy1?w|MOdg$mwGsa_oEm&^m2Vj7}`?n#;iiyirGJ zm5k8IRLBYGy1}!$?wmp<=`NEritb&vBgh&4r-{q(Z`@Vv(Q}j@+Z||Q4kFsA-Ynv* p$;QMmrrtuVbLfx`AwOWS2G?pZdK?Us#ILguIrC)fks@L^@E3iT%uN6Q diff --git a/src/config/__pycache__/development.cpython-312.pyc b/src/config/__pycache__/development.cpython-312.pyc deleted file mode 100644 index bea4dd51c00660978b0410d052f6327a3bdad605..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1008 zcmYk5&ubGw6vtmeQRBt`U8s9rqzX3~NK`}Td__r0ArKlV#16-O{0@22iv z3?cMg!RZK}ILBAPd4UMB5iv-pX|zIi$Uq^qiir6T5sQZ2nf+Yb+z#g>?O~w9Pu;_3 z&wo7qaQNoI&qq&we188^<>U<+yF-O=y^WyWFY4QqFQDT+;1`G@+khEE>oBKnl8|lD zFpUs%5maK?QB7e@5j&Ey!Lz~QRQBRyMpq&Ku|Ak7E(2g{z*V(z;Ak(vz`T6o>jndNOOruDimku3gu#n=}K#;Zz0q8EZSUs3l>>d`a(8w2&c1c2m&x|MY%e?3&m8MzbA2z@%Z@AWt2NF? zPiI}rTl^e=%s`89&Z@;iaPpII^<(rqyr4W?HKwv-i~W7R&U1hUJV6%Bk!cvl0XqK; RP5h3ejVmuF{vdei!hcDWA7%gm diff --git a/src/config/__pycache__/production.cpython-312.pyc b/src/config/__pycache__/production.cpython-312.pyc deleted file mode 100644 index 01ef08d262bea9f5550cd49cc2bbde8d0064279f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1006 zcmYk5&ubGw6vt2^;D={Jo#plf&+Q`KJWWp=Ixu=FNs7H!Fs%( z*c=WZ^j*Nk3U-|HtKb|W3+c#GtUyhv2lRk~0%#do>LX-nB=Amc=j!TCFcWI@0i8U1 z_2biC%20{22~ZqBe=n8>n~r3_3&lPg{nb}-1pUY2`c-X4o z4eI&!FKAnmOPS9#MybzZAT~V09HUNnC|@W}mu6(pH9YSDWfqV7Mx{nDwBgo>lq>^6 z(8fxpmq#!br5N)l#&sAJq+%Y&_)bLqM1jMkpYCAql3hWU^T(r0794z{KZE3!|2Cv|ix2QwPw}`jpQ@2d3 zRl;1uE_=)@o7Ab=>t#5l&Qa#v9VYI%G{TXWat|GAgRSm?=F)!ukv4D^LaFP`I|u5K zw$Mr~Ht!xlbg?y(Zmt~MKGM>l4~#XJ_g9a!%dP%Y+e@`lqwUPmRw~{0(yi3E@V;u} zY~*6sm5jy)0eC-j2*)fs%z2$pgo)3h-QlqCWYwt1jx4mte1oL{75I56FlVZwD932% T8=CkXN-9^L5B)*#kcIyMTUj3+ diff --git a/src/config/__pycache__/settings.cpython-312.pyc b/src/config/__pycache__/settings.cpython-312.pyc deleted file mode 100644 index 4c9fbe8f3a08bcdeab38f47154278a1cf28b7951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2089 zcmZXUO>7%Q6vub%^+yvscJt+YIgXt)shcWEKLS!!0a9rzK`9a?t+ZGgZFa}WCXRPI zvrgiwLO!%og;cPFfB*u@fdi^kwI@U?M2{d2Y~(}SIaNaa2&d8~E-`sujZDsT7^(Q~wdUEHU?oAgjzXw|jGS7u2Rfc@N8OWyk4WFvu01g`d;}B*j zYDiE(P*4pE3VF0gP*~6&wFS4rycSQ32(496M2+G$(4w9e6Iz=`aX~SU_6mv%+N-wX z4w%<2s6*`(Svm!Esa=A)J?a*e@F*duM^LZYBdAYMzuGHkKu}Wc6Er9&rS=ON@@PO% zT2Mw!3K|wPq7Dk$Cumen2^!;shm8F=koOtcE6Po;)M~nXAUn>&=dk0L#W`AvfY8_- zzEgMZvCs*ktrRd}5t~>GSa(oSTf&zDhbE>brdVhh6KYyTCb1UHM4D})MOQwsZX)^EU{J~4$WiDC}2uA2jSyc?}9apsDtf8h+>WC^T;eZm@I3B%>dUE+xlT- zYje1%?g&Rv5wAKsLNnu0WMD@#3r%U0HwNq3&Nz*9UaY2U#@j&d=Q8}XX&I*eLc}KD zm_~#?kM%_apJ^JVQ*P$ilVCAm#>XIZ?~=BPbZg1hie-40 zijEl$!FZBK&*37kK5hmyYtzY>>HeQC60jh_uwz9X`m*`(U*{dWbqx%3zI<6)Xo@?| zmv9e&;@t#0kzq-p3{E|ZN4ew);;%11O z3>X%M>j#?1DU$<$-%DhiOIdFCDHMhuC7Oa=x+*>IHKn&ckgA-mo~$V;&^x;uv0e3!M8iw$irksg^kB~5u&Z=d zjhd1H7~StG$u(M2#_Gc(uF}5-3y str: - """获取数据库连接URL""" - return f"mysql+pymysql://{self.db_username}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_database}" diff --git a/src/crawler/__init__.py b/src/crawler/__init__.py deleted file mode 100644 index ab71b3be..00000000 --- a/src/crawler/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Crawler 模块 - 使用配置爬取岗位数据""" -from .config import CrawlerConfig, FieldSelector, MAX_PAGES, DEFAULT_TIMEOUT -from .main import crawl - -__all__ = [ - "crawl", - "CrawlerConfig", - "FieldSelector", - "MAX_PAGES", - "DEFAULT_TIMEOUT", -] diff --git a/src/crawler/__pycache__/__init__.cpython-312.pyc b/src/crawler/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 651a7875009fdb959a38b752705e540158781bc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmX@j%ge<81YI4xSt5)K439w^7+``jKAQj;(-~42q8L&bQix{IgDp@tzO7($Sor@C7b5e^Go~>B;bnbKoU4^^^S$eycdU8ZKkezJ=}-3dKAXDXS?{`+KzA@`vfSc@>T}M|OUq2Z#p{-t znv)WonvsSss82|Ih^Mcse`ts%<1N-?ke!;WMeIPc zikLwJ3y5F^5o|!hPm}W&dwhIKesX;LEw*H^71-3>V#!U+%mW((QlFTU6Cb~l;WN+# zhF@mRRxzQ)sYS&x+4*^?F)pda*(Lb}G5Kj}sYL~enK8vh$uVGeq!z`*$7kkcmc+;F z6;$5hu*uC&Da}c>E8+(l1M+RL1d#Z^%*e=ilfm>pgU@9KpSuj=pIF$qnHt%Pc!81t D_Pc!n diff --git a/src/crawler/__pycache__/config.cpython-312.pyc b/src/crawler/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 8fb4d001ccc210c4d59633cac1ab0efca91e9471..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1232 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 diff --git a/src/crawler/__pycache__/main.cpython-312.pyc b/src/crawler/__pycache__/main.cpython-312.pyc deleted file mode 100644 index dd58ff6304d42d10becd057c1545ef1fb6d31ef8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2839 zcma)8TWlN06`kR}`4B0JFVl)bN^w-irer%!?8vHJTUseV0q0>Lh4oRE=B}kx`4EOn z&_byqh?7DoY*mg_6&fR;@+_^JzcK_^fSP_(;OgcvU1%&=a2aO`ug*(rJFpU&6gcPh;k~n1< zvS6ez3Y%n7>=0|pT#`%iLwrgY5>n!j2s)d>tE?iZk|L^B)wTstj`NE2GB;!&w?^!_ zCQ$T|cs{9;(8??CtepAfZ8~EF8?JbEJe`P*CFA3Hq79Gew{h)(qi)}qNgvmSBaG3M zpa3zVDjHD}y5Xpe(`r7Z$B#9JS~@lsA65-MVfq>3h#FUtYBp=|MAdU7ov_fE>jm%wMzT3;d|G$ry(BwHG8*5JqiZ_b6Bt zPT?;J7hzSjtk1;NBEvK{qL~rV#BkK?Hns&(kuF&;*7LBAz^@}PitPvAK#2JC=7wKE zDDVYzzgZ`wbbo~*h}2lk49m!t0GePFL9-QTcPnhMOD=;xNkPQ8Q+66_64z@~D) zTw|25z}tghQ@_Zg z6UmxUpam2 zgPJXU*r1V~(CWG0u1x)Ub^5}}uYSKe^Y+Rg&U^*0vddkB_N`BbZQu;$~M9?PY5Eolh4mQpi0-Dn=o z9E+L$Sv3iCm?4_0lQJry_sS60X;Nqqq)?~ETq_u?zV+U%^Y7exBNX(qgaq8T|J>2A|04q%MNGsjL~d{c|1d6jff0MK9NwzbS;x6 zJRlNEYlOaQ4C@Q=WKNX{$q<7}Br{pn;KqoS)+07T$;?axp#($d4KVPSAsi!_d{!lf z(1?JgvlBOcE)X@FOX^tzt655Ibj#}HUQ3oFy3Ln`O1e{VE!{OX4wKTi34xR$8ddfq zpyN-`+KZ^_L$>BA@2k-l`zrp2ifucW{kw{+=ce=FvNQY_XZXT%mB5zs*4M4&Kx8=( zncZ6qL`s3)vb%Rus01D^2evNPZIm34=?%uVEJGxeJcCJ-L~dt2(H<~Yg3);0m1Oz zdPEQu{7cU8=UELl^QA3=9r#V$-^;(nfOI}I*u?x1_aETrkRQZ3KbA2+7ck{+EHnJv zPL}GQ@XPI%D|Re%{1t~pWxr3BELWmf7WgY&ER}bAL4Q@ia)7@o2C3YRTWFUC5Vx@1Chy}Gp5zAFEDQTM&@3Vb2}e_u2q$Nx*|vc1fghE;aFkAz{gb2mSUX zuR!Hi0rWsx0}+QI#9~S&5sMj~ufFBFY9Dv;G0kL@+RdilLvV*{UxS-@fjySVB!T|4 ztZuN1me9=``YyTyPKZ28UB%v1M#&}B{bVN$=~q;CH#Bf!F#Z^MK0&)aLE*omgUjgP eKlm=(e$G)v(9L?x?!!ku)aM@ijA}?XxP1wu&Cihl diff --git a/src/crawler/__pycache__/utils.cpython-312.pyc b/src/crawler/__pycache__/utils.cpython-312.pyc deleted file mode 100644 index 9a63108281716d292bc5f49c7496f98924fa183d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8429 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 diff --git a/src/crawler/config.py b/src/crawler/config.py deleted file mode 100644 index 72274fce..00000000 --- a/src/crawler/config.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Crawler 配置定义""" -from typing import TypedDict, Literal - - -class FieldSelector(TypedDict, total=False): - """字段选择器""" - selector: list[str] # CSS选择器数组,多个选择器的结果会用换行拼接 - sample: str | None - - -class CrawlerConfig(TypedDict, total=False): - """Crawler 输入配置""" - - # 初始输入 - url: str - - # Step 2: page_analysis - job_item_selector: str - item_change_type: Literal["redirect", "new_tab", "in_page"] - - # Step 3: next_page - next_page_selector: str | None # None = 无分页 - page_change_type: Literal["url_change", "content_change", "new_tab"] - - # Step 4: detail_analysis - field_selectors: dict[str, FieldSelector] - detail_area_selector: str | None # 仅 in_page 有值 - - -# 常量 -MAX_PAGES = 10 # 最大页数 -DEFAULT_TIMEOUT = 10000 # 默认超时时间(毫秒) diff --git a/src/crawler/crawler.py b/src/crawler/crawler.py deleted file mode 100644 index 40108efb..00000000 --- a/src/crawler/crawler.py +++ /dev/null @@ -1,326 +0,0 @@ -"""三种爬取流程实现""" -import asyncio -import random - -from playwright.async_api import Page, BrowserContext -from .config import CrawlerConfig, MAX_PAGES, DEFAULT_TIMEOUT -from .utils import click_next_page, extract_data, safe_click - - -async def crawl_redirect( - page: Page, - context: BrowserContext, - config: CrawlerConfig -) -> list[dict]: - """ - 流程 A:redirect - - 特点:点击岗位后整页跳转,返回后状态丢失,需重新打开并翻页恢复 - """ - url = config["url"] - job_item_selector = config["job_item_selector"] - next_page_selector = config.get("next_page_selector") - page_change_type = config.get("page_change_type", "url_change") - field_selectors = config["field_selectors"] - - results: list[dict] = [] - page_index = 1 - item_index = 1 - - # 初始化:获取当前页岗位数 - elements = await page.query_selector_all(job_item_selector) - items_per_page = len(elements) - - while True: - # 1. 检查是否结束 - if items_per_page == 0: - break - if page_index > MAX_PAGES: - break - - # 随机睡眠 - await asyncio.sleep(random.uniform(0.2, 1.0)) - - # 2. 处理当前岗位 - try: - elements = await page.query_selector_all(job_item_selector) - if item_index - 1 < len(elements): - await safe_click(elements[item_index - 1]) - await page.wait_for_load_state("networkidle") - - # 等待详情内容加载 - desc_selectors = field_selectors.get("description", {}).get("selector", []) - first_selector = desc_selectors[0] if desc_selectors else None - if first_selector and first_selector.strip(): - try: - await page.wait_for_selector(first_selector, timeout=10000) - except Exception: - pass - else: - await asyncio.sleep(1) - - # 等待标题 - job_title_selectors = field_selectors.get("job_title", {}).get("selector", []) - first_selector = job_title_selectors[0] if desc_selectors else None - if first_selector and first_selector.strip(): - try: - await page.wait_for_selector(first_selector, timeout=10000) - except Exception: - pass - else: - await asyncio.sleep(1) - - # 等待 - await asyncio.sleep(0.5) - - # 提取数据 - data = await extract_data(page, field_selectors) - data["detail_url"] = page.url - results.append(data) - except Exception as e: - print(f"处理岗位失败 (page={page_index}, item={item_index}): {e}") - - # 3. 计算下一个位置 - item_index += 1 - - if item_index > items_per_page: - page_index += 1 - item_index = 1 - - if page_index > MAX_PAGES: - break - if not next_page_selector or not next_page_selector.strip(): - break - - # 4. 重新打开并恢复到目标页 - # SPA 网站需要 reload 或先访问空白页再返回 - try: - await page.goto("about:blank") - await page.goto(url, wait_until="load", timeout=60000) - - try: - await page.wait_for_selector(job_item_selector, timeout=10000) - except Exception: - pass # 超时也继续,让后续逻辑处理空结果 - - except Exception as e: - print(f"恢复页面失败: {e}") - return results - - # 翻页到目标页码 - for i in range(page_index - 1): - if not next_page_selector or not next_page_selector.strip(): - break - success, page = await click_next_page( - page, context, next_page_selector, page_change_type, - job_item_selector - ) - if not success: - return results - - # 重新计算当前页岗位数 - elements = await page.query_selector_all(job_item_selector) - items_per_page = len(elements) - - return results - - -async def crawl_new_tab( - page: Page, - context: BrowserContext, - config: CrawlerConfig -) -> list[dict]: - """ - 流程 B:new_tab - - 特点:点击后新标签打开详情,关闭新标签后原页面状态保持 - """ - job_item_selector = config["job_item_selector"] - next_page_selector = config.get("next_page_selector") - page_change_type = config.get("page_change_type", "url_change") - field_selectors = config["field_selectors"] - - results: list[dict] = [] - page_index = 1 - - while page_index <= MAX_PAGES: - # 1. 获取当前页岗位 - all_elements = await page.query_selector_all(job_item_selector) - items_per_page = len(all_elements) - if items_per_page == 0: - break - - # 随机睡眠 - await asyncio.sleep(random.uniform(0.2, 1.0)) - - # 2. 遍历当前页所有岗位 - for item_index in range(1, items_per_page + 1): - try: - elements = await page.query_selector_all(job_item_selector) - if item_index - 1 >= len(elements): - continue - - # 监听新标签页 - async with context.expect_page(timeout=DEFAULT_TIMEOUT) as new_page_info: - await safe_click(elements[item_index - 1]) - - new_page = await new_page_info.value - - # 等待详情内容加载 - desc_selectors = field_selectors.get("description", {}).get("selector", []) - first_selector = desc_selectors[0] if desc_selectors else None - if first_selector and first_selector.strip(): - try: - await new_page.wait_for_selector(first_selector, timeout=10000) - except Exception: - pass - else: - await asyncio.sleep(1) - - # 等待标题 - job_title_selectors = field_selectors.get("job_title", {}).get("selector", []) - first_selector = job_title_selectors[0] if desc_selectors else None - if first_selector and first_selector.strip(): - try: - await new_page.wait_for_selector(first_selector, timeout=10000) - except Exception: - pass - else: - await asyncio.sleep(1) - - # 等待 - await asyncio.sleep(0.5) - - # 提取数据 - data = await extract_data(new_page, field_selectors) - data["detail_url"] = new_page.url - results.append(data) - - # 关闭新标签 - await new_page.close() - - except Exception as e: - print(f"处理岗位失败 (page={page_index}, item={item_index}): {e}") - # 尝试关闭可能存在的新标签 - try: - pages = context.pages - if len(pages) > 1: - await pages[-1].close() - except Exception: - pass - - # 3. 翻页 - if not next_page_selector or not next_page_selector.strip(): - break - - success, page = await click_next_page( - page, context, next_page_selector, page_change_type, - job_item_selector - ) - if not success: - break - - page_index += 1 - - return results - - -async def crawl_in_page( - page: Page, - context: BrowserContext, - config: CrawlerConfig -) -> list[dict]: - """ - 流程 C:in_page - - 特点:点击后弹窗/详情区展示,通过刷新页面恢复状态,兼容性更强 - """ - url = config["url"] - job_item_selector = config["job_item_selector"] - next_page_selector = config.get("next_page_selector") - page_change_type = config.get("page_change_type", "url_change") - field_selectors = config["field_selectors"] - detail_area_selector = config.get("detail_area_selector") - - if not detail_area_selector: - raise ValueError("in_page 模式需要 detail_area_selector") - - results: list[dict] = [] - page_index = 1 - item_index = 1 - - # 初始化:获取当前页岗位数 - elements = await page.query_selector_all(job_item_selector) - items_per_page = len(elements) - - while True: - # 1. 检查是否结束 - if items_per_page == 0: - break - if page_index > MAX_PAGES: - break - - # 2. 处理当前岗位 - try: - elements = await page.query_selector_all(job_item_selector) - if item_index - 1 < len(elements): - await safe_click(elements[item_index - 1]) - - # 等待详情区域出现 - await page.wait_for_timeout(timeout=1000) - - # 随机睡眠 - await asyncio.sleep(random.uniform(0.2, 0.6)) - - # 在详情区域内提取数据 - detail_element = await page.query_selector(detail_area_selector) - if detail_element: - data = await extract_data(detail_element, field_selectors) - else: - data = await extract_data(page, field_selectors) - - data["detail_url"] = url # in_page 记录列表页 URL - results.append(data) - except Exception as e: - print(f"处理岗位失败 (page={page_index}, item={item_index}): {e}") - - # 3. 计算下一个位置 - item_index += 1 - - if item_index > items_per_page: - page_index += 1 - item_index = 1 - - if page_index > MAX_PAGES: - break - if not next_page_selector or not next_page_selector.strip(): - break - - # 4. 刷新页面恢复状态 - try: - await page.goto("about:blank") - await page.goto(url, wait_until="load", timeout=60000) - try: - await page.wait_for_selector(job_item_selector, timeout=DEFAULT_TIMEOUT) - except Exception: - pass - except Exception as e: - print(f"恢复页面失败: {e}") - return results - - # 翻页到目标页码 - for i in range(page_index - 1): - if not next_page_selector or not next_page_selector.strip(): - break - success, page = await click_next_page( - page, context, next_page_selector, page_change_type, - job_item_selector - ) - if not success: - return results - - # 重新计算当前页岗位数 - elements = await page.query_selector_all(job_item_selector) - items_per_page = len(elements) - - return results diff --git a/src/crawler/main.py b/src/crawler/main.py deleted file mode 100644 index 3eb9a478..00000000 --- a/src/crawler/main.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Crawler 入口""" -from playwright.async_api import async_playwright -from .config import CrawlerConfig -from .crawler import crawl_redirect, crawl_new_tab, crawl_in_page - - -async def crawl(config: CrawlerConfig, headless: bool = False) -> list[dict]: - """ - 爬取岗位数据 - - Args: - config: 爬取配置 - headless: 是否无头模式 - - Returns: - 岗位数据列表 - """ - url = config["url"] - item_change_type = config["item_change_type"] - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=headless) - context = await browser.new_context() - page = await context.new_page() - - try: - # 打开列表页 - await page.goto(url, wait_until="load", timeout=60000) - - # 等待岗位元素出现(最多等 10 秒) - job_item_selector = config["job_item_selector"] - try: - await page.wait_for_selector(job_item_selector, timeout=10000) - except Exception: - pass # 超时也继续,让后续逻辑处理空结果 - - # 根据 item_change_type 分发到对应流程 - if item_change_type == "redirect": - results = await crawl_redirect(page, context, config) - elif item_change_type == "new_tab": - results = await crawl_new_tab(page, context, config) - elif item_change_type == "in_page": - results = await crawl_in_page(page, context, config) - else: - raise ValueError(f"未知的 item_change_type: {item_change_type}") - - return results - - except Exception as e: - print(e) - - finally: - await browser.close() - diff --git a/src/crawler/utils.py b/src/crawler/utils.py deleted file mode 100644 index a201f38d..00000000 --- a/src/crawler/utils.py +++ /dev/null @@ -1,200 +0,0 @@ -"""工具函数""" -import asyncio -from playwright.async_api import Page, BrowserContext, ElementHandle, Locator -from .config import FieldSelector, DEFAULT_TIMEOUT - - -async def safe_click(target: Page | Locator | ElementHandle, selector: str | None = None, timeout: int = 30000) -> None: - """ - 安全点击:处理 hover 显示的元素 - - Args: - target: Page、Locator 或 ElementHandle - selector: 当 target 是 Page 时需要提供选择器 - timeout: 点击超时时间 - """ - # 获取 locator - if isinstance(target, Page): - if not selector: - raise ValueError("当 target 是 Page 时必须提供 selector") - element = target.locator(selector).first - elif isinstance(target, Locator): - element = target - else: - # ElementHandle: 直接操作 - await target.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await target.hover() - await asyncio.sleep(0.3) - await target.click(timeout=timeout) - return - - # Locator: 滚动 -> hover -> 点击 - await element.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await element.hover() - await asyncio.sleep(0.3) - await element.click(timeout=timeout) - - -async def is_button_clickable(page: Page, selector: str | None) -> bool: - """检查按钮是否可点击""" - # 空选择器 - if not selector or not selector.strip(): - return False - - element = await page.query_selector(selector) - if not element: - return False - - # 检查 disabled 属性 - disabled = await element.get_attribute("disabled") - if disabled is not None: - return False - - # 检查 class 是否包含 disabled - class_name = await element.get_attribute("class") or "" - if "disabled" in class_name.lower(): - return False - - # 检查是否可见 - is_visible = await element.is_visible() - if not is_visible: - return False - - return True - - -async def click_next_page( - page: Page, - context: BrowserContext, - next_page_selector: str, - page_change_type: str, - job_item_selector: str, - timeout: int = DEFAULT_TIMEOUT -) -> tuple[bool, Page]: - """ - 统一翻页函数 - - 返回: (success, page) - new_tab 模式时 page 引用会变 - """ - # 检查按钮是否可点 - if not await is_button_clickable(page, next_page_selector): - return False, page - - try: - if page_change_type == "url_change": - before_url = page.url - await safe_click(page, next_page_selector, timeout=timeout) - await page.wait_for_url(lambda url: url != before_url, timeout=timeout) - await page.wait_for_load_state("networkidle") - return True, page - - elif page_change_type == "content_change": - # 记录翻页前第一个岗位的内容 - first_item = await page.query_selector(job_item_selector) - before_text = "" - if first_item: - before_text = await first_item.inner_text() - - await safe_click(page, next_page_selector, timeout=timeout) - - # 等待内容变化 - await page.wait_for_function( - """(args) => { - const el = document.querySelector(args.selector); - return el && el.innerText !== args.text; - }""", - arg={"selector": job_item_selector, "text": before_text}, - timeout=timeout - ) - await asyncio.sleep(0.5) - return True, page - - elif page_change_type == "new_tab": - # 监听新标签页 - async with context.expect_page(timeout=timeout) as new_page_info: - await safe_click(page, next_page_selector, timeout=timeout) - - new_page = await new_page_info.value - await new_page.wait_for_load_state("networkidle") - - # 关闭原标签 - await page.close() - - return True, new_page - - else: - return False, page - - except Exception as e: - print(f"翻页失败: {e}") - return False, page - - -async def extract_data( - scope: Page | ElementHandle, - field_selectors: dict[str, FieldSelector] -) -> dict[str, str | None]: - """ - 数据提取函数 - - Args: - scope: 提取范围(整个 page 或 detail_element) - field_selectors: 字段选择器字典 - - Returns: - 提取的数据字典 - """ - data: dict[str, str | None] = {} - - for field_name, selector_info in field_selectors.items(): - try: - # 获取选择器,统一转为数组 - raw_selectors = selector_info.get("selector") or selector_info.get("selectors") or [] - if isinstance(raw_selectors, str): - selectors = [raw_selectors] if raw_selectors.strip() else [] - else: - selectors = raw_selectors - - if not selectors: - data[field_name] = None - continue - - # 遍历所有选择器,提取文本并拼接 - texts: list[str] = [] - for selector in selectors: - # 跳过空选择器 - if not selector or not selector.strip(): - continue - - # 仅仅在详情允许查多个 - if field_name == "description": - elements = await scope.query_selector_all(selector) - # 标签特殊处理 - elif field_name == "job_title": - element = await scope.query_selector(selector) - if element is None or not (await element.inner_text()).strip(): - elements = await scope.query_selector_all(selector) - else: - elements = [element] if element else [] - - else: - element = await scope.query_selector(selector) - elements = [element] if element else [] - - for element in elements: - text = await element.inner_text() - if text: - texts.append(text.strip()) - # 去重 - unique_texts = [] - for text in texts: - if text not in unique_texts: # 直接判断是否已在列表中 - unique_texts.append(text) - - data[field_name] = "\n".join(unique_texts) if unique_texts else None - except Exception: - data[field_name] = None - - return data diff --git a/src/database/__init__.py b/src/database/__init__.py deleted file mode 100644 index cb7ae7d0..00000000 --- a/src/database/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from src.database.connection import engine, SessionLocal, get_db -from src.database.base import Base -from src.database.models import ( - CrawlTask, - TaskSearch, - TaskPageAnalysis, - TaskNextPage, - TaskDetailAnalysis, - TaskCrawl, - JobData, -) - -__all__ = [ - "engine", - "SessionLocal", - "get_db", - "Base", - "CrawlTask", - "TaskSearch", - "TaskPageAnalysis", - "TaskNextPage", - "TaskDetailAnalysis", - "TaskCrawl", - "JobData", -] diff --git a/src/database/__pycache__/__init__.cpython-312.pyc b/src/database/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a48718f0a7e905c02e67f4d0fb6145a26f3651cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 584 zcma))ziS&Y6vw5z^ZoGMwQ1aty=1w?YoL^p)SgN(2Dghi5b~YkTrAxg=|bwUTj)Q~ zy<7j5Oj!jXL#J*|G6XtB&m@6ls)i5meR+JpPeSKu+C}#KI7^RL7@ERb7?1!`i&6I4Y-?5YOY1VVtM-egu6fbgtGuu@Z=TJ;@H)lxp6RkFxzPU+Z30S} z5S0E#w{{csZ+GxM|7uNjK5uHC54oPV%_46O54l>fDz`ED5qha;67u z+h$~8TqOa+I7(w2gtX}^p24`@B>t!}fiVp|jPJleP5oQkSaSCS|C?YHgZO}6p-b{;6j!SYOo{}QUiMmhp(uI%* uTpdhzP#>TT>THpgbtbt-Ulvhyvww8&N6X|5JLsZrtEw4>4 zt$uh|`}A~e=H>d*^Yytq8}dmS@`O#X6<`zlBaV;L3Xe0)m_vNe8po7}FV!Q#j4&Jo zjz7XSn?phw*8K$w02g|q=|78e`f28e>|U|v~( zQ24QiC{331O(q0o*z;db2~4P$K?%N5OE9hMNm-kDzdAerPtcW*k85wA3`*4+It>up za(T#h3By7&1?8BW^T_X$=T2pZ8Kx{dPJEm_kJ)%Y3R#lRV_LABETeW7S%H(2;nzy=sP(20~}uk-CH^^4pz1J-JzR9)nrSxwY}QfQH>^dV!EMJ^sP3KI{d72 a`NHSnuPyzR_@=JI_POv55RP_ Session: - """获取数据库会话""" - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py deleted file mode 100644 index e7042e90..00000000 --- a/src/database/models/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from src.database.models.crawl_task import CrawlTask -from src.database.models.task_search import TaskSearch -from src.database.models.task_page import TaskPageAnalysis -from src.database.models.task_next import TaskNextPage -from src.database.models.task_detail import TaskDetailAnalysis -from src.database.models.task_crawl import TaskCrawl -from src.database.models.job_data import JobData - -__all__ = [ - "CrawlTask", - "TaskSearch", - "TaskPageAnalysis", - "TaskNextPage", - "TaskDetailAnalysis", - "TaskCrawl", - "JobData", -] diff --git a/src/database/models/__pycache__/__init__.cpython-312.pyc b/src/database/models/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ce91897f5ea45edbbadf25e983946ea17d4bad09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmaJ-Jxc>Y5Z%osxx2(uQSAJ{G9bZ1EP@0isVst^Hpjub$*p>3FBf)CG^L&R2khBI!WnE3((51Mdz0?nVimSSo*26l*uAWPSFyQQ*#oi+i=O=!n z_KM9Kyzo0lUh89Nhd^q0A1P@D7r+*g9m;dHKazT46AQUH%=Oewj(Olq$h#_+iT@Ke_lvGlDtEkFKic2KxN zNUeoVq7$o(jfUAsMO|fwc{Yx+vomGJGKrvK^s}$%dtZ?d$tcN%XpSd^ qw+G29t=eV<%5nJYCcvA+IiIqfDcha07Gay{_Iuf@mu_9N45}{+ugoO? diff --git a/src/database/models/__pycache__/crawl_task.cpython-312.pyc b/src/database/models/__pycache__/crawl_task.cpython-312.pyc deleted file mode 100644 index b2ab54080b2ee70bdc5fe85ae70f5b61a930e7e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1743 zcmah~O>7%Q6rQ!$>-Asa{HHAiha^xpBm^X&6bpqkq@s{0N}WbpS*$kRvFm1koY~zL zmx~V|g`S)f$*sLLat^7uM9L+cS_uw`YzaIPst~PcZlJ;i3Bj9n?6^V|EBVd5dEfiy zy?JlO-$f!p1mCUI$Tf>c=oh9mKHt&g>8CLH8mUM^DyMohPUj?!V{@_qgI8TF;3*r<2lMLvg z!i;1^PbGVriZ^@E4l^az7TRGkQmL2+-_fZr*|L6A{w%+FH_P4P0F}6Z9-cJJ;h;l4Jh4Lh)E2JPV;{uH+j&0_QG{HJHZ0gCWhyMWaG~h(T(KKaA!&GpL z+=_!~IBn{A#VE*zqT@r>70`fw6=xJjvuP-88kt;Hwrrd~jDeB*^Vm>XOGla}#dfS& zAaWzLY^D;{EVRQmZAEK@4oz>-h^AQf(TZZzAOr~)LX{Oe?jgO*Ni+>7&#n^<)<@TP zf{L;mqAb&(EbFG~Xc*>USzd7zjRg;yUzSxf4U>orM+Irt*GT|~3A`VOM41rTKHeOh zO}i{Bh5=F9In%IYnXrpSx?Q_v6CyCRkBO5^oMPeyCZIVHIn4xnCaBjiZ;%(68UwM3 zo`OVwOB`M)c#zu+PhGkToqQWuR;}mp%xq6%xqsSFYkT0 zKYyhfTP%K3dS$(9ZKT34{w|={=-tH~rTWrD@#9kOx_9kVg`cPmjBQ@uUMk<*d$&3; zQ=BQiy*{}1UWK0l{F%4zUD=uZMtaa)eRZ}@wQg^mJm?&%@UykyH@Dx}`=B~Jz0dcQ zuCJ%pKCkf8wbAoC=N=@hqw_A+ncK;Y*AKeRR`_|~8+v2AfA8G=#J7p+;9T)iNhp1G z5RFy%x!T}(!?G_e{UBuyq)c@%+k{m4EE#E>$80=8hM3|Wf6!Xd6fJ!f>xFum-7>nx zV}+t^$M!Lk=xzhJO+h=L24jsHjIlxx_K#CP__J*0ZpmH=Dkkb}_dXd1m0cBU7{qU0 jj^k>m|0i_%AxczG;vpJ;j9&gj=;W?)CH_yuMwjMahP~Q$ diff --git a/src/database/models/__pycache__/job_data.cpython-312.pyc b/src/database/models/__pycache__/job_data.cpython-312.pyc deleted file mode 100644 index 212a01fb6d0ccafafce7c898b0f696714ce7468e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2956 zcma)7OKcm*8QvwA4^gBZmMlLcTYQN&9b0mm#7G>WNF2qAWm<78N_Dq~#cFpXt-KF< zcIimYg@+c94*|$S5&GubY{)3&>Y_^k=2QW_h~O3l2M7`+v0L2=q`mag&c7t3W#ppf z;pfiG_xFn znC`gWJ!Q;CWziC&mN>7X(LFd#vm{9OkfmrS?IWQp%HlQMLW>9!|BQtdeUStw=y{f& z6E2HZEE1m4L{)8~(jMZ!A?9*OCK1i~7bHW?YdTfTT@g(rHGF8u{h-G=2pTawO9S`J z(r|gN?BO{NgM7#@du8uaZr}X?3Zjk^R09ghzH$2BL{Z$+z&@(eg$lM%6d9H~*4c)6 zIYb$q`-})>bjhLn0fuR654Z6m2fU|U>z;-VzWWfXyN%WJQdZ9)R&N{2<)h11?;+Na zHdfzDZ5=tp>VE}R-yzn~77O*uA(vN24huZpOkfK_Bc*>1(yD+C*n5Y}mX6>{K728J* zw^braF|oD|j<=9bB@+9UNV@FDDXW#ykrp1cYfc=}oH{_F*z!k>+u)Wi3nRd8*6hsx z z7hv3xuzQuI6RR_a6-~se!X30qB4XY$6kWndL%KygihPwqf_h$6VN4RAqRVK7L@d#~ zBS={MR239???2S&G{psEv>@hH3-{3t(g8Lt#Zu8;fv(dm9)NAXDXP?*!q7*61XV*4 zEyd7DKr*zPsITt52cQsI$sw#DT|%S-$@wOzQ%0tQl^ie>hK@}Vl944Us*uMj>20bI zbWuaXl4vfGZtx+mD|hqFjj&|UiLP7DF@`ZldN7i3Ua|&9k0oU8!Kfrh z6jOk(bBGGc1XDq}cTZFmnR>U9Q!o-lYcC4u0Knj-2un*yx+9pDXyr{3QALwlaJEE? zglLATHJQ@VoqA0~f9LQF2?~vP1QHSi&5-jd5(E+!gu8iBbs`l=KoDd@ zqT3NcaNe`VK86<;(=;)-7eog*=v8Q8y9Ghib%XjwGieF}?uT=Dk?=7GWLg+Q&*GCH z20@$xaT)}DBN#jkA_^h~A`T(}g4UJ6Nf09-&VV=z!Ve-10^VYL9>fJIh*vj0#iOvi zNW~N8Cn}kL51UvTgPxtIFXtGI9I575tB*%>t2hD-5D=y*(PI9NsdK6C`s0Ngy90?w zQ(F@|H?#J@Y~jk9f6aW*S>tBwvC+r(DDeaS9bDt;o920qEwzJEmyl^HSRVgKDoCct$$XTuw&N>SJ$o<-zfK&E?2{+YuvSZ z;^MP`EpBtDV%dpI;iI*M;!OF?5?}3#*0@YPKK9JNVQzL-vUYr`Fu68Y{GgmHO;sbq zHEyat@{4Dew&I(YDR(4Om@4{e^B}jq#=6Y$->61_2tT2klW~NAyZ71`%14tZj-NXoZn_4x5>F3 zuD`fYo+;gd z-E_JOzX=QIW)d>*s-j9SxVGB3an5COTHd)Xa4SgH0cg~~nsXtYOG`VDjHZ_MOhn8|OMi_e+(KmEh( O61yh7fK6o7Z_^?Gf`PC}dz5<(IZN*t)6Dt~HIL?NYwpO8XArIn@C;u)JI`(tL; zC83A<&`JTRs&i-yw{QzeROM)iOL{6&FGRLPJyL~G3oW-+w3nXxX4f`OThuwc**EW- z_h#OEv)@OfVGhQvh3M7E0LT5rl==YO9WOF4ea#_G<`9p3I&bhY&$Dm8?lb(d-wt){NZCd49&fo)sOVj6}Z!~v`7&`3rz zHT#OEgyAmmP?;g&^MXs3na{CjPQ%{p8vCiFD5h#)MWJCuF)WnV*>{VgT+6F^%@R}; zWTjy_s=)OjCEhxTz=?^##1T`>%~~c-jbnS(v2rOZlffjXYAKtfQ;-PtGW?lRaAU07sh8c7%*~S+bIAldll=~q z>NBn?B!7qx7tTHD8d^GC8hFKnPS&%|6*@h zI9BZ&T|QiTx6*gCFtPAaF@5vnvT*cwk=wa%*(u@5;CSI|@wMCW#l2-=9F`M@*0SqP zW#p_Ea!I%|y3w(#ES!BZa&Qf;k5@(}{$ZJ@jvg*a4~8qF=L@HcVsUP1c=3~sw%ujn ze09%}Qul)cl|57LfuyAacUm{vN6Nxfb!h)u_xh3V#=ae^3{83aHrfU@TOP`nAIX^w zIa3+RHjs*tCHw37oJ_Qm5vH&jLi=9RRXu$L8}nWlxeemBgM^(fDYx?vv6H{T7ll7LHoI>A1}A$U6951J diff --git a/src/database/models/__pycache__/task_detail.cpython-312.pyc b/src/database/models/__pycache__/task_detail.cpython-312.pyc deleted file mode 100644 index 60ab935d4f67d6efc1ab0047deff6da129d811a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2388 zcmai#O-vg{6oA+EdTngXKg1!Jf07tllmtqm1kog=6o^7-V$x=1X|;IAuwj3k*>!@G zLy_84BqwvI3YQ8l6%j}!iYmFvg`SGki;-7KJyI1#ZK7~%mG;t8XLfC4D@EPI+nIUa zdo%Ctd$T|Ld>#rv4;OtmV+=+8Mke{knH#)`;=$LHK(Uly6YP>rwy`!F89OAq>|h-- z&C;@yb;=CO$S&3;yIHrXr=>F4!+K;d>ow<2sa*E4J{x7HKB5HX3rcVS--g55hpkAs zgJtn%JiDfYSpU1Pzx(#p=lA#4zJIy+<*RR!zdwEY^5NR+we>y12W`kL@H)^%83Y;R zxhT$zDLR+|5HZ(uC@M3^m0xfVV=#n4M2En)nFDh1zW#RfhfdDyBVua!ki zRS{EN#8l@QbLX1E&dg?Q!Dmg;zI7#h{eSYcMSMdEzaLq#|GFanPzk^9-(Ph8dcq6- zyDo~#W#e$(3N#4CNtstNJh+%V92QM#b;QbH58_(`lhs0Ukd z$8b8f;RMr&<8xNXe~>1GJI)(}qRhhtHwO~P$H#S5R3Z?_K+%z16h<)OR^pPxPfGwf zMMVJf$QOb99m(2E6!u#2sgNrM6an`*=3N4a@^MLrbwt5vI%=gZiURA)tw%*52^#VN2vx|*K-2gcfIRr9PzM6X>&P1w6;YeDCLWv- zd|?qq&~Am`R+V^+TzAN0y-j61a&w%*%YfsMhvQ^bh)ZN#&T+Tmyp+?pI8IO_c-hC{ zB;v$c>(GlshB$f()(}Dd1IQub4^heCuc@dwk)o<-90%)(p4@yrC z1UZlP6)u4LRF*#XOI`26zV{p662#oHlMzcrz9*?B@ zo?P1LGLBBB>4s%ySzEy;n#^{cdOVP7e{y!mX>?7b>H6gD<;W6tH-X)qdjE&p+mWaD z(xFd{lQ+_I4i45f3mZl!0p_GpG4e{uZ1En$1u zI6h_txk|5e@78u@=&@|~>6Ck?)9AjkFrH+R^Q)appY2v1%g|S{J!ew>oqnTd(hStS z+P_k{TYa3IvE#&+fBVc&gFg-$9g~*bZdGfkCe2PiW23ul)aaNgAO<}H`|{suB3KD~ z$b=LJZj)OQFGXg7oUp3ZEJU;X%rY{oihNpBu}}7@P&O;tta4P1%cM77CcUIqV0nHw zc#4e7&HITL_n_5BL+}iqlIubvRr!a*X0v6f&R?m(b87S%HTs-7@seu)i}BlLZOe6k JQ{-*B`4_oUiTnTn diff --git a/src/database/models/__pycache__/task_next.cpython-312.pyc b/src/database/models/__pycache__/task_next.cpython-312.pyc deleted file mode 100644 index 3b2427c1bc9f1d74697d755c50795c5cfcaad363..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2269 zcmah}O-vg{6rQ!0wXre(I0V99API&BrAbO6iqbTpG$ab4#H7;7(rV)!V8i}6v+D-Z zs`a6*NKVeFaEY8tMFa;fm58{|Q;~YXvZd4`RZ-NYDz{c?FFp0m`UfK#br0Xpy!XAI zdGp@<7zp?fd=D1`cbWu*eqo#9S5a8Jo`%IYNI@b}IK`!LIwx`*Te~%v?iSrTFY~7jPcIM=(#jkgtdQ!VL+p3O30`*-|XT}U0&tO7@ zn>JC68R{ttydyvi1CjD!Ja5ygo4TxN1#QShh3j%Gh7}snoxc<{wYY9T1pbn2;b=a$ zJeCUbt&0@|BawrmxTc^KqFZr^ybIw9EGTZpy~dZ0<-uNDc^EukpHeXl{{jnmYn~Fz zU&itlS-`APDp$FDc*O@6{*pxiEUF8BtBMx5MhUD{uR?z86Kl)7)|7E-%Q$sqoVp^% ziCkZbEUYz@eAbu6ZQRFiD)ED5{u>xy3EuZ0RLJ(wKC8X+m0L9)uxc(^;U=Y=8D#nI z^MAO+JhYG5e86W*iFtS*vjv!~`+T;(fqCQr^T-|(x851b-$Xf<)ux?U7pl_se|B&1 z)?Vs}jxui@%-cdR)JdztvNbmjtv?~p;CL^H|8uZ9q5UNrsFDF(DF$R(e^3^Nk@3ff zs>@_Sn!^h;AjfS}HKGLT*s!TfRYrm0HR75k-_|g#Pz?po(?FES_q2QfRoU%?G^i(r z4F!f3_#OpM%W=&njZ7g}wrs~O!rC~*k!B|I6Kq4ss2MlxT{aQ4QkUl?N2(&;AvQQ- z)riGyDNZyMMh%VA8Z&M)iG?*BwM|0n3xepZY|LQEUWj2@Ju6%6(5fNZs%g*wCd4F? zjxCE-!Uq`<8!M7*Q~$JTsMc(LcxmjU>s2VM4ti zYfF+y6VtPoim<+-Zp$VFUUo!U7#v~H#-N=+2ZKrmoeaDTx){K)B65@gd!0!)0CgMY zJ<`Lr#{r(A*MR7s_bv}brYuaX$ed~5$OyLPY%>-yr>8NA$!f$R(Fhbnz74Nw1Wt`L zD^hB+f!G46W*!-^Jgm>*YQI7`{`60cy@~5DgWajC8zVat;cRd!acNOlv>x~~{8X-M zVCDYS2Rk24WxH-At|teVx*xrp;cvZcIq_6jA*;TPvF&iSWim0kIGQ~3;97>C%yqoA z^6`e8?YNk@mJBYrAGK!qi@CPGm2(^KXWK3$#uh(GMjw2d;V=9tp!WW!gX=x3!&}F) zZCCbGS3uQ&{+Y00ZTi7~-(=!O^48MjRL}C&bkhkIcPxLJ6u;b;Za9|V z$8vq=HoV(C*}fZzt4Sd_pXzz^MY*MM(#JNe2Vey5YO)rc z#ri_N6P+e=TFdDcr+L^N(&|0tfJtiQYAy+9Ms(ZmaM>J>WrmvD%i<&tOrM(ppn`4_{#V}<|# diff --git a/src/database/models/__pycache__/task_page.cpython-312.pyc b/src/database/models/__pycache__/task_page.cpython-312.pyc deleted file mode 100644 index b6f269631546132b38ea20699e5ae9e9407a8caa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2269 zcmah}O>7fa5PoZ~*MD*T90#YS&BDqxIQUxUk)RR$EDLpk(FR|>Z^-2{&B~`h#qP_IgdF!7zqO^OMeQ)NQ zZ{EzCdEW;DJ_O(Wg}{vvhtMx{Q~q3~#j7b;JVX-Wk;F(2g;5!vVd&baI8-O^R9T)? zUA#->cusZmZrjEx9@WcxRUhxO_b$b+*6=k9a-a{9#C?h+H?CQA7Gv;%q$li6?1SAk z6UUDn+Wz|Ci*LTzUU>N1qm}K2&wu?g{bGH6`~D-?zN9W;#_~v_iA`C>VVC6_m1o8^ z6VG5`ao0>DYcrO+tniEhFakh}jo^7R?69~?Vmyu|E1=qcA*L${RRd0TR5Wm`c&4JP z6yYleEdoY71BGx*Li)T@a`3DJVHf5kr{r8?E9tng2YdGb1NKR-5%`x-fLn7{Q2r{E zr;Gw*jpSWris2<6c=#(G0r03T@z#_*aGexbt6hcj(T}gMVy&yf)K_5|sxS>@j2*eL z5}8|Ts&F<|#cke&Z>iveRs2m9F9q+o5h|Uwb(hype$`&huX(kVy&%VGWD7n2F8)0g zWN;U<^)=4+3bJh%at|QecX4*S0om~ya_YvL%DzZmT{b8gmok3_hcYO45*x#1Sz4$53^{AP6Qn332ce#FCp8{C9l>hR7c$ zvMQ3KFo%;?Kunmrti=dcv1VEhSvm(0kCspraZ16KOV%VjZv|pRysZ=i$f~}PiYXci? zwXDSxrjQ_t48w;mTXlNEq#6S&IA-d^YAh*Yv!Vtmo5?t~8cI6Z#A-2!6~Kh(L{PC| zKxHf+9EzA&5=7JTPs^HY%oaC3$Q|ApY6v@uhxO=MB2Hf^%UAr{#xBbv2%4y3L9l#+ zpz2aWq3aq!xS0@@lEp0uk{$zfK!9>V35$9{pKAhnBGN#Gm*(n}q2zS}+?u9C$z)wK z1c5YDJAJ-Ln+-BdLg0x<3{vHT^c} zq1yu>9-~(v(ckZ%JsF)eFfpQYx`v};*qAf*cvPRB#w0GvQG>*yPzP}eUe_ojj1?nV zX||zwk_0Ft6*M2?E?n$qsKAc=)I6BF^gP(Jd?7QoH4(`NCsU&f+=6k>pJOKr`-WET zY<#fw(PVz#_0*;G@x`78@8#I*&)fSRb1P)kml@xTN&MllvzjNj;T^-ptkPn^T zF`Wm~!0=NpW32muzyE6LO8WZZ+2!7)3)z-F8syN)r^h$;ub-@V(i#ZUdnPiOh*e9ue;l4obg;qr}K4%d<}-Owh0_P?o!N^BOZ z$zm7U4Q98M-7d6ytlAyWkWN&)N$tk98lV$~%AGJo+kyDYfyi5QWygJ+vUo;{qj8EH tgDstPgZA9-PKIF$sPAVKdWJ@SK%>vl;cc}4Pp*}jWzyzfi2iMwe*hs8YnlK6 diff --git a/src/database/models/__pycache__/task_search.cpython-312.pyc b/src/database/models/__pycache__/task_search.cpython-312.pyc deleted file mode 100644 index e1fb2bcc3aa7ed367aa5c62c2b09d4e8370d595c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2214 zcmah}OKcNI7@oD)>sOqoF-aig;e^D2M!Z2KLXf7Z0t%$yP-$gpwee1pCHpY5yCtSl z>qCe13fiTRVkKTwO*-0s5OvVE80sh?aXd$oFdve{4@VQ z-~XI{{_g?-AHeI%Y~bAYE&zTZOle`>TF=Mv_6ZOG3j|7VNR&*m6h+QX$ss#gr%bc7 z>|$Lq!!oj)bz66|6bU3eEjLN59ilze6@D{=CAkf7q8rSc4Kjks6;5!Bk(%ZMHxn1rf)!; z99DEV36aU1(UGW3n(mUoGl+pv3^eH|oY5l=lNsR?2`HEW+1k0dDjBkZZPEk02IF>2 zHZ2$7eFrH51T2LM;TXf|vrfUm(hdM!kP)1MbD1uuttx528X>S;y@cZ6Q5z&XTVEZ3F9xxL#BehK_-9 zf>)B9!pl$uGyo@!Hhf+D`irNZ-LgkS@!Fl@^;8d@4o?;y6hD7ZymTuPaoFzMSl>#> zYnwBWB;x-FOrL7##6A|70f>-_I2mdhKM74A4jbuE;CS8iPl$@BP1%eOR|iiMa#6DH zQ57RWW|8T$ck6PS>EW!1InMNPoU96l1o63wD27ZexSmLoEh z5zZ8#Izqgdm`)KtN)5;HilXA8imIY<914bHg_0Jl=)Jx~5(S7&LP#wCE?@vUFw^I_Eyf)^_LUQ-vLeGL0+y@;gSYzUuGVDhdPp$gU3VHE z9{RTboBn)w)Yi+^wr#vwWygPHC$j8BK0H~5Xl@9(1$ZnbW2Yb?G)Bpeg diff --git a/src/database/models/crawl_task.py b/src/database/models/crawl_task.py deleted file mode 100644 index 78772bcd..00000000 --- a/src/database/models/crawl_task.py +++ /dev/null @@ -1,27 +0,0 @@ -"""爬虫任务主表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class CrawlTask(Base): - """爬虫任务主表 app_crawl_task""" - __tablename__ = "app_crawl_task" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - company_name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) - - # 配置阶段 - config_step: Mapped[int] = mapped_column(Integer, default=1) - config_status: Mapped[str] = mapped_column(String(32), default="pending") - - # 爬取阶段 - crawl_status: Mapped[str | None] = mapped_column(String(32), default=None) - total_crawl_times: Mapped[int] = mapped_column(Integer, default=0) - last_crawl_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - - # 时间戳 - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/job_data.py b/src/database/models/job_data.py deleted file mode 100644 index a5c9bca7..00000000 --- a/src/database/models/job_data.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Job data table.""" -from datetime import datetime - -from sqlalchemy import BigInteger, String, DateTime, Text, SmallInteger -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class JobData(Base): - """岗位数据表 app_job_data""" - - __tablename__ = "app_job_data" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - task_crawl_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - job_title: Mapped[str | None] = mapped_column(String(255), default=None, index=True) - salary: Mapped[str | None] = mapped_column(String(128), default=None) - location: Mapped[str | None] = mapped_column(String(2048), default=None) - company: Mapped[str | None] = mapped_column(String(255), default=None) - experience: Mapped[str | None] = mapped_column(String(64), default=None) - education: Mapped[str | None] = mapped_column(String(64), default=None) - description: Mapped[str | None] = mapped_column(Text, default=None) - detail_url: Mapped[str | None] = mapped_column(String(1024), default=None) - - company_name_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) - job_unique_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) - content_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) - recruit_category: Mapped[int] = mapped_column(SmallInteger, default=0, index=True) - - sources: Mapped[int] = mapped_column(SmallInteger, default=0) - is_independent_url: Mapped[int] = mapped_column(SmallInteger, default=1) - is_valid: Mapped[int] = mapped_column(SmallInteger, default=1, index=True) - - expire_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - check_status: Mapped[str] = mapped_column(String(32), default="pending") - last_check_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/task_crawl.py b/src/database/models/task_crawl.py deleted file mode 100644 index 68605bb8..00000000 --- a/src/database/models/task_crawl.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Step5-数据爬取任务表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime, Text, JSON -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class TaskCrawl(Base): - """数据爬取任务表 app_task_crawl""" - __tablename__ = "app_task_crawl" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - crawl_task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - status: Mapped[str] = mapped_column(String(32), default="pending", index=True) - retry_count: Mapped[int] = mapped_column(Integer, default=0) - max_retry: Mapped[int] = mapped_column(Integer, default=1) - - # 输入 - input_config: Mapped[dict] = mapped_column(JSON, nullable=False) - - # 输出 - crawled_count: Mapped[int | None] = mapped_column(Integer, default=None) - - error_message: Mapped[str | None] = mapped_column(Text, default=None) - started_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - finished_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/task_detail.py b/src/database/models/task_detail.py deleted file mode 100644 index 28be74f9..00000000 --- a/src/database/models/task_detail.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Step4-详情页分析任务表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime, Text, JSON -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class TaskDetailAnalysis(Base): - """详情页分析任务表 app_task_detail_analysis""" - __tablename__ = "app_task_detail_analysis" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - crawl_task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - status: Mapped[str] = mapped_column(String(32), default="pending", index=True) - retry_count: Mapped[int] = mapped_column(Integer, default=0) - max_retry: Mapped[int] = mapped_column(Integer, default=1) - - # 输入 - input_url: Mapped[str] = mapped_column(String(1024), nullable=False) - input_job_selector: Mapped[str] = mapped_column(String(512), nullable=False) - input_change_type: Mapped[str] = mapped_column(String(32), nullable=False) - - # 输出 - output_detail_selector: Mapped[str | None] = mapped_column(String(512), default=None) - output_fields: Mapped[dict | None] = mapped_column(JSON, default=None) - - error_message: Mapped[str | None] = mapped_column(Text, default=None) - started_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - finished_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/task_next.py b/src/database/models/task_next.py deleted file mode 100644 index 9fb1ae0f..00000000 --- a/src/database/models/task_next.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Step3-分页分析任务表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime, Text, SmallInteger -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class TaskNextPage(Base): - """分页分析任务表 app_task_next_page""" - __tablename__ = "app_task_next_page" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - crawl_task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - status: Mapped[str] = mapped_column(String(32), default="pending", index=True) - retry_count: Mapped[int] = mapped_column(Integer, default=0) - max_retry: Mapped[int] = mapped_column(Integer, default=1) - - # 输入 - input_url: Mapped[str] = mapped_column(String(1024), nullable=False) - - # 输出 - output_selector: Mapped[str | None] = mapped_column(String(512), default=None) - output_change_type: Mapped[str | None] = mapped_column(String(32), default=None) - has_pagination: Mapped[int | None] = mapped_column(SmallInteger, default=None) - - error_message: Mapped[str | None] = mapped_column(Text, default=None) - started_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - finished_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/task_page.py b/src/database/models/task_page.py deleted file mode 100644 index b1a45080..00000000 --- a/src/database/models/task_page.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Step2-岗位列表分析任务表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime, Text -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class TaskPageAnalysis(Base): - """岗位列表分析任务表 app_task_page_analysis""" - __tablename__ = "app_task_page_analysis" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - crawl_task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - status: Mapped[str] = mapped_column(String(32), default="pending", index=True) - retry_count: Mapped[int] = mapped_column(Integer, default=0) - max_retry: Mapped[int] = mapped_column(Integer, default=1) - - # 输入 - input_url: Mapped[str] = mapped_column(String(1024), nullable=False) - - # 输出 - output_selector: Mapped[str | None] = mapped_column(String(512), default=None) - output_change_type: Mapped[str | None] = mapped_column(String(32), default=None) - output_item_count: Mapped[int | None] = mapped_column(Integer, default=None) - - error_message: Mapped[str | None] = mapped_column(Text, default=None) - started_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - finished_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/database/models/task_search.py b/src/database/models/task_search.py deleted file mode 100644 index e7b5bf01..00000000 --- a/src/database/models/task_search.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Step1-搜索招聘页面任务表""" -from datetime import datetime -from sqlalchemy import BigInteger, String, Integer, DateTime, Text -from sqlalchemy.orm import Mapped, mapped_column - -from src.database.base import Base - - -class TaskSearch(Base): - """搜索招聘页面任务表 app_task_search""" - __tablename__ = "app_task_search" - - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - crawl_task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) - - status: Mapped[str] = mapped_column(String(32), default="pending", index=True) - retry_count: Mapped[int] = mapped_column(Integer, default=0) - max_retry: Mapped[int] = mapped_column(Integer, default=1) - - # 输入输出 - input_company_name: Mapped[str] = mapped_column(String(255), nullable=False) - input_url: Mapped[str | None] = mapped_column(String(1024), default=None, comment="已知招聘地址(表格导入)") - output_url: Mapped[str | None] = mapped_column(String(512), default=None) - - error_message: Mapped[str | None] = mapped_column(Text, default=None) - started_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - finished_at: Mapped[datetime | None] = mapped_column(DateTime, default=None) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/src/detail_analysis_graph/__init__.py b/src/detail_analysis_graph/__init__.py deleted file mode 100644 index 92f9cad4..00000000 --- a/src/detail_analysis_graph/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""详情页分析模块""" -from .main import analyze_job_detail -from .graph import create_graph - -__all__ = ["analyze_job_detail", "create_graph"] diff --git a/src/detail_analysis_graph/__pycache__/__init__.cpython-312.pyc b/src/detail_analysis_graph/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index f7749b440460c91aedaca3684d00bd805f72e649..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 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 diff --git a/src/detail_analysis_graph/__pycache__/nodes.cpython-312.pyc b/src/detail_analysis_graph/__pycache__/nodes.cpython-312.pyc deleted file mode 100644 index 42b97ccd65acbdcca5905e06645b551a0f906f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14445 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 IkvG==7Zgq?Bme*a diff --git a/src/detail_analysis_graph/__pycache__/prompts.cpython-312.pyc b/src/detail_analysis_graph/__pycache__/prompts.cpython-312.pyc deleted file mode 100644 index b395d2d28debcdb2f1b42b6be3501efb62816256..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3416 zcma)9-EZ4e6i?asva|=prh#~|G9=cqw6tR&MS!T*A_m<+TR~_micI3tFgK2D2dFBf zc5T|ENt(d=m9AY|=+{b{w4-bD(T_iX7kFf^^SySSHfa(MJaMkC?WI*ah#J@SJ@6kxKAFePlb8&=qTKbIDC#P4qwHU3g1&#p7I^Ka;W`q z<|n`(e|C#s`~m9Nk8d6?A7r-Ad@$ zIousayop5Qoh*EJpvW|=u z9&C;=HB~HK2&EF*{7N;R>MQ*`%G6q~aWbjztW=wj44S)+Z;ewU^i!OULV#*60m#%l z*bP0qVfvQoR*Oq0yb7b>i56L>HoG0x3Uhd3lK5grWO_T8Rdbyvo-c(HaHHlHQMg-M zn=h3TuOV8rzR)bV4SV` zM*PZUgO~;Q7XvP9koXPnNrZerF}%>XZ=k*cqZY&Tj{WHJCl}pJM+b!JlzD;o zDx&0cvreF*odD-)GAi+$EC$G!IWRDZZcaH__5@lU!9y#hbP7e*&%6Oo%nwijzLFfb?jBfHWt~wQ*+M=VK_EKr^fQ+AOKml?gpN^k6eWSpwyZW`LkkrlSQ?$oo8; zqVR1&Mcz*}{xb}!4rJ#dX;N&!Qjg+S1fGRbW_`TumHa`%3KoxM@l+1&bkmKoYY21@ zHjGqXxiu;bQb9qPI38LfaTq#CRSTlgyW;xVFZVl5Wr7Whqlfx;H36yLG*IO!)mYC7UTl$)%^ECUh?w+FJI03LUa%i zP;wEEM?sjN1WFS>tGRx7JatNoEor~TV0}C~00KZfHT2}vDG)^*O_=e$3CkPm0v@S_ zn>IUWKzL&VfN84na33CDQgh2hMRG)ZQDO*Ip+N9b&j7OvEMYKnwoKpj`5ryq4U(g7 zj-qgc>H+Agw!Cc<2X?Nt2AplJg-Z~=Ap!OZqQjA4Ln3-W%0RbanQ~R3xzM6{oaqPG zwWT=#dL5>_;QK0k-vo|W!hZNi9_$g%C~UQU$F+f53( zZvcRjS1mP50@NP#K%B?`SAl;(J`g876xk!3)){Hf zB&LJ^UXeJj0`0)ZDI9GDbQKT~)YnTmpV`6_a3Up3lX}^u_8?TSrgpoa<_5L#n0|Ky zPt2n3Wcfjm){Rr&%Bsb6CN&DMb< zzzOX#g!Q$=1zVeg5;RRkt_rr_d+r@LONIpgJ?VMCBtL~@CtMyLs;H>=%kkpFXI^{d e@E?xj51T3~Uxu4w55K5*;ibcWI*v-ug4w@W+8@pU diff --git a/src/detail_analysis_graph/__pycache__/state.cpython-312.pyc b/src/detail_analysis_graph/__pycache__/state.cpython-312.pyc deleted file mode 100644 index 762aa715940f5881ce6740ac366959dfe04d109e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 971 zcmYk4&ubGw6vt5tgjT3eBN$fY+gr3l5MQltvnO9%|p-AS_TW_O*LD7h(V zK?{POT6*vz_ya}3gW$oxz+*1yKu@-{-U<~Do_w=OqXRqh`M$Sr-prf#Et^dv7@Lo? ztH?y?yB4Rz>cYt^z!q{)5jlnv^Nd=o7&A1Ro>{|1Y@isrjvTy>oH&a)*89Xe+=(s5 z8_7bd-UsT(=I6tQj}AWU?0ShiX7wo1qhya#5hWlZ9Z?b}6H$sK#&m|b(--M9 zP&Oi+y(oJ+ zb?&Lb)nJ9VlGTV{9LCfSVqRTr=J&Fc&ZZ*mcHEjBA+_Vcnx|tF5$-BQo-V*U{)5qp6ge*ZeW!U zJ=%D{-AYx?P|@&hLPIwyOXt*+dZTE!3;OruT&K=g_%uiZJ~08)v1u5_E*jcH6MN{= RFKfccZ5Q5L{*3^l=)W868+-r& diff --git a/src/detail_analysis_graph/graph.py b/src/detail_analysis_graph/graph.py deleted file mode 100644 index 171a3f6f..00000000 --- a/src/detail_analysis_graph/graph.py +++ /dev/null @@ -1,89 +0,0 @@ -"""图定义""" -from langgraph.graph import StateGraph, END -from .state import DetailAnalysisState -from .nodes import ( - open_list_page, - click_first_job, - find_detail_area, - extract_field_selectors, - validate_data -) - - -def check_error(state: DetailAnalysisState) -> str: - """检查是否有错误""" - if state.get("error"): - return "error" - return "continue" - - -def check_need_find_area(state: DetailAnalysisState) -> str: - """检查是否需要找详情区域""" - # in_page 需要找弹窗区域,redirect/new_tab 直接用整个页面 - if state.get("change_type") == "in_page": - return "find_area" - return "skip" - - -def check_validation(state: DetailAnalysisState) -> str: - """检查验证结果""" - if state.get("is_valid"): - return "success" - - retry_count = state.get("retry_count", 0) - if retry_count >= 3: - return "max_retry" - - return "retry" - - -def create_graph(): - """创建流程图""" - graph = StateGraph(DetailAnalysisState) - - # 添加节点 - graph.add_node("open_list_page", open_list_page) - graph.add_node("click_first_job", click_first_job) - graph.add_node("find_detail_area", find_detail_area) - graph.add_node("extract_field_selectors", extract_field_selectors) - graph.add_node("validate_data", validate_data) - - # 入口 - graph.set_entry_point("open_list_page") - - # 流程 - graph.add_conditional_edges( - "open_list_page", - check_error, - {"error": END, "continue": "click_first_job"} - ) - - # 根据 change_type 决定是否找详情区域 - graph.add_conditional_edges( - "click_first_job", - check_need_find_area, - { - "find_area": "find_detail_area", - "skip": "extract_field_selectors" - } - ) - - graph.add_conditional_edges( - "find_detail_area", - check_error, - {"error": END, "continue": "extract_field_selectors"} - ) - - graph.add_edge("extract_field_selectors", "validate_data") - - graph.add_conditional_edges( - "validate_data", - check_validation, - { - "success": END, - "max_retry": END, - "retry": "extract_field_selectors" - } - ) - - return graph.compile() diff --git a/src/detail_analysis_graph/main.py b/src/detail_analysis_graph/main.py deleted file mode 100644 index d79c1be4..00000000 --- a/src/detail_analysis_graph/main.py +++ /dev/null @@ -1,108 +0,0 @@ -"""入口""" -import asyncio -import sys -from pathlib import Path - -# 支持直接运行 -if __name__ == "__main__": - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from playwright.async_api import async_playwright -from src.detail_analysis_graph.graph import create_graph - - -async def analyze_job_detail(url: str, job_item_selector: str, change_type: str, headless: bool = False) -> dict: - """ - 分析岗位详情页,提取字段选择器 - - Args: - url: 列表页 URL - job_item_selector: 岗位项选择器 - change_type: 点击后变化类型 (redirect / new_tab / in_page) - - Returns: - { - "status": "success" | "failed", - "detail_area_selector": str, - "fields": { - "job_title": {"selector": str, "sample": str}, - "description": {"selectors": list[str], "sample": str}, - "salary": {"selector": str | None, "sample": str | None}, - "location": {"selector": str | None, "sample": str | None}, - "company": {"selector": str | None, "sample": str | None}, - "experience": {"selector": str | None, "sample": str | None}, - "education": {"selector": str | None, "sample": str | None}, - "detail_url": {"sample": str} - }, - "error": str | None - } - """ - async with async_playwright() as p: - browser = await p.chromium.launch(headless=headless) - context = await browser.new_context(viewport={"width": 1280, "height": 800}) - page = await context.new_page() - - try: - graph = create_graph() - - result = await graph.ainvoke({ - "url": url, - "job_item_selector": job_item_selector, - "change_type": change_type, - "page": page - }) - - if result.get("error"): - return { - "status": "failed", - "error": result["error"] - } - - if result.get("is_valid"): - return { - "status": "success", - "detail_area_selector": result.get("detail_area_selector"), - "fields": result.get("fields") - } - else: - return { - "status": "failed", - "error": "验证失败,已达最大重试次数", - "failed_attempts": result.get("failed_attempts") - } - - finally: - await browser.close() - - -async def main(): - """测试""" - url = "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - job_item_selector = ".BHGkB li" - change_type = "in_page" - - result = await analyze_job_detail(url, job_item_selector, change_type) - - print("\n最终结果:") - if result["status"] == "success": - print(f"✅ 成功") - print(f" 详情区域: {result['detail_area_selector']}") - print(" 字段:") - for name, info in result["fields"].items(): - sample = str(info.get('sample', '')) - # detail_url 不截断,其他字段截断显示 - if name != "detail_url": - sample = sample[:50] - if "selector" in info: - print(f" {name}: {info['selector']} -> {sample}") - elif "selectors" in info: - print(f" {name}: {info['selectors']} -> {sample}") - else: - print(f" {name}: {sample}") - else: - print(f"❌ 失败") - print(f" 原因: {result.get('error')}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/detail_analysis_graph/nodes.py b/src/detail_analysis_graph/nodes.py deleted file mode 100644 index 3bba962d..00000000 --- a/src/detail_analysis_graph/nodes.py +++ /dev/null @@ -1,331 +0,0 @@ -"""节点实现""" -import asyncio -import re - -from langchain_core.output_parsers import PydanticOutputParser -from pydantic import BaseModel, Field -from langchain_core.messages import HumanMessage -from src.bash_model import GeneralLlm, AnalyseLlm -from .state import DetailAnalysisState -from .prompts import FIND_DETAIL_AREA_PROMPT, EXTRACT_FIELDS_PROMPT, VALIDATE_DATA_PROMPT - - -# 结构化输出模型 -class SelectorResult(BaseModel): - selector: str | None = Field(description="CSS选择器") - reason: str = Field(description="原因") - - -class FieldSelectors(BaseModel): - job_title: str = Field(description="岗位名称选择器") - description: list[str] = Field(description="岗位详情选择器列表") - salary: str | None = Field(description="薪资选择器") - location: str | None = Field(description="地点选择器") - company: str | None = Field(description="公司选择器") - experience: str | None = Field(description="经验选择器") - education: str | None = Field(description="学历选择器") - - -class ValidationResult(BaseModel): - is_valid: bool = Field(description="是否有效") - reason: str = Field(description="判断依据") - - -async def safe_wait_networkidle(page, timeout=5000): - """尝试等待 networkidle,超时则忽略(处理长连接/心跳网站)""" - try: - await page.wait_for_load_state("networkidle", timeout=timeout) - except: - pass - - -def get_active_page(state: DetailAnalysisState): - """获取当前活动页面(处理 new_tab 情况)""" - page = state["page"] - if state["change_type"] == "new_tab": - context = page.context - if len(context.pages) > 1: - return context.pages[-1] - return page - - -async def open_list_page(state: DetailAnalysisState) -> dict: - """节点1: 打开列表页""" - url = state["url"] - job_item_selector = state["job_item_selector"] - page = state["page"] - - print(f"[节点1] 打开列表页: {url}") - - await page.goto(url, wait_until="load", timeout=60000) - - try: - await page.wait_for_selector(job_item_selector, timeout=10000) - except Exception: - await asyncio.sleep(3) - - # 验证选择器 - count = await page.locator(job_item_selector).count() - if count == 0: - return {"error": f"job_item_selector 未匹配到元素: {job_item_selector}"} - - print(f"[节点1] 找到 {count} 个岗位") - - return {"failed_attempts": [], "retry_count": 0} - - -async def click_first_job(state: DetailAnalysisState) -> dict: - """节点2: 点击第1个岗位""" - job_item_selector = state["job_item_selector"] - change_type = state["change_type"] - page = state["page"] - - print("【节点2】 点击第1个岗位...") - - context = page.context - tabs_before = len(context.pages) - - # 记录点击前的 URL - url_before = page.url - - # 点击(先 hover 触发可能的显示效果) - element = page.locator(job_item_selector).first - await element.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await element.hover() - await asyncio.sleep(0.3) - await element.click() - - # 延时等待 - await asyncio.sleep(1) - - # 根据 change_type 处理,获取当前活动页面 - active_page = page - if change_type == "new_tab": - await asyncio.sleep(2) - - if len(context.pages) > tabs_before: - active_page = context.pages[-1] - await active_page.wait_for_load_state("load") - await safe_wait_networkidle(active_page, 3000) - else: - return {"error": "点击后未打开新标签页"} - - elif change_type == "redirect": - # 等待 URL 变化(包括 hash 路由) - for _ in range(30): - await asyncio.sleep(0.2) - if page.url != url_before: - break - await page.wait_for_load_state("load") - await safe_wait_networkidle(page, 3000) - await asyncio.sleep(2) - - else: # in_page - # 等待详情区域加载(页内弹窗/抽屉) - await asyncio.sleep(2) - await safe_wait_networkidle(page, 3000) - await asyncio.sleep(1) - - # 获取详情页 URL(使用活动页面) - detail_url = active_page.url - print(f"【节点2】 详情页 URL: {detail_url}") - - # 获取清理后的 HTML(使用活动页面) - html = await active_page.evaluate(""" - () => { - const clone = document.body.cloneNode(true); - clone.querySelectorAll('style, script, noscript, svg, link').forEach(el => el.remove()); - return clone.innerHTML; - } - """) - - print(f"【节点2】 获取 HTML 完成,长度: {len(html)}") - - return { - "detail_html": html, - "detail_url": detail_url - } - - -async def find_detail_area(state: DetailAnalysisState) -> dict: - """节点3: 找详情区域""" - detail_html = state["detail_html"] - page = get_active_page(state) - - print("[节点3] 分析详情区域...") - - # 截断 - display_html = detail_html - # if len(display_html) > 100000: - # display_html = display_html[:100000] + "\n... (已截断)" - - # LLM 分析 - prompt = FIND_DETAIL_AREA_PROMPT.format(html=display_html) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=SelectorResult) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - # llm = AnalyseLlm.with_structured_output(SelectorResult) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - if not result.selector: - return {"error": f"未找到详情区域: {result.reason}"} - - print(f"[节点3] 找到详情区域: {result.selector}") - - # 获取区域 HTML - try: - locator = page.locator(result.selector).first - await locator.wait_for(state="visible", timeout=5000) - detail_area_html = await locator.inner_html(timeout=5000) - print(f"[节点3] 详情区域 HTML 长度: {len(detail_area_html)}") - except Exception as e: - return {"error": f"详情区域选择器无效: {result.selector}, {e}"} - - return { - "detail_area_selector": result.selector, - "detail_area_html": detail_area_html - } - - -async def extract_field_selectors(state: DetailAnalysisState) -> dict: - """节点4: 提取字段选择器""" - detail_url = state["detail_url"] - failed_attempts = state.get("failed_attempts", []) - retry_count = state.get("retry_count", 0) - page = get_active_page(state) - - # 根据是否有详情区域决定使用的 HTML - detail_area_selector = state.get("detail_area_selector") - if detail_area_selector: - detail_area_html = state["detail_area_html"] - else: - # redirect/new_tab: 用整个页面 - detail_area_html = state["detail_html"] - detail_area_selector = "body" - - print(f"[节点4] 提取字段选择器... (重试: {retry_count}, 区域: {detail_area_selector})") - - # LLM 分析 - prompt = EXTRACT_FIELDS_PROMPT.format( - detail_area_html=detail_area_html, - detail_area_selector=detail_area_selector, - failed_attempts=str(failed_attempts) if failed_attempts else "无" - ) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=FieldSelectors) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - - # llm = AnalyseLlm.with_structured_output(FieldSelectors) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - # 用选择器提取数据 - fields = {} - - # job_title (单选择器) - job_title_text = await extract_text(page, detail_area_selector, result.job_title) - fields["job_title"] = {"selector": result.job_title, "sample": job_title_text} - - # description (多选择器拼接) - desc_parts = [] - used_selectors = [] - for sel in result.description: - text = await extract_text(page, detail_area_selector, sel) - if text: - desc_parts.append(text) - used_selectors.append(sel) - fields["description"] = { - "selectors": used_selectors, - "sample": "\n".join(desc_parts) - } - - # 其他字段 (单选择器,独立元素才提取) - for field_name in ["salary", "location", "company", "experience", "education"]: - selector = getattr(result, field_name) - if selector: - text = await extract_text(page, detail_area_selector, selector) - fields[field_name] = {"selector": selector, "sample": text} - else: - fields[field_name] = {"selector": None, "sample": None} - - # detail_url - fields["detail_url"] = {"sample": detail_url} - - print(f"[节点4] job_title: {fields['job_title']['sample'][:50] if fields['job_title']['sample'] else 'None'}...") - - return {"fields": fields} - - -async def validate_data(state: DetailAnalysisState) -> dict: - """节点5: 验证数据""" - fields = state["fields"] - failed_attempts = state.get("failed_attempts", []) - retry_count = state.get("retry_count", 0) - - print("[节点5] 验证数据...") - - job_title = fields.get("job_title", {}).get("sample", "") - description = fields.get("description", {}).get("sample", "") - - # 检查必须字段 - if not job_title: - print("[节点5] job_title 为空") - return { - "is_valid": False, - "failed_attempts": failed_attempts + [{"reason": "job_title为空", "fields": fields}], - "retry_count": retry_count + 1 - } - - if not description: - print(f"[节点5] description 为空, 选择器: {fields.get('description', {}).get('selectors')}") - return { - "is_valid": False, - "failed_attempts": failed_attempts + [{"reason": "description为空", "fields": fields}], - "retry_count": retry_count + 1 - } - - # LLM 验证 - extracted_data = f"job_title: {job_title}\ndescription: {description[:500]}..." - prompt = VALIDATE_DATA_PROMPT.format(extracted_data=extracted_data) - llm = GeneralLlm.with_structured_output(ValidationResult) - result = await llm.ainvoke([HumanMessage(content=prompt)]) - - if result.is_valid: - print("[节点5] 验证成功!") - return {"is_valid": True} - else: - print(f"[节点5] 验证失败: {result.reason}") - return { - "is_valid": False, - "failed_attempts": failed_attempts + [{"reason": result.reason, "fields": fields}], - "retry_count": retry_count + 1 - } - - -async def extract_text(page, area_selector: str, field_selector: str) -> str: - """从页面提取文本,匹配多个元素时全部提取""" - if not field_selector: - return "" - - # 拼接完整选择器 - if area_selector and area_selector != "body": - full_selector = f"{area_selector} {field_selector}" - else: - full_selector = field_selector - - # 获取所有匹配的元素 - try: - elements = await page.query_selector_all(full_selector) - if elements: - texts = [] - for el in elements: - text = (await el.inner_text()).strip() - if text: - texts.append(text) - return "\n".join(texts) - except Exception: - pass - - return "" diff --git a/src/detail_analysis_graph/prompts.py b/src/detail_analysis_graph/prompts.py deleted file mode 100644 index cafe97f5..00000000 --- a/src/detail_analysis_graph/prompts.py +++ /dev/null @@ -1,107 +0,0 @@ -"""LLM 提示词""" - -# 节点3: 找详情区域 -FIND_DETAIL_AREA_PROMPT = """分析以下 HTML,找到岗位详情内容所在的区域。 - -## HTML -{html} - -## 任务 -找到包含岗位详情信息的容器的 CSS 选择器。 - -## 重要要求 -找到的区域必须同时包含: -1. 岗位名称/标题 -2. 岗位描述/要求 - -如果标题和描述在不同容器,请选择它们共同的父容器。 - -## 重要约束 -1. 选择器必须唯一匹配一个元素 -2. 如果基础选择器匹配多个元素,必须使用伪类精确定位,例如: - - .list:nth-child(2) - - .container > ul:first-of-type - - #main .job-list:nth-of-type(1) -3. 避免使用随机/哈希 class - -## 常见结构 -- 弹窗: .modal, [role="dialog"], .popup, .drawer -- 详情页: .detail, .job-info, .position-detail, main, .content - -## 输出 -仅输出 JSON: -{{ - "selector": "CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到则: -{{ - "selector": null, - "reason": "原因" #字数限制30字 -}} -""" - -# 节点4: 提取字段选择器 -EXTRACT_FIELDS_PROMPT = """分析以下岗位详情 HTML,提取各字段的 CSS 选择器。 - -## 详情区域 HTML -{detail_area_html} - -## 详情区域选择器 -{detail_area_selector} - -## 之前失败的尝试(避免重复) -{failed_attempts} - -## 任务 -找到各字段的 CSS 选择器。 - -## 重要原则 -1. job_title 和 description 是核心字段,必须提取 -2. description 应包含完整的岗位信息(职责、要求、公司介绍等都放这里) -3. 其他字段(salary/location/company/experience/education)只有在页面上有**独立、明确的元素**时才提取 -4. 如果这些字段的信息混在详情文本中,不要单独提取,设为 null -5. 选择器规范:优先使用标签选择器(h1、h2、p、ul等)或常规类名,避免使用包含特殊字符(+、-、$、@)的类名 - -## 字段说明 -1. job_title (必须): 岗位名称,一个选择器 -2. description (必须): 完整的岗位详情,可用多个选择器拼接(职责、要求、介绍等) -3. salary: 薪资 - 必须是独立元素(如单独显示"15-25K"),否则为 null -4. location: 工作地点 - 必须是独立元素,否则为 null -5. company: 公司名称 - 必须是独立元素,否则为 null -6. experience: 经验要求 - 必须是独立元素(如"3-5年"),否则为 null -7. education: 学历要求 - 必须是独立元素(如"本科及以上"),否则为 null - -## 输出 -仅输出 JSON: -{{ - "job_title": "选择器", - "description": ["选择器1", "选择器2"], - "salary": "选择器"或null, - "location": "选择器"或null, - "company": "选择器"或null, - "experience": "选择器"或null, - "education": "选择器"或null -}} -""" - -# 节点5: 验证数据 -VALIDATE_DATA_PROMPT = """判断以下提取的数据是否为有效的岗位信息。 - -## 提取的数据 -{extracted_data} - -## 判断标准(宽松判断) -1. job_title 不为空且看起来像岗位名称(如"软件工程师"、"产品经理"等) -2. description 不为空且包含岗位相关内容(职责、要求、介绍等) - -只要不是明显的乱码、无关内容或空白,就应该返回 true。 - -## 输出 -仅输出 JSON: -{{ - "is_valid": true或false, - "reason": "判断依据" #字数限制30字 -}} -""" diff --git a/src/detail_analysis_graph/state.py b/src/detail_analysis_graph/state.py deleted file mode 100644 index de807f2b..00000000 --- a/src/detail_analysis_graph/state.py +++ /dev/null @@ -1,34 +0,0 @@ -"""状态定义""" -from typing import TypedDict -from playwright.async_api import Page - - -class DetailAnalysisState(TypedDict, total=False): - """岗位详情解析状态""" - - # 输入 - url: str - job_item_selector: str - change_type: str # redirect / new_tab / in_page - page: Page - - # 节点2: click_first_job - detail_html: str - detail_url: str - - # 节点3: find_detail_area - detail_area_selector: str - detail_area_html: str - - # 节点4: extract_field_selectors - fields: dict # {field_name: {selector/selectors, sample}} - - # 节点5: validate_data - is_valid: bool - - # 重试 - failed_attempts: list[dict] - retry_count: int - - # 错误 - error: str diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 0a11a6f3..00000000 --- a/src/main.py +++ /dev/null @@ -1,43 +0,0 @@ -"""项目主入口""" -import logging -from contextlib import asynccontextmanager -from fastapi import FastAPI -from src.scheduler import start_scheduler, shutdown_scheduler -from src.browser import close_browser - -# 配置日志 -import sys -logging.basicConfig( - level=logging.INFO, - format="%(levelname)s: %(message)s", - stream=sys.stdout -) - - -@asynccontextmanager -async def lifespan(app: FastAPI): - """应用生命周期管理""" - # 启动时 - start_scheduler() - yield - # 关闭时 - shutdown_scheduler() - await close_browser() - - -app = FastAPI( - title="Crawler", - version="1.0.0", - lifespan=lifespan -) - - -@app.get("/health") -def health_check(): - """健康检查""" - return {"status": "ok"} - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/next_page_graph/__init__.py b/src/next_page_graph/__init__.py deleted file mode 100644 index 680f2ff0..00000000 --- a/src/next_page_graph/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import analyze_next_page -from .graph import create_graph -from .state import NextPageState - -__all__ = ["analyze_next_page", "create_graph", "NextPageState"] diff --git a/src/next_page_graph/__pycache__/__init__.cpython-312.pyc b/src/next_page_graph/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 135d0f8ae8aba1b214ae4732abad928be0953035..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 334 zcmX|6Jxjzu5Z$@WUgR`|_yZKI(ulRlA!vOFViOjI<+2y!Z9dqIVC?L~KVWYs_)qR1 zETqxOPPop>N&J}N&71f3EiDnri@|TJQknKnW2tW&tW~B-+?rdh8jYD%^%c$0jLdWL zE*g?!ZaIZ*(1ReD#i0)hm6wC=#Sj~8FI=A(UilDF8H4!3))Zo2)6*k)>9lonS(RFz zX}4^uT2_mNwzbNovvc{s)z2;xF)#B5@t|(~#&J=lH%3owMBnxIxE+E|4gek7@7Qj~ N_TDBZ_o3Y+{TphFS$Y5f diff --git a/src/next_page_graph/__pycache__/graph.cpython-312.pyc b/src/next_page_graph/__pycache__/graph.cpython-312.pyc deleted file mode 100644 index f1cfd08d44f67610189bb4b4534dd9a46e67794a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2514 zcmbtW-)kII6uvWaXJ>Y^+fCA>n{3l&E0j32TC|Fytu5H%gEk1Qpv%HA&P>v6c4wA5 zv(kkYk|Ope7*R;I!75fr4X7`DXbb)WzNBWM>kw(dZrZ$UNuGT2oI9I!6H;36z}|b# zx#ylc^PMx_-QN?57y-F(E^*@PC?S8Nu`0BwET05ridZB|EWr{BLQ%|$MJX!@#F8vp zphY<=b6qY(ib_@~s#&!d%|?r{Yz+JnTecLtV}x4jr!*UHv!h@q+UywEn;=#k5v>l; zla;uZC~tw|-!q)icbHK+o>{yx@o?s1{pW8U+&sU8PpFAO?5J<}HeU~<9Z2PLC;9uu~5xBhM#vG-C(v6baJl)`}B{?zVA9g;uE8g zw;;cc-az*7tRTW{zswvHPg)fT6#Oke55p8WMNYJ@Hlr+Iw&`U;MXX2~E%(9F;t%7C zSAVRZ`)uj^8}&=K>Q}Emy#3|kx6^@|_jLXQ0kvGm4ph^1{Jc}PuM2@RYWteNI$*zt zNi+Yv`Su&R4?LTBxiJXN9kRVK-!0|bkrA7fjC{^xW{$rcLZFW#qB%akSgHiQ=5gB` zTMr5Afy@dXfrS_+jR;W_XDbtx$#hj2s3m)A9lgu2Le^k`&BL)c1A!FNM zSc6q(h*h%!@JX^A5Hg5idtewRYq9Bj7HnW&OMu|Qo+MDADdM-jPQTeZgsk@3Y5aQS z*3On<34UbNSVc#8Qmp8!&m1fHoQN_0sd=d%-!Lz>)jIi)ZTM3n?&}08ea(s3w(Gv2A2 z$giAB_%{+LrdurK3pQMOGUxN~J_tD|ZoQ+kxTDfWbR-u%>QbP`52~^~^o@${qF96@|)?#wIxZE)AE{3u@{?PpZ~8 zRO=mD=pT3-m7}p0B?@7EZDFYeweLYH4FM4LSfLoBLQD^dcYVJ9{Qipv=4hI;qvvZG-EE~)actIY@Ix@LSW)Y{|iSuB-;Q0 diff --git a/src/next_page_graph/__pycache__/main.cpython-312.pyc b/src/next_page_graph/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 6d2d14618e2b77bcf77b2e275b213c0fa6ce789e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3989 zcmcIneQXoS6`%30*BgK46CBKklMsRfNsd4-V8T}@hL$_7J#>{Wiewq@CU$UaYj;g9 zIj4k!JIoPXOzA-=Ux|8Ga-}VtbW|sS)N)FqO6?yu339r}NvMrO_^YOzR6zZsZ`Rqw zd{x&!I@Zj*nfH5d-n{X=H~VLkNspj>7&En7f%-ccsD+&+9U4~UiX)n(9x$SqI zQ*~U{WTi2&SQ{{O7@Lg>%>+yx=4La{Mo!1G9Lrm{JU+jb;q*te%>|s{2x=~z+ldt{ z!|f|_Ey>a_UwCj%&xA`QPGkOKyBk}Gw!4vk$R742KtdI9_0bUekM&PB05cJOOOj%q9 z?O~`7V-!NW5r$KeX1$1zYTr6^7|!cmK%CN?rasVWQI967Y0uT8Cckaj4N|;A_{x8u@I~CI!RT$k!Oh26?n*-a0&HxDCyV-(--2< z-XFS_j-8$v8V2GYH~Oag`@kg$S05_;?M{rO2~BB)5gnK-(A1ye=ZR-Xh`4aOTox zFS-wS0)9@`i4m`tgiThpNjBu{!a>PQcAEmpNV-aQ8pNWvt?B@V$om_d(v z(YFgwzCC3ro3NA({&><_dQpEypRziVR>#%#39Dn=+L$VAjOiw=%Tm@=N$aX{YfY-K z1|~`tr%Ec5C6(hPRVhnVjLy(ydF6MC?I-udc4Qc2wM|+VKhRQ3%(2(M)gx13yeWPl z?u*w<80~kA1@S$DYX>V6g)1kF4l?{neAhse!n|v*ykr_O4R?&&8&iuKW4ot-{j_ZD zgt6>tAy_cq)_?HW!GZ1xOZDBQRg?Br!#_;g8zw7ihkKG0n;z(BYhh*)Dq3{9=0wfO zx|FdjVJ!RC>lo#i!|s!ovdmVDmQ_tIbzBUc2@SVh{pt8p*JM@wrGrBU6AeH3Q~h|= zo~#n{%aq)*ls%p-Y=m0#c+yh!II|epHa*HzBUAaKjIz?B%xidl^q7ot47z13dy;F> zzr{yzRPR}KYVZlXV*~Ru3aF3mt{UnZ-qFbXej!jdO?bx^=B8PpE3r$%++0DE`BmFY zE{o=?_1L9lzS^)&2ZIq*1e}o~?9#C##RN5CHG;~K<=BD8l`R~lqA-eLj#d)1R(TrN=Af8)RL-mpBv>Nx%!6fKdmbQ^4NeYY3 zx;l>c2%@J$j0D?iyM6vHzSbM+sH>=(ovRCb>%{Kv&OJ(`*H548y>~r!@A}E<3+J3l zm=#D!iIC5gznyy@vQRwz;Y9@tF>EevI_*k8kC3iJwhTTL4I^GeRdEqB`_8i5FlBPaIT(*ea!sfsM8JspTsu7Mw%tisF@8ifEWo# zB6(EX?LsAVGEE|+-~h4-sz@GUN-Vsq-`Kl-s%ZJ(-ti(w%H-&EP0@J?y={VCJgGCq zd`aEHfep8G%kQwJxPF3NOnURcfx-6VvNegN^$GLZ33eS!Ci2S$R}3#nmNz8I)+fvx zZnMs*91+8SYjAyX@#=)FCSk6fVCyCg1^v!r&cuSM3B$^4aC|jUy)jwQm?+o6h*#a{2`eVyF^(R zhHspJf1g19s^nWI=;`1A1z8`yeHecb5{v{;Ng&KtNVULZ!XAN#Y*#%0Ayk|FGV+A| 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 diff --git a/src/next_page_graph/graph.py b/src/next_page_graph/graph.py deleted file mode 100644 index fda9b9a5..00000000 --- a/src/next_page_graph/graph.py +++ /dev/null @@ -1,90 +0,0 @@ -"""LangGraph 流程定义""" -from langgraph.graph import StateGraph, END -from .state import NextPageState -from .nodes import fetch_page, find_pagination_area, find_next_button, validate_next - - -def check_pagination_area(state: NextPageState) -> str: - """检查分页区域结果""" - # 无分页或已标记成功,直接结束 - if state.get("is_valid"): - return "done" - return "continue" - - -def check_find_next(state: NextPageState) -> str: - """检查节点3结果""" - # 仅一页(无可点击按钮但成功) - if state.get("is_valid"): - return "done" - - if state.get("selector"): - return "validate" - - retry_count = state.get("retry_count", 0) - if retry_count >= 3: - return "max_retry" - - return "retry" - - -def check_validation(state: NextPageState) -> str: - """检查验证结果""" - if state.get("is_valid"): - return "success" - - retry_count = state.get("retry_count", 0) - if retry_count >= 3: - return "max_retry" - - return "retry" - - -def create_graph() -> StateGraph: - """创建流程图""" - - graph = StateGraph(NextPageState) - - # 添加节点 - graph.add_node("fetch_page", fetch_page) - graph.add_node("find_pagination_area", find_pagination_area) - graph.add_node("find_next_button", find_next_button) - graph.add_node("validate_next", validate_next) - - # 入口 - graph.set_entry_point("fetch_page") - - # 流程 - graph.add_edge("fetch_page", "find_pagination_area") - - # 节点2: 无分页直接结束,有分页继续找按钮 - graph.add_conditional_edges( - "find_pagination_area", - check_pagination_area, - {"done": END, "continue": "find_next_button"} - ) - - # 节点3: 仅一页直接结束,有按钮去验证,失败重试 - graph.add_conditional_edges( - "find_next_button", - check_find_next, - { - "done": END, - "validate": "validate_next", - "retry": "find_next_button", - "max_retry": END - } - ) - - # 节点4: 验证成功结束,失败重试 - graph.add_conditional_edges( - "validate_next", - check_validation, - { - "success": END, - "max_retry": END, - "retry": "find_next_button" - } - ) - - return graph.compile() diff --git a/src/next_page_graph/main.py b/src/next_page_graph/main.py deleted file mode 100644 index 58f7746e..00000000 --- a/src/next_page_graph/main.py +++ /dev/null @@ -1,85 +0,0 @@ -"""入口""" -import asyncio -import sys -from pathlib import Path - -# 支持直接运行 -if __name__ == "__main__": - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from playwright.async_api import async_playwright -from src.next_page_graph.graph import create_graph - - -async def analyze_next_page(url: str, headless: bool = False) -> dict: - """ - 分析分页列表页,提取"下一页"的 CSS 选择器 - - Args: - url: 分页列表页 URL - headless: 是否无头模式 - - Returns: - dict: {status, selector, change_type} 或 {status, reason, tried_selectors} - """ - async with async_playwright() as p: - browser = await p.chromium.launch(headless=headless) - context = await browser.new_context() - page = await context.new_page() - - try: - graph = create_graph() - - initial_state = { - "url": url, - "page": page - } - - print(f"\n{'='*50}") - print(f"开始分析: {url}") - print(f"{'='*50}\n") - - final_state = await graph.ainvoke(initial_state) - - print(f"\n{'='*50}") - print("分析完成") - print(f"{'='*50}\n") - - # 构造结果 - if final_state.get("is_valid"): - return { - "status": "success", - "selector": final_state["selector"], - "change_type": final_state["change_type"] - } - else: - return { - "status": "failed", - "reason": final_state.get("error", "验证失败"), - "tried_selectors": final_state.get("failed_selectors", []) - } - - finally: - await browser.close() - - -async def main(): - """测试""" - url = "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - - result = await analyze_next_page(url) - - print("\n最终结果:") - if result["status"] == "success": - print(f"✅ 成功") - print(f" 选择器: {result['selector']}") - print(f" 变化类型: {result['change_type']}") - else: - print(f"❌ 失败") - print(f" 原因: {result['reason']}") - if result.get("tried_selectors"): - print(f" 尝试过: {result['tried_selectors']}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/next_page_graph/nodes.py b/src/next_page_graph/nodes.py deleted file mode 100644 index aabf8427..00000000 --- a/src/next_page_graph/nodes.py +++ /dev/null @@ -1,227 +0,0 @@ -"""节点实现""" -import asyncio -import hashlib - -from langchain_core.output_parsers import PydanticOutputParser -from pydantic import BaseModel, Field -from langchain_core.messages import HumanMessage -from src.bash_model import GeneralLlm, AnalyseLlm -from .state import NextPageState -from .prompts import FIND_PAGINATION_AREA_PROMPT, FIND_NEXT_BUTTON_PROMPT - - -# 结构化输出模型 -class SelectorResult(BaseModel): - """选择器结果""" - selector: str | None = Field(description="CSS选择器,找不到则为None") - reason: str = Field(description="选择或找不到的原因") - - -async def fetch_page(state: NextPageState) -> dict: - """节点1: 获取页面 HTML""" - url = state["url"] - page = state["page"] - - print(f"[节点1] 访问页面: {url}") - - await page.goto(url, wait_until="load", timeout=60000) - await asyncio.sleep(3) - - # 获取清理后的 HTMLa - html = await page.evaluate(""" - () => { - const clone = document.body.cloneNode(true); - clone.querySelectorAll('style, script, noscript, svg, link').forEach(el => el.remove()); - return clone.innerHTML; - } - """) - - print(f"[节点1] 获取 HTML 完成,长度: {len(html)}") - - return { - "html": html, - "failed_selectors": [], - "retry_count": 0 - } - - -async def find_pagination_area(state: NextPageState) -> dict: - """节点2: 找分页区域""" - html = state["html"] - page = state["page"] - - print("[节点2] 分析分页区域...") - - # LLM 分析 - prompt = FIND_PAGINATION_AREA_PROMPT.format(html=html) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=SelectorResult) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - # - # llm = AnalyseLlm.with_structured_output(SelectorResult) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - if not result.selector: - # 无分页控件,直接成功 - print(f"[节点2] 无分页控件: {result.reason}") - return { - "has_pagination": 0, - "is_valid": True - } - - print(f"[节点2] 找到分页区域: {result.selector}") - - # 获取区域 HTML - try: - pagination_html = await page.inner_html(result.selector) - except Exception as e: - # 选择器无效,当作无分页 - print(f"[节点2] 选择器无效: {result.selector}, {e}") - return { - "has_pagination": 0, - "is_valid": True - } - - return { - "has_pagination": 1, - "pagination_selector": result.selector, - "pagination_html": pagination_html - } - - -async def find_next_button(state: NextPageState) -> dict: - """节点3: 找下一页按钮""" - pagination_html = state["pagination_html"] - pagination_selector = state["pagination_selector"] - failed_selectors = state.get("failed_selectors", []) - retry_count = state.get("retry_count", 0) - page = state["page"] - - print(f"[节点3] 分析下一页按钮... (重试: {retry_count})") - - # LLM 分析 - prompt = FIND_NEXT_BUTTON_PROMPT.format( - pagination_html=pagination_html, - failed_selectors="\n".join(failed_selectors) if failed_selectors else "无" - ) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=SelectorResult) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - # llm = AnalyseLlm.with_structured_output(SelectorResult) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - next_selector = result.selector - if not next_selector: - # 有分页控件但无可点击的下一页(仅一页),统一设为无分页 - print(f"[节点3] 无可点击的下一页: {result.reason}") - return { - "selector": None, - "has_pagination": 0, - "is_valid": True - } - - # 组合完整选择器 - selector = f"{pagination_selector} {next_selector}" - - # 检查是否已失败过 - if selector in failed_selectors: - print(f"[节点3] 选择器已失败过: {selector}") - return { - "selector": None, - "failed_selectors": failed_selectors, - "retry_count": retry_count + 1 - } - - print(f"[节点3] 选择器: {selector}") - - # 验证元素存在 - element = await page.query_selector(selector) - if not element: - print("[节点3] 选择器未匹配到元素") - return { - "selector": None, - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1 - } - - return {"selector": selector} - - -async def validate_next(state: NextPageState) -> dict: - """节点4: 验证下一页""" - selector = state["selector"] - page = state["page"] - url = state["url"] - failed_selectors = state.get("failed_selectors", []) - retry_count = state.get("retry_count", 0) - - print(f"[节点4] 验证选择器: {selector}") - - context = page.context - - # 1. 记录状态 - before_url = page.url - tabs_before = len(context.pages) - content_before = await page.content() - hash_before = hashlib.md5(content_before.encode()).hexdigest() - - # 2. 点击(先 hover 触发可能的显示效果) - try: - element = page.locator(selector).first - await element.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await element.hover() - await asyncio.sleep(0.3) - await element.click() - except Exception as e: - print(f"[节点4] 点击失败: {e}") - return { - "is_valid": False, - "change_type": "no_change", - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1 - } - - await asyncio.sleep(3) - - # 3. 检测变化 - tabs_after = len(context.pages) - after_url = page.url - content_after = await page.content() - hash_after = hashlib.md5(content_after.encode()).hexdigest() - - if tabs_after > tabs_before: - change_type = "new_tab" - # 关闭新标签 - await context.pages[-1].close() - elif after_url != before_url: - change_type = "url_change" - elif hash_after != hash_before: - change_type = "content_change" - else: - change_type = "no_change" - - print(f"[节点4] 变化类型: {change_type}") - - # 4. 恢复状态 - if change_type in ("url_change", "content_change"): - await page.goto(url, wait_until="load", timeout=60000) - await asyncio.sleep(2) - - # 5. 判断结果 - if change_type == "no_change": - print("[节点4] 验证失败: 点击后无变化") - return { - "is_valid": False, - "change_type": change_type, - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1 - } - else: - print("[节点4] 验证成功!") - return { - "is_valid": True, - "change_type": change_type - } diff --git a/src/next_page_graph/prompts.py b/src/next_page_graph/prompts.py deleted file mode 100644 index efa80381..00000000 --- a/src/next_page_graph/prompts.py +++ /dev/null @@ -1,80 +0,0 @@ -"""LLM 提示词""" - -# 节点2: 找分页区域 -FIND_PAGINATION_AREA_PROMPT = """分析以下 HTML,找到分页组件所在的区域。 - -## HTML -{html} - -## 任务 -找到包含分页组件的容器的 CSS 选择器。 - -## 判断建议 -1. 包含页码数字(1, 2, 3...) -2. 包含"上一页"/"下一页" ">" ">>" "<" "<<""或类似按钮 -3. 常见 class: pagination, pager, pages, page-nav -4. 通常在页面底部 -5. 根据你的专业判断最可能的元素 - -## 重要约束 -1. 选择器必须唯一匹配一个元素 -2. 如果基础选择器匹配多个元素,必须使用伪类精确定位,例如: - - .list:nth-child(2) - - .container > ul:first-of-type - - #main .job-list:nth-of-type(1) -3. 避免使用随机/哈希 class - -## 输出 -仅输出 JSON: -{{ - "selector": "CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到则: -{{ - "selector": null, - "reason": "原因" #字数限制30字 -}} -""" - -# 节点3: 找下一页按钮 -FIND_NEXT_BUTTON_PROMPT = """分析以下分页区域 HTML,找到可点击的"下一页"按钮。 - -## 分页区域 HTML -{pagination_html} - -## 之前失败的选择器(避免使用) -{failed_selectors} - -## 任务 -找到可点击的"下一页"按钮的 CSS 选择器(相对于分页区域)。 - -## 判断依据 -1. 文本包含: "下一页"、"Next"、"»"、">"、"→"、"›" -2. Class 包含: next, pagination-next, next-page -3. 属性: [rel="next"], [aria-label*="next"] -4. 位置: 分页区域中靠右侧的可点击元素 -5. 根据你的专业判断最可能的元素 - -## 必须排除(举例) -- disabled 属性的元素 -- class 包含 disabled 的元素 -- hidden 或 display:none 的元素 -- 灰色/不可点击状态的按钮 - -如果下一页按钮存在但是 disabled 状态,返回 null。 - -## 输出 -仅输出 JSON: -{{ - "selector": "相对于分页区域的CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到可点击的下一页按钮: -{{ - "selector": null, - "reason": "原因(如:按钮disabled/仅一页/无按钮)" #字数限制30字 -}} -""" diff --git a/src/next_page_graph/state.py b/src/next_page_graph/state.py deleted file mode 100644 index c1e30e0a..00000000 --- a/src/next_page_graph/state.py +++ /dev/null @@ -1,33 +0,0 @@ -"""状态定义""" -from typing import TypedDict -from playwright.async_api import Page - - -class NextPageState(TypedDict, total=False): - """下一页选择器分析状态""" - - # 输入 - url: str - page: Page - - # 节点1: fetch_page - html: str - - # 节点2: find_pagination_area - pagination_selector: str - pagination_html: str - has_pagination: int # 0=无分页, 1=有分页 - - # 节点3: find_next_button - selector: str - - # 节点4: validate_next - change_type: str # url_change / content_change / new_tab / no_change - is_valid: bool - - # 重试 - failed_selectors: list[str] - retry_count: int - - # 错误 - error: str diff --git a/src/page_analysis_graph/__init__.py b/src/page_analysis_graph/__init__.py deleted file mode 100644 index fb310ec0..00000000 --- a/src/page_analysis_graph/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import analyze_job_list -from .graph import create_graph -from .state import AnalysisState - -__all__ = ["analyze_job_list", "create_graph", "AnalysisState"] diff --git a/src/page_analysis_graph/__pycache__/__init__.cpython-312.pyc b/src/page_analysis_graph/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index f425885cde97566d2e7ead9d2e2a1019334bbea6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 zcmXv}yH3O~5VYg?f({}f@c|&ZG>94@5RaM$(L|P3=Hd&va~`ro5jr}=575)WukZ(! z(m+LrbR89I16zz|$2UpH2n&m8T zrsJCDoRX3h@iWaQ1J5R(6m1!Go3fF5xKprbuR&L~M{Q$flKvBUHHrKxz%}LRM%Z-=}8>;>s#x#jNAx{6@M*vh!)dTExgIhbS+u~NMBa>oFf0Ero_+pgOk?f9S Pdn7yW)8qTZtfT$|r9oQZ diff --git a/src/page_analysis_graph/__pycache__/graph.cpython-312.pyc b/src/page_analysis_graph/__pycache__/graph.cpython-312.pyc deleted file mode 100644 index 1bbdca75eb5ae76ec3e71d9503d225e6d21eed48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2545 zcma)8-ES0C6ut1S_P6_5Z=BG?!pJ}7-5Cc4>VGS1AFZTHJNvxZG* z(&7i@!&sxHjWmK0;$jVGq6rA`AMgd}CajYhn#h*+i8XC}@X2%T>~6PB8#u|Hd+xoz zd(ZTD&ON7p1Oh$+a&9az_`X8O-#Dp?STUAPf-yl15+Md>@Ch!-NBE==5jbK9hL{kO zQbc05l<*|wh@4a+O41wgCVdeftb0t!kj;jeXeb|v5r5s>3-dtT+z0bU!16KPXaGBy z^M?c3&EWnmEj924)iNh~=YJi2oIhW>{`I3y&=PS2ZdENaVd{38 zI+BH691k_^EKTX?qbdRz_*>cmYJ!|1gY~-=l)+6`Rw-Lx#n6Vu`e)40Yt!hmUqIobeTE&P<;|N0Ieh`w!^)QS$J zQ)cvtX${%wOf(&fnKYxtqZZYp$d;;Am{bQa3Q?wC&g7g%{e-Cx)o7%xaIK8@w}BcV zbF%+TZZvndX=_2=Rt$C&8(NlNgREi?6OFNV8jNRPZ}SHH@`f;KM9Y^>j96Q}juLpqvKC4VrV87x7PW zyh-h(%2pkT4YUcCukkDhv=eL#kN5Uc^y##?bkYY(Ve5;D4;ksLTz6R3-Bdj1PxQ(#9jUJgi6)V`Rek1s)Skd;ox30lr=B|;I zQpLU!d0!*g308T;^j#wcZ(})NVhA?Yao3in_-T9{AngA2&I)#ii-5;0t~%|R5}tZm zv@vy`^$fo=#?<8$ffESPggkfLnb0Iy6G)cx;1);pBGsS+k%< z$g+n7+P@s0@oyVB3KdB5o#`L#A3t~{R1kL+MK7E6T{%_|_pLj-+wx*z$14T#06Y6Y z?7Sy-PVOp*+w1PS+*1(u&WYX!V$VIXr>L~gDw~U~FZ^iTQ}!>*qU0(2!EfDe*IfHc z#kQ@po!$S)qSv=9D}ZUO!*tCm9go^W007u3jKAr#LuU`2@4G98SUKzuZ*pW+HPX7O zI>7||yWzGtmkSmoMkV%6H(U~i%33#8f@M>eek|RLc}C|o!hE@GD+iNlBbzW^qgbV| zs95iUDhnLP{YknXlAV8%-iM@rhV+*`BG)>(yG&rnA6~+t-00(4CWp%ehM%CP!cY!* ZVd+?zz>wFMa42`|9?xim?rh7*lIHG& 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 C<__Ng diff --git a/src/page_analysis_graph/__pycache__/nodes.cpython-312.pyc b/src/page_analysis_graph/__pycache__/nodes.cpython-312.pyc deleted file mode 100644 index 394947e09db7d12e6a4d0c98ee2497b77cb67d4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12687 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 diff --git a/src/page_analysis_graph/__pycache__/prompts.cpython-312.pyc b/src/page_analysis_graph/__pycache__/prompts.cpython-312.pyc deleted file mode 100644 index b21424d8ae4b87031dfd7b1ebfd1f9b0ba655ef1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2759 zcmcIm&2JM&6t_uQ+LdyGL&c#QIXw_HALXN$s6|Riss<$iLE1xP$=cc*V%XT$?)nf# zNOr)9jqTWNATfzUVwwa9#10T(W5>=P&_ml}Il|6*?BYX z{ocIa@6GQG4U7eTSHle#{(#X7GO7Hm<-zNNFqpBpECUvob-?P{H?YrDH&8cH-}I5c z0hW4tdXbt)YD;-N7xz!pSu8j_uHH;4g=IyKq96KudtPqE)JO@3*KsU_llSn$8KoG< z;Tb)1N1MHZvzz$F9UQu>Z7jYBj4@1Oqq&P29g>FmOXghJjN(WfFFnvVmSKw#h^W!K zu{M9DhEFvQqvKX5{mVZprQSG$Vl%{XBejOWd&7jGV0C zGBXTe>nlMmzoO1B5MWj0cspcd3iw)1kpnokX+W;!0Dz#_OdKm?>diD>$ZMIv&NkDO zrlgXF7(I*Lm9n*}Jt$}oOIjw2v$KE_%0wwe@yZzBW)MOxsFgry_eeu6&LKDNI()RL z{N3sGNUYn#36N4h-#+LTM5)C)*dmSiIBL>34C|;B(6>|%DH|L<(nOL~TR6sSLQUt} z@KhMfF`7R!D|%@PPvjY;5TYY=>fGtRc4l-GoY*QAW@|?_D3&U8TQgY|I9Bv}$Xa<} zaskgR+7N2QYcuNgb>l`7ho2n#3TDhDs8Og)MFv)^3TFD&fhWg-XFgeMC6_8bl^Q%dN$74>q66tYRpD!eKcb`D}jAl~ zVVHV-gD{RB6VGUoi8d;qMrPA=8N$UWVpN=}VpnILfO|wL9a5rQd!*-{0(y`Pn5qlQ zS~^;t(yNB48jBdeW)X87d@gQ+#wo=`2ttvk6nU2FI^jhlsMxZ)xMc(%<7}8YLV^d^ zk+}LWV+!GJorYkVQ8i2DnyASN0cotp^jwf0Sv6($6sJl7Au1?pDo5|HT5PpaVq#2f(r7?f2eAF)~pe-0dhu}PtL-Y?1v%&~9 zq@ICrcw(HviB&@IWDJUv-Zg!)DDo1}3dbJ-mCB*M6&UGIyNGReLbIgRL_*(QZ^KWQ zm13Gk)dsBAH+}I3@1d2eTNiw+{FE)JR;4=qFz{G1w|9%Olu+ua0e9wg9Q-f@?G zP4&r)8Tk_@cZt*PRFU!-#I$Yc6B(;oITxproZNEcYjxJ4dj|(O!NA-G1@Ok}TmP5jRF#8wc6uM{t diff --git a/src/page_analysis_graph/__pycache__/state.cpython-312.pyc b/src/page_analysis_graph/__pycache__/state.cpython-312.pyc deleted file mode 100644 index 871fa811567ad0bf81640e253638cefd6d1132e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 909 zcmX|<&rj4q6vwCSZ1-n(VL|)>3Ni7p7cSli(HMycQ8ZB!v)N=aw8L(p+t!(`rZ>%^ zK~8wWcraXz#-oXUgU7urlW+y@1M_=Cm{PyOz^42t&e;kL*S#a$L4E&HbnLSuHn2n)vx(l#}7^-V<2z?c{ zt{V)qjHAzkhnYY|LQ2CaWl}Oz$|jVBxLm5`6Uu>BNGQ+nMU^+>6w~NZ8f__x%e9KM zZu!)Ug)1IKG-90z(C<;dxyWhQnmO8jef(i}P_;BV+>ptqK7BIs#Lr8 z`y*rB2f)>HJv`noSGy})BVwLkU=R`wwC$jv#svH>&rw?qPJ2rxw=Ma zLW5nu$<-5-R$wqto{#p3jx6Z diff --git a/src/page_analysis_graph/graph.py b/src/page_analysis_graph/graph.py deleted file mode 100644 index 80dce0aa..00000000 --- a/src/page_analysis_graph/graph.py +++ /dev/null @@ -1,82 +0,0 @@ -"""LangGraph 流程定义""" -from langgraph.graph import StateGraph, END -from .state import AnalysisState -from .nodes import fetch_page, find_job_area, find_job_item, validate_selector - - -def check_job_area(state: AnalysisState) -> str: - """检查节点2结果(成功/失败,重试在节点内部完成)""" - if state.get("job_area_selector"): - return "success" - return "failed" - - -def check_find_job_item(state: AnalysisState) -> str: - """检查节点3结果""" - # 有 selector 说明成功 - if state.get("selector") and state.get("item_count", 0) > 0: - return "success" - - retry_count = state.get("retry_count", 0) - if retry_count >= 3: - return "max_retry" - - return "retry" - - -def check_validation(state: AnalysisState) -> str: - """检查验证结果""" - if state.get("is_valid"): - return "success" - - retry_count = state.get("retry_count", 0) - if retry_count >= 3: - return "max_retry" - - return "retry" - - -def create_graph() -> StateGraph: - """创建流程图""" - - graph = StateGraph(AnalysisState) - - # 添加节点 - graph.add_node("fetch_page", fetch_page) - graph.add_node("find_job_area", find_job_area) - graph.add_node("find_job_item", find_job_item) - graph.add_node("validate_selector", validate_selector) - - # 入口 - graph.set_entry_point("fetch_page") - - # 流程 - graph.add_edge("fetch_page", "find_job_area") - - graph.add_conditional_edges( - "find_job_area", - check_job_area, - {"success": "find_job_item", "failed": END} - ) - - graph.add_conditional_edges( - "find_job_item", - check_find_job_item, - { - "success": "validate_selector", - "retry": "find_job_item", - "max_retry": END - } - ) - - graph.add_conditional_edges( - "validate_selector", - check_validation, - { - "success": END, - "max_retry": END, - "retry": "find_job_item" - } - ) - - return graph.compile() diff --git a/src/page_analysis_graph/main.py b/src/page_analysis_graph/main.py deleted file mode 100644 index 4fcb1f29..00000000 --- a/src/page_analysis_graph/main.py +++ /dev/null @@ -1,87 +0,0 @@ -"""入口""" -import asyncio -import sys -from pathlib import Path - -# 支持直接运行 -if __name__ == "__main__": - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from playwright.async_api import async_playwright -from src.page_analysis_graph.graph import create_graph - - -async def analyze_job_list(url: str, headless: bool = False) -> dict: - """ - 分析招聘页面,提取岗位列表的 CSS 选择器 - - Args: - url: 招聘列表页 URL - headless: 是否无头模式 - - Returns: - dict: {status, selector, item_count, change_type} 或 {status, reason, tried_selectors} - """ - async with async_playwright() as p: - browser = await p.chromium.launch(headless=headless) - context = await browser.new_context() - page = await context.new_page() - - try: - graph = create_graph() - - initial_state = { - "url": url, - "page": page - } - - print(f"\n{'='*50}") - print(f"开始分析: {url}") - print(f"{'='*50}\n") - - final_state = await graph.ainvoke(initial_state) - - print(f"\n{'='*50}") - print("分析完成") - print(f"{'='*50}\n") - - # 构造结果 - if final_state.get("is_valid"): - return { - "status": "success", - "selector": final_state["selector"], - "item_count": final_state["item_count"], - "change_type": final_state["change_type"] - } - else: - return { - "status": "failed", - "reason": final_state.get("error", "验证失败"), - "tried_selectors": final_state.get("failed_selectors", []) - } - - finally: - await browser.close() - - -async def main(): - """测试""" - url = "https://dearsamsung.zhiye.com/#/samsung/pc/szzw" - # url ="https://careers.tencent.com/search.html" - result = await analyze_job_list(url) - - print("\n最终结果:") - if result["status"] == "success": - print(f"✅ 成功") - print(f" 选择器: {result['selector']}") - print(f" 岗位数: {result['item_count']}") - print(f" 变化类型: {result['change_type']}") - else: - print(f"❌ 失败") - print(f" 原因: {result['reason']}") - if result.get("tried_selectors"): - print(f" 尝试过: {result['tried_selectors']}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/page_analysis_graph/nodes.py b/src/page_analysis_graph/nodes.py deleted file mode 100644 index 2f5cea94..00000000 --- a/src/page_analysis_graph/nodes.py +++ /dev/null @@ -1,327 +0,0 @@ -"""节点实现""" -import asyncio -import hashlib - -from langchain_core.output_parsers import PydanticOutputParser -from pydantic import BaseModel, Field -from langchain_core.messages import HumanMessage -from src.bash_model import GeneralLlm, AnalyseLlm -from .state import AnalysisState -from .prompts import FIND_JOB_AREA_PROMPT, FIND_JOB_ITEM_PROMPT, VALIDATE_JOB_DETAIL_PROMPT - - -# 结构化输出模型 -class SelectorResult(BaseModel): - """选择器结果""" - selector: str | None = Field(description="CSS选择器,找不到则为None") - reason: str = Field(description="选择或找不到的原因") - - -class ValidationResult(BaseModel): - """验证结果""" - is_job_detail: bool = Field(description="是否为岗位详情页") - reason: str = Field(description="判断依据") - - -async def fetch_page(state: AnalysisState) -> dict: - """节点1: 获取页面 HTML""" - url = state["url"] - page = state["page"] - - print(f"[节点1] 访问页面: {url}") - - await page.goto(url, wait_until="load", timeout=60000) - await asyncio.sleep(3) - - # 获取 HTML 并移除 style/script 标签(减少无效内容) - html = await page.evaluate(""" - () => { - const clone = document.body.cloneNode(true); - clone.querySelectorAll('style, script, noscript, svg, link').forEach(el => el.remove()); - return clone.innerHTML; - } - """) - - print(f"[节点1] 获取 HTML 完成,长度: {len(html)}") - - return { - "html": html, - "failed_selectors": [], - "retry_count": 0 - } - - -async def find_job_area(state: AnalysisState, max_retries: int = 3) -> dict: - """节点2: 找招聘区域(内部重试)""" - html = state["html"] - page = state["page"] - - failed_selectors = [] - - for attempt in range(max_retries): - print(f"[节点2] 分析招聘区域... (尝试: {attempt + 1}/{max_retries})") - - # LLM 分析 - prompt = FIND_JOB_AREA_PROMPT.format( - html=html, - failed_selectors="\n".join(failed_selectors) if failed_selectors else "无" - ) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=SelectorResult) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - - # llm = AnalyseLlm.with_structured_output(SelectorResult) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - if not result.selector: - print(f"[节点2] 未找到招聘区域: {result.reason}") - continue - - # 检查是否已失败过 - if result.selector in failed_selectors: - print(f"[节点2] 选择器已失败过: {result.selector}") - continue - - print(f"[节点2] 找到区域: {result.selector}") - - # 验证选择器 - try: - # 检查是否唯一匹配 - elements = await page.query_selector_all(result.selector) - if len(elements) == 0: - print(f"[节点2] 选择器未匹配到元素: {result.selector}") - failed_selectors.append(result.selector) - continue - if len(elements) > 1: - print(f"[节点2] 选择器匹配到 {len(elements)} 个元素,需要更精确的选择器") - failed_selectors.append(result.selector) - continue - - job_area_html = await page.inner_html(result.selector) - return { - "job_area_selector": result.selector, - "job_area_html": job_area_html - } - except Exception as e: - print(f"[节点2] 选择器无效: {result.selector}, {e}") - failed_selectors.append(result.selector) - continue - - # 所有重试都失败 - return { - "job_area_selector": None, - "error": f"找不到招聘区域,尝试过: {failed_selectors}" - } - - -async def find_job_item(state: AnalysisState) -> dict: - """节点3: 找岗位单元选择器""" - job_area_html = state["job_area_html"] - job_area_selector = state["job_area_selector"] - failed_selectors = state.get("failed_selectors", []) - retry_count = state.get("retry_count", 0) - page = state["page"] - - print(f"[节点3] 分析岗位单元... (重试: {retry_count})") - - # 截断 - display_html = job_area_html - # if len(display_html) > 30000: - # display_html = display_html[:30000] + "\n... (已截断)" - - # LLM 分析 - prompt = FIND_JOB_ITEM_PROMPT.format( - job_area_html=display_html, - job_area_selector=job_area_selector, - failed_selectors="\n".join(failed_selectors) if failed_selectors else "无", - ) - - chain = AnalyseLlm | PydanticOutputParser(pydantic_object=SelectorResult) - result = await chain.ainvoke([HumanMessage(content=prompt)]) - - # llm = AnalyseLlm.with_structured_output(SelectorResult) - # result = await llm.ainvoke([HumanMessage(content=prompt)]) - - item_selector = (result.selector or "").strip() - if not item_selector: - return { - "selector": None, - "item_count": 0, - "error": f"未找到岗位单元: {result.reason}", - "failed_selectors": failed_selectors, - "retry_count": retry_count + 1, - } - - # 归一化:有些模型会“偷懒”输出完整路径(包含 job_area_selector),导致拼接成 ".A .A li" 这种必然失败的选择器 - normalized = item_selector - if normalized.startswith(job_area_selector): - normalized = normalized[len(job_area_selector) :].strip() - # 处理 "{job_area_selector} ..." 这种情况(前缀已去掉后可能还残留空格) - if normalized.startswith(job_area_selector): - normalized = normalized[len(job_area_selector) :].strip() - - # 如果归一化后变空,说明模型返回的就是区域本身;此时直接失败,让它重试产出真正的“子元素”选择器 - if not normalized: - return { - "selector": None, - "item_count": 0, - "failed_selectors": failed_selectors, - "retry_count": retry_count + 1, - } - - # 组合完整选择器 - # 支持 "> li" 这种相对写法: - # - ".A" + " > li" => ".A > li" - # - ".A" + "li" => ".A li" - if normalized.startswith(">"): - selector = f"{job_area_selector} {normalized}" - else: - selector = f"{job_area_selector} {normalized}" - - # 检查是否已经失败过 - if selector in failed_selectors: - print(f"[节点3] 选择器已失败过: {selector}") - return { - "selector": None, - "item_count": 0, - "failed_selectors": failed_selectors, - "retry_count": retry_count + 1, - } - - print(f"[节点3] 选择器: {selector}") - - # 验证匹配数量 - elements = await page.query_selector_all(selector) - item_count = len(elements) - - if item_count == 0: - print("[节点3] 选择器未匹配到元素") - return { - "selector": None, - "item_count": 0, - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1, - } - - print(f"[节点3] 匹配到 {item_count} 个岗位") - - return { - "selector": selector, - "item_count": item_count, - } - - -async def validate_selector(state: AnalysisState) -> dict: - """节点4: 验证选择器""" - selector = state["selector"] - page = state["page"] - url = state["url"] - failed_selectors = state.get("failed_selectors", []) - retry_count = state.get("retry_count", 0) - - print(f"[节点4] 验证选择器: {selector}") - - context = page.context - - # 1. 记录状态 - before_url = page.url - tabs_before = len(context.pages) - content_before = await page.content() - hash_before = hashlib.md5(content_before.encode()).hexdigest() - - # 2. 点击第一个岗位 - try: - element = page.locator(selector).first - await element.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await element.hover() - await asyncio.sleep(0.3) - await element.click() - except Exception as e: - return { - "is_valid": False, - "change_type": "no_change", - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1, - "error": f"点击失败: {e}" - } - - await asyncio.sleep(6) - - # 3. 检测变化类型 - tabs_after = len(context.pages) - after_url = page.url - content_after = await page.content() - hash_after = hashlib.md5(content_after.encode()).hexdigest() - - if tabs_after > tabs_before: - change_type = "new_tab" - # 切到新标签获取内容 - new_page = context.pages[-1] - await asyncio.sleep(2) - content_summary = await get_page_summary(new_page) - await new_page.close() - elif after_url != before_url: - change_type = "redirect" - content_summary = await get_page_summary(page) - elif hash_after != hash_before: - change_type = "in_page" - content_summary = await get_page_summary(page) - else: - change_type = "no_change" - content_summary = "" - - print(f"[节点4] 变化类型: {change_type}") - - # 4. 判断是否有效 - if change_type == "no_change": - is_valid = False - reason = "点击后无变化" - else: - # LLM 判断 - prompt = VALIDATE_JOB_DETAIL_PROMPT.format( - change_type=change_type, - content_summary=content_summary - ) - llm = GeneralLlm.with_structured_output(ValidationResult) - result = await llm.ainvoke([HumanMessage(content=prompt)]) - is_valid = result.is_job_detail - reason = result.reason - - # 5. 恢复状态 - if change_type == "redirect" or change_type == "in_page": - await page.goto(url, wait_until="load", timeout=60000) - await asyncio.sleep(2) - - if is_valid: - print(f"[节点4] 验证成功!") - return { - "is_valid": True, - "change_type": change_type - } - else: - print(f"[节点4] 验证失败: {reason}") - return { - "is_valid": False, - "change_type": change_type, - "failed_selectors": failed_selectors + [selector], - "retry_count": retry_count + 1 - } - - -async def get_page_summary(page) -> str: - """获取页面内容摘要""" - title = await page.title() - - # 获取 h1 - h1 = "" - h1_el = await page.query_selector("h1") - if h1_el: - h1 = await h1_el.inner_text() - - # 获取正文前 1500 字 - body_text = await page.inner_text("body") - body_text = body_text[:1500] if len(body_text) > 1500 else body_text - - return f"标题: {title}\nH1: {h1}\n正文:\n{body_text}" diff --git a/src/page_analysis_graph/prompts.py b/src/page_analysis_graph/prompts.py deleted file mode 100644 index 0d985fcd..00000000 --- a/src/page_analysis_graph/prompts.py +++ /dev/null @@ -1,108 +0,0 @@ -"""LLM 提示词""" - -# 节点2: 找招聘区域 -FIND_JOB_AREA_PROMPT = """分析以下 HTML,找到包含岗位列表的容器元素。 - -## HTML -{html} - -## 之前失败的选择器(避免使用) -{failed_selectors} - -## 任务 -找到包含招聘岗位列表的容器的 CSS 选择器。 - -## 判断依据 -1. 容器内有多个重复的子元素(通常 > 5 个) -2. 子元素包含招聘相关信息:职位名称、薪资、地点、公司等 -3. 常见 class: list, job, position, result, items, card - -## 重要约束 -1. 选择器必须唯一匹配一个元素 -2. 如果基础选择器匹配多个元素,必须使用伪类精确定位,例如: - - .list:nth-child(2) - - .container > ul:first-of-type - - #main .job-list:nth-of-type(1) -3. 避免使用随机/哈希 class - -## 输出 -仅输出 JSON: -{{ - "selector": "CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到则: -{{ - "selector": null, - "reason": "原因" #字数限制30字 -}} -""" - -# 节点3: 找岗位可点击元素 -FIND_JOB_ITEM_PROMPT = """分析以下招聘区域 HTML,找到可点击进入岗位详情的元素选择器。 - -## 区域 HTML -{job_area_html} - -## 区域选择器 -{job_area_selector} - -## 之前失败的选择器(避免使用) -{failed_selectors} - -## 任务 -找到“可点击进入岗位详情”的元素选择器(相对于区域,不要包含区域选择器本身)。 - -## 优先级(从高到低) -1. 岗位名称/标题链接(a 标签,包含职位名称的 -2. 整个岗位卡片/列表项(仅当整体可点击时) -3. “申请”、“查看”、“详情”按钮 -4. 根据你的专业分析 - -### 严格遵守 -1,当前没有失败的选择器时, 优先级选择岗位名称 -2. 验证遵守优先级 - - -## 重要约束 -1. selector 必须是相对选择器,例如:"a"、"a.title"、".job-card a"、"li a" -2. 严禁输出包含区域选择器的完整路径 -3. 必须能匹配到多个元素(通常 >= 5 个) -4. 避免使用随机/哈希 class - -## 输出 -仅输出 JSON: -{{ - "selector": "相对于区域的CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到则: -{{ - "selector": null, - "reason": "原因" #字数限制30字 -}} -""" - -# 节点4: 判断是否岗位详情 -VALIDATE_JOB_DETAIL_PROMPT = """判断以下内容是否为岗位详情页。 - -## 变化类型 -{change_type} - -## 页面内容摘要 -{content_summary} - -## 判断标准 -只要同时包含以下两项即可判断为岗位详情页: -1. 岗位名称 -2. 岗位描述/工作内容/职责要求 - -## 输出 -仅输出 JSON: -{{ - "is_job_detail": true或false, - "reason": "判断依据" #字数限制30字 -}} -""" diff --git a/src/page_analysis_graph/state.py b/src/page_analysis_graph/state.py deleted file mode 100644 index 63fc38c5..00000000 --- a/src/page_analysis_graph/state.py +++ /dev/null @@ -1,33 +0,0 @@ -"""状态定义""" -from typing import TypedDict -from playwright.async_api import Page - - -class AnalysisState(TypedDict, total=False): - """LangGraph 分析状态""" - - # 输入 - url: str - page: Page - - # 节点1: fetch_page - html: str - - # 节点2: find_job_area - job_area_selector: str - job_area_html: str - - # 节点3: find_job_item - selector: str - item_count: int - - # 节点4: validate_selector - change_type: str # new_tab / redirect / in_page / no_change - is_valid: bool - - # 重试 - failed_selectors: list[str] - retry_count: int - - # 错误 - error: str diff --git a/src/scheduler/__init__.py b/src/scheduler/__init__.py deleted file mode 100644 index 92506dd6..00000000 --- a/src/scheduler/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.scheduler.scheduler import scheduler, start_scheduler, shutdown_scheduler - -__all__ = ["scheduler", "start_scheduler", "shutdown_scheduler"] diff --git a/src/scheduler/__pycache__/__init__.cpython-312.pyc b/src/scheduler/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 723699de9c76e24fbd78df111a266c12c6a4cdd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293 zcmX@j%ge<81YI4xSu=q2V-N=hn4pZ$3P8qmh7^Vr#vF!R#wf;IrYNRd<|t-HhIF9V zBE~3|N+wO_mmuYu%(pm;lQU9NN^??+Zt)kFBo>v#BLswsGfGQR^2_rOVt$&ex5SH! zlJ(%K5Ue6*ppiu^Ac7T0++vT9Pt3`Qk6+2~8OUY$W#McU6Iz^FR2-9?pO+frl3JWy zl3x&$pO%(dRFIe%12i@UZfs0^d}dx|NqoFsLFFwDo7{Ym8|{j?fd(-Gaj^i9_`uA_ V$atS2=psYVBQA$V_99N8AOJ;yR0041 diff --git a/src/scheduler/__pycache__/jobs.cpython-312.pyc b/src/scheduler/__pycache__/jobs.cpython-312.pyc deleted file mode 100644 index 0debd17dacbe12ab07d62eba54979e9fbdaef244..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39311 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 task.max_retry: - task.status = "failed" - # 更新主表 - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_status = "failed" - logger.warning(f"搜索失败,已达最大重试次数") - else: - task.status = "pending" # 下次继续 - logger.info(f"搜索失败,等待重试 ({task.retry_count}/{task.max_retry})") - - db.commit() - - except Exception as e: - logger.error(f"[job_step1_search] 异常: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_step1_search] 完成") - - -async def job_step2_page(): - """Step 2: 岗位列表分析""" - logger.info("[job_step2_page] 开始") - db = SessionLocal() - - try: - # 1. 查询 pending 任务(加行锁防止并发重复处理) - task = db.query(TaskPageAnalysis).filter( - TaskPageAnalysis.status == "pending" - ).with_for_update(skip_locked=True).first() - if not task: - logger.info("[job_step2_page] 无待处理任务") - return - - logger.info(f"[job_step2_page] 执行 task_id={task.id}, url={task.input_url}") - - # 2. 更新状态为 running - task.status = "running" - task.started_at = datetime.now() - db.commit() - - # 3. 执行分析 - result = None - error_msg = None - - try: - context = await create_stealth_context() - page = await context.new_page() - - try: - graph = create_page_graph() - final_state = await graph.ainvoke({ - "url": task.input_url, - "page": page - }) - - if final_state.get("is_valid"): - result = { - "selector": final_state["selector"], - "item_count": final_state["item_count"], - "change_type": final_state["change_type"] - } - else: - error_msg = final_state.get("error", "验证失败") - finally: - await context.close() - - except Exception as e: - error_msg = str(e) - logger.error(f"分析异常: {e}") - - # 4. 处理结果 - task.finished_at = datetime.now() - - if result: - # 检查岗位数量 - item_count = result["item_count"] - - if item_count == 0: - # 岗位数为0,标记失败,不触发下一步 - task.status = "failed" - task.output_selector = result["selector"] - task.output_change_type = result["change_type"] - task.output_item_count = 0 - task.error_message = "岗位列表无数据" - - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_status = "failed" - - logger.warning(f"分析失败: 岗位列表无数据, selector={result['selector']}") - else: - # 成功 - task.status = "success" - task.output_selector = result["selector"] - task.output_change_type = result["change_type"] - task.output_item_count = item_count - - # 更新主表 - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_step = 2 - - # 创建 Step3 任务 - next_task = TaskNextPage( - crawl_task_id=task.crawl_task_id, - input_url=task.input_url - ) - db.add(next_task) - - logger.info(f"分析成功: selector={result['selector']}, count={item_count}") - else: - # 失败 - task.retry_count += 1 - task.error_message = error_msg or "分析失败" - - if task.retry_count >= task.max_retry: - task.status = "failed" - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_status = "failed" - logger.warning(f"分析失败,已达最大重试次数") - else: - task.status = "pending" - logger.info(f"分析失败,等待重试 ({task.retry_count}/{task.max_retry})") - - db.commit() - - except Exception as e: - logger.error(f"[job_step2_page] 异常: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_step2_page] 完成") - - -async def job_step3_next(): - """Step 3: 分页分析""" - logger.info("[job_step3_next] 开始") - db = SessionLocal() - - try: - # 1. 查询 pending 任务(加行锁防止并发重复处理) - task = db.query(TaskNextPage).filter( - TaskNextPage.status == "pending" - ).with_for_update(skip_locked=True).first() - if not task: - logger.info("[job_step3_next] 无待处理任务") - return - - logger.info(f"[job_step3_next] 执行 task_id={task.id}, url={task.input_url}") - - # 2. 更新状态为 running - task.status = "running" - task.started_at = datetime.now() - db.commit() - - # 3. 执行分析 - final_state = None - error_msg = None - - try: - context = await create_stealth_context() - page = await context.new_page() - - try: - graph = create_next_graph() - final_state = await graph.ainvoke({ - "url": task.input_url, - "page": page - }) - finally: - await context.close() - - except Exception as e: - error_msg = str(e) - logger.error(f"分析异常: {e}") - - # 4. 处理结果 - task.finished_at = datetime.now() - - if final_state and final_state.get("is_valid"): - # 成功(包括无分页、仅一页、可翻页) - task.status = "success" - task.has_pagination = final_state.get("has_pagination", 0) - - # 有可翻页的下一页按钮 - if final_state.get("selector"): - task.output_selector = final_state["selector"] - task.output_change_type = final_state.get("change_type") - - # 更新主表 - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_step = 3 - - # 创建 Step4 任务,查询 Step2 获取所需数据 - step2_task = db.query(TaskPageAnalysis).filter( - TaskPageAnalysis.crawl_task_id == task.crawl_task_id, - TaskPageAnalysis.status == "success" - ).first() - - if step2_task: - detail_task = TaskDetailAnalysis( - crawl_task_id=task.crawl_task_id, - input_url=task.input_url, - input_job_selector=step2_task.output_selector, - input_change_type=step2_task.output_change_type - ) - db.add(detail_task) - - logger.info(f"分析成功: has_pagination={task.has_pagination}") - else: - # 失败 - task.retry_count += 1 - task.error_message = error_msg or "分析失败" - - if task.retry_count >= task.max_retry: - task.status = "failed" - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_status = "failed" - logger.warning(f"分析失败,已达最大重试次数") - else: - task.status = "pending" - logger.info(f"分析失败,等待重试 ({task.retry_count}/{task.max_retry})") - - db.commit() - - except Exception as e: - logger.error(f"[job_step3_next] 异常: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_step3_next] 完成") - - -async def job_step4_detail(): - """Step 4: 详情页分析""" - logger.info("[job_step4_detail] 开始") - db = SessionLocal() - - try: - # 1. 查询 pending 任务(加行锁防止并发重复处理) - task = db.query(TaskDetailAnalysis).filter( - TaskDetailAnalysis.status == "pending" - ).with_for_update(skip_locked=True).first() - if not task: - logger.info("[job_step4_detail] 无待处理任务") - return - - logger.info(f"[job_step4_detail] 执行 task_id={task.id}, url={task.input_url}") - - # 2. 更新状态为 running - task.status = "running" - task.started_at = datetime.now() - db.commit() - - # 3. 执行分析 - result = None - error_msg = None - - try: - context = await create_stealth_context() - page = await context.new_page() - - try: - graph = create_detail_graph() - final_state = await graph.ainvoke({ - "url": task.input_url, - "job_item_selector": task.input_job_selector, - "change_type": task.input_change_type, - "page": page - }) - - if final_state.get("is_valid"): - result = { - "detail_area_selector": final_state.get("detail_area_selector"), - "fields": final_state.get("fields") - } - else: - error_msg = final_state.get("error", "验证失败") - finally: - await context.close() - - except Exception as e: - error_msg = str(e) - logger.error(f"分析异常: {e}") - - # 4. 处理结果 - task.finished_at = datetime.now() - - if result: - # 成功 - task.status = "success" - task.output_detail_selector = result["detail_area_selector"] - task.output_fields = result["fields"] - - # 更新主表 - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_step = 4 - crawl_task.config_status = "success" - crawl_task.crawl_status = "pending" - - # 汇总配置创建 Step5 任务,查询 Step3 获取分页信息 - step3_task = db.query(TaskNextPage).filter( - TaskNextPage.crawl_task_id == task.crawl_task_id, - TaskNextPage.status == "success" - ).first() - - input_config = { - "url": task.input_url, - "job_item_selector": task.input_job_selector, - "item_change_type": task.input_change_type, - "next_page_selector": step3_task.output_selector if step3_task else None, - "page_change_type": step3_task.output_change_type if step3_task else None, - "detail_area_selector": result["detail_area_selector"], - "field_selectors": result["fields"] - } - - crawl_task_record = TaskCrawl( - crawl_task_id=task.crawl_task_id, - input_config=input_config - ) - db.add(crawl_task_record) - - logger.info(f"分析成功: fields={list(result['fields'].keys()) if result['fields'] else []}") - else: - # 失败 - task.retry_count += 1 - task.error_message = error_msg or "分析失败" - - if task.retry_count >= task.max_retry: - task.status = "failed" - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.config_status = "failed" - logger.warning(f"分析失败,已达最大重试次数") - else: - task.status = "pending" - logger.info(f"分析失败,等待重试 ({task.retry_count}/{task.max_retry})") - - db.commit() - - except Exception as e: - logger.error(f"[job_step4_detail] 异常: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_step4_detail] 完成") - - -def convert_fields_for_crawler(fields: dict) -> dict: - """转换字段格式适配 Crawler""" - result = {} - for name, info in fields.items(): - if name == "detail_url": - continue # crawler 不需要 selector - if "selectors" in info: # description 是数组 - result[name] = {"selector": info["selectors"], "sample": info.get("sample")} - elif "selector" in info and info["selector"]: - selector = info["selector"] - result[name] = { - "selector": selector if isinstance(selector, list) else [selector], - "sample": info.get("sample") - } - return result -def calc_content_hash(data: dict) -> str: - content = "|".join([ - str(data.get("job_title") or "").strip().lower(), - str(data.get("salary") or "").strip().lower(), - str(data.get("location") or "").strip().lower(), - str(data.get("company") or "").strip().lower(), - str(data.get("experience") or "").strip().lower(), - str(data.get("education") or "").strip().lower(), - str(data.get("description") or "").strip().lower(), - str(data.get("detail_url") or "").strip().lower(), - ]) - return hashlib.md5(content.encode("utf-8")).hexdigest() - - -def calc_company_name_hash(company_name: str | None) -> str: - return hashlib.md5(str(company_name or "").strip().lower().encode("utf-8")).hexdigest() - - -def calc_job_unique_hash(data: dict, company_name: str | None, recruit_category: int) -> str: - content = "|".join([ - str(company_name or "").strip().lower(), - str(data.get("job_title") or "").strip().lower(), - str(data.get("location") or "").strip().lower(), - str(data.get("detail_url") or "").strip().lower(), - str(recruit_category), - ]) - return hashlib.md5(content.encode("utf-8")).hexdigest() - - -def infer_recruit_category(data: dict) -> int: - text = " ".join([ - str(data.get("job_title") or ""), - str(data.get("salary") or ""), - str(data.get("description") or ""), - ]).lower() - if any(keyword in text for keyword in ["校招", "校园", "应届", "应届生", "new grad", "graduate", "grad"]): - return 1 - if any(keyword in text for keyword in ["实习", "intern", "internship", "暑期", "见习"]): - return 2 - return 0 - - -async def job_step5_crawl(): - """Step 5: 数据爬取""" - logger.info("[job_step5_crawl] start") - db = SessionLocal() - - try: - task = db.query(TaskCrawl).filter(TaskCrawl.status == "pending").with_for_update(skip_locked=True).first() - if not task: - logger.info("[job_step5_crawl] no pending task") - return - - logger.info(f"[job_step5_crawl] task_id={task.id}") - task.status = "running" - task.started_at = datetime.now() - - crawl_task = db.query(CrawlTask).filter(CrawlTask.id == task.crawl_task_id).first() - if crawl_task: - crawl_task.crawl_status = "running" - - db.commit() - - results = [] - error_msg = None - - try: - config = task.input_config.copy() - config["field_selectors"] = convert_fields_for_crawler(config.get("field_selectors", {})) - results = await crawl(config, headless=settings.browser_headless) - except Exception as e: - error_msg = str(e) - logger.error(f"crawl error: {e}") - - task.finished_at = datetime.now() - - if error_msg is None: - task.status = "success" - task.crawled_count = len(results) - - saved_count = 0 - if results: - item_change_type = task.input_config.get("item_change_type", "redirect") - is_independent = 1 if item_change_type != "in_page" else 0 - - for item in results: - recruit_category = infer_recruit_category(item) - company_name = crawl_task.company_name if crawl_task else "" - company_name_hash = calc_company_name_hash(company_name) - job_unique_hash = calc_job_unique_hash(item, company_name, recruit_category) - content_hash = calc_content_hash(item) - - exists = db.query(JobData).filter( - JobData.job_unique_hash == job_unique_hash, - JobData.is_valid == 1, - ).first() - - if not exists: - job_data = JobData( - task_crawl_id=task.id, - job_title=item.get("job_title"), - salary=item.get("salary"), - location=item.get("location"), - company=company_name, - experience=item.get("experience"), - education=item.get("education"), - description=item.get("description"), - detail_url=item.get("detail_url"), - company_name_hash=company_name_hash, - job_unique_hash=job_unique_hash, - content_hash=content_hash, - recruit_category=recruit_category, - is_independent_url=is_independent, - expire_at=datetime.now() + timedelta(days=settings.job_expire_days), - ) - db.add(job_data) - saved_count += 1 - - if crawl_task: - crawl_task.crawl_status = "success" - crawl_task.total_crawl_times += 1 - crawl_task.last_crawl_at = datetime.now() - - logger.info(f"crawl success: total={len(results)}, saved={saved_count}") - else: - task.retry_count += 1 - task.error_message = error_msg - - if task.retry_count >= task.max_retry: - task.status = "failed" - if crawl_task: - crawl_task.crawl_status = "failed" - logger.warning("crawl failed: max retries reached") - else: - task.status = "pending" - logger.info(f"crawl failed: retry {task.retry_count}/{task.max_retry}") - - db.commit() - - except Exception as e: - logger.error(f"[job_step5_crawl] error: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_step5_crawl] done") - - -async def job_periodic_crawl(): - """周期爬取: 检查超过设置间隔未爬取的任务,创建新的爬取任务""" - logger.info("[job_periodic_crawl] start") - db = SessionLocal() - - try: - threshold = datetime.now() - timedelta(days=settings.crawl_interval_days) - tasks = db.query(CrawlTask).filter( - CrawlTask.crawl_status == "success", - CrawlTask.last_crawl_at < threshold, - ).all() - - if not tasks: - logger.info("[job_periodic_crawl] no tasks") - return - - logger.info(f"[job_periodic_crawl] found {len(tasks)} tasks") - - for crawl_task in tasks: - last_crawl = db.query(TaskCrawl).filter( - TaskCrawl.crawl_task_id == crawl_task.id, - TaskCrawl.status == "success", - ).order_by(TaskCrawl.id.desc()).first() - - if not last_crawl or not last_crawl.input_config: - logger.warning(f"task {crawl_task.id} has no valid config, skip") - continue - - new_crawl = TaskCrawl( - crawl_task_id=crawl_task.id, - input_config=last_crawl.input_config, - ) - db.add(new_crawl) - crawl_task.crawl_status = "pending" - logger.info(f"created periodic crawl for task {crawl_task.id}") - - db.commit() - - except Exception as e: - logger.error(f"[job_periodic_crawl] error: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_periodic_crawl] done") - - -async def job_check_validity(): - """Validate job URLs and refresh expiry.""" - logger.info("[job_check_validity] start") - db = SessionLocal() - - try: - now = datetime.now() - check_timeout = now - timedelta(hours=2) - - pending_jobs = db.query(JobData).filter( - JobData.is_valid == 1, - JobData.check_status == "pending", - JobData.expire_at <= now, - ).limit(20).all() - - timeout_jobs = db.query(JobData).filter( - JobData.is_valid == 1, - JobData.check_status == "checking", - JobData.last_check_at < check_timeout, - ).limit(20).all() - - job_ids = set() - jobs = [] - for job in pending_jobs + timeout_jobs: - if job.id not in job_ids and len(jobs) < 20: - job_ids.add(job.id) - jobs.append(job) - - if not jobs: - logger.info("[job_check_validity] no jobs") - return - - logger.info(f"[job_check_validity] checking {len(jobs)} jobs") - - for job in jobs: - job.check_status = "checking" - job.last_check_at = now - db.commit() - - independent_jobs = [j for j in jobs if j.is_independent_url == 1] - non_independent_jobs = [j for j in jobs if j.is_independent_url == 0] - - for job in non_independent_jobs: - job.is_valid = 0 - job.check_status = "pending" - logger.info(f"job {job.id} non-independent, mark invalid") - - if independent_jobs: - domain_groups: dict[str, list[JobData]] = {} - for job in independent_jobs: - if job.detail_url: - domain = urlparse(job.detail_url).netloc - domain_groups.setdefault(domain, []).append(job) - else: - job.is_valid = 0 - job.check_status = "pending" - - async def check_domain(domain: str, domain_jobs: list[JobData]): - async with httpx.AsyncClient(timeout=10.0) as client: - for job in domain_jobs: - try: - resp = await client.get(job.detail_url, follow_redirects=True) - if resp.status_code == 200: - job.expire_at = now + timedelta(days=settings.job_expire_days) - logger.debug(f"job {job.id} valid, extended") - else: - job.is_valid = 0 - logger.info(f"job {job.id} status {resp.status_code}, invalid") - except Exception as e: - job.is_valid = 0 - logger.info(f"job {job.id} request failed: {e}") - finally: - job.check_status = "pending" - - await asyncio.gather(*[ - check_domain(domain, domain_jobs) - for domain, domain_jobs in domain_groups.items() - ]) - - db.commit() - - except Exception as e: - logger.error(f"[job_check_validity] error: {e}") - db.rollback() - finally: - db.close() - logger.info("[job_check_validity] done") - - -async def job_generate_company(): - """Generate company list.""" - logger.info("[job_generate_company] start") - - try: - result = generate_companies() - logger.info(f"[job_generate_company] result: {result}") - except Exception as e: - logger.error(f"[job_generate_company] error: {e}") - - logger.info("[job_generate_company] done") diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py deleted file mode 100644 index 6c9cab97..00000000 --- a/src/scheduler/scheduler.py +++ /dev/null @@ -1,117 +0,0 @@ -"""定时任务调度器""" -import logging -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from datetime import datetime - -from src.config import settings -from src.scheduler.jobs import ( - job_step1_search, - job_step2_page, - job_step3_next, - job_step4_detail, - job_step5_crawl, - job_periodic_crawl, - job_check_validity, - job_generate_company, -) - -logger = logging.getLogger(__name__) - -# 创建调度器 -scheduler = AsyncIOScheduler( - job_defaults={ - 'coalesce': True, # 合并错过的任务 - 'max_instances': 3, # 同一任务最多3个实例并发 - } -) - - -def start_scheduler(): - """启动调度器并注册所有任务""" - - # Step 1: 搜索招聘页面 - - scheduler.add_job( - job_step1_search, - 'interval', - seconds=settings.job_step1_search_interval, - id='job_step1_search', - max_instances=2, - name='搜索招聘页面', - next_run_time=datetime.now() # 立即执行第一次 - ) - - # Step 2: 岗位列表分析 - scheduler.add_job( - job_step2_page, - 'interval', - seconds=settings.job_step2_page_interval, - id='job_step2_page', - name='岗位列表分析' - ) - - # Step 3: 分页分析 - scheduler.add_job( - job_step3_next, - 'interval', - seconds=settings.job_step3_next_interval, - id='job_step3_next', - name='分页分析' - ) - - #Step 4: 详情页分析 - scheduler.add_job( - job_step4_detail, - 'interval', - seconds=settings.job_step4_detail_interval, - id='job_step4_detail', - name='详情页分析' - ) - - # Step 5: 数据爬取 - scheduler.add_job( - job_step5_crawl, - 'interval', - seconds=settings.job_step5_crawl_interval, - id='job_step5_crawl', - next_run_time=datetime.now(), - name='数据爬取' - ) - - # # 周期爬取 - # scheduler.add_job( - # job_periodic_crawl, - # 'interval', - # seconds=settings.job_periodic_crawl_interval, - # id='job_periodic_crawl', - # name='周期爬取' - # ) - # - # # 有效性检查 - # scheduler.add_job( - # job_check_validity, - # 'interval', - # seconds=settings.job_check_validity_interval, - # id='job_check_validity', - # name='有效性检查' - # ) - - # AI生成公司名 - scheduler.add_job( - job_generate_company, - 'interval', - seconds=settings.job_generate_company_interval, - id='job_generate_company', - max_instances=1, # 单实例 - next_run_time=datetime.now(), - name='AI生成公司名' - ) - - scheduler.start() - logger.info("定时任务调度器已启动") - - -def shutdown_scheduler(): - """关闭调度器""" - scheduler.shutdown() - logger.info("定时任务调度器已关闭") diff --git a/src/search_company_graph/__init__.py b/src/search_company_graph/__init__.py deleted file mode 100644 index 2e39d57d..00000000 --- a/src/search_company_graph/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""搜索公司招聘页面模块""" -from .main import search_company_recruitment - -__all__ = ["search_company_recruitment"] diff --git a/src/search_company_graph/__pycache__/__init__.cpython-312.pyc b/src/search_company_graph/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 58232c197585fcc963ff3da052ec24d639b4cab3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmX@j%ge<81YI4xSq(t?F^B^LOi;#WDIjAyLkdF_LkeRGQx0P;Qxp>;Lke>`V-#~G zizaKS98kft33HxrS@g7Z&C~uJ&$?&7Xq@qK;ntUP7d>0C@af#?FBySKG#PJ66{jW^ zC1=DZ=jRqA=2gZQr6w1ZW|rip=9LsN1C{t`vfN^ik59=@j*q{^lAD;B2Nnj2CFbPB z$FF4g3{=JN%h}l~CbT%Us5mA&KQA@LCAB!aB)=dgKP@e_s30*jrno3M2F@&om}4~II(@{-WlNn zk|R-(Kn1Bl;wvgqAr%m*K)+B6qJKbNLSqSYq5`SdNuFvHp8C|8JzwI4M6Bf9ncdmB z-QWCn#((;JZUXJ(kgspOhme0zv6@g4=7vBxPh=7%GAG;OT*4N%CHOGU5t)~TxR4OT zB9q0qJ>dvD63(zQ;R?GF?ywu??TRQn6i<&JJC6%tZ<+1_-B+f&L9Zy&J)rwxudT-> zdqJ*D`$83|S~$+WNV4}68W}j)Qut+X`ueH-50{?*Hav?b4%rO%em$ZqY~A4ZcI}zv zaGoLkf-cq>Z{{ zlICDgV`Y{>;^xySJv#-TC+arNZcN{_c;{SH|<1i?f;G*|QG{r^oX{7iaGN zH1o%a!pP|K&)*ppQ8;i+#zUrPTr(U8BN|GEC#fj1WM23NXYjp>euJYBa-!k4TmJiK z_lMn|X$sZ4`_-h0U*X&L~h4)l&fhCFk4PoxLalT~Z8j@F#NKIf^Qg9Wk(EF>HQs~v=u!RjRwHrz|d zG43d*S&%itMB!?taAmx3;Z}a+d&H{@DOv&RhBK*_;_E_WSg2_gtb}X~3`0QJH1yxn zLRaAyPxPP??Jt?)N6?Yh1RY$zvMV2nQ-8>cc z!OF_FE2qK=bm7ZvgtQp4r>eoFdH30yU(ehcoW6Iq@a?GKrizw|>x=+mkD5x#hNw`g zQp4U8iNzJ!@G)9g#AZm)8gOQk!UUkk1i*X+KzwvbTfqpgsD+@*kfNP9eHh`9rmSO4 z&R@GQQI~ZDbN)J*@HW1R%6!cGz|YF4$hNYWFaL=6GG5q@aysJ8+EJT@SARDl@Wf2a z>S58uEPw0%%+SsJ$m!`z$EQ#JTF49?S-wBaDT?$CYG=XOyDrt&JKUsm0<|z=YMp-U1dA1@6!~paX{0ufF2C=$dTZa{G&^ z`W=%(AQxyjf8d(~lg*oMS5E~#$aEpp!O`7g6;n-{{&H-}`PU)Vt4yrH$$WEy#NJmI z+~qvc=Tni6cVo>$>KLXf(VATtu>c&Oe}CLcR=jSv_E>RS(N@s$T&%s;R@7O!LgUxk z+I6q#b&0Ltnq3$d5->X@=7S|o=5&lJYqAJs&oOG7C^3xf!8Es|VR2k^%H{kDI=&*- zbYvuJ@CD}*|I6(a^#*Il!rlc*w#%+je$gqH^J3RAm$6p1SCrjLPS9*wEsPT-aX$%p zQiyAQ=yLwv@1>G_`2ni}i;^Hx4)e<#@&lhcY~{LckXMq9vm+i!d@e^izg!cg3f42T z&UD1pXe6$6wp;7lz>$VoBt!0qjPn%h4na>m?2! zVUZJW)tQ~cJ5TMJ6dD*4xW~Viq)tl;Q(Q9|p*w5|S<>7| z7*l!1gb+kOHrg9(sAaRLRG66hpM6OtM}^oxzn$Q&@kq)0ZODoSPwOh5GPolg?b z%+L>A&F=no-`lru-<^JM?{6lP5kYxBX7a5C^mj5)i!6b`{U(gi0OC+9;xMQ2<5n#9 znpO?;l%MLPTWO4VEk|#92_eDBYkaI~B-{HeS*#)sYY?x8O7n)3unQ@^Vx`Y*pLI^P zaZ;nK#>8W-Kd+N*Wo69xO`Yaevy63qOJ{y-K43Ga=k=Tcs*x|?^7uktUkU~`>N)mZ zZEF!{dKa}8XU=B@f3a^~u73)K`Qno^)2b&Jb!)mc4oidqL2}3OcXxLQ-u8|#%<0|^^KO5*10d@Wcz2k0wF~Z02aJN9 zA$OqL6>xX*lEE7YMZ&I#;Ft6ryqoj$q9`!}AC3qC4>jWz>2y%HL%n|*W>+B$k2X9L zdW2N>7oqL&kbempzts#-?`gHDhl*0ZnXnQwYVm1RlEy_fuL}Kx+7=g@(!Xy8r4&rD`TVR zJ7utnGv=;~>U~A3)odTl{FO3iqGuv+d`2%!|JApr_c!`VviqutGpnn6@}had_oI0b zBic7hm1lc3su@3L-VlsDgj(R2d52(}`!-_qHi`28!D>B<5@Y(45b z26Z*m8(y!bE%=D$%}h3 zN-|h2(^ZbYeyBHkn#e27q}7#^IO6K zNj_4MI~4MFyF?|Oxa3qL>4_SNfIt{o(uM>tIIn|*v^ltmm#B6=ENMOdpvc2kBwhmH zVkMF|BtJR+Mi`SnLxP`;nhcT1yWb4;T|x$C%YI z{K}U0vC+s*G(?2&%jq+C2-ML{^C1qLHzaj=6FZEW`ecdgi*_p z#v`?(g$pLwIx<`vU%CHvS$Nwv=Y;8~X{dAD)|e=3jIDZlZS4eWe|!;mST^VI>;toh zD#lBf#C~+$QaWj^ibvCWIP*Zw-kSdT3D!Q!+W)l%qx>p}-MVmFDY(*lA_HUet_W@Yh8}AQ{nNXPJIPPqao@7R--Wv9p31Db*5Oi9vH@wyar) zot65Lg|cQn(Y#Dyr;WKxGX%33EL^t8ZK2$j$Sqi_(p)Z+aRnxtl`^);xT@H>RC{>| zcD`!7T+a}^lma_f5VN9CcZI<#4D1!1j$pG6)_UOe&J;os@|Eut0OiWZ;hvmpUlg63 zxdK8e*5oGI9vp^Cpi($W)q=|u7VJ3^I#==rTAzN>hu28hl34~=RxC} z!5ubW$>jToQ@j3>j14P!LrIGbTPa|e2)W#UbEwaj8vNvG9morWAB1vf9F&qH;2upF zSE5L6An^bz!i&&LG~pYe8sJK(;u>gVLb+X3+56_5qM9N9c+qPK(`&uXJG5ccSUy2l zT-TXy>P@5hH52;zH;PJQ%gLW^`F<)MiC14^EALULxC~guU>-F)uIcBc3y`_-kx2OJ zor30K+>e_J>3z!pIW#R!$8(a-<>G=KmkU^%d_8!B5+!zvk}d>a7k=*sf%r?lGRS8F zXhFV%uMWl=0Ct5FL2@O+jb1+yG9fofcv5lp6Qox8CUJ+nN~$T$iqu@9o!Q#=e!n!*@g zN9+w`zJZLlkQHc#W@B5gQl*o$Zs*EvD|f!N?X7t8Uh%-9Bl?3)iSn1n%3n^D*N>Ig zC(4_~%A3Y#Et{Z!kVFe^p%-tV`g_b0Ts6EfjiCEd=|_@$g9a6_L_B z8c0cuH`{<4ryDt++P~Ap?K%6q_Zz(t zX?D+k2Yqwrd*1JLU*8=*G#GRggl7f~ogd#%QU8T6p`Z!^cUYRDMk$`!Pw_ObaMJr} zcq*NWF6Dk@7qg$CF`aR$y43sC1XejUUF?3S#;bXaQ`@E6uOl$)OzP6_*LNBA8@i19 zjWng8wo<(INs8AA%AY9Z{`M!6)FepNN2QubssU1sQK>0>HlHk*d6STOh~ZP7RP9eA zu!1)~$?UiAsX}TnFAd()d5fTf_Y5eT4)2*!?-}r(74@D8@7YoBS@527LSfD6(*vHj zo_YM{<8RNs@x7bRP2NVlo2?4Jc9Y#D?CRkKr(d#}$65?n5Oy8!RH ztwOgT+MP|#uG=uJ6eK0J2zIfpqs47^3s$8+ZS&Xm?x}Cs+uF3fd1p)O1ABJwdSI_V z|0|pKY~Ql4b z`M6uOx4D}f-G^O!1XrKa-Dif(`BNivr(U@I{Co4y{ov+1&(DAFY+o)U&kdZpJuvjp zo~F6M(Oa)f{OR4{`LhFauYR}s#?+5rgW2H)SDWbQbvt^x{Y;FTw2pJ;6t?Y$ibx?x+mY`rH$12jXTBmi7x z5Isd*Wpb~k=SXE+X41C~G!7OIx`wP?X6sdR#(49Lxq4vBV8aYkEoO(RCSJ}Rkp1PB zbCHE)1z5_ch17pV5FLk31eXxE{p}wQ-}>n{gNv653G-t=og4Y#jj3PGfBVH--yD;d z@b)YJA{rsnpXzY6ib9*%=Wurk-R@SLWU_j5gRjoN@cNDSUYdXQ4Kly5bRzB%F%_dU zj4T+D70L(5&pgu8;}o$^QbEi>2=}EtEjb9~g(6%sR|!Nox9L~85~-?bhTA1E8DnW) zW>>2CJ3F2O0ZxqHMu zoDR9p{NM}Frd;9Xcb}Yl?&Y}`$H^30wTn+Vb}LVDWX9tryAX?Dge&2)LWGl@bv1{R zDjuB4*)yu1cH0g{7<4$-Mb>fqNXs$Zd{w3LVH4AHnyL3kqXg4GDN{RG7` zk3y*zm{H}oR4QoK4QnTdPBsKZp1dm5Q@ExwR1^Bc!yFfQ^1%GD@ zni!0rM|t#XC>(!xnj2{o2|VjzUZh@9gvuj6Jk!rS2E6`+HUtmIDWK7FBhSIs1F~@YrSEgw_U>x>)4PK=fAr4nGp}+k_X(#^DN}gv z=IcX$dKdnN=caym`{`$I{q#Gx2j02$-f6#y@9Apm>2?DVarW4GfxkTngaWvO#_i}5 zdivbWBCei21$Ksxiwk8|u5JVO=z?@C293JhT$>Zl5Ld?mSLj1RWqYKDKVc)O%|N%y z++v?#U7xrCtZF;bCx|Cnf`_l(=`1TH<0|Lmft7RJJt4?-w7r~j0&6U_+79)Io9%5K zWr7ni6r46u=;}Etlvx+)Og0&AsvV+^cWi`uPuPIX`99_*J64yIt_79qK{(qsOmuIR&BD&k9HF&OR)p>J=T`ZmY`A^n&;!E&&|;8oS#qboILZ z4C3IY1s4jMWX(h*TYh6Kmf{*nMB?o7L3E4yY=F9vnmLy9ht!fmO+b;Q%?>b1gT&q3d(e=yJKV|@WBeQ;^ z;=G!!U#~u|A#ed*U#C9LsW81n1vSnuqoLsWa&7%u)%jHlOkYDIyp}<@j>dSSw!xsf zz;X?&>OvX4X*qkrnuc&)60~rURYC4WgA(AjFm(ltkggHxYlclEQ(vHV(Iub&20l!Q zfj%k-WmU-Zv7h$PuB=6Bh2_lnS}jyNP;(Crbo~&}b)c9DX*vURIY`m_8D8ySj)G92 zrQ9Li1$cpn36%)<5v1JxDvwH3cvMJ7JGJsSqT@yS1pXds(ZeiKhK-crwP(Ylq^aQ! z21llLhdG8POl4uL6%=&<#?!BX5vvp8q~UcZ0m&CBKFOoxiDVA29$*F;G{6RcSs8vQ zO!1-TN(OYN&M=LSjMNk6R4o+n89)5a!|yck8|1zugt^iSF|7OYE%fIPfzKy@<~f$_<(fE>KV@d)tP_JwHUN(AdHUCPN2aC*DT z)db3k4TTK5quW(hD&t*hwJst#$cP0S0FG7fhWUD=&n=XdhDKPr=;XyR=*6aU?$xJ3 zhX2#M--Q{KPY2f_3WtIdw_v)+CIhq8(F@aQz^UIh=5c z%N!t|pg@LC8;%ur3Q5Elf`;4mRfoHythB@J?yaq?lovWwd_f5pDH1Kz$Q_SlA_{URjjo>do<7*FU6r^Deia#=pTU7ym0~TF z5!Yd~9wU^E?y3(A4Adn}RLz;S>g^hT!98!BpdH zg>m@xAh!4Rp;4e4R6p9`LahsT7Y11pG@~RKG^Yex7q>$xRH0pUAcYWVFrcPDnlqd+ z1gPv%_70VurTq()ovIz!`RQ6ppK;xiHG1&$L21bzucc)+xkX}Iphjl4#H9VZDQA%J z8O^6UhB`+2#vbt+3kQ{usm~pAO(zuv6e@$|TH%^0W~$9wxM?8m3V9|0qVA6v(1d zasx}LwDf@{kSe9Tia(^<0)rcW5RgVI6txGm9FvD$*oh3 zzqY)+bE@vL0tDL!zVg9+mu*t+zTdO^14=5b67e;kT0gXY#N$mWmYCvzi-ysE7PyZx z?WI2pq*1vI^yeUersc)Rv43-Y1*Y+FOQVJ!r5mc4v21{TMmN+j|6B;rc}sG`TE+Qd zx?#2Ydz5htr) z@nmt5sOt#@N>J{DqTK?)P2o|v=o29FpgEcdhhc#adI$zy1?sV~Gc3Y{(kUurY6iiN z5mP)&B0(#nzPC`&N}~2P|5xpwP=dJS1aXTP;}Or0xOGAqBW|&McwiTkoj`Gp7N2gu z_{RMB)5uxv>tKRo_yy6}xH4FuMT7^iN-e_pWO@z9u@~sI9aTf_kl+R_78)z(9tE*R zrX00gDOkS2>RQV6$B9Go84qdSZXk8xrm$Tbrj#vGCrHn+NG$!vM*F!oJD|dq35zko zVD&^2O)Rg9yfqV&E8<=}X@J?RD=mBY>!k;mTT2OHJ!yo2*FjRz0oMjdwW?*Rst^Ug zS|(n8-S(}`yTNo{-?G`SY8QL@dI?>%stEZM(Q@lI;LyPwA++MyM6{v!Rb4ooI0aeU z2w5b06ad%vA(H7JQ=l?m&o6qp>r7XG(pBi;=o>TcSab!Mf1z|)nS+K;GbvM^FV%7f zh?m)CGLLFcYo+XE6T7CWepUB&o!7K|P<1uQV$>kFV zr#Ag+*W0_irX2}NUQbR5tbvX`zYBEN0>nmTz6wc7c>c}R3}`-1Z_1#bv}|IjGxR1k z^IbJSg4)At|Xe!f8s#1#<&hB4YH_u(fa# zELTWhJ6reLPFGcBS9y+0}8EpYnM{BrXI}DSDMC67KYhggh z;%Y))2{cczzadsyu)sw~;KG_=9TJ8Y3_Q#07Ow-umQJJr@=3h@oMAz|u!ZyQjL-O! zAOT-EJ~x`;Nr>2L<4g4`qt+wYjaLA2jqsYp+9H7*5~=|B2h#q(}O^r5fzy7F13Tw= zbK>jL`Uj-t4|;WbW|=(_v&W}1o?14vOtLJS*y`2Q%rZ3+Qxi}qRR$0;XN^mzjZ4S- zrv%Bk)N5>ulra z*OJnZJR7dEdL++=d)MVM*=z5)&cO6NZfh){C+S87^JXqU=M5m*G3Sj0UPd>nne)rD zApHVEH?qtH6@#!Yy-`nH;OIsjbD@wyxFi$P>*&TD=E4Ruz!yz)V;*xcr3&CnP#0n~ ziCGfsXsC5buWig!Ua}}KH-knv6WhO(hh<`jJ*qWCn*8#_K18U1WGkUI37z4qmrx2t z;l;|iu;CC06!t|zqRXL(+!kuTp8-0*jg(+6)Ob{# zcs&{yz+edKSwHJx#dHswkSOXx7`aqnFl5nmh~-`MaK1;=i3BSyc-T-4Fd433qAuHb zy&JhF37#-ZLxW>9^K|DdECYB#O=8&8_OnsM-ssWt$w70b$-@GfXQFz~VJMG=XWJ=Y zYTC05A?8Csyz$1K1R&l8U`h>Z54^^sj%5{CH&nk(0T);5e)VJOmf$n);wq&t8=58b zy9$UsaaWMN6J+a)2sex}UFt&$B~$vUgWTNUw}Q6Jh~aa1VRf6$W>pZfCa!|&czcEB zCa_jDll$6mdreKdxLcD?-2BIh&}|G6KCO)3*kBqog0(VDZUmM)< z0)DdkPh273PRP-HwCAuOq8uf22jZ5YLz|D=g;mA-G3x+E^$>x-0I~{yQjq);nVGN* zbuXAn;d)8wbavs5G8!es)mS#U8}V^EPD1ZEeX@mQ8)Ugq@j{*=2br@NPz(n@7PL?t zc8N!z$a`4!{xEBSYwgVPX>-NE&RFK6+c?AA4^-S(G;1iEHk3^|FXzk{9vawkO_w&S zTRyE@K2yHItJ^rx5MZ<_oi8JM^wHCgzS=aEK2vJ)~eZi~B@!ejoMk-h(t=b~#w$3oyu9XDKW zacs$$MKTurp5?&6kXiwzj+|w_ynJ6?fsZTvRIAdf1C&ao4pdNPuq+T>qEYFf5y`ZC zqJE-UGFHy8E3W5rzQQ73;SyhQ2~;wH4Me5Y{WYM1vVU{!hM9c4UJWGuytbgBhB{w{ zHi+|92H}e3kbWVn9(l~gDlkb<7i+YQxyp-mX^k1mODQzKm&_{2y_BIsI9Ca93~#^% zCA{G=*nP2zKqq9y2UHFc3jK<9DlAkg7AXUEh;SJt2-peu30#2Donaml>HQ}`5JX~C zv`W#>bl;b_C0lCXn|?OEBVVG8B>c$f!}#|8v}!ieko;Peu^F zqEED&BN&5RN&1WQLW~GY`cM}pa%FWbck7K`-5z~o?(JX5&XkLKP!c7$4JwK}Mra3W zzWv`2`p_Rzmd~m|_$2PLvVc1G*3VFAd}G+J5=0T)08!bV9~ziHI}}swCAfN zjATa^m(eC8i@NAZB_jP2_hE#)l2~YDqYR2nVh2VbGEt%fBjQnu0+TG6D?IhK;%LZAS8!}p0Ye{ zI_hwN9~x*ttw)JtviMCXdkZUxu(@Pwk1=&*%TU*>amloC$-q{hC3|$=>3vc`<)qnb zsTpYYW#+xCJ)@P1R!tu9X0DSM3s^?zWxR9|~XB)t;?WteOVb?wwH>iQddw!01yoG;jl@>FUYG zx3;~xZL(G}ZkS;=Udt|vM#)qzRc!cm%5=phsl4IWD=)QN&i8Kq zn$);Y+V`+@pj~>nLozyM*iINrM&@ABwT#@chyRda9o!mFq#8=WXKvJS+A*Fat=%E* z-8bU!rthCM?UzjZ<2=|T(^431N|rA>e>S^nI=gDJ?sBn|UFFT*J)5>$O4|+CWaLNt zyg`ktCQHCVWiGpGqjIbv#0Rx7JX6*+eTrn>HKT97Ua-tpvU)0Ix}?@uQtB(Sf~t^a z1pUHb44B}`a-a#^_l)JUhN@{p)s%k5usKSLI@jdYtwSYB_XSGS-E^V_RZgmBSL~Qx zu|uldDe0PKm|fS<445}|2=7bAS|we@3}d^lG0bWTKhYFk&splrDuVI8Ty_Spa80@Z zT;b|qaWj|rEcu_Rl&A$MK@0kZf-;qT9?(-M+`W*mpg-&@m`_ zW&7n8DgD72(;i=H_SoTx`ZGPBq*h!nt?~tz-&axTTd@)*FE{-!l**D0x?k*qfFI&t z1F6u#y+9_xsXU^+u?w{~*f6=JzQU@>2d4{HFI3_*P~~r~7SMzruTO2NgIgPV`wHfK z`cj1JQn#;GTrknwtJN1$7=$hL`H*q3n%<#iF4n99_|meB9VLqQX?llN{k|d>;e2|B zUj2RngK%;B4zuF@T6#ya`u+6`!W%Oo{Wlr(jw1DMGO^5Wa#Nt4-&D{|I_5XFdOf`S z+xmj0Wa@G@-DF@c=P(Et)SDsWgK8Sk_+Yhmhf?*yMkSil@-fPnJU=1jE6J18i9$zQa1}<)IVc&46v#Lwi}9#JJA%)|ogLLe zcH&-QTZk&3^JRLB+A#^wP-yi122fR%olD||2(O8;3e`)b2=^O7=|$l=F8n0;9^QWu zb3|B0?pzjE310cI3cj!=^n~g~T=-x26jsF(_JpEpT=-x2lpmVGg*|=gHB0s+dklvg z;q;e+Aua*E4N=IbUBkgDuZSB2yvFV_vKLT{jthJtUvzFs=u9TSJSkM+3#g4L3Y+3D zgN?@orzYVEY^RY=#GWWnsTISAiDLa9pj4xFNCI5b6oR=YZ2sgkJo?yCgCBj&=_D$3 zO27UwJ*d>=8BwW8`(GzD#aInritAZPzT*0A z#DFq7Gk3nQXkEk`Jv(N_$w zvVvIy)&Zi|LLWi>oqSrzmEAmdj_YHgVCBjc-2Ay$thbT1M%`**)wg~o*XCAX5m1un zpZ$KEhAf*MZX;@asIG_m4ePPo{OI`ncLvd+;OS@QUzn72@aEXAqCY^o@y@g1DhoRl z>#!@(_9!R>dNE$4$zzXG(qRA#m2{XzGGmd+LKAEl>9A2m{sI$}^@u|Uv?m)(!g&!z z9w&=@R$0rnfAq<}li=Mbg1tD}i~)u5%kih*3El1Wp^hEFVd(j}cL!jiz;*BD z>+iwL)N-;-G9bkTN9BSWqNw$c4@_q%Vm#?f5hCx-H0aYG8s{>X3Qyo>^NRJecT2EW0+=>~q?XqQH=QYE! zS;NX{!%Ck43=AcARZ63JV9Q;WO0vk8rRR2gb@!o~4@&3acco@@gWmYeMsg3iY-06K z>t3sysFaMgGwga8fA$j4W_`tLr&dlE-xpEngLkA$#<%=v_ba=lB{h<6)eN&bk!=8M z9I5Fe?P%rjnu`Z^1{fpYV@@9}KV3d{^yP1y`G(hAIcr`uZC*8~@fj_%#ud}X6<%Z2 zEL$b9Ro9G`Q(Z${V+~#-H_LJo%Rw1T`F77Nn}#nJzcmZX60iV=P2#w;4OBvM-+_DwCo=7BYV7|Xg5b+ zET(rQGZ#zhE8*pROTjJ+^?nV#%gnsLia~f?eHCQomiZu?q_3u%lb8?IVEPp!-E3s8BxCxO<#e-&xl+y`yrQ0lMn9}d-wmb% zmfme(KGHG>8;p?tQ4YO3pZO@a2H@YCX~5#Q7VWN$%HQUv@2XY)7O(?&l?rofRS0iX z0{lA#4Nd<}MPio3NhAg&Al*b_vv#*m`MaFr-74kpHqropOkq18t5gW^ zvUYcl^5aYerexCy=P&?+@fq3PSo(di~~M#L8n3D-a!n?gx<&fX9+)hpZvc>Hi$1kVPFQ7 z>qCgZd`Ht)Df7Qm#{Z;B{z$F(BgXmvNp(z99d}tJT{D(-mx9M#Jwzkx!f*pU;=q>% z`cr)_ojuVIpx`m3yn~OxhWqG}@wPh@JOYo=g|vBcNq~aK)RH^+2yCNs>D6P0vCirV z9_y?QY$&DGV~++XcuctO;3Ke??xbn;`0@aJu}iBbi|>$UU_ZFqLZO@h_;%6iskFP~ JN&Y9p{|B_~Fya6J diff --git a/src/search_company_graph/__pycache__/prompts.cpython-312.pyc b/src/search_company_graph/__pycache__/prompts.cpython-312.pyc deleted file mode 100644 index 8f415645284ac6de1e6c9c1a96b18d2575ef9052..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3474 zcmai1OHUhD6gDAkT1#1#MK?*O&MInx^H4XajnuqUT0$ZqRYR#P*Gw7`gOSIU5}`;o z#Ms79LShpz4-;OY7(*b$`1u36Y1LI(hPiiUR*95dS3URMu`_m(mW7!+_uTXN&Uel| z^ZUVrdL8_Y_zvD&IjGa^kwy8hPJMXY01t6px2{vyUDsLHy{~g$cYSC5o&85YaKaf~ zOUoq=N21bvR!)bUf9=!hkZ%}IN5$NNmb`p5_KDw%&eo-yKt*xtG7o;n^gbOyR(!>>lUP+BlBwD9LBa)SNVNPZ=xX@WP&J zP_I97gwv|l58QC{TJO^LVlIb9l6o;07BeI4pjK4Oq`+wuiB-G+&0t6>j50&q)wY(G zc^}!HpExaMR$k`)#Py(Gb=tLr!}n1(0x!|dHVWqyZ>a2)m>)+TpOjtFpKRoiXB9;< zOg9)|K`C>iihB$Upv6JXVz%?VU1+?0`*tJXaE!ZY`$e--UmI1iW1Pcm<@+3Ead1q3 zs@jaQ7=tJnKH>Yyqj~*t4#(D&cy7<-mY>~|*U~IX@}7b$O=gg1Q!ETCQ`2&Kf%#0p zrl^CMw7M#0{E!DGj}q%>$_4My{a|(O*ijsx1%0X%$&*|hgPcP?JnV`4uKjXrlYU@8 z&v6E)-6|NGxStF+9FBgW$!M$z+3@o*B4+2!f~^k(4T{T;15c#ru=HTVz;Q=VDvlqn zE03bcw|(+USn2Nq4S;82CWL(J$oHt$RINr7ojKlRk-+F;4`M^5L`&>NR6Y!G9C|0 z$u*V%X=EF_+^pY{&a*g3L^+N-&Ka~RGeEEgNT9U3h@w8G6f`i_(9KpGXXgc{)gdrh zns0`=fhnwnf+mZQW$p{L&*PYvR2b-{8U5)-l?05Z(wYlzxk0;_$%%zA6wIQT89)ih3mlXl zEQ{H&S_iZuS&pC;yvHzDdo1Q><#dX*XagF7ezt&pqhfBA;w9$Y zVrGIs5sVpVp+Y{@dQojtw`%m_XKraZAm##6E(8IPBcA6Z21=5OFl(Yr%!nrEl<^t1 zcq0*dSRp(~Y#H$eY9d5M)S71W0?N z#tAp|?*!=`4M}5NYg+3!b4Cj%^WqImG&wAL)Ht5qfr_VHVg$Mn?SMhnED=ED4}k)> z%JJkD3;=Rz6pgJw50N(DR%64!e_k>@g2~QX?9LuXFWjCcYmeZ7tHfb<^4J%bWw&!3|8DLMU6E_osG>?TBymLW-syM#O;u~32%Ab<{7tUZ=ne78y9t-QryvkO(T z5%JKs4hRB~Dr1o1nn2M}HEWa?Z`_Nd8F#AvWb+WXB4tZ3jg7o2Mg~Z!aYVz+d9`3Qh6sGn5oj67kpv=qPmvhuoTMaoV3#FG+k-%WSp-J0F_kC@ z!CY0sPm`p%&{oc1DWen=cV;SsuoiyM#;TWuLrVh>E)yT3!}8K&HSu(UmP^x65tT}= zWK7M-|IS}!evr}XTzh+kQ-+#~P&eK+DxEG@#5<=hje$O=)mo#rJbS%o_-Z}6fA@gN z)NQkvOuG#ie(Y%b=3IxVrMdO{cGH!%%a^Wn>>mE%LR<62Yo@jf=i06|cU-#AN(9hA(Ck*g#_|^5g&pzM(r|yvb0Eqnyd?V66 diff --git a/src/search_company_graph/__pycache__/state.cpython-312.pyc b/src/search_company_graph/__pycache__/state.cpython-312.pyc deleted file mode 100644 index 3026151e53c69e6a414ad3468bb9c246d7125d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 969 zcmZ9K&ubGw6vt4`yN&OkAMeZ4sv=T1Lywvhmf?fIH}nQIx^dkdE)=c9XYgA?6x9^C zL~I61*HnsW+>DLE3Dtnqh@=fil%TjQ&*El0(n%n#=acAB@;~oX&pRDa3gR+7?`)59 zlEQqoATrZbXGU|{*L@nC1?%q0i@o=oJ4;(TPe1KGdH!R5egE}`{g<128;jj0h^*nZ zJlzS2qqnHQ9z0wWd6b4Q$^+XcO}}M}yrDa$WkO;SFq?}$BVdd=zEs3=O!`1%{xOM^ z%+w9-Gi=M4qbA|hriSk_E=rDm&uYkIjQT7jh8sA($VM?nQ+FEF6zQY#P(Z{fV=kMj zrbUuOHzh=*32C`zU{l~MA$J4amTv%+AjEVH0G0bp6bR8B$Az9)uEPmoav%h@E$$1| zg0lFw>)LP7A(+wc%j2W<8BQ6m&$$k*Pf$MRyI$R$ou$mvtvY8${ir?1oog`NYu34J z`iK`YIjhkof!~5{32oyG2O8=hY~z7WI=?UpqFia?Q}7=sulKKBYvbY0iJ>+g0!wkA zjZ2+EabXI?$;$fpYS_kSIzz+KsR%g+!QNL|xZcU+7bn^9alI=rxp5yJYEM-)HV9^+ z`Jrby4JOA|lssDx?=!2>^hb0aItI}_E9yq~&J~#`%63}1C&#+&y~NIeRN&*%<3Lpu aWg8X0qRLlv{-;(}3M str: - """入口路由:有已知候选链接则跳过搜索""" - if state.get("candidate_urls"): - return "has_url" - return "need_search" - - -def check_links(state: SearchState) -> str: - """检查是否有候选链接""" - urls = state.get("candidate_urls", []) - if not urls: - return "no_links" - return "has_links" - - -def check_verify_result(state: SearchState) -> str: - """检查验证结果""" - # 已找到招聘列表页 - if state.get("result_url"): - return "found" - - # 有错误(候选用完) - if state.get("error"): - return "failed" - - # 未找到,需要跳转 - return "need_navigate" - - -def check_navigate_result(state: SearchState) -> str: - """检查导航后的状态""" - current_url_index = state.get("current_url_index", 0) - candidate_urls = state.get("candidate_urls", []) - - # 候选用完 - if current_url_index >= len(candidate_urls): - return "no_more_candidates" - - # 回 Node 3 验证 - return "verify" - - -def create_graph() -> StateGraph: - """创建流程图""" - - graph = StateGraph(SearchState) - - # 添加节点 - graph.add_node("route_entry", lambda state: state) # 路由节点,不修改状态 - graph.add_node("search_sogou", search_sogou) - graph.add_node("extract_links", extract_links) - graph.add_node("visit_and_verify", visit_and_verify) - graph.add_node("navigate_to_recruitment", navigate_to_recruitment) - - # 入口路由 - graph.set_entry_point("route_entry") - - # 路由:有已知URL直接验证,否则走搜索 - graph.add_conditional_edges( - "route_entry", - route_entry, - { - "has_url": "visit_and_verify", - "need_search": "search_sogou" - } - ) - - # 流程 - graph.add_edge("search_sogou", "extract_links") - - graph.add_conditional_edges( - "extract_links", - check_links, - { - "no_links": END, - "has_links": "visit_and_verify" - } - ) - - graph.add_conditional_edges( - "visit_and_verify", - check_verify_result, - { - "found": END, - "failed": END, - "need_navigate": "navigate_to_recruitment" - } - ) - - graph.add_conditional_edges( - "navigate_to_recruitment", - check_navigate_result, - { - "no_more_candidates": END, - "verify": "visit_and_verify" - } - ) - - return graph.compile() diff --git a/src/search_company_graph/main.py b/src/search_company_graph/main.py deleted file mode 100644 index 037498fb..00000000 --- a/src/search_company_graph/main.py +++ /dev/null @@ -1,88 +0,0 @@ -"""入口""" -import asyncio -import sys -from pathlib import Path - -# 支持直接运行 -if __name__ == "__main__": - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from playwright.async_api import async_playwright -from playwright_stealth import Stealth -from src.search_company_graph.graph import create_graph - - -async def search_company_recruitment(company_name: str, input_url: str = None, headless: bool = False) -> list[str]: - """ - 搜索公司招聘列表页面 - - Args: - company_name: 公司名称 - input_url: 已知招聘地址(可选),有值时跳过搜索直接验证 - headless: 是否无头模式 - - Returns: - list[str]: 招聘列表页 URL 列表,失败返回空列表 - """ - async with async_playwright() as p: - browser = await p.chromium.launch(headless=headless, channel="chrome") - context = await browser.new_context() - - # 应用 stealth - stealth = Stealth() - await stealth.apply_stealth_async(context) - - page = await context.new_page() - - try: - graph = create_graph() - - initial_state = { - "company_name": company_name, - "page": page - } - - # 已知URL时,直接作为候选链接跳过搜索 - if input_url: - initial_state["candidate_urls"] = [input_url] - initial_state["current_url_index"] = 0 - initial_state["clicked_selectors"] = [] - initial_state["navigate_retry_count"] = 0 - - print(f"\n{'='*50}") - print(f"开始搜索: {company_name}") - print(f"{'='*50}\n") - - final_state = await graph.ainvoke(initial_state) - - print(f"\n{'='*50}") - print("搜索完成") - print(f"{'='*50}\n") - - # 返回结果 - result_url = final_state.get("result_url") - if result_url: - return [result_url] - return [] - - finally: - await browser.close() - - -async def main(): - """测试""" - company_name = "五粮液" - - result = await search_company_recruitment(company_name) - - print("\n最终结果:") - if result: - print(f"✅ 成功找到招聘页面:") - for url in result: - print(f" {url}") - else: - print("❌ 未找到招聘页面") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/search_company_graph/nodes.py b/src/search_company_graph/nodes.py deleted file mode 100644 index 7eadf1b3..00000000 --- a/src/search_company_graph/nodes.py +++ /dev/null @@ -1,457 +0,0 @@ -"""节点实现""" -import asyncio -from pydantic import BaseModel, Field -from langchain_core.messages import HumanMessage -from src.bash_model import GeneralLlm -from .state import SearchState -from .prompts import EXTRACT_LINKS_PROMPT, VERIFY_RECRUITMENT_LIST_PROMPT, FIND_RECRUITMENT_ENTRY_PROMPT - - -# 结构化输出模型 -class ExtractLinksResult(BaseModel): - """提取链接结果""" - urls: list[str] = Field(description="候选URL列表,最多3个") - reason: str = Field(description="选择理由") - - -class VerifyResult(BaseModel): - """验证结果""" - is_recruitment_list: bool = Field(description="是否为招聘列表页") - reason: str = Field(description="判断依据") - - -class FindEntryResult(BaseModel): - """找入口结果""" - selector: str | None = Field(description="CSS选择器,找不到则为None") - reason: str = Field(description="选择或找不到的原因") - - -async def get_clean_html(page, max_retries: int = 3) -> str: - """获取清理后的页面 HTML(移除 style/script 等),带重试逻辑""" - for attempt in range(max_retries): - try: - # 等待页面加载稳定 - try: - await page.wait_for_load_state("domcontentloaded", timeout=5000) - except Exception: - pass - - # 额外等待一下,确保页面不再跳转 - await asyncio.sleep(1) - - html = await page.evaluate(""" - () => { - const clone = document.body.cloneNode(true); - clone.querySelectorAll('style, script, noscript, svg, link').forEach(el => el.remove()); - return clone.innerHTML; - } - """) - return html - except Exception as e: - if attempt < max_retries - 1: - # 可能是页面正在跳转,等待后重试 - await asyncio.sleep(2) - else: - # 最后一次尝试失败,返回空字符串 - print(f"[get_clean_html] 获取页面内容失败: {e}") - return "" - - -async def get_search_results(page) -> str: - """提取搜狗搜索结果(结构化文本),并解析搜狗跳转链接获取真实 URL""" - # 第一步:提取搜索结果基本信息 - raw_results = await page.evaluate(""" - () => { - // 获取自然搜索结果(排除广告和推荐框) - const results = document.querySelectorAll('.vrwrap:not(.middle-better-hintBox)'); - if (!results.length) return []; - - return Array.from(results).map((el, i) => { - const title = el.querySelector('.vr-title a, h3 a')?.innerText?.trim() || ''; - - // 优先从后续的 .r-sech 元素的 data-url 获取完整 URL - let url = ''; - const nextEl = el.nextElementSibling; - if (nextEl && nextEl.classList.contains('r-sech')) { - url = nextEl.getAttribute('data-url') || ''; - } - - // 备选:从链接 href 获取 - if (!url) { - const linkEl = el.querySelector('.vr-title a, h3 a'); - url = linkEl?.href || ''; - } - - const desc = el.querySelector('.fz-mid, .star-wiki')?.innerText?.trim() || ''; - return { title, url, desc }; - }).filter(item => item.url && (item.url.startsWith('http://') || item.url.startsWith('https://'))); - } - """) - - if not raw_results: - # 回退到纯文本提取 - return await page.evaluate("() => document.body.innerText.slice(0, 30000)") - - # 第二步:解析搜狗跳转链接获取真实 URL - resolved_results = [] - for item in raw_results: - url = item.get('url', '') - - # 如果是搜狗跳转链接,尝试解析真实 URL - if 'sogou.com/link' in url: - try: - real_url = await resolve_sogou_redirect(page, url) - if real_url: - url = real_url - except Exception: - pass # 解析失败则保留原 URL - - resolved_results.append({ - 'title': item.get('title', ''), - 'url': url, - 'desc': item.get('desc', '') - }) - - # 第三步:格式化输出 - output_lines = [] - for i, item in enumerate(resolved_results): - output_lines.append(f"{i+1}. {item['title']}\nURL: {item['url']}\n{item['desc']}") - - return "\n\n".join(output_lines) - - -async def resolve_sogou_redirect(page, sogou_url: str) -> str: - """解析搜狗跳转链接,获取真实目标 URL""" - try: - # 使用 fetch 获取跳转页面内容 - response_text = await page.evaluate(""" - async (url) => { - try { - const resp = await fetch(url, { redirect: 'manual' }); - const text = await resp.text(); - return text; - } catch (e) { - return ''; - } - } - """, sogou_url) - - if not response_text: - return '' - - # 从 meta refresh 标签提取真实 URL - # 匹配模式: - import re - match = re.search(r"URL='([^']+)'", response_text, re.IGNORECASE) - if match: - return match.group(1) - - # 备选匹配模式: url=http://xxx.com - match = re.search(r'url=([^"\s>]+)', response_text, re.IGNORECASE) - if match: - return match.group(1) - - return '' - except Exception: - return '' - - -async def search_sogou(state: SearchState) -> dict: - """Node 1: 搜狗搜索""" - page = state["page"] - company_name = state["company_name"] - - print(f"[Node 1] 搜狗搜索: {company_name} 校园招聘 官网") - - # 访问搜狗 - await page.goto("https://www.sogou.com") - await asyncio.sleep(1) - - # 搜索 - await page.fill('input[name="query"]', f"{company_name} 校园招聘 官网") - await page.press('input[name="query"]', "Enter") - try: - await page.wait_for_load_state("networkidle", timeout=10000) - except Exception: - pass # 超时也继续 - await asyncio.sleep(2) - - # 获取搜索结果(结构化文本) - search_result_text = await get_search_results(page) - - print(f"[Node 1] 获取搜索结果完成,长度: {len(search_result_text)}") - - # 强制删除微公众号信息网页 - search_result_text = search_result_text.replace("mp.weixin.qq.com", "") - - return {"search_result_html": search_result_text} - - -async def extract_links(state: SearchState) -> dict: - """Node 2: 提取候选链接""" - search_text = state["search_result_html"] - company_name = state["company_name"] - - print("[Node 2] 分析搜索结果,提取候选链接...") - - # LLM 分析 - prompt = EXTRACT_LINKS_PROMPT.format(company_name=company_name, html=search_text) - llm = GeneralLlm.with_structured_output(ExtractLinksResult) - - try: - result = await llm.ainvoke([HumanMessage(content=prompt)]) - except Exception as e: - print(f"[Node 2] LLM 调用失败: {e}") - return { - "candidate_urls": [], - "current_url_index": 0, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - # 规范化 URL(确保有协议前缀) - normalized_urls = [] - for url in result.urls: - if url and not url.startswith(('http://', 'https://')): - url = 'https://' + url - if url: - normalized_urls.append(url) - - print(f"[Node 2] 提取到 {len(normalized_urls)} 个候选链接:") - for i, url in enumerate(normalized_urls): - print(f" {i+1}. {url}") - - return { - "candidate_urls": normalized_urls, - "current_url_index": 0, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - -async def visit_and_verify(state: SearchState) -> dict: - """Node 3: 访问并验证是否为招聘列表页""" - context = state["page"].context - candidate_urls = state.get("candidate_urls", []) - current_url_index = state.get("current_url_index", 0) - - # 确保有有效页面 - if not context.pages: - page = await context.new_page() - else: - # 只保留最新的页面 - page = context.pages[-1] - for p in context.pages[:-1]: - try: - await p.close() - except Exception: - pass - - # 检查是否还有候选链接 - if current_url_index >= len(candidate_urls): - print("[Node 3] 候选链接已用完") - return {"error": "所有候选链接均未找到招聘列表页"} - - current_url = candidate_urls[current_url_index] - clicked_selectors = state.get("clicked_selectors", []) - - # 判断是否从 Node 4 跳转回来(已经点击过入口) - if clicked_selectors: - # 从 Node 4 返回,直接使用当前页面 - print(f"[Node 3] 验证跳转后的页面: {page.url}") - page_html = await get_clean_html(page) - else: - # 首次访问该候选链接 - print(f"[Node 3] 访问候选链接 {current_url_index + 1}/{len(candidate_urls)}: {current_url}") - - # 重试机制 - max_retries = 3 - for attempt in range(max_retries): - try: - await page.goto(current_url, wait_until="domcontentloaded", timeout=20000) - # 等待页面稳定(处理可能的跳转) - await asyncio.sleep(2) - try: - await page.wait_for_load_state("networkidle", timeout=5000) - except Exception: - pass - await asyncio.sleep(1) - break - except Exception as e: - print(f"[Node 3] 访问失败 ({attempt + 1}/{max_retries}): {e}") - if attempt == max_retries - 1: - return { - "current_url": current_url, - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - await asyncio.sleep(2) - - # 获取页面内容 - page_html = await get_clean_html(page) - - # 如果获取失败,尝试下一个候选 - if not page_html: - print("[Node 3] 获取页面内容失败,尝试下一个候选") - return { - "current_url": current_url, - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - # 截断 - display_html = page_html - # if len(display_html) > 50000: - # display_html = display_html[:50000] + "\n... (已截断)" - - # LLM 判断 - prompt = VERIFY_RECRUITMENT_LIST_PROMPT.format(html=display_html) - llm = GeneralLlm.with_structured_output(VerifyResult) - - try: - result = await llm.ainvoke([HumanMessage(content=prompt)]) - except Exception as e: - print(f"[Node 3] LLM 调用失败: {e}") - # LLM 失败,尝试下一个候选 - return { - "current_url": current_url, - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - print(f"[Node 3] 是否为招聘列表页: {result.is_recruitment_list}") - print(f"[Node 3] 原因: {result.reason}") - - if result.is_recruitment_list: - return { - "current_url": current_url, - "page_html": page_html, - "result_url": page.url # 使用实际 URL(可能有跳转) - } - - return { - "current_url": current_url, - "page_html": page_html - } - - -async def navigate_to_recruitment(state: SearchState) -> dict: - """Node 4: 找入口并跳转""" - context = state["page"].context - - # 确保有有效页面 - if not context.pages: - # 没有页面了,换下一个候选 - current_url_index = state.get("current_url_index", 0) - return { - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - page = context.pages[-1] - - page_html = state.get("page_html", "") - clicked_selectors = state.get("clicked_selectors", []) - navigate_retry_count = state.get("navigate_retry_count", 0) - current_url_index = state.get("current_url_index", 0) - - print(f"[Node 4] 尝试找跳转入口 (第 {navigate_retry_count + 1}/10 次)") - - # 检查重试次数 - if navigate_retry_count >= 5: - print("[Node 4] 已尝试 5 次,换下一个候选链接") - return { - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - # 检查 page_html 是否有效 - if not page_html or len(page_html) < 100: - print("[Node 4] page_html 无效,重新获取") - page_html = await get_clean_html(page) - if not page_html or len(page_html) < 100: - print("[Node 4] 页面内容仍无效,换下一个候选") - return { - "current_url_index": current_url_index + 1, - "clicked_selectors": [], - "navigate_retry_count": 0 - } - - # 截断 HTML - display_html = page_html - # if len(display_html) > 50000: - # display_html = display_html[:50000] + "\n... (已截断)" - - # LLM 分析 - clicked_str = "\n".join(clicked_selectors) if clicked_selectors else "无" - prompt = FIND_RECRUITMENT_ENTRY_PROMPT.format( - html=display_html, - clicked_selectors=clicked_str - ) - llm = GeneralLlm.with_structured_output(FindEntryResult) - - try: - result = await llm.ainvoke([HumanMessage(content=prompt)]) - except Exception as e: - print(f"[Node 4] LLM 调用失败: {e}") - return { - "page_html": page_html, - "clicked_selectors": clicked_selectors, - "navigate_retry_count": navigate_retry_count + 1 - } - - if not result.selector: - print(f"[Node 4] 未找到入口: {result.reason}") - # 找不到也计入重试,继续循环 - return { - "page_html": page_html, - "clicked_selectors": clicked_selectors, - "navigate_retry_count": navigate_retry_count + 1 - } - - print(f"[Node 4] 找到入口: {result.selector}") - print(f"[Node 4] 原因: {result.reason}") - - # 记录点击前的状态 - tabs_before = len(context.pages) - - try: - element = page.locator(result.selector).first - await element.scroll_into_view_if_needed() - await asyncio.sleep(0.2) - await element.hover() - await asyncio.sleep(0.3) - await element.click() - await asyncio.sleep(3) - except Exception as e: - print(f"[Node 4] 点击失败: {e}") - return { - "page_html": page_html, - "clicked_selectors": clicked_selectors + [result.selector], - "navigate_retry_count": navigate_retry_count + 1 - } - - # 检查是否打开新标签页 - tabs_after = len(context.pages) - if tabs_after > tabs_before: - page = context.pages[-1] - print(f"[Node 4] 打开新标签页: {page.url}") - - # 等待页面加载 - try: - await page.wait_for_load_state("networkidle", timeout=10000) - except Exception: - pass - - new_html = await get_clean_html(page) - print(f"[Node 4] 点击后页面: {page.url}") - - return { - "page_html": new_html, - "clicked_selectors": clicked_selectors + [result.selector], - "navigate_retry_count": navigate_retry_count + 1 - } diff --git a/src/search_company_graph/prompts.py b/src/search_company_graph/prompts.py deleted file mode 100644 index 179a68b4..00000000 --- a/src/search_company_graph/prompts.py +++ /dev/null @@ -1,119 +0,0 @@ -"""LLM 提示词""" - -# Node 2: 从搜索结果提取链接 -EXTRACT_LINKS_PROMPT = """分析以下搜狗搜索结果,找出最可能是 {company_name} 校园招聘 官方页面的链接。 - -## 搜索结果 -{html} - -## 任务 -从上面的搜索结果中提取最可能的官网页面 URL(最多3个),按可能性排序,并去重。 - -## 优先级 -1. 公司官网的招聘频道(如 careers.xxx.com, jobs.xxx.com, xxx.com/careers, talent.xxx.com) -2. 公司官网首页 - -## 排除 -- 新闻、资讯页面 -- 招聘相关但非该公司的页面 -- 第三方招聘地址 -- 和{company_name}公司无关地址 - -## 输出 -仅输出 JSON: -{{ - "urls": ["https://xxx.com/careers"], - "reason": "选择理由" #字数限制15字 -}} - -找不到则: -{{ - "urls": [], - "reason": "原因" #字数限制15字 -}} -""" - -# Node 3: 判断是否为岗位列表页 -VERIFY_RECRUITMENT_LIST_PROMPT = """判断以下页面是否为岗位列表页。 - -## 页面 HTML -{html} - -## 判断标准 -是岗位列表页(举例): -- 展示多个岗位/职位的页面 -- 平铺展示多个岗位的页面(无分页) -- 有列表结构但显示「暂无岗位」的空列表页 - -## 严格标准 -“是岗位列表页”必须满足以下条件之一: - -### 情况1:有岗位数据 -- 页面包含 >= 2 个岗位信息 -- 每个岗位至少有:岗位名称、工作地点/薪资/发布时间之一 -- 岗位以列表/卡片/表格形式展示 - -### 情况2:空列表页(无岗位数据) -必须同时满足以下所有条件: -1. 存在明确的空状态提示,如: - - "暂无岗位"、"无符合条件的职位"、"No results"、"暂无数据" - - 空列表图标 + 提示文字 -2. 存在岗位筛选/搜索功能,如: - - 岗位类型下拉框、地点筛选、关键词搜索框 -3. 页面结构简洁,主体区域明显是用于展示列表 - -### 不是岗位列表页的情况 -- 只有招聘宣传语(如"加入我们"、"企业文化"),无岗位展示区域 -- 只有招聘流程介绍,无具体岗位 -- 只有公司介绍/团队介绍 -- 只有单个岗位的详情页 -- 纯导航页/入口页(需要点击才能看到岗位) - -不是岗位列表页(举例): -- 单个岗位详情页 -- 公司介绍页 -- 招聘宣传页(无岗位列表区域) - -根据以上示例,判断当前页面更接近哪一类。 - -## 输出 -仅输出 JSON: -{{ - "is_recruitment_list": true或false, - "reason": "判断依据" #字数限制20字 -}} -""" - -# Node 4: 找跳转入口 -FIND_RECRUITMENT_ENTRY_PROMPT = """分析以下页面,找出最可能跳转到校园招聘岗位列表的元素。 - -## 页面 HTML -{html} - -## 已尝试过的选择器(避免使用) -{clicked_selectors} - -## 任务 -找到一个最可能通向招聘岗位列表的可点击元素。 - -## 可能的线索 -- 文字:招聘、加入我们、社会招聘、校园招聘、查看职位、岗位列表、人才、career、jobs、join us ... -- 搜索按钮、搜索图标(点击后可能展示岗位列表) -- 导航菜单中的相关项 -- 页面主体区域的按钮或链接 -- 即使没有明确招聘文字,也可能是通向招聘的入口 -- 根据您的理解选择可能连接到岗位列表的线索 - -## 输出 -仅输出 JSON: -{{ - "selector": "CSS选择器", - "reason": "选择原因" #字数限制30字 -}} - -找不到则: -{{ - "selector": null, - "reason": "原因" #字数限制15字 -}} -""" diff --git a/src/search_company_graph/state.py b/src/search_company_graph/state.py deleted file mode 100644 index dad31eae..00000000 --- a/src/search_company_graph/state.py +++ /dev/null @@ -1,31 +0,0 @@ -"""状态定义""" -from typing import TypedDict -from playwright.async_api import Page - - -class SearchState(TypedDict, total=False): - """搜索公司招聘页面的状态""" - - # 输入 - company_name: str - page: Page - - # Node 1: search_sogou - search_result_html: str - - # Node 2: extract_links - candidate_urls: list[str] # 候选链接列表(最多3个) - current_url_index: int # 当前尝试的索引 - - # Node 3: visit_and_verify - current_url: str # 当前访问的 URL - page_html: str # 当前页面 HTML - - # Node 4: navigate_to_recruitment - clicked_selectors: list[str] # 已点击过的选择器 - navigate_retry_count: int # 页内跳转重试次数(上限10次) - page_changed: bool # 点击后页面是否变化 - - # 结果 - result_url: str # 最终找到的招聘列表页 URL - error: str # 错误信息 diff --git a/test_crawler.py b/test_crawler.py deleted file mode 100644 index 0a6887ec..00000000 --- a/test_crawler.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Crawler 测试脚本""" -import asyncio -from src.crawler import crawl, CrawlerConfig - - -# 中兴招聘测试配置 -# test_config: CrawlerConfig = { -# "url": "https://app.mokahr.com/social-recruitment/zte/47588#/jobs", -# "job_item_selector": ".jobs-list-WmE84RgZxp .container-aOp138AX_X.normal-TBuWTpDMcE.list-oR2doUijv4", -# "item_change_type": "redirect", -# "next_page_selector": ".sd-Pagination-pagination-2kuN2 .sd-Pagination-forward-3z80f", -# "page_change_type": "url_change", -# "field_selectors": { -# "job_title": {"selector": [".title-ROUQFdjmhP"]}, -# "description": {"selector": [".job-description-VvfEUGocNE"]}, -# "location": {"selector": [".info-UcB_mxJq8y span:first-child"]}, -# "company": {"selector": [".basic-info-dB86EjV5uU span:nth-child(2)"]}, -# }, -# "detail_area_selector": None, -# } - -# 美宜佳招聘测试配置 -# test_config: CrawlerConfig = { -# "url": "https://meiyijia.jobs.feishu.cn/social/position/list", -# "job_item_selector": ".listItems__fca8c0 a", -# "item_change_type": "new_tab", -# "next_page_selector": ".pager__fca8c0 .atsx-pagination-next:not(.atsx-pagination-disabled)", -# "page_change_type": "url_change", -# "field_selectors": { -# "job_title": {"selector": [".positionItem-title-text"]}, -# "description": {"selector": [".positionItem-jobDesc"]}, -# "location": {"selector": [".positionItem-subTitle span"]}, -# }, -# "detail_area_selector": None, -# } - -# 三星招聘测试配置 -test_config: CrawlerConfig = { - "url": "https://dearsamsung.zhiye.com/#/samsung/pc/szzw", - "job_item_selector": ".BHGkB li", - "item_change_type": "in_page", - "next_page_selector": "._8x6MD .ant-pagination-next:not([aria-disabled='true']) .ant-pagination-item-link", - "page_change_type": "content_change", - "field_selectors": { - "job_title": {"selector": ["h2"]}, - "description": {"selector": ['.aCl-8 p', '.aCl-8 pre']}, - }, - "detail_area_selector": ".FLf6j", -} - - -async def main(): - results = await crawl(test_config, headless=False) - print(f"\n爬取完成,共 {len(results)} 条数据") - for i, item in enumerate(results): - print(f"\n--- 岗位 {i+1} ---") - for k, v in item.items(): - print(f"{k}: {v}") - - -if __name__ == "__main__": - asyncio.run(main())