rabbitmq
This commit is contained in:
539
docs/技术方案.md
539
docs/技术方案.md
@@ -3,12 +3,13 @@
|
|||||||
## 1. 项目概述
|
## 1. 项目概述
|
||||||
|
|
||||||
### 1.1 需求背景
|
### 1.1 需求背景
|
||||||
从八爪鱼API采集招聘数据,筛选近7天发布的数据,通过内置Kafka服务提供消息队列,供外部系统消费。
|
从八爪鱼API采集招聘数据,筛选近7天发布的数据,通过RabbitMQ消息队列提供数据消费接口,支持消息级别TTL自动过期。
|
||||||
|
|
||||||
### 1.2 核心功能
|
### 1.2 核心功能
|
||||||
- 增量采集八爪鱼API招聘数据
|
- 增量采集八爪鱼API招聘数据(从后往前采集,最新数据优先)
|
||||||
- 日期过滤(发布日期 + 采集时间均在7天内)
|
- 日期过滤(发布日期 + 采集时间均在7天内)
|
||||||
- 内置Kafka服务
|
- RabbitMQ消息队列(支持消息TTL,7天自动过期)
|
||||||
|
- 容器启动自动开始采集
|
||||||
- 提供REST API消费接口
|
- 提供REST API消费接口
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -22,16 +23,18 @@
|
|||||||
│ │
|
│ │
|
||||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
│ │ 八爪鱼API │───▶│ 采集服务 │───▶│ 日期过滤器 │ │
|
│ │ 八爪鱼API │───▶│ 采集服务 │───▶│ 日期过滤器 │ │
|
||||||
│ │ (数据源) │ │ (增量采集) │ │ (7天内数据) │ │
|
│ │ (数据源) │ │ (从后往前) │ │ (7天内数据) │ │
|
||||||
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
|
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
│ │ 内置 Kafka 服务 │ │
|
│ │ RabbitMQ 服务 │ │
|
||||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
|
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||||
│ │ │ Zookeeper │ │ Broker │ │ Topic:job_data │ │ │
|
│ │ │ Queue: job_data │ │ │
|
||||||
│ │ │ (Docker) │ │ (Docker) │ │ │ │ │
|
│ │ │ - 消息TTL: 7天 (604800000ms) │ │ │
|
||||||
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
|
│ │ │ - 过期消息自动删除 │ │ │
|
||||||
|
│ │ │ - 持久化存储 │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||||
│ └──────────────────────────────────────────────────────────┘ │
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
@@ -52,14 +55,13 @@
|
|||||||
|
|
||||||
| 组件 | 技术方案 | 版本 | 说明 |
|
| 组件 | 技术方案 | 版本 | 说明 |
|
||||||
|------|---------|------|------|
|
|------|---------|------|------|
|
||||||
| 运行环境 | Python | 3.10+ | 主开发语言 |
|
| 运行环境 | Python | 3.11+ | 主开发语言 |
|
||||||
| HTTP客户端 | httpx | 0.27+ | 异步HTTP请求 |
|
| HTTP客户端 | httpx | 0.27+ | 异步HTTP请求 |
|
||||||
| 消息队列 | Kafka | 3.6+ | Docker部署 |
|
| 消息队列 | RabbitMQ | 3.12+ | 支持消息级别TTL |
|
||||||
| Kafka客户端 | kafka-python | 2.0+ | Python Kafka SDK |
|
| MQ客户端 | pika | 1.3+ | Python RabbitMQ SDK |
|
||||||
| API框架 | FastAPI | 0.109+ | REST接口 |
|
| API框架 | FastAPI | 0.109+ | REST接口 |
|
||||||
| 容器编排 | Docker Compose | 2.0+ | Kafka/Zookeeper部署 |
|
| 容器编排 | Docker Compose | 2.0+ | 服务部署 |
|
||||||
| 任务调度 | APScheduler | 3.10+ | 定时增量采集 |
|
| 数据存储 | SQLite | 内置 | 存储采集进度 |
|
||||||
| 数据存储 | SQLite | 内置 | 存储采集进度(offset) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,36 +86,42 @@ job_crawler/
|
|||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── api_client.py # 八爪鱼API客户端
|
│ │ ├── api_client.py # 八爪鱼API客户端
|
||||||
│ │ ├── crawler.py # 采集核心逻辑
|
│ │ ├── crawler.py # 采集核心逻辑
|
||||||
│ │ ├── kafka_service.py # Kafka服务
|
│ │ ├── rabbitmq_service.py # RabbitMQ服务
|
||||||
│ │ └── progress_store.py # 进度存储
|
│ │ └── progress_store.py # 进度存储
|
||||||
│ ├── utils/ # 工具函数
|
│ ├── utils/ # 工具函数
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── date_parser.py # 日期解析
|
│ │ └── date_parser.py # 日期解析
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ └── main.py # 应用入口
|
│ └── main.py # 应用入口
|
||||||
├── docker-compose.yml # 容器编排(含Kafka+App)
|
├── config/ # 配置文件
|
||||||
|
│ ├── config.yml # 运行配置
|
||||||
|
│ └── config.yml.docker # Docker配置模板
|
||||||
|
├── docker-compose.yml # 容器编排
|
||||||
├── Dockerfile # 应用镜像构建
|
├── Dockerfile # 应用镜像构建
|
||||||
|
├── deploy.sh # 部署脚本(Linux)
|
||||||
|
├── deploy.bat # 部署脚本(Windows)
|
||||||
├── requirements.txt # Python依赖
|
├── requirements.txt # Python依赖
|
||||||
├── .env.example # 配置模板
|
|
||||||
├── .dockerignore # Docker忽略文件
|
|
||||||
└── README.md # 使用说明
|
└── README.md # 使用说明
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 核心模块设计
|
## 5. 核心模块设计
|
||||||
|
|
||||||
### 5.1 增量采集模块
|
### 5.1 增量采集模块
|
||||||
|
|
||||||
#### 采集策略
|
#### 采集策略(从后往前)
|
||||||
```python
|
```python
|
||||||
# 增量采集流程
|
# 增量采集流程
|
||||||
1. 读取上次采集的offset(首次为0)
|
1. 获取数据总数 total
|
||||||
2. 调用API: GET /data/all?taskId=xxx&offset={offset}&size=100
|
2. 读取上次采集的起始位置 last_start_offset
|
||||||
3. 解析返回数据,过滤近7天数据
|
3. 计算本次采集范围:
|
||||||
4. 推送到Kafka
|
- start_offset = total - batch_size (从最新数据开始)
|
||||||
5. 更新offset = offset + size
|
- end_offset = last_start_offset (截止到上次位置)
|
||||||
6. 循环直到 offset >= total
|
4. 循环采集: offset 从 start_offset 递减到 end_offset
|
||||||
|
5. 每批数据过滤后立即发送到RabbitMQ
|
||||||
|
6. 采集完成后保存 last_start_offset = 本次起始位置
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 进度持久化
|
#### 进度持久化
|
||||||
@@ -121,9 +129,12 @@ job_crawler/
|
|||||||
```sql
|
```sql
|
||||||
CREATE TABLE crawl_progress (
|
CREATE TABLE crawl_progress (
|
||||||
task_id TEXT PRIMARY KEY,
|
task_id TEXT PRIMARY KEY,
|
||||||
current_offset INTEGER,
|
last_start_offset INTEGER, -- 上次采集的起始位置
|
||||||
total INTEGER,
|
total INTEGER,
|
||||||
last_update TIMESTAMP
|
last_update TIMESTAMP,
|
||||||
|
status TEXT,
|
||||||
|
filtered_count INTEGER,
|
||||||
|
produced_count INTEGER
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -139,59 +150,58 @@ CREATE TABLE crawl_progress (
|
|||||||
|
|
||||||
#### 过滤逻辑
|
#### 过滤逻辑
|
||||||
```python
|
```python
|
||||||
def is_within_7_days(aae397: str, collect_time: str) -> bool:
|
def is_within_days(aae397: str, collect_time: str, days: int = 7) -> bool:
|
||||||
"""
|
"""
|
||||||
判断数据是否在近7天内
|
判断数据是否在指定天数内
|
||||||
条件:发布日期 AND 采集时间 都在7天内
|
条件:发布日期 AND 采集时间 都在N天内
|
||||||
"""
|
"""
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
seven_days_ago = today - timedelta(days=7)
|
cutoff_date = today - timedelta(days=days)
|
||||||
|
|
||||||
publish_date = parse_aae397(aae397) # 解析发布日期
|
publish_date = parse_aae397(aae397)
|
||||||
collect_date = parse_collect_time(collect_time) # 解析采集时间
|
collect_date = parse_collect_time(collect_time)
|
||||||
|
|
||||||
return publish_date >= seven_days_ago and collect_date >= seven_days_ago
|
return publish_date >= cutoff_date and collect_date >= cutoff_date
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 Kafka服务模块
|
### 5.3 RabbitMQ服务模块
|
||||||
|
|
||||||
#### Docker Compose配置
|
#### 消息TTL机制
|
||||||
```yaml
|
```python
|
||||||
version: '3.8'
|
# 队列声明时设置消息TTL
|
||||||
services:
|
channel.queue_declare(
|
||||||
zookeeper:
|
queue='job_data',
|
||||||
image: confluentinc/cp-zookeeper:7.5.0
|
durable=True,
|
||||||
ports:
|
arguments={
|
||||||
- "2181:2181"
|
'x-message-ttl': 604800000 # 7天(毫秒)
|
||||||
environment:
|
}
|
||||||
ZOOKEEPER_CLIENT_PORT: 2181
|
)
|
||||||
|
|
||||||
kafka:
|
# 发送消息时也设置TTL(双重保障)
|
||||||
image: confluentinc/cp-kafka:7.5.0
|
channel.basic_publish(
|
||||||
ports:
|
exchange='',
|
||||||
- "9092:9092"
|
routing_key='job_data',
|
||||||
environment:
|
body=message,
|
||||||
KAFKA_BROKER_ID: 1
|
properties=pika.BasicProperties(
|
||||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
delivery_mode=2, # 持久化
|
||||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
|
expiration='604800000' # 7天
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
)
|
||||||
depends_on:
|
)
|
||||||
- zookeeper
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Topic设计
|
#### 优势
|
||||||
- Topic名称: `job_data`
|
- 消息级别TTL,精确控制每条消息的过期时间
|
||||||
- 分区数: 3
|
- 过期消息自动删除,无需手动清理
|
||||||
- 副本数: 1
|
- 队列中始终保持最近7天的有效数据
|
||||||
- 消息格式: JSON
|
|
||||||
|
|
||||||
### 5.4 REST API接口
|
### 5.4 REST API接口
|
||||||
|
|
||||||
| 接口 | 方法 | 说明 |
|
| 接口 | 方法 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/consume` | GET | 消费Kafka数据,支持batch_size参数 |
|
| `/consume` | GET | 消费队列数据,支持batch_size参数 |
|
||||||
| `/consume/stream` | GET | SSE流式消费 |
|
| `/queue/size` | GET | 获取队列消息数量 |
|
||||||
| `/status` | GET | 查看采集进度和状态 |
|
| `/status` | GET | 查看采集进度和状态 |
|
||||||
|
| `/tasks` | GET | 获取任务列表 |
|
||||||
| `/crawl/start` | POST | 手动触发采集任务 |
|
| `/crawl/start` | POST | 手动触发采集任务 |
|
||||||
| `/crawl/stop` | POST | 停止采集任务 |
|
| `/crawl/stop` | POST | 停止采集任务 |
|
||||||
|
|
||||||
@@ -207,13 +217,17 @@ GET /consume?batch_size=10
|
|||||||
"code": 0,
|
"code": 0,
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"job_title": "机动车司机/驾驶",
|
"_id": "uuid",
|
||||||
"company": "青岛唐盛物流有限公司",
|
"_task_id": "00f3b445-...",
|
||||||
"salary": "1-1.5万",
|
"_crawl_time": "2026-01-15T10:30:00",
|
||||||
"location": "青岛黄岛区",
|
"Std_class": "机动车司机/驾驶",
|
||||||
"publish_date": "2026-01-13",
|
"aca112": "保底1万+五险+港内A2驾驶员",
|
||||||
"collect_time": "2026-01-15",
|
"AAB004": "青岛唐盛物流有限公司",
|
||||||
"url": "https://www.zhaopin.com/..."
|
"acb241": "1-1.5万",
|
||||||
|
"aab302": "青岛黄岛区",
|
||||||
|
"aae397": "1月13日",
|
||||||
|
"Collect_time": "2026-01-15",
|
||||||
|
...
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"count": 10
|
"count": 10
|
||||||
@@ -225,13 +239,20 @@ GET /consume?batch_size=10
|
|||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"data": {
|
"data": {
|
||||||
"task_id": "00f3b445-d8ec-44e8-88b2-4b971a228b1e",
|
"tasks": [
|
||||||
"total": 257449,
|
{
|
||||||
"current_offset": 156700,
|
"task_id": "00f3b445-...",
|
||||||
"progress": "60.87%",
|
"task_name": "青岛招聘数据",
|
||||||
"kafka_lag": 1234,
|
"total": 270000,
|
||||||
"status": "running",
|
"last_start_offset": 269900,
|
||||||
"last_update": "2026-01-15T10:30:00"
|
"status": "completed",
|
||||||
|
"filtered_count": 15000,
|
||||||
|
"produced_count": 15000,
|
||||||
|
"is_running": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"queue_size": 12345,
|
||||||
|
"running_count": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -240,113 +261,47 @@ GET /consume?batch_size=10
|
|||||||
|
|
||||||
## 6. 数据模型
|
## 6. 数据模型
|
||||||
|
|
||||||
### 6.1 原始数据字段映射
|
### 6.1 原始数据保留
|
||||||
|
数据采集后保留原始字段名,仅添加元数据:
|
||||||
|
|
||||||
| 原始字段 | 含义 | 输出字段 |
|
| 字段 | 说明 |
|
||||||
|---------|------|---------|
|
|------|------|
|
||||||
| Std_class | 职位分类 | job_category |
|
| _id | 唯一标识(UUID) |
|
||||||
| aca112 | 职位名称 | job_title |
|
| _task_id | 任务ID |
|
||||||
| AAB004 | 公司名称 | company |
|
| _crawl_time | 入库时间 |
|
||||||
| acb241 | 薪资范围 | salary |
|
| 其他字段 | 保留原始API返回的所有字段 |
|
||||||
| aab302 | 工作地点 | location |
|
|
||||||
| aae397 | 发布日期 | publish_date |
|
|
||||||
| Collect_time | 采集时间 | collect_time |
|
|
||||||
| ACE760 | 职位链接 | url |
|
|
||||||
| acb22a | 职位描述 | description |
|
|
||||||
| Experience | 经验要求 | experience |
|
|
||||||
| aac011 | 学历要求 | education |
|
|
||||||
|
|
||||||
### 6.2 Kafka消息格式
|
### 6.2 RabbitMQ消息格式
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "uuid",
|
"_id": "uuid",
|
||||||
"job_category": "机动车司机/驾驶",
|
"_task_id": "00f3b445-d8ec-44e8-88b2-4b971a228b1e",
|
||||||
"job_title": "保底1万+五险+港内A2驾驶员",
|
"_crawl_time": "2026-01-15T10:30:00",
|
||||||
"company": "青岛唐盛物流有限公司",
|
"Std_class": "机动车司机/驾驶",
|
||||||
"salary": "1-1.5万",
|
"aca112": "保底1万+五险+港内A2驾驶员",
|
||||||
"location": "青岛黄岛区",
|
"AAB004": "青岛唐盛物流有限公司",
|
||||||
"publish_date": "2026-01-13",
|
"AAB019": "民营",
|
||||||
"collect_time": "2026-01-15",
|
"acb241": "1-1.5万",
|
||||||
"url": "https://www.zhaopin.com/...",
|
"aab302": "青岛黄岛区",
|
||||||
"description": "...",
|
"AAE006": "青岛市黄岛区...",
|
||||||
"experience": "5-10年",
|
"aae397": "1月13日",
|
||||||
"education": "学历不限",
|
"Collect_time": "2026-01-15",
|
||||||
"crawl_time": "2026-01-15T10:30:00"
|
"ACE760": "https://www.zhaopin.com/...",
|
||||||
|
"acb22a": "岗位职责...",
|
||||||
|
"Experience": "5-10年",
|
||||||
|
"aac011": "学历不限",
|
||||||
|
"acb240": "1人",
|
||||||
|
"AAB022": "交通/运输/物流",
|
||||||
|
"Num_employers": "20-99人",
|
||||||
|
"AAE004": "张先生/HR",
|
||||||
|
"AAB092": "公司简介..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 部署流程
|
|
||||||
|
|
||||||
### 7.1 Docker Compose 一键部署(推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 配置环境变量
|
|
||||||
cd job_crawler
|
|
||||||
cp .env.example .env
|
|
||||||
# 编辑 .env 填入 API_USERNAME 和 API_PASSWORD
|
|
||||||
|
|
||||||
# 2. 启动所有服务(Zookeeper + Kafka + App)
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 3. 查看日志
|
|
||||||
docker-compose logs -f app
|
|
||||||
|
|
||||||
# 4. 停止服务
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 单独构建镜像
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建镜像
|
|
||||||
docker build -t job-crawler:latest .
|
|
||||||
|
|
||||||
# 推送到私有仓库(可选)
|
|
||||||
docker tag job-crawler:latest your-registry/job-crawler:latest
|
|
||||||
docker push your-registry/job-crawler:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 Kubernetes 部署(可选)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 示例 Deployment
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: job-crawler
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: job-crawler
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: job-crawler
|
|
||||||
image: job-crawler:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 8000
|
|
||||||
env:
|
|
||||||
- name: KAFKA_BOOTSTRAP_SERVERS
|
|
||||||
value: "kafka:9092"
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: job-crawler-secrets
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 服务端口
|
|
||||||
| 服务 | 端口 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| FastAPI | 8000 | HTTP API |
|
|
||||||
| Kafka | 9092 | 外部访问 |
|
|
||||||
| Kafka | 29092 | 容器内部访问 |
|
|
||||||
| Zookeeper | 2181 | Kafka协调 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 配置说明
|
## 7. 配置说明
|
||||||
|
|
||||||
### 配置文件 `config/config.yml`
|
### 配置文件 `config/config.yml`
|
||||||
|
|
||||||
@@ -360,133 +315,44 @@ app:
|
|||||||
# 八爪鱼API配置
|
# 八爪鱼API配置
|
||||||
api:
|
api:
|
||||||
base_url: https://openapi.bazhuayu.com
|
base_url: https://openapi.bazhuayu.com
|
||||||
task_id: 00f3b445-d8ec-44e8-88b2-4b971a228b1e
|
|
||||||
username: "your_username"
|
username: "your_username"
|
||||||
password: "your_password"
|
password: "your_password"
|
||||||
batch_size: 100
|
batch_size: 100
|
||||||
|
# 多任务配置
|
||||||
|
tasks:
|
||||||
|
- id: "00f3b445-d8ec-44e8-88b2-4b971a228b1e"
|
||||||
|
name: "青岛招聘数据"
|
||||||
|
enabled: true
|
||||||
|
- id: "task-id-2"
|
||||||
|
name: "任务2"
|
||||||
|
enabled: false
|
||||||
|
|
||||||
# Kafka配置
|
# RabbitMQ配置
|
||||||
kafka:
|
rabbitmq:
|
||||||
bootstrap_servers: kafka:29092 # Docker内部网络
|
host: rabbitmq # Docker内部服务名
|
||||||
topic: job_data
|
port: 5672
|
||||||
consumer_group: job_consumer_group
|
username: guest
|
||||||
|
password: guest
|
||||||
|
queue: job_data
|
||||||
|
message_ttl: 604800000 # 消息过期时间:7天(毫秒)
|
||||||
|
|
||||||
# 采集配置
|
# 采集配置
|
||||||
crawler:
|
crawler:
|
||||||
interval: 300 # 采集间隔(秒)
|
filter_days: 7 # 数据有效期(天)
|
||||||
filter_days: 7 # 过滤天数
|
max_expired_batches: 3 # 连续过期批次阈值
|
||||||
|
max_workers: 5 # 最大并行任务数
|
||||||
|
auto_start: true # 容器启动时自动开始采集
|
||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
database:
|
database:
|
||||||
path: /app/data/crawl_progress.db
|
path: data/crawl_progress.db
|
||||||
```
|
|
||||||
|
|
||||||
### 配置加载优先级
|
|
||||||
|
|
||||||
1. 环境变量 `CONFIG_PATH` 指定配置文件路径
|
|
||||||
2. 默认路径 `config/config.yml`
|
|
||||||
|
|
||||||
### Docker挂载
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
volumes:
|
|
||||||
- ./config:/app/config:ro # 配置文件(只读)
|
|
||||||
- app_data:/app/data # 数据持久化
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 异常处理
|
## 8. 部署流程
|
||||||
|
|
||||||
| 异常场景 | 处理策略 |
|
### 8.1 Docker Compose 一键部署
|
||||||
|---------|---------|
|
|
||||||
| API请求失败 | 重试3次,指数退避 |
|
|
||||||
| Token过期 | 返回错误,需手动更新 |
|
|
||||||
| Kafka连接失败 | 重试连接,数据暂存本地 |
|
|
||||||
| 日期解析失败 | 记录日志,跳过该条数据 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 监控指标
|
|
||||||
|
|
||||||
- 采集进度百分比
|
|
||||||
- Kafka消息堆积量(lag)
|
|
||||||
- 每分钟采集条数
|
|
||||||
- 过滤后有效数据比例
|
|
||||||
- API响应时间
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 后续扩展
|
|
||||||
|
|
||||||
1. **多任务支持**: 支持配置多个taskId并行采集
|
|
||||||
2. **数据去重**: 基于职位URL去重
|
|
||||||
3. **告警通知**: 采集异常时发送通知
|
|
||||||
4. **Web管理界面**: 可视化监控采集状态
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Docker 镜像构建
|
|
||||||
|
|
||||||
### Dockerfile 说明
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 安装系统依赖
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc
|
|
||||||
|
|
||||||
# 安装Python依赖
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# 复制应用代码
|
|
||||||
COPY app/ ./app/
|
|
||||||
|
|
||||||
# 创建数据目录
|
|
||||||
RUN mkdir -p /app/data
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
ENV PYTHONPATH=/app
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 构建命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建
|
|
||||||
docker build -t job-crawler:latest .
|
|
||||||
|
|
||||||
# 运行测试
|
|
||||||
docker run --rm -p 8000:8000 \
|
|
||||||
-e API_USERNAME=xxx \
|
|
||||||
-e API_PASSWORD=xxx \
|
|
||||||
-e KAFKA_BOOTSTRAP_SERVERS=host.docker.internal:9092 \
|
|
||||||
job-crawler:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. 代码分层说明
|
|
||||||
|
|
||||||
| 层级 | 目录 | 职责 |
|
|
||||||
|------|------|------|
|
|
||||||
| API层 | `app/api/` | 路由定义、请求处理、响应格式化 |
|
|
||||||
| 服务层 | `app/services/` | 业务逻辑、外部服务调用 |
|
|
||||||
| 模型层 | `app/models/` | 数据结构定义、数据转换 |
|
|
||||||
| 工具层 | `app/utils/` | 通用工具函数 |
|
|
||||||
| 核心层 | `app/core/` | 配置、日志等基础设施 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. 快速启动
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 配置
|
# 1. 配置
|
||||||
@@ -494,25 +360,57 @@ cd job_crawler
|
|||||||
cp config/config.yml.docker config/config.yml
|
cp config/config.yml.docker config/config.yml
|
||||||
# 编辑 config/config.yml 填入账号密码
|
# 编辑 config/config.yml 填入账号密码
|
||||||
|
|
||||||
# 2. 一键启动
|
# 2. 构建镜像
|
||||||
docker-compose up -d
|
./deploy.sh build
|
||||||
|
|
||||||
# 3. 访问API文档
|
# 3. 启动服务
|
||||||
# http://localhost:8000/docs
|
./deploy.sh up
|
||||||
|
|
||||||
# 4. 启动采集
|
# 4. 查看日志
|
||||||
curl -X POST http://localhost:8000/crawl/start
|
./deploy.sh logs
|
||||||
|
|
||||||
# 5. 查看进度
|
# 5. 查看状态
|
||||||
curl http://localhost:8000/status
|
./deploy.sh status
|
||||||
|
```
|
||||||
|
|
||||||
# 6. 消费数据
|
### 8.2 部署脚本命令
|
||||||
curl http://localhost:8000/consume?batch_size=10
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `./deploy.sh build` | 构建镜像 |
|
||||||
|
| `./deploy.sh up` | 启动服务 |
|
||||||
|
| `./deploy.sh down` | 停止服务 |
|
||||||
|
| `./deploy.sh restart` | 重启应用 |
|
||||||
|
| `./deploy.sh logs` | 查看应用日志 |
|
||||||
|
| `./deploy.sh status` | 查看服务状态 |
|
||||||
|
| `./deploy.sh reset` | 清理数据卷并重启 |
|
||||||
|
|
||||||
|
### 8.3 服务端口
|
||||||
|
|
||||||
|
| 服务 | 端口 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| FastAPI | 8000 | HTTP API |
|
||||||
|
| RabbitMQ | 5672 | AMQP协议 |
|
||||||
|
| RabbitMQ | 15672 | 管理界面 |
|
||||||
|
|
||||||
|
### 8.4 访问地址
|
||||||
|
|
||||||
|
- API文档: http://localhost:8000/docs
|
||||||
|
- RabbitMQ管理界面: http://localhost:15672 (guest/guest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 数据流向
|
||||||
|
|
||||||
|
```
|
||||||
|
八爪鱼API → 采集服务(过滤7天内数据) → RabbitMQ(TTL=7天) → 第三方消费
|
||||||
|
↓
|
||||||
|
过期自动删除
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Token自动刷新机制
|
## 10. Token自动刷新机制
|
||||||
|
|
||||||
系统实现了Token自动管理:
|
系统实现了Token自动管理:
|
||||||
|
|
||||||
@@ -521,20 +419,37 @@ curl http://localhost:8000/consume?batch_size=10
|
|||||||
3. 请求前检查Token有效期(提前5分钟刷新)
|
3. 请求前检查Token有效期(提前5分钟刷新)
|
||||||
4. 遇到401错误自动重新获取Token
|
4. 遇到401错误自动重新获取Token
|
||||||
|
|
||||||
```python
|
---
|
||||||
# app/services/api_client.py 核心逻辑
|
|
||||||
async def _get_token(self) -> str:
|
|
||||||
# 检查token是否有效(提前5分钟刷新)
|
|
||||||
if self._access_token and time.time() < self._token_expires_at - 300:
|
|
||||||
return self._access_token
|
|
||||||
|
|
||||||
# 调用 /token 接口获取新token
|
## 11. 异常处理
|
||||||
response = await client.post(f"{self.base_url}/token", json={
|
|
||||||
"username": self.username,
|
|
||||||
"password": self.password,
|
|
||||||
"grant_type": "password"
|
|
||||||
})
|
|
||||||
|
|
||||||
self._access_token = token_data.get("access_token")
|
| 异常场景 | 处理策略 |
|
||||||
self._token_expires_at = time.time() + expires_in
|
|---------|---------|
|
||||||
|
| API请求失败 | 重试3次,指数退避 |
|
||||||
|
| Token过期 | 自动刷新Token |
|
||||||
|
| RabbitMQ连接失败 | 自动重连 |
|
||||||
|
| 日期解析失败 | 记录日志,跳过该条数据 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 快速启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 配置
|
||||||
|
cd job_crawler
|
||||||
|
cp config/config.yml.docker config/config.yml
|
||||||
|
# 编辑 config/config.yml 填入账号密码
|
||||||
|
|
||||||
|
# 2. 一键启动
|
||||||
|
./deploy.sh build
|
||||||
|
./deploy.sh up
|
||||||
|
|
||||||
|
# 3. 查看采集日志
|
||||||
|
./deploy.sh logs
|
||||||
|
|
||||||
|
# 4. 消费数据
|
||||||
|
curl http://localhost:8000/consume?batch_size=10
|
||||||
|
|
||||||
|
# 5. 查看队列大小
|
||||||
|
curl http://localhost:8000/queue/size
|
||||||
```
|
```
|
||||||
|
|||||||
311
docs/采集流程时序图.md
311
docs/采集流程时序图.md
@@ -1,68 +1,58 @@
|
|||||||
# 增量采集流程时序图
|
# 增量采集流程时序图
|
||||||
|
|
||||||
## 1. 核心逻辑变更
|
## 1. 核心逻辑
|
||||||
|
|
||||||
### 原逻辑(从前往后)
|
### 采集方向(从后往前)
|
||||||
```
|
```
|
||||||
offset: 0 → 100 → 200 → ... → total
|
offset: total-100 → total-200 → ... → last_start_offset
|
||||||
问题:新数据在末尾,每次都要遍历全部旧数据
|
优势:先采集最新数据,下次只采集新增部分
|
||||||
```
|
```
|
||||||
|
|
||||||
### 新逻辑(从后往前)
|
### 消息队列
|
||||||
```
|
- 使用 RabbitMQ,支持消息级别 TTL
|
||||||
offset: total-100 → total-200 → ... → 0
|
- 消息过期时间:7天,过期自动删除
|
||||||
优势:先采集最新数据,遇到过期数据即可停止
|
- 每批数据过滤后立即发送,不等待任务结束
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 容器启动与自动采集时序图
|
## 2. 容器启动与自动采集
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
│ Docker │ │ App │ │ Crawler │ │ 八爪鱼API │ │ Kafka │
|
│ Docker │ │ App │ │ Crawler │ │ RabbitMQ │
|
||||||
│ 容器 │ │ FastAPI │ │ Manager │ │ │ │ │
|
│ 容器 │ │ FastAPI │ │ Manager │ │ │
|
||||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||||
│ │ │ │ │
|
│ │ │ │
|
||||||
│ docker-compose │ │ │ │
|
│ docker-compose │ │ │
|
||||||
│ up │ │ │ │
|
│ up │ │ │
|
||||||
│───────────────>│ │ │ │
|
│───────────────>│ │ │
|
||||||
│ │ │ │ │
|
│ │ │ │
|
||||||
│ │ lifespan启动 │ │ │
|
│ │ lifespan启动 │ │
|
||||||
│ │ 读取config.yml │ │ │
|
│ │ auto_start=true│ │
|
||||||
│ │───────────────>│ │ │
|
│ │───────────────>│ │
|
||||||
│ │ │ │ │
|
│ │ │ │
|
||||||
│ │ │ 遍历enabled=true的任务 │
|
│ │ │ 遍历enabled任务 │
|
||||||
│ │ │────────┐ │ │
|
│ │ │ 创建TaskCrawler │
|
||||||
│ │ │ │ │ │
|
│ │ │────────┐ │
|
||||||
│ │ │<───────┘ │ │
|
│ │ │<───────┘ │
|
||||||
│ │ │ │ │
|
│ │ │ │
|
||||||
│ │ │ 为每个任务创建 │ │
|
|
||||||
│ │ │ TaskCrawler │ │
|
|
||||||
│ │ │────────┐ │ │
|
|
||||||
│ │ │ │ │ │
|
|
||||||
│ │ │<───────┘ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ auto_start_all │ │ │
|
|
||||||
│ │───────────────>│ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ 并行启动所有任务│
|
│ │ │ 并行启动所有任务│
|
||||||
│ │ │═══════════════════════════════>│
|
│ │ │═══════════════>│
|
||||||
│ │ │ │ │
|
│ │ │ │
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 单任务采集流程(从后往前,实时发送)
|
## 3. 单任务采集流程(从后往前,实时发送)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
│ TaskCrawler │ │ 八爪鱼API │ │ DateFilter │ │ Kafka │
|
│ TaskCrawler │ │ 八爪鱼API │ │ DateFilter │ │ RabbitMQ │
|
||||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 1.获取数据总数 │ │ │
|
│ 1.获取数据总数 │ │ │
|
||||||
│───────────────>│ │ │
|
│───────────────>│ │ │
|
||||||
│<───────────────│ │ │
|
│<───────────────│ │ │
|
||||||
│ total=257449 │ │ │
|
│ total=270000 │ │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 2.读取上次进度,计算采集范围 │ │
|
│ 2.读取上次进度,计算采集范围 │ │
|
||||||
│ start_offset = total - 100 = 257349 │
|
│ start_offset = total - 100 = 269900 │
|
||||||
│ end_offset = last_start_offset (上次起始位置) │
|
│ end_offset = last_start_offset (上次起始位置) │
|
||||||
│────────┐ │ │ │
|
│────────┐ │ │ │
|
||||||
│<───────┘ │ │ │
|
│<───────┘ │ │ │
|
||||||
@@ -72,44 +62,36 @@ offset: total-100 → total-200 → ... → 0
|
|||||||
│ ╚══════════════════════════════════════════════════════════╝
|
│ ╚══════════════════════════════════════════════════════════╝
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 3.请求一批数据 │ │ │
|
│ 3.请求一批数据 │ │ │
|
||||||
│ offset=257349 │ │ │
|
│ offset=269900 │ │ │
|
||||||
│───────────────>│ │ │
|
│───────────────>│ │ │
|
||||||
│<───────────────│ │ │
|
│<───────────────│ │ │
|
||||||
│ 返回100条 │ │ │
|
│ 返回100条 │ │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 4.过滤数据 │ │ │
|
│ 4.过滤数据(7天内有效) │ │
|
||||||
│───────────────────────────────>│ │
|
│───────────────────────────────>│ │
|
||||||
│<───────────────────────────────│ │
|
│<───────────────────────────────│ │
|
||||||
│ 有效数据95条 │ │ │
|
│ 有效95条,过期5条 │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 5.立即发送到Kafka (不等待任务结束) │
|
│ 5.立即发送到RabbitMQ │ │
|
||||||
|
│ (消息TTL=7天,过期自动删除) │ │
|
||||||
│────────────────────────────────────────────────>│
|
│────────────────────────────────────────────────>│
|
||||||
│<────────────────────────────────────────────────│
|
│<────────────────────────────────────────────────│
|
||||||
│ 发送成功 │ │ │
|
│ 发送成功 │ │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 6.更新offset,保存进度 │ │
|
│ 6.更新offset,继续循环 │ │
|
||||||
│ offset = 257349 - 100 = 257249 │ │
|
│ offset = 269900 - 100 = 269800 │ │
|
||||||
│────────┐ │ │ │
|
│────────┐ │ │ │
|
||||||
│<───────┘ │ │ │
|
│<───────┘ │ │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ 7.检查是否继续 │ │ │
|
│ 7.检查停止条件 │ │ │
|
||||||
│ offset >= end_offset ? │ │
|
│ offset >= end_offset ? 继续 │ │
|
||||||
|
│ offset < end_offset ? 停止 │ │
|
||||||
│────────┐ │ │ │
|
│────────┐ │ │ │
|
||||||
│<───────┘ 是→继续循环 │ │
|
│<───────┘ │ │ │
|
||||||
│ 否→结束 │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ ╔══════════════════════════════════════════════════════════╗
|
|
||||||
│ ║ 停止条件: ║
|
|
||||||
│ ║ - offset < end_offset (已采集到上次位置) ║
|
|
||||||
│ ║ - 首次采集时连续3批全过期 ║
|
|
||||||
│ ║ - 手动停止 ║
|
|
||||||
│ ╚══════════════════════════════════════════════════════════╝
|
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
```
|
```
|
||||||
|
|
||||||
**关键点:每批数据过滤后立即发送Kafka,不等待整个任务完成**
|
## 4. 进度记录与增量采集
|
||||||
|
|
||||||
## 4. 进度记录与增量采集逻辑
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
@@ -118,171 +100,96 @@ offset: total-100 → total-200 → ... → 0
|
|||||||
│ │
|
│ │
|
||||||
│ 首次采集: │
|
│ 首次采集: │
|
||||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ total = 257449 │ │
|
│ │ total = 270000 │ │
|
||||||
│ │ start_offset = total - batch_size = 257349 │ │
|
│ │ start_offset = total - 100 = 269900 │ │
|
||||||
│ │ end_offset = 0 (采集到最开始,或遇到过期数据停止) │ │
|
│ │ end_offset = 0 (首次采集,遇到连续过期数据停止) │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ 采集完成后保存: │ │
|
│ │ 采集完成后保存: last_start_offset = 269900 │ │
|
||||||
│ │ - last_start_offset = 257349 (本次采集的起始位置) │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ 下次采集: │
|
│ 下次采集: │
|
||||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ total = 260000 (新增了数据) │ │
|
│ │ total = 270500 (新增500条) │ │
|
||||||
│ │ start_offset = total - batch_size = 259900 │ │
|
│ │ start_offset = 270500 - 100 = 270400 │ │
|
||||||
│ │ end_offset = last_start_offset = 257349 (上次的起始位置) │ │
|
│ │ end_offset = 269900 (上次的起始位置) │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ 只采集 259900 → 257349 这部分新增数据 │ │
|
│ │ 只采集 270400 → 269900 这部分新增数据 │ │
|
||||||
|
│ │ 采集完成后保存: last_start_offset = 270400 │ │
|
||||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ 流程图: │
|
|
||||||
│ │
|
|
||||||
│ 获取 total │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────┐ │
|
|
||||||
│ │ 读取上次进度 │ │
|
|
||||||
│ │ last_start_offset │ │
|
|
||||||
│ └───────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────┐ ┌─────────────────────────────────┐ │
|
|
||||||
│ │last_start_offset │ 是 │ end_offset = last_start_offset │ │
|
|
||||||
│ │ 存在? │────>│ (从上次位置截止) │ │
|
|
||||||
│ └───────────────────┘ └─────────────────────────────────┘ │
|
|
||||||
│ │ 否 │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────────────────────────┐ │
|
|
||||||
│ │ end_offset = 0 │ │
|
|
||||||
│ │ (首次采集,采集到最开始或遇到过期停止) │ │
|
|
||||||
│ └───────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────┐ │
|
|
||||||
│ │ start_offset = │ │
|
|
||||||
│ │ total - batch_size│ │
|
|
||||||
│ └───────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────────────────────────┐ │
|
|
||||||
│ │ 从 start_offset 向前采集 │ │
|
|
||||||
│ │ 直到 offset <= end_offset │ │
|
|
||||||
│ └───────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────────────────────────┐ │
|
|
||||||
│ │ 保存 last_start_offset = 本次起始位置 │ │
|
|
||||||
│ └───────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. 停止条件
|
## 5. 停止条件
|
||||||
|
|
||||||
采集停止的条件(满足任一即停止):
|
采集停止的条件(满足任一即停止):
|
||||||
1. `offset <= end_offset` - 已采集到上次的起始位置
|
1. `offset < end_offset` - 已采集到上次的起始位置
|
||||||
2. 连续3批数据全部过期 - 数据太旧(仅首次采集时生效)
|
2. 连续3批数据全部过期 - 数据太旧(仅首次采集时生效)
|
||||||
3. 手动调用停止接口
|
3. 手动调用停止接口
|
||||||
|
|
||||||
## 6. 完整流程示例
|
## 6. 消息过期机制
|
||||||
|
|
||||||
### 首次采集
|
```
|
||||||
数据总量 `total = 257449`,`batch_size = 100`,无历史进度:
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ RabbitMQ 消息TTL │
|
||||||
| 轮次 | offset | 请求范围 | 有效数据 | 动作 |
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|------|--------|----------|----------|------|
|
│ │
|
||||||
| 1 | 257349 | 257349-257449 | 98 | 发送到Kafka,继续 |
|
│ 消息发送时设置 TTL = 7天 (604800000ms) │
|
||||||
| 2 | 257249 | 257249-257349 | 95 | 发送到Kafka,继续 |
|
│ │
|
||||||
| ... | ... | ... | ... | ... |
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
| N | 1000 | 1000-1100 | 0 | expired_batches=1 |
|
│ │ 消息A │ │ 消息B │ │ 消息C │ │ 消息D │ │
|
||||||
| N+1 | 900 | 900-1000 | 0 | expired_batches=2 |
|
│ │ 1月8日 │ │ 1月10日 │ │ 1月14日 │ │ 1月15日 │ │
|
||||||
| N+2 | 800 | 800-900 | 0 | expired_batches=3,**停止** |
|
│ │ 已过期 │ │ 即将过期 │ │ 有效 │ │ 有效 │ │
|
||||||
|
│ │ 自动删除 │ │ │ │ │ │ │ │
|
||||||
保存进度:`last_start_offset = 257349`
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ ↓ │
|
||||||
### 第二次采集(1小时后)
|
│ RabbitMQ自动清理 │
|
||||||
数据总量 `total = 257600`(新增151条),读取 `last_start_offset = 257349`:
|
│ │
|
||||||
|
│ 优势: │
|
||||||
| 轮次 | offset | 请求范围 | end_offset | 动作 |
|
│ - 消息级别TTL,精确控制每条消息的过期时间 │
|
||||||
|------|--------|----------|------------|------|
|
│ - 过期消息自动删除,无需手动清理 │
|
||||||
| 1 | 257500 | 257500-257600 | 257349 | 发送到Kafka,继续 |
|
│ - 队列中始终保持最近7天的有效数据 │
|
||||||
| 2 | 257400 | 257400-257500 | 257349 | 发送到Kafka,继续 |
|
│ │
|
||||||
| 3 | 257300 | 257300-257400 | 257349 | offset < end_offset,**停止** |
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
保存进度:`last_start_offset = 257500`
|
|
||||||
|
|
||||||
## 7. 代码变更点
|
|
||||||
|
|
||||||
### 7.1 progress_store - 保存 last_start_offset
|
|
||||||
```python
|
|
||||||
# 进度表增加字段
|
|
||||||
# last_start_offset: 上次采集的起始位置,作为下次采集的截止位置
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.2 crawler.py - TaskCrawler.start()
|
|
||||||
```python
|
|
||||||
async def start(self):
|
|
||||||
total = await api_client.get_total_count(self.task_id)
|
|
||||||
|
|
||||||
# 读取上次进度
|
## 7. 配置说明
|
||||||
progress = progress_store.get_progress(self.task_id)
|
|
||||||
last_start_offset = progress.last_start_offset if progress else None
|
|
||||||
|
|
||||||
# 计算本次采集范围
|
|
||||||
start_offset = total - self.batch_size # 从最新数据开始
|
|
||||||
end_offset = last_start_offset if last_start_offset else 0 # 截止到上次起始位置
|
|
||||||
|
|
||||||
# 保存本次起始位置
|
|
||||||
this_start_offset = start_offset
|
|
||||||
|
|
||||||
current_offset = start_offset
|
|
||||||
expired_batches = 0
|
|
||||||
|
|
||||||
while current_offset >= end_offset and self._running:
|
|
||||||
valid_count = await self._crawl_batch(current_offset)
|
|
||||||
|
|
||||||
# 仅首次采集时检查过期(end_offset=0时)
|
|
||||||
if end_offset == 0:
|
|
||||||
if valid_count == 0:
|
|
||||||
expired_batches += 1
|
|
||||||
if expired_batches >= 3:
|
|
||||||
break # 连续3批过期,停止
|
|
||||||
else:
|
|
||||||
expired_batches = 0
|
|
||||||
|
|
||||||
current_offset -= self.batch_size
|
|
||||||
|
|
||||||
# 保存进度,记录本次起始位置供下次使用
|
|
||||||
progress_store.save_progress(
|
|
||||||
task_id=self.task_id,
|
|
||||||
last_start_offset=this_start_offset,
|
|
||||||
...
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 main.py - 自动启动
|
|
||||||
```python
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
logger.info("服务启动中...")
|
|
||||||
|
|
||||||
# 自动启动所有任务
|
|
||||||
from app.services import crawler_manager
|
|
||||||
asyncio.create_task(crawler_manager.start_all())
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
logger.info("服务关闭中...")
|
|
||||||
crawler_manager.stop_all()
|
|
||||||
kafka_service.close()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 8. 配置说明
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# config.yml
|
# config.yml
|
||||||
|
|
||||||
|
# RabbitMQ配置
|
||||||
|
rabbitmq:
|
||||||
|
host: rabbitmq # Docker内部服务名
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
queue: job_data
|
||||||
|
message_ttl: 604800000 # 消息过期时间:7天(毫秒)
|
||||||
|
|
||||||
|
# 采集配置
|
||||||
crawler:
|
crawler:
|
||||||
filter_days: 7 # 数据有效期(天)
|
filter_days: 7 # 数据有效期(天)
|
||||||
max_expired_batches: 3 # 连续过期批次阈值,超过则停止
|
max_expired_batches: 3 # 连续过期批次阈值(首次采集时生效)
|
||||||
auto_start: true # 容器启动时自动开始采集
|
auto_start: true # 容器启动时自动开始采集
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 8. API接口
|
||||||
|
|
||||||
|
| 接口 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/status` | GET | 获取采集状态 |
|
||||||
|
| `/tasks` | GET | 获取任务列表 |
|
||||||
|
| `/crawl/start` | POST | 启动采集任务 |
|
||||||
|
| `/crawl/stop` | POST | 停止采集任务 |
|
||||||
|
| `/consume` | GET | 消费队列数据 |
|
||||||
|
| `/queue/size` | GET | 获取队列消息数量 |
|
||||||
|
|
||||||
|
## 9. 数据流向
|
||||||
|
|
||||||
|
```
|
||||||
|
八爪鱼API → 采集服务(过滤7天内数据) → RabbitMQ(TTL=7天) → 第三方消费
|
||||||
|
↓
|
||||||
|
过期自动删除
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Optional
|
|||||||
from fastapi import APIRouter, Query, BackgroundTasks, HTTPException
|
from fastapi import APIRouter, Query, BackgroundTasks, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from app.models import ApiResponse, ConsumeResponse, StatusResponse
|
from app.models import ApiResponse, ConsumeResponse, StatusResponse
|
||||||
from app.services import crawler_manager, kafka_service
|
from app.services import crawler_manager, rabbitmq_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -84,29 +84,18 @@ async def stop_crawl(
|
|||||||
|
|
||||||
@router.get("/consume", response_model=ConsumeResponse)
|
@router.get("/consume", response_model=ConsumeResponse)
|
||||||
async def consume_data(
|
async def consume_data(
|
||||||
batch_size: int = Query(10, ge=1, le=100, description="批量大小"),
|
batch_size: int = Query(10, ge=1, le=100, description="批量大小")
|
||||||
timeout: int = Query(5000, ge=1000, le=30000, description="超时时间(毫秒)")
|
|
||||||
):
|
):
|
||||||
"""消费Kafka数据"""
|
"""消费RabbitMQ数据"""
|
||||||
try:
|
try:
|
||||||
messages = kafka_service.consume(batch_size, timeout)
|
messages = rabbitmq_service.consume(batch_size)
|
||||||
return ConsumeResponse(data=messages, count=len(messages))
|
return ConsumeResponse(data=messages, count=len(messages))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"消费数据失败: {e}")
|
logger.error(f"消费数据失败: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/consume/stream")
|
@router.get("/queue/size")
|
||||||
async def consume_stream():
|
async def get_queue_size():
|
||||||
"""SSE流式消费"""
|
"""获取队列消息数量"""
|
||||||
async def event_generator():
|
return {"queue_size": rabbitmq_service.get_queue_size()}
|
||||||
consumer = kafka_service.get_consumer()
|
|
||||||
try:
|
|
||||||
for message in consumer:
|
|
||||||
data = json.dumps(message.value, ensure_ascii=False)
|
|
||||||
yield f"data: {data}\n\n"
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"流式消费错误: {e}")
|
|
||||||
finally:
|
|
||||||
consumer.close()
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ class ApiConfig(BaseModel):
|
|||||||
tasks: List[TaskConfig] = []
|
tasks: List[TaskConfig] = []
|
||||||
|
|
||||||
|
|
||||||
class KafkaConfig(BaseModel):
|
class RabbitMQConfig(BaseModel):
|
||||||
bootstrap_servers: str = "localhost:9092"
|
host: str = "localhost"
|
||||||
topic: str = "job_data"
|
port: int = 5672
|
||||||
consumer_group: str = "job_consumer_group"
|
username: str = "guest"
|
||||||
|
password: str = "guest"
|
||||||
|
queue: str = "job_data"
|
||||||
|
message_ttl: int = 604800000 # 7天(毫秒)
|
||||||
|
|
||||||
|
|
||||||
class CrawlerConfig(BaseModel):
|
class CrawlerConfig(BaseModel):
|
||||||
@@ -49,7 +52,7 @@ class Settings(BaseModel):
|
|||||||
"""应用配置"""
|
"""应用配置"""
|
||||||
app: AppConfig = AppConfig()
|
app: AppConfig = AppConfig()
|
||||||
api: ApiConfig = ApiConfig()
|
api: ApiConfig = ApiConfig()
|
||||||
kafka: KafkaConfig = KafkaConfig()
|
rabbitmq: RabbitMQConfig = RabbitMQConfig()
|
||||||
crawler: CrawlerConfig = CrawlerConfig()
|
crawler: CrawlerConfig = CrawlerConfig()
|
||||||
database: DatabaseConfig = DatabaseConfig()
|
database: DatabaseConfig = DatabaseConfig()
|
||||||
|
|
||||||
@@ -71,7 +74,7 @@ class Settings(BaseModel):
|
|||||||
return cls(
|
return cls(
|
||||||
app=AppConfig(**data.get('app', {})),
|
app=AppConfig(**data.get('app', {})),
|
||||||
api=api_config,
|
api=api_config,
|
||||||
kafka=KafkaConfig(**data.get('kafka', {})),
|
rabbitmq=RabbitMQConfig(**data.get('rabbitmq', {})),
|
||||||
crawler=CrawlerConfig(**data.get('crawler', {})),
|
crawler=CrawlerConfig(**data.get('crawler', {})),
|
||||||
database=DatabaseConfig(**data.get('database', {}))
|
database=DatabaseConfig(**data.get('database', {}))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from fastapi import FastAPI
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.logging import setup_logging
|
from app.core.logging import setup_logging
|
||||||
from app.api import router
|
from app.api import router
|
||||||
from app.services import kafka_service
|
from app.services import rabbitmq_service
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -28,7 +28,7 @@ async def lifespan(app: FastAPI):
|
|||||||
logger.info("服务关闭中...")
|
logger.info("服务关闭中...")
|
||||||
from app.services import crawler_manager
|
from app.services import crawler_manager
|
||||||
crawler_manager.stop_all()
|
crawler_manager.stop_all()
|
||||||
kafka_service.close()
|
rabbitmq_service.close()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
|||||||
@@ -1,60 +1,24 @@
|
|||||||
"""招聘数据模型"""
|
"""招聘数据模型"""
|
||||||
from pydantic import BaseModel
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class JobData(BaseModel):
|
class JobData:
|
||||||
"""招聘数据模型"""
|
"""招聘数据 - 保留原始数据格式"""
|
||||||
id: str = ""
|
|
||||||
task_id: str = "" # 任务ID
|
|
||||||
job_category: str = "" # Std_class - 职位分类
|
|
||||||
job_title: str = "" # aca112 - 职位名称
|
|
||||||
company: str = "" # AAB004 - 公司名称
|
|
||||||
company_type: str = "" # AAB019 - 企业类型
|
|
||||||
salary: str = "" # acb241 - 薪资范围
|
|
||||||
location: str = "" # aab302 - 工作地点
|
|
||||||
address: str = "" # AAE006 - 详细地址
|
|
||||||
publish_date: str = "" # aae397 - 发布日期
|
|
||||||
collect_time: str = "" # Collect_time - 采集时间
|
|
||||||
url: str = "" # ACE760 - 职位链接
|
|
||||||
description: str = "" # acb22a - 职位描述
|
|
||||||
experience: str = "" # Experience - 经验要求
|
|
||||||
education: str = "" # aac011 - 学历要求
|
|
||||||
headcount: str = "" # acb240 - 招聘人数
|
|
||||||
industry: str = "" # AAB022 - 行业
|
|
||||||
company_size: str = "" # Num_employers - 公司规模
|
|
||||||
contact: str = "" # AAE004 - 联系人
|
|
||||||
company_intro: str = "" # AAB092 - 公司简介
|
|
||||||
crawl_time: str = "" # 入库时间
|
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, raw_data: dict, task_id: str = ""):
|
||||||
super().__init__(**data)
|
self.raw_data = raw_data
|
||||||
if not self.id:
|
self.task_id = task_id
|
||||||
self.id = str(uuid.uuid4())
|
# 添加元数据
|
||||||
if not self.crawl_time:
|
self.raw_data["_id"] = str(uuid.uuid4())
|
||||||
self.crawl_time = datetime.now().isoformat()
|
self.raw_data["_task_id"] = task_id
|
||||||
|
self.raw_data["_crawl_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""转换为字典(原始数据 + 元数据)"""
|
||||||
|
return self.raw_data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_raw(cls, raw: dict) -> "JobData":
|
def from_raw(cls, raw: dict, task_id: str = "") -> "JobData":
|
||||||
"""从原始API数据转换"""
|
"""从原始API数据创建"""
|
||||||
return cls(
|
return cls(raw.copy(), task_id)
|
||||||
job_category=raw.get("Std_class", ""),
|
|
||||||
job_title=raw.get("aca112", ""),
|
|
||||||
company=raw.get("AAB004", ""),
|
|
||||||
company_type=raw.get("AAB019", "").strip(),
|
|
||||||
salary=raw.get("acb241", ""),
|
|
||||||
location=raw.get("aab302", ""),
|
|
||||||
address=raw.get("AAE006", ""),
|
|
||||||
publish_date=raw.get("aae397", ""),
|
|
||||||
collect_time=raw.get("Collect_time", ""),
|
|
||||||
url=raw.get("ACE760", ""),
|
|
||||||
description=raw.get("acb22a", ""),
|
|
||||||
experience=raw.get("Experience", ""),
|
|
||||||
education=raw.get("aac011", ""),
|
|
||||||
headcount=raw.get("acb240", ""),
|
|
||||||
industry=raw.get("AAB022", ""),
|
|
||||||
company_size=raw.get("Num_employers", ""),
|
|
||||||
contact=raw.get("AAE004", ""),
|
|
||||||
company_intro=raw.get("AAB092", ""),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class CrawlStatus(BaseModel):
|
|||||||
total: int
|
total: int
|
||||||
last_start_offset: Optional[int] = None
|
last_start_offset: Optional[int] = None
|
||||||
progress: str
|
progress: str
|
||||||
kafka_lag: int = 0
|
queue_size: int = 0
|
||||||
status: str
|
status: str
|
||||||
last_update: str
|
last_update: str
|
||||||
filtered_count: int = 0
|
filtered_count: int = 0
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""服务模块"""
|
"""服务模块"""
|
||||||
from .api_client import api_client, BazhuayuClient
|
from .api_client import api_client, BazhuayuClient
|
||||||
from .kafka_service import kafka_service, KafkaService
|
from .rabbitmq_service import rabbitmq_service, RabbitMQService
|
||||||
from .progress_store import progress_store, ProgressStore
|
from .progress_store import progress_store, ProgressStore
|
||||||
from .crawler import crawler_manager, CrawlerManager, TaskCrawler
|
from .crawler import crawler_manager, CrawlerManager, TaskCrawler
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"api_client", "BazhuayuClient",
|
"api_client", "BazhuayuClient",
|
||||||
"kafka_service", "KafkaService",
|
"rabbitmq_service", "RabbitMQService",
|
||||||
"progress_store", "ProgressStore",
|
"progress_store", "ProgressStore",
|
||||||
"crawler_manager", "CrawlerManager", "TaskCrawler"
|
"crawler_manager", "CrawlerManager", "TaskCrawler"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import logging
|
|||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from app.services.api_client import api_client
|
from app.services.api_client import api_client
|
||||||
from app.services.kafka_service import kafka_service
|
from app.services.rabbitmq_service import rabbitmq_service
|
||||||
from app.services.progress_store import progress_store
|
from app.services.progress_store import progress_store
|
||||||
from app.utils import is_within_days
|
from app.utils import is_within_days
|
||||||
from app.models import JobData
|
from app.models import JobData
|
||||||
@@ -134,21 +134,20 @@ class TaskCrawler:
|
|||||||
aae397 = raw.get("aae397", "")
|
aae397 = raw.get("aae397", "")
|
||||||
collect_time = raw.get("Collect_time", "")
|
collect_time = raw.get("Collect_time", "")
|
||||||
if is_within_days(aae397, collect_time, self.filter_days):
|
if is_within_days(aae397, collect_time, self.filter_days):
|
||||||
job = JobData.from_raw(raw)
|
job = JobData.from_raw(raw, self.task_id)
|
||||||
job.task_id = self.task_id
|
|
||||||
filtered_jobs.append(job)
|
filtered_jobs.append(job)
|
||||||
|
|
||||||
valid_count = len(filtered_jobs)
|
valid_count = len(filtered_jobs)
|
||||||
expired_count = len(data_list) - valid_count
|
expired_count = len(data_list) - valid_count
|
||||||
self._total_filtered += valid_count
|
self._total_filtered += valid_count
|
||||||
|
|
||||||
# 立即发送到Kafka
|
# 立即发送到RabbitMQ
|
||||||
produced = 0
|
produced = 0
|
||||||
if filtered_jobs:
|
if filtered_jobs:
|
||||||
produced = kafka_service.produce_batch(filtered_jobs)
|
produced = rabbitmq_service.produce_batch(filtered_jobs)
|
||||||
self._total_produced += produced
|
self._total_produced += produced
|
||||||
|
|
||||||
logger.info(f"[{self.task_name}] offset={offset}, 获取={len(data_list)}, 有效={valid_count}, 过期={expired_count}, 发送Kafka={produced}")
|
logger.info(f"[{self.task_name}] offset={offset}, 获取={len(data_list)}, 有效={valid_count}, 过期={expired_count}, 发送MQ={produced}")
|
||||||
|
|
||||||
return valid_count
|
return valid_count
|
||||||
|
|
||||||
@@ -236,7 +235,7 @@ class CrawlerManager:
|
|||||||
return crawler.get_status() if crawler else {}
|
return crawler.get_status() if crawler else {}
|
||||||
return {
|
return {
|
||||||
"tasks": [c.get_status() for c in self._crawlers.values()],
|
"tasks": [c.get_status() for c in self._crawlers.values()],
|
||||||
"kafka_lag": kafka_service.get_lag(),
|
"queue_size": rabbitmq_service.get_queue_size(),
|
||||||
"running_count": sum(1 for c in self._crawlers.values() if c.is_running)
|
"running_count": sum(1 for c in self._crawlers.values() if c.is_running)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class KafkaService:
|
|||||||
def produce(self, job_data: JobData) -> bool:
|
def produce(self, job_data: JobData) -> bool:
|
||||||
"""发送消息到Kafka"""
|
"""发送消息到Kafka"""
|
||||||
try:
|
try:
|
||||||
future = self.producer.send(self.topic, key=job_data.id, value=job_data.model_dump())
|
data = job_data.to_dict()
|
||||||
|
future = self.producer.send(self.topic, key=data.get("_id"), value=data)
|
||||||
future.get(timeout=10)
|
future.get(timeout=10)
|
||||||
return True
|
return True
|
||||||
except KafkaError as e:
|
except KafkaError as e:
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ api:
|
|||||||
name: "任务3"
|
name: "任务3"
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Kafka配置
|
# RabbitMQ配置
|
||||||
kafka:
|
rabbitmq:
|
||||||
bootstrap_servers: kafka:29092
|
host: rabbitmq
|
||||||
topic: job_data
|
port: 5672
|
||||||
consumer_group: job_consumer_group
|
username: guest
|
||||||
|
password: guest
|
||||||
|
queue: job_data
|
||||||
|
message_ttl: 604800000 # 消息过期时间:7天(毫秒)
|
||||||
|
|
||||||
# 采集配置
|
# 采集配置
|
||||||
crawler:
|
crawler:
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ api:
|
|||||||
name: "任务2"
|
name: "任务2"
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Kafka配置(Docker内部网络)
|
# RabbitMQ配置
|
||||||
kafka:
|
rabbitmq:
|
||||||
bootstrap_servers: kafka:29092
|
host: rabbitmq
|
||||||
topic: job_data
|
port: 5672
|
||||||
consumer_group: job_consumer_group
|
username: guest
|
||||||
|
password: guest
|
||||||
|
queue: job_data
|
||||||
|
message_ttl: 604800000 # 消息过期时间:7天(毫秒)
|
||||||
|
|
||||||
# 采集配置
|
# 采集配置
|
||||||
crawler:
|
crawler:
|
||||||
|
|||||||
@@ -1,51 +1,23 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
zookeeper:
|
rabbitmq:
|
||||||
image: confluentinc/cp-zookeeper:7.5.0
|
image: rabbitmq:3.12-management
|
||||||
container_name: job-zookeeper
|
container_name: job-rabbitmq
|
||||||
ports:
|
ports:
|
||||||
- "2181:2181"
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
environment:
|
environment:
|
||||||
ZOOKEEPER_CLIENT_PORT: 2181
|
RABBITMQ_DEFAULT_USER: guest
|
||||||
ZOOKEEPER_TICK_TIME: 2000
|
RABBITMQ_DEFAULT_PASS: guest
|
||||||
volumes:
|
volumes:
|
||||||
- zookeeper_data:/var/lib/zookeeper/data
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "nc", "-z", "localhost", "2181"]
|
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- job-network
|
- job-network
|
||||||
|
|
||||||
kafka:
|
|
||||||
image: confluentinc/cp-kafka:7.5.0
|
|
||||||
container_name: job-kafka
|
|
||||||
ports:
|
|
||||||
- "9092:9092"
|
|
||||||
- "29092:29092"
|
|
||||||
environment:
|
|
||||||
KAFKA_BROKER_ID: 1
|
|
||||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
|
||||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
|
||||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
||||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
|
||||||
volumes:
|
|
||||||
- kafka_data:/var/lib/kafka/data
|
|
||||||
depends_on:
|
|
||||||
zookeeper:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- job-network
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: job-crawler:latest
|
image: job-crawler:latest
|
||||||
container_name: job-crawler
|
container_name: job-crawler
|
||||||
@@ -57,7 +29,7 @@ services:
|
|||||||
- ./config:/app/config:ro
|
- ./config:/app/config:ro
|
||||||
- app_data:/app/data
|
- app_data:/app/data
|
||||||
depends_on:
|
depends_on:
|
||||||
kafka:
|
rabbitmq:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
@@ -68,6 +40,5 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
zookeeper_data:
|
rabbitmq_data:
|
||||||
kafka_data:
|
|
||||||
app_data:
|
app_data:
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
fastapi==0.109.0
|
fastapi==0.109.0
|
||||||
uvicorn==0.27.0
|
uvicorn==0.27.0
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
kafka-python==2.0.2
|
pika==1.3.2
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
pydantic==2.5.3
|
pydantic==2.5.3
|
||||||
python-dotenv==1.0.0
|
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user