Compare commits
15
Commits
7fcc20a15b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c04e70c32d | ||
|
|
2f402a91d3 | ||
|
|
a9995aba50 | ||
|
|
2ce0ce7f4a | ||
|
|
d99a7557a5 | ||
|
|
6079e758e9 | ||
|
|
7db413dcbf | ||
|
|
1a57be8921 | ||
|
|
fe36b53e69 | ||
|
|
ec0f162ab7 | ||
|
|
6bb03242cd | ||
|
|
e01b28a372 | ||
|
|
9063a05bd8 | ||
|
|
74451cf5a6 | ||
|
|
786b4840be |
@@ -30,4 +30,4 @@ OSS_BUCKET=offerpie
|
|||||||
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
|
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
|
||||||
|
|
||||||
# 公告落库并发线程数(不要超过 MYSQL_POOL_SIZE + MYSQL_MAX_OVERFLOW)
|
# 公告落库并发线程数(不要超过 MYSQL_POOL_SIZE + MYSQL_MAX_OVERFLOW)
|
||||||
SPIDER_MAX_WORKERS=12
|
SPIDER_MAX_WORKERS=32
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ OSS_BUCKET=offerpie
|
|||||||
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
|
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
|
||||||
|
|
||||||
# 公告落库并发线程数(不要超过 MYSQL_POOL_SIZE + MYSQL_MAX_OVERFLOW)
|
# 公告落库并发线程数(不要超过 MYSQL_POOL_SIZE + MYSQL_MAX_OVERFLOW)
|
||||||
SPIDER_MAX_WORKERS=12
|
SPIDER_MAX_WORKERS=16
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
# 使用 Python 3.12 slim 镜像
|
||||||
|
# 必须固定 bookworm(Debian 12):
|
||||||
|
# 1. 下面的 sources.list 写的是 bookworm,python:3.12-slim 已滚动到 trixie(Debian 13),混用会导致包名找不到
|
||||||
|
# 2. Playwright 1.49 的支持列表里没有 Debian 13,install --with-deps 探测到未知发行版会直接失败
|
||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
ENV ENV=prod
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Playwright 浏览器统一装到系统目录,避免落在 HOME 里
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
|
# 浏览器下载走 npmmirror 镜像,国内构建更快(网络可直连时可去掉)
|
||||||
|
ENV PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright
|
||||||
|
|
||||||
|
# 时区 + 系统依赖(libgl1、libglib2.0-0 是 opencv-python 的运行时依赖)
|
||||||
|
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
|
||||||
|
&& rm -rf /etc/apt/sources.list.d/* \
|
||||||
|
&& echo "deb https://mirrors.aliyun.com/debian/ bookworm main non-free contrib" > /etc/apt/sources.list \
|
||||||
|
&& echo "deb https://mirrors.aliyun.com/debian-security/ bookworm-security main non-free contrib" >> /etc/apt/sources.list \
|
||||||
|
&& echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main non-free contrib" >> /etc/apt/sources.list \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl libgl1 libglib2.0-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 先拷贝依赖声明,利用 Docker 层缓存
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt \
|
||||||
|
-i https://mirrors.aliyun.com/pypi/simple/ \
|
||||||
|
--trusted-host mirrors.aliyun.com
|
||||||
|
|
||||||
|
# 安装 Chromium 及其系统依赖(供 app/tool/browser.py 使用)
|
||||||
|
RUN playwright install --with-deps chromium \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 预热 OCR 模型,避免首次采集时才去下载 onnx 模型
|
||||||
|
# 参数与 app/tool/ocr.py 保持一致,确保命中同一份模型缓存
|
||||||
|
RUN python -c "from rapidocr import RapidOCR; RapidOCR(params={'Global.use_cls': False, 'Global.text_score': 0.5})"
|
||||||
|
|
||||||
|
# 拷贝应用代码和环境配置(init_*.py 存量初始化脚本本地跑,不进镜像)
|
||||||
|
COPY app/ ./app/
|
||||||
|
COPY .env.prod ./.env.prod
|
||||||
|
|
||||||
|
# 纯后台定时任务服务,无 HTTP 端口
|
||||||
|
CMD ["python", "-m", "app.main"]
|
||||||
Vendored
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* OfferPie Spider 部署流水线
|
||||||
|
*
|
||||||
|
* Jenkins 容器挂载了宿主机 docker.sock,镜像共享。
|
||||||
|
* 直接在 workspace 内 build + compose up。
|
||||||
|
*
|
||||||
|
* 只负责常驻定时任务服务(app.main);存量初始化脚本 init_*.py 在本地手动跑。
|
||||||
|
*/
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(name: 'BRANCH', choices: ['master', 'dev'], description: '选择要部署的分支')
|
||||||
|
choice(name: 'ACTION', choices: ['deploy', 'stop'], description: '操作:deploy=构建部署,stop=停止服务')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
IMAGE_NAME = 'offerpie-spider'
|
||||||
|
IMAGE_TAG = 'latest'
|
||||||
|
CONTAINER_NAME = 'offerpie-spider'
|
||||||
|
// 固定 compose 项目名:默认取 workspace 目录名,并发构建时会变成 xxx@2 导致容器名冲突
|
||||||
|
COMPOSE_PROJECT_NAME = 'offerpie-spider'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('停止服务') {
|
||||||
|
when {
|
||||||
|
expression { params.ACTION == 'stop' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker compose down || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('拉取代码') {
|
||||||
|
when {
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
echo "拉取 ${params.BRANCH} 分支代码"
|
||||||
|
git branch: "${params.BRANCH}",
|
||||||
|
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
|
||||||
|
url: 'https://git.jianshixingqiu.com/offerpai/campus_spider.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('构建镜像') {
|
||||||
|
when {
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('部署') {
|
||||||
|
when {
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
// 停旧启新,docker-compose.yml 就在当前 workspace
|
||||||
|
sh "docker compose down || true"
|
||||||
|
sh "docker compose up -d"
|
||||||
|
sleep 5
|
||||||
|
sh "docker ps -f name=${CONTAINER_NAME} --format '{{.Status}}'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('清理') {
|
||||||
|
when {
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker image prune -f || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
script {
|
||||||
|
if (params.ACTION == 'deploy') {
|
||||||
|
echo "✅ 部署成功!容器 ${CONTAINER_NAME} 已启动"
|
||||||
|
} else {
|
||||||
|
echo "✅ 服务已停止"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ 操作失败,请检查日志'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给
|
|||||||
"company_intro": "公司简介",
|
"company_intro": "公司简介",
|
||||||
"target_audience": "面向对象",
|
"target_audience": "面向对象",
|
||||||
"major_require": "专业要求",
|
"major_require": "专业要求",
|
||||||
|
"recruit_position": "招聘岗位",
|
||||||
"remark": "备注",
|
"remark": "备注",
|
||||||
"written_exam": "有",
|
"written_exam": "有",
|
||||||
"apply_start_time": "2026-07-01 00:00:00",
|
"apply_start_time": "2026-07-01 00:00:00",
|
||||||
@@ -26,9 +27,10 @@ EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给
|
|||||||
"publish_time": "2026-07-01 00:00:00",
|
"publish_time": "2026-07-01 00:00:00",
|
||||||
"recruit_years": [2027],
|
"recruit_years": [2027],
|
||||||
"batches": ["暑期实习"],
|
"batches": ["暑期实习"],
|
||||||
"tags": ["秋招提前批"],
|
"tags": ["六险一金", "不限专业", "带薪年假", "导师带教"],
|
||||||
"cities": ["北京市"],
|
"cities": ["北京市"],
|
||||||
"categories": ["IT技术"],
|
"categories": ["后端开发", "人工智能"],
|
||||||
|
"industries": ["互联网", "人工智能"],
|
||||||
"educations": ["本科", "硕士"]
|
"educations": ["本科", "硕士"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -40,6 +42,8 @@ EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给
|
|||||||
- company_intro:公司介绍段落,原文摘录,最多 300 字。公告中没有公司介绍时,可以根据你自己对该公司的了解补充一段简介;不了解该公司时填 null
|
- company_intro:公司介绍段落,原文摘录,最多 300 字。公告中没有公司介绍时,可以根据你自己对该公司的了解补充一段简介;不了解该公司时填 null
|
||||||
- target_audience:面向对象,如「2027届本硕博」「2026届及2027届毕业生」,最多 200 字
|
- target_audience:面向对象,如「2027届本硕博」「2026届及2027届毕业生」,最多 200 字
|
||||||
- major_require:专业要求,如「计算机、电子信息、自动化等相关专业」,最多 200 字
|
- major_require:专业要求,如「计算机、电子信息、自动化等相关专业」,最多 200 字
|
||||||
|
- recruit_position:本次招聘的具体岗位名称,多个岗位用「、」连接,如「后端开发工程师、算法工程师、产品经理」,
|
||||||
|
最多 500 字。只填岗位名称本身,不要带岗位职责、任职要求、招聘人数等描述。岗位过多时保留最主要的若干个
|
||||||
- remark:其他值得注意的补充信息,如薪资待遇、福利、流程安排、注意事项,最多 1000 字
|
- remark:其他值得注意的补充信息,如薪资待遇、福利、流程安排、注意事项,最多 1000 字
|
||||||
- written_exam:是否有笔试,只能填「有」「无」「未明确」三者之一
|
- written_exam:是否有笔试,只能填「有」「无」「未明确」三者之一
|
||||||
- apply_start_time:投递开始时间,格式 yyyy-MM-dd HH:mm:ss
|
- apply_start_time:投递开始时间,格式 yyyy-MM-dd HH:mm:ss
|
||||||
@@ -58,15 +62,34 @@ EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给
|
|||||||
- source:信息来源说明,如公告中提到的发布方、公众号名称
|
- source:信息来源说明,如公告中提到的发布方、公众号名称
|
||||||
- publish_time:公告发布或更新时间,格式 yyyy-MM-dd HH:mm:ss
|
- publish_time:公告发布或更新时间,格式 yyyy-MM-dd HH:mm:ss
|
||||||
- recruit_years:招聘届数数组,整数年份,如 [2027]、[2026, 2027]
|
- recruit_years:招聘届数数组,整数年份,如 [2027]、[2026, 2027]
|
||||||
- batches:批次数组,如「实习」「暑期实习」「寒假实习」「秋招专场」「春招提前批」「春招补招」「正式批」
|
- batches:招聘批次数组,**只能从下列固定值中原样选取**,不得改写或自造:
|
||||||
|
「实习」「暑期实习」「寒假实习」
|
||||||
|
「秋招提前批」「秋招正式批」「秋招补招」
|
||||||
|
「春招提前批」「春招正式批」「春招补招」
|
||||||
|
「校园招聘」「社招」
|
||||||
|
判断依据是公告标题、正文中的批次表述以及投递时间所处季节,规则如下:
|
||||||
|
- 实习岗且能判断季节的用「暑期实习」「寒假实习」,只说实习没说季节的用「实习」
|
||||||
|
- 校招正式岗按季节和阶段选:提前批/内推批归「XX提前批」,主招/正式批/专场归「XX正式批」,补录/二次招聘归「XX补招」
|
||||||
|
- 确定是校招但分不清秋招/春招或所处阶段时,用「校园招聘」兜底
|
||||||
|
- 面向社会人士、要求工作经验的用「社招」
|
||||||
|
最多 3 个,无法判断时填 []
|
||||||
- cities:工作城市数组,规范到市级,如「北京市」「深圳市」
|
- cities:工作城市数组,规范到市级,如「北京市」「深圳市」
|
||||||
- categories:岗位大类数组,如「IT技术」「人工智能」「通信」「芯片硬件」「产品运营」「职能支持」
|
- categories:岗位分类数组。**只能从文末「岗位分类数据」列表中原样选取**,不得改写、合并、简化或自造分类名。
|
||||||
|
根据 recruit_position 中的岗位判断,可多选,最多 5 个,按相关度从高到低排列。
|
||||||
|
找不到精确匹配时,选该岗位所属领域下的「其他XX职位」;连所属领域都无法判断时填 []
|
||||||
|
- industries:行业分类数组。**只能从文末「行业分类数据」列表中原样选取**,不得改写、合并、简化或自造行业名。
|
||||||
|
根据 company_name、company_intro 中体现的公司主营业务判断,可多选,最多 3 个,按相关度从高到低排列。
|
||||||
|
找不到精确匹配时,选同一领域下最接近的行业(如该领域有「其他XX」则用它);连所属领域都无法判断时填「其他行业」
|
||||||
- educations:学历要求数组,如「大专」「本科」「硕士」「博士」
|
- educations:学历要求数组,如「大专」「本科」「硕士」「博士」
|
||||||
- tags:其他关键标签数组,如「竞争力薪酬」「六险一金」「可远程」「不限专业」
|
- tags:其他关键标签数组,如「竞争力薪酬」「六险一金」「可远程」「不限专业」。
|
||||||
|
提取 3-6 个,从公告的薪资福利、工作方式、专业/学历门槛、培养机制、流程特点、地点等维度概括,
|
||||||
|
允许在不改变原意的前提下把原文表述凝练成短标签,每个标签 2-8 字,不与 batches、cities、educations 重复。
|
||||||
|
宁缺勿滥:公告内容确实撑不起 3 个标签时就按实际数量输出,不要为了凑数拆分同一条信息或写空泛标签
|
||||||
|
|
||||||
## 提取规则
|
## 提取规则
|
||||||
|
|
||||||
1. 只提取公告中明确出现的信息,不要推测、不要补全、不要编造(company_intro 例外)。
|
1. 只提取公告中明确出现的信息,不要推测、不要补全、不要编造(company_intro、tags 例外,
|
||||||
|
tags 允许对公告内容做概括提炼,但不能提炼出公告里没有依据的内容)。
|
||||||
2. 字符串字段找不到时填 null,数组字段找不到时填 []。不要填「无」「未知」「暂无」这类占位文字(written_exam 例外)。
|
2. 字符串字段找不到时填 null,数组字段找不到时填 []。不要填「无」「未知」「暂无」这类占位文字(written_exam 例外)。
|
||||||
3. 时间统一输出 yyyy-MM-dd HH:mm:ss。
|
3. 时间统一输出 yyyy-MM-dd HH:mm:ss。
|
||||||
- 只有日期没有时刻:开始时间补 00:00:00,截止时间补 23:59:59。
|
- 只有日期没有时刻:开始时间补 00:00:00,截止时间补 23:59:59。
|
||||||
@@ -75,5 +98,58 @@ EXTRACT_SYSTEM_PROMPT = """你是招聘公告信息提取专家。用户会给
|
|||||||
4. apply_end_time 和 apply_end_desc 可以同时有值,也可以只有一个。
|
4. apply_end_time 和 apply_end_desc 可以同时有值,也可以只有一个。
|
||||||
5. 数组元素去重,保持公告中出现的顺序,单个元素不超过 64 字。
|
5. 数组元素去重,保持公告中出现的顺序,单个元素不超过 64 字。
|
||||||
6. 一份公告涉及多家公司时,以主体招聘方为准。
|
6. 一份公告涉及多家公司时,以主体招聘方为准。
|
||||||
7. 直接输出 JSON,不要输出任何解释文字。
|
7. batches、categories 和 industries 都是封闭枚举:batches 必须是字段说明中列出的 11 个值之一,
|
||||||
|
categories 必须与文末「岗位分类数据」列表中的某一项完全一致(含标点「/」),
|
||||||
|
industries 必须与文末「行业分类数据」列表中的某一项完全一致(含标点「/」「(020)」等)。
|
||||||
|
出现枚举之外的值视为错误输出。
|
||||||
|
8. 直接输出 JSON,不要输出任何解释文字。
|
||||||
|
|
||||||
|
## 岗位分类数据
|
||||||
|
|
||||||
|
categories 的取值范围如下(按领域分组,冒号后的名称才是合法取值):
|
||||||
|
|
||||||
|
- 技术研发:后端开发、前端/移动开发、测试、运维/技术支持、人工智能、数据、技术项目管理、销售技术支持、高端技术职位、其他技术职位
|
||||||
|
- 硬件通信:电子/硬件开发、半导体/芯片、电气/自动化、通信
|
||||||
|
- 产品运营:产品经理、游戏策划/制作、客服、内容运营、电商运营、业务运营、线下运营、编辑、高端运营职位、其他运营职位
|
||||||
|
- 销售:销售、销售管理、销售行政/商务、外贸销售、教培销售、汽车销售、房地产销售/招商、服务业销售、医疗销售、广告/会展销售、金融销售、其他销售职位
|
||||||
|
- 职能:人力资源、行政、法律服务、其他职能职位
|
||||||
|
- 财务:会计、审计/税务、高级财务职位、其他财务岗位
|
||||||
|
- 生产制造:普工、机械加工、技工、运输设备操作、质量管理、机械设计/制造、生产营运、生产安全、化工、服装/纺织/皮革、新能源汽车、汽车研发/制造、环保、其他生产制造职位
|
||||||
|
- 服务业:零售、美容美发、理疗保健、家政/保洁、安保服务、维修服务、汽车服务、宠物服务、运动健身、驾驶员、其他服务业职位
|
||||||
|
- 餐饮:前厅、后厨、餐饮管理、甜点饮品、其他餐饮岗位
|
||||||
|
- 酒店旅游:酒店、旅游服务、其他旅游职位
|
||||||
|
- 教育培训:教师、幼少儿教师、教育行政、文化艺术、科学探索培训、职业培训、教育产品研发、其他教育培训职位
|
||||||
|
- 设计:视觉/交互设计、环境设计、工业设计、服装设计、美术/3D/动画、游戏设计、高端设计职位、其他设计职位
|
||||||
|
- 房地产建筑:工程管理、装饰装修、物业管理、建筑/规划设计、房地产规划开发、建筑/装修工人、高端房地产职位、其他房地产职位
|
||||||
|
- 传媒:直播、影视、广告、采编/写作/出版、其他传媒职位
|
||||||
|
- 市场:市场营销、推广/投放、政府事务、公关、调研分析、其他市场职位
|
||||||
|
- 采购物流贸易:物流/运输、配送理货、仓储、供应链、采购、进出口贸易、其他采购/贸易职位
|
||||||
|
- 医疗健康:护士/护理、医生/医技、药店、生物医药、临床试验、医疗器械、其他医疗健康职位
|
||||||
|
- 金融:银行、证券/基金/期货、中后台、投融资、保险、其他金融职位
|
||||||
|
- 咨询翻译:咨询/调研、翻译、其他咨询类职位
|
||||||
|
- 其他:能源/地质、农/林/牧/渔、高级管理职位、其他职位类别
|
||||||
|
|
||||||
|
|
||||||
|
## 行业分类数据
|
||||||
|
|
||||||
|
industries 的取值范围如下(按领域分组,冒号后的名称才是合法取值):
|
||||||
|
|
||||||
|
- 互联网/IT:互联网、生活服务(020)、游戏、云计算、大数据、新零售、电子商务、企业服务、社交网络与媒体、在线教育、广告营销、信息安全、计算机软件、医疗健康、人工智能、计算机服务、物联网
|
||||||
|
- 电子硬件通信:半导体/芯片、智能硬件/消费电子、电子/硬件开发、运营商/增值服务、通信/网络设备、计算机硬件、电子/半导体/集成电路
|
||||||
|
- 生活服务:餐饮、酒店/民宿、保健/养生、婚庆/摄影、美容/美发、美容、美发、休闲/娱乐、家政服务、宠物服务、运动/健身、旅游/景区、回收/维修、其他生活服务
|
||||||
|
- 消费品零售:批发/零售、服装/纺织、日化、进出口贸易、家具/家居、家具/家电/家居、家用电器、珠宝/首饰、食品/饮料/烟酒、其他消费品
|
||||||
|
- 房地产建筑:房地产开发经营、房地产中介/租赁、物业管理、房屋建筑工程、土木工程、工程施工、建筑设计、建筑材料、建筑工程咨询服务、机电工程、装修装饰、土地与公共设施管理
|
||||||
|
- 教育培训:培训/辅导机构、学校/学历教育、职业培训、学前教育、学术/科研
|
||||||
|
- 文娱传媒:文化艺术/娱乐、广播/影视、新闻/出版、广告/公关/会展、体育
|
||||||
|
- 制造业:通用设备、专用设备、自动化设备、电气机械/器材、机械设备/机电/重工、仪器仪表、仪器仪表/工业自动化、计算机/通信/其他电子设备、铁路/船舶/航空/航天制造、金属制品、非金属矿物制品、橡胶/塑料制品、化学原料/化学制品、新材料、原材料及加工/模具、印刷/包装/造纸、其他制造业
|
||||||
|
- 专业服务:咨询、法律、财务/审计/税务、人力资源服务、检测/认证/知识产权、翻译、其他专业服务
|
||||||
|
- 医疗医药:医疗服务、医美服务、生物/制药、医疗器械、医药批发零售、医疗研发外包、IVD
|
||||||
|
- 汽车:汽车研发/制造、新能源汽车、汽车零部件、汽车智能网联、汽车后市场、4S店/后市场、汽车经销商、摩托车/自行车制造
|
||||||
|
- 物流运输:物流/仓储、交通/运输、公路物流、跨境物流、快递、即时配送、同城货运、客运服务、港口/铁路/公路/机场、装卸搬运和仓储业
|
||||||
|
- 能源环保:新能源、光伏、风电、储能、动力电池、其他新能源、电力/热力/燃气/水利、石油/石化、化工、采掘/冶炼、矿产/地质、环保
|
||||||
|
- 金融:银行、证券/期货、基金、保险、投资/融资、财富管理、互联网金融、信托租赁/拍卖/典当/担保、其他金融业
|
||||||
|
- 其他:农/林/牧/渔、政府/公共事业、非盈利机构、其他行业
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class RecruitAnnouncement(MysqlBase):
|
|||||||
company_intro: Mapped[Optional[str]] = mapped_column(Text, comment="公司简介")
|
company_intro: Mapped[Optional[str]] = mapped_column(Text, comment="公司简介")
|
||||||
target_audience: Mapped[Optional[str]] = mapped_column(String(500), comment="面向对象,如2027届本硕博")
|
target_audience: Mapped[Optional[str]] = mapped_column(String(500), comment="面向对象,如2027届本硕博")
|
||||||
major_require: Mapped[Optional[str]] = mapped_column(String(500), comment="专业要求")
|
major_require: Mapped[Optional[str]] = mapped_column(String(500), comment="专业要求")
|
||||||
|
recruit_position: Mapped[Optional[str]] = mapped_column(String(1000), comment="招聘岗位")
|
||||||
remark: Mapped[Optional[str]] = mapped_column(String(1000), comment="备注")
|
remark: Mapped[Optional[str]] = mapped_column(String(1000), comment="备注")
|
||||||
written_exam: Mapped[Optional[str]] = mapped_column(String(16), comment="是否笔试:有 / 无 / 未明确")
|
written_exam: Mapped[Optional[str]] = mapped_column(String(16), comment="是否笔试:有 / 无 / 未明确")
|
||||||
apply_start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, comment="投递开始时间")
|
apply_start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, comment="投递开始时间")
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ class RecruitAnnouncementBatch(MysqlBase):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
||||||
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
||||||
batch_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="批次值,如实习/暑期实习/寒假实习/秋招专场/春招提前批/春招补招")
|
batch_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="批次值,枚举:实习/暑期实习/寒假实习/秋招提前批/秋招正式批/秋招补招/春招提前批/春招正式批/春招补招/校园招聘/社招")
|
||||||
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ class RecruitAnnouncementCategory(MysqlBase):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
||||||
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
||||||
category_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位大类值,如IT技术/人工智能/通信")
|
category_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位分类值,取自固定分类表,如后端开发/人工智能/产品经理")
|
||||||
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"""MySQL: bg_recruit_announcement_industry 招聘公告-行业关联表模型"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, DateTime, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.core.database import MysqlBase
|
||||||
|
|
||||||
|
|
||||||
|
class RecruitAnnouncementIndustry(MysqlBase):
|
||||||
|
"""招聘公告-行业关联表"""
|
||||||
|
|
||||||
|
__tablename__ = "bg_recruit_announcement_industry"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
||||||
|
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
||||||
|
industry_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="行业分类值,取自固定分类表,如互联网/人工智能/半导体/芯片")
|
||||||
|
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
||||||
@@ -15,5 +15,5 @@ class RecruitAnnouncementTag(MysqlBase):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, comment="主键ID(雪花)")
|
||||||
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
announcement_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="公告id")
|
||||||
tag_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="标签值,如秋招提前批/竞争力薪酬/校招")
|
tag_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="标签值,如竞争力薪酬/六险一金/不限专业,不与批次、城市、学历重复")
|
||||||
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
create_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from app.models.recruit_announcement_batch import RecruitAnnouncementBatch
|
|||||||
from app.models.recruit_announcement_category import RecruitAnnouncementCategory
|
from app.models.recruit_announcement_category import RecruitAnnouncementCategory
|
||||||
from app.models.recruit_announcement_city import RecruitAnnouncementCity
|
from app.models.recruit_announcement_city import RecruitAnnouncementCity
|
||||||
from app.models.recruit_announcement_education import RecruitAnnouncementEducation
|
from app.models.recruit_announcement_education import RecruitAnnouncementEducation
|
||||||
|
from app.models.recruit_announcement_industry import RecruitAnnouncementIndustry
|
||||||
from app.models.recruit_announcement_tag import RecruitAnnouncementTag
|
from app.models.recruit_announcement_tag import RecruitAnnouncementTag
|
||||||
from app.models.recruit_announcement_year import RecruitAnnouncementYear
|
from app.models.recruit_announcement_year import RecruitAnnouncementYear
|
||||||
from app.service.company_service import find_or_create_company
|
from app.service.company_service import find_or_create_company
|
||||||
@@ -34,8 +35,15 @@ def _parse_datetime(value: str | None) -> datetime | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate(value: object, limit: int) -> str | None:
|
||||||
|
"""转成字符串并按上限截断,空值返回 None。"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return str(value)[:limit]
|
||||||
|
|
||||||
|
|
||||||
def _save_announcement(announcement_id: int, company_id: int, url: str, data: dict) -> None:
|
def _save_announcement(announcement_id: int, company_id: int, url: str, data: dict) -> None:
|
||||||
"""保存公告主表和六张关联表,单事务提交。"""
|
"""保存公告主表和七张关联表,单事务提交。"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
with MysqlSession() as session:
|
with MysqlSession() as session:
|
||||||
@@ -49,6 +57,7 @@ def _save_announcement(announcement_id: int, company_id: int, url: str, data: di
|
|||||||
company_intro=data.get("company_intro"),
|
company_intro=data.get("company_intro"),
|
||||||
target_audience=data.get("target_audience"),
|
target_audience=data.get("target_audience"),
|
||||||
major_require=data.get("major_require"),
|
major_require=data.get("major_require"),
|
||||||
|
recruit_position=_truncate(data.get("recruit_position"), 1000),
|
||||||
remark=data.get("remark"),
|
remark=data.get("remark"),
|
||||||
written_exam=data.get("written_exam"),
|
written_exam=data.get("written_exam"),
|
||||||
apply_start_time=_parse_datetime(data.get("apply_start_time")),
|
apply_start_time=_parse_datetime(data.get("apply_start_time")),
|
||||||
@@ -122,6 +131,17 @@ def _save_announcement(announcement_id: int, company_id: int, url: str, data: di
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 行业
|
||||||
|
for industry in data.get("industries") or []:
|
||||||
|
session.execute(
|
||||||
|
insert(RecruitAnnouncementIndustry).values(
|
||||||
|
id=next_id(),
|
||||||
|
announcement_id=announcement_id,
|
||||||
|
industry_name=str(industry)[:64],
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 学历要求
|
# 学历要求
|
||||||
for edu in data.get("educations") or []:
|
for edu in data.get("educations") or []:
|
||||||
session.execute(
|
session.execute(
|
||||||
|
|||||||
+22
-2
@@ -25,6 +25,24 @@ from playwright.async_api import Browser, BrowserContext, Page, async_playwright
|
|||||||
# 页面打开超时(毫秒)
|
# 页面打开超时(毫秒)
|
||||||
_GOTO_TIMEOUT = 30000
|
_GOTO_TIMEOUT = 30000
|
||||||
|
|
||||||
|
# Chromium 启动参数:
|
||||||
|
# --no-sandbox 容器内以 root 运行时 Chrome 沙箱不可用,不加会直接启动失败
|
||||||
|
# --disable-dev-shm-usage 容器 /dev/shm 偏小时改用磁盘,避免渲染进程崩溃
|
||||||
|
_LAUNCH_ARGS = ["--no-sandbox", "--disable-dev-shm-usage"]
|
||||||
|
|
||||||
|
# 浏览器上下文固定参数:不显式指定的话,UA 会跟着宿主系统变
|
||||||
|
# (Windows 开发机是 Windows UA,Linux 容器是 X11 UA),目标站会按 UA 返回不同页面模板,
|
||||||
|
# 导致同一套选择器在容器里全部落空
|
||||||
|
_CONTEXT_OPTIONS: dict[str, Any] = {
|
||||||
|
"user_agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
"viewport": {"width": 1920, "height": 1080},
|
||||||
|
"locale": "zh-CN",
|
||||||
|
"timezone_id": "Asia/Shanghai",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NodeSnapshot:
|
class NodeSnapshot:
|
||||||
@@ -202,12 +220,14 @@ class _BrowserRuntime:
|
|||||||
async with self._launch_lock:
|
async with self._launch_lock:
|
||||||
if self._browser is None:
|
if self._browser is None:
|
||||||
self._playwright = await async_playwright().start()
|
self._playwright = await async_playwright().start()
|
||||||
self._browser = await self._playwright.chromium.launch(headless=True)
|
self._browser = await self._playwright.chromium.launch(
|
||||||
|
headless=True, args=_LAUNCH_ARGS
|
||||||
|
)
|
||||||
return self._browser
|
return self._browser
|
||||||
|
|
||||||
async def _open_page_impl(self, url: str, wait_ms: int) -> str:
|
async def _open_page_impl(self, url: str, wait_ms: int) -> str:
|
||||||
browser = await self._ensure_browser()
|
browser = await self._ensure_browser()
|
||||||
context = await browser.new_context()
|
context = await browser.new_context(**_CONTEXT_OPTIONS)
|
||||||
page = await context.new_page()
|
page = await context.new_page()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 招聘公告数据表设计
|
-- 招聘公告数据表设计
|
||||||
-- 说明:分析对象为「招聘公告」,多值属性(招聘届数、批次、标签组、
|
-- 说明:分析对象为「招聘公告」,多值属性(招聘届数、批次、标签组、
|
||||||
-- 热招城市、岗位大类、学历要求)直接拆关联表存原始值,
|
-- 热招城市、岗位大类、行业、学历要求)直接拆关联表存原始值,
|
||||||
-- 不引入任何字典表,避免归一化对公告解析结果的限制。
|
-- 不引入任何字典表,避免归一化对公告解析结果的限制。
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ CREATE TABLE `bg_recruit_announcement` (
|
|||||||
`company_intro` TEXT NULL COMMENT '公司简介',
|
`company_intro` TEXT NULL COMMENT '公司简介',
|
||||||
`target_audience` VARCHAR(500) NULL COMMENT '面向对象,如2027届本硕博',
|
`target_audience` VARCHAR(500) NULL COMMENT '面向对象,如2027届本硕博',
|
||||||
`major_require` VARCHAR(500) NULL COMMENT '专业要求',
|
`major_require` VARCHAR(500) NULL COMMENT '专业要求',
|
||||||
|
`recruit_position` VARCHAR(1000) NULL COMMENT '招聘岗位',
|
||||||
`remark` VARCHAR(1000) NULL COMMENT '备注',
|
`remark` VARCHAR(1000) NULL COMMENT '备注',
|
||||||
`written_exam` VARCHAR(16) NULL COMMENT '是否笔试:有 / 无 / 未明确',
|
`written_exam` VARCHAR(16) NULL COMMENT '是否笔试:有 / 无 / 未明确',
|
||||||
`apply_start_time` DATETIME NULL COMMENT '投递开始时间',
|
`apply_start_time` DATETIME NULL COMMENT '投递开始时间',
|
||||||
@@ -37,6 +38,8 @@ CREATE TABLE `bg_recruit_announcement` (
|
|||||||
KEY `idx_company_name` (`company_name`),
|
KEY `idx_company_name` (`company_name`),
|
||||||
KEY `idx_written_exam` (`written_exam`),
|
KEY `idx_written_exam` (`written_exam`),
|
||||||
KEY `idx_apply_start_time` (`apply_start_time`),
|
KEY `idx_apply_start_time` (`apply_start_time`),
|
||||||
|
KEY `idx_apply_end_time` (`apply_end_time`),
|
||||||
|
KEY `idx_publish_time` (`publish_time`),
|
||||||
KEY `idx_announcement_url` (`announcement_url`),
|
KEY `idx_announcement_url` (`announcement_url`),
|
||||||
KEY `idx_clean_status` (`clean_status`),
|
KEY `idx_clean_status` (`clean_status`),
|
||||||
KEY `idx_status` (`status`)
|
KEY `idx_status` (`status`)
|
||||||
@@ -63,7 +66,7 @@ CREATE TABLE `bg_recruit_announcement_year` (
|
|||||||
CREATE TABLE `bg_recruit_announcement_batch` (
|
CREATE TABLE `bg_recruit_announcement_batch` (
|
||||||
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
||||||
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
`batch_name` VARCHAR(64) NOT NULL COMMENT '批次值,如实习/暑期实习/寒假实习/秋招专场/春招提前批/春招补招',
|
`batch_name` VARCHAR(64) NOT NULL COMMENT '批次值,枚举:实习/暑期实习/寒假实习/秋招提前批/秋招正式批/秋招补招/春招提前批/春招正式批/春招补招/校园招聘/社招',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_announcement_id` (`announcement_id`),
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
@@ -77,7 +80,7 @@ CREATE TABLE `bg_recruit_announcement_batch` (
|
|||||||
CREATE TABLE `bg_recruit_announcement_tag` (
|
CREATE TABLE `bg_recruit_announcement_tag` (
|
||||||
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
||||||
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
`tag_name` VARCHAR(64) NOT NULL COMMENT '标签值,如秋招提前批/竞争力薪酬/校招',
|
`tag_name` VARCHAR(64) NOT NULL COMMENT '标签值,如竞争力薪酬/六险一金/不限专业,不与批次、城市、学历重复',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_announcement_id` (`announcement_id`),
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
@@ -105,7 +108,7 @@ CREATE TABLE `bg_recruit_announcement_city` (
|
|||||||
CREATE TABLE `bg_recruit_announcement_category` (
|
CREATE TABLE `bg_recruit_announcement_category` (
|
||||||
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
||||||
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
`category_name` VARCHAR(64) NOT NULL COMMENT '岗位大类值,如IT技术/人工智能/通信',
|
`category_name` VARCHAR(64) NOT NULL COMMENT '岗位分类值,取自固定分类表,如后端开发/人工智能/产品经理',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_announcement_id` (`announcement_id`),
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
@@ -113,6 +116,20 @@ CREATE TABLE `bg_recruit_announcement_category` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-岗位大类关联表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-岗位大类关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 行业关联表
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE `bg_recruit_announcement_industry` (
|
||||||
|
`id` BIGINT NOT NULL COMMENT '主键ID(雪花)',
|
||||||
|
`announcement_id` BIGINT NOT NULL COMMENT '公告id',
|
||||||
|
`industry_name` VARCHAR(64) NOT NULL COMMENT '行业分类值,取自固定分类表,如互联网/人工智能/半导体/芯片',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
|
KEY `idx_industry_name` (`industry_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-行业关联表';
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 学历要求关联表
|
-- 学历要求关联表
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
@@ -125,3 +142,16 @@ CREATE TABLE `bg_recruit_announcement_education` (
|
|||||||
KEY `idx_announcement_id` (`announcement_id`),
|
KEY `idx_announcement_id` (`announcement_id`),
|
||||||
KEY `idx_education_name` (`education_name`)
|
KEY `idx_education_name` (`education_name`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-学历要求关联表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘公告-学历要求关联表';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 截断数据
|
||||||
|
-- ============================================================
|
||||||
|
truncate bg_recruit_announcement;
|
||||||
|
truncate bg_recruit_announcement_batch;
|
||||||
|
truncate bg_recruit_announcement_category;
|
||||||
|
truncate bg_recruit_announcement_city;
|
||||||
|
truncate bg_recruit_announcement_education;
|
||||||
|
truncate bg_recruit_announcement_industry;
|
||||||
|
truncate bg_recruit_announcement_tag;
|
||||||
|
truncate bg_recruit_announcement_year;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
spider:
|
||||||
|
image: offerpie-spider:latest
|
||||||
|
container_name: offerpie-spider
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- ENV=prod
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
# OCR 推理线程数,与下面的 cpus 配额保持一致
|
||||||
|
- OMP_NUM_THREADS=8
|
||||||
|
volumes:
|
||||||
|
- /opt/offerpie/spider/logs:/app/logs
|
||||||
|
# Chromium 共享内存,页面渲染走内存比落盘快
|
||||||
|
shm_size: 2gb
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
# 宿主 32G,实测全量采集峰值约 4G,上限给足避免 OOM kill
|
||||||
|
memory: 8G
|
||||||
|
# 宿主 12 核
|
||||||
|
cpus: '8'
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""读取校招鸭公告 URL 文件并批量处理(一次性执行)。
|
||||||
|
|
||||||
|
运行(项目根目录下):
|
||||||
|
python init_xiaozhaoya.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.core.database import close_db, init_db
|
||||||
|
from app.core.logger import log
|
||||||
|
from app.service.announcement_batch_service import save_announcements
|
||||||
|
|
||||||
|
|
||||||
|
URLS_PATH = Path(__file__).resolve().parent / "doc" / "xiaozhaoya_announcement_links.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_urls() -> list[str]:
|
||||||
|
"""从本地 JSON 读取公告地址,过滤无效值并保持原始顺序去重。"""
|
||||||
|
with URLS_PATH.open("r", encoding="utf-8") as json_file:
|
||||||
|
payload = json.load(json_file)
|
||||||
|
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
raise ValueError(f"公告地址 JSON 必须是数组:{URLS_PATH}")
|
||||||
|
|
||||||
|
urls: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for value in payload:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
continue
|
||||||
|
url = value.strip()
|
||||||
|
if not url.startswith(("http://", "https://")) or url in seen:
|
||||||
|
continue
|
||||||
|
seen.add(url)
|
||||||
|
urls.append(url)
|
||||||
|
|
||||||
|
log.info("从文件读取到 {} 条公告地址(原始 {} 条)", len(urls), len(payload))
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""读取 URL,初始化数据库并直接调用批量处理服务。"""
|
||||||
|
urls = load_urls()
|
||||||
|
init_db()
|
||||||
|
try:
|
||||||
|
save_announcements(urls)
|
||||||
|
finally:
|
||||||
|
close_db()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user