完成所有来源数据清洗和表格导入
This commit is contained in:
407
README.md
407
README.md
@@ -1,241 +1,95 @@
|
||||
# jobs_robots
|
||||
|
||||
本项目用于采集 Telegram 招聘数据并进行结构化清洗,当前统一使用 MySQL。
|
||||
Telegram 招聘数据采集与清洗项目,当前主流程为:
|
||||
|
||||
## 1. 当前流程
|
||||
1. 抓取原始消息到本地 MySQL
|
||||
2. 清洗为结构化岗位数据
|
||||
3. 每日定时增量执行
|
||||
4. 同步本地 MySQL 到云端 MySQL
|
||||
|
||||
1. `main.py`
|
||||
- 从 `config.json` 读取数据源、时间窗口、限频、MySQL 配置。
|
||||
- 爬取 Telegram 消息,写入 MySQL `messages`。
|
||||
- 维护每个来源的增量游标到 `sync_state`。
|
||||
## 1. 项目结构
|
||||
|
||||
2. `clean_to_structured.py`
|
||||
- 从 MySQL `messages` 增量读取新增消息(按 `messages.id` + `clean_state` 检查点)。
|
||||
- 按来源规则清洗(`@DeJob_official` 有专用规则,其他走通用规则)。
|
||||
- 仅保留招聘类数据,写入 MySQL `structured_jobs`。
|
||||
- `main.py`: Telegram 增量爬取,写入 `messages`,维护 `sync_state`
|
||||
- `clean_to_structured.py`: 按来源规则清洗,写入 `structured_jobs`,维护 `clean_state`
|
||||
- `import_excel_jobs.py`: 读取 `sheets/` Excel,导入结构化数据,实习数据落 `internship_jobs_raw`
|
||||
- `sync_to_cloud_mysql.py`: 本地 MySQL -> 云端 MySQL 增量同步
|
||||
- `run_daily_incremental.sh`: 每日调度入口(滚动窗口、抓取、清洗、云同步)
|
||||
- `config.json`: 运行配置(本地使用)
|
||||
- `config.example.json`: 配置模板
|
||||
|
||||
3. `run_daily_incremental.sh`
|
||||
- 每日调度入口。
|
||||
- 运行前自动更新 `config.json` 时间窗口(滚动窗口)。
|
||||
- 依次执行 `main.py` 和 `clean_to_structured.py`。
|
||||
## 2. 环境要求
|
||||
|
||||
## 2. 数据库表
|
||||
- Python `>=3.13`
|
||||
- MySQL 8.x(本地)
|
||||
- MySQL 8.x(云端,可选)
|
||||
- 已完成 Telethon 登录(项目目录下会生成 `scraper.session`)
|
||||
|
||||
### 2.1 原始层
|
||||
依赖安装:
|
||||
|
||||
- `messages`
|
||||
- 原始消息存储(`source + message_id` 唯一)
|
||||
- `sync_state`
|
||||
- Telegram 增量抓取游标
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 2.2 清洗层
|
||||
## 3. 配置说明
|
||||
|
||||
- `structured_jobs`
|
||||
- 结构化岗位数据(`source + message_id` 唯一)
|
||||
- `clean_state`
|
||||
- 清洗增量检查点(`pipeline_name -> last_message_row_id`)
|
||||
先复制模板并修改:
|
||||
|
||||
## 2.3 字段级数据字典(详细)
|
||||
|
||||
### messages(Telegram 原始消息)
|
||||
|
||||
- `id`(BIGINT, PK, 自增)
|
||||
含义:MySQL 行主键,清洗增量检查点使用这个字段。
|
||||
示例:`530812`
|
||||
|
||||
- `source`(VARCHAR)
|
||||
含义:消息来源标识(频道/群组),通常是 `@xxx`。
|
||||
示例:`@DeJob_official`
|
||||
|
||||
- `chat_id`(BIGINT, 可空)
|
||||
含义:Telegram 实体 ID。
|
||||
示例:`-1001234567890`
|
||||
|
||||
- `message_id`(BIGINT)
|
||||
含义:该 source 内部的消息 ID。
|
||||
约束:与 `source` 组成唯一键。
|
||||
|
||||
- `content`(LONGTEXT, 可空)
|
||||
含义:抓取到的消息正文(含非文本补充段,如 `MEDIA_JSON`)。
|
||||
示例:招聘 markdown 文本 + `[MEDIA_TYPE] ...`
|
||||
|
||||
- `date`(DATETIME)
|
||||
含义:消息时间(UTC)。
|
||||
示例:`2026-02-26 09:31:10`
|
||||
|
||||
- `created_at`(DATETIME)
|
||||
含义:该条记录写入数据库时间。
|
||||
|
||||
### sync_state(抓取增量状态)
|
||||
|
||||
- `source`(VARCHAR, PK)
|
||||
含义:来源标识,与 `messages.source` 对应。
|
||||
|
||||
- `last_message_id`(BIGINT)
|
||||
含义:该来源已抓取到的最大 message_id。
|
||||
用途:下次抓取时只拉 `message_id > last_message_id`。
|
||||
|
||||
- `updated_at`(DATETIME)
|
||||
含义:该来源游标最近更新时间。
|
||||
|
||||
### structured_jobs(清洗后结构化岗位)
|
||||
|
||||
- `id`(BIGINT, PK, 自增)
|
||||
含义:结构化表主键。
|
||||
|
||||
- `source`(VARCHAR)
|
||||
含义:来源标识。
|
||||
示例:`@DeJob_official`
|
||||
|
||||
- `source_channel`(VARCHAR, 可空)
|
||||
含义:来源品牌/渠道归类。
|
||||
示例:`DeJob`
|
||||
|
||||
- `parser_name`(VARCHAR)
|
||||
含义:使用的解析器名称。
|
||||
示例:`dejob_official` / `generic`
|
||||
|
||||
- `parser_version`(VARCHAR)
|
||||
含义:解析器版本号,用于规则演进追踪。
|
||||
示例:`v1`
|
||||
|
||||
- `chat_id`(BIGINT, 可空)
|
||||
含义:原始 Telegram chat_id。
|
||||
|
||||
- `message_id`(BIGINT)
|
||||
含义:原始消息 ID(source 内)。
|
||||
约束:与 `source` 组成唯一键。
|
||||
|
||||
- `message_date`(DATETIME)
|
||||
含义:原始消息时间(UTC)。
|
||||
|
||||
- `job_type`(VARCHAR, 可空)
|
||||
含义:岗位类型标记。当前仅保留 `招聘`。
|
||||
示例:`招聘`
|
||||
|
||||
- `company_name`(VARCHAR, 可空)
|
||||
含义:公司/项目方名称。
|
||||
示例:`88EX`
|
||||
|
||||
- `industry_tags_json`(JSON)
|
||||
含义:行业/赛道标签数组。
|
||||
示例:`[\"CEX\",\"Infra\"]`
|
||||
|
||||
- `company_intro`(LONGTEXT, 可空)
|
||||
含义:公司简介文本。
|
||||
|
||||
- `company_url`(TEXT, 可空)
|
||||
含义:公司官网/介绍页链接。
|
||||
|
||||
- `work_mode`(VARCHAR)
|
||||
含义:办公模式。
|
||||
枚举:`remote | onsite | hybrid | unknown`
|
||||
|
||||
- `job_nature`(VARCHAR)
|
||||
含义:用工性质。
|
||||
枚举:`full_time | part_time | contract | intern | freelance | unknown`
|
||||
|
||||
- `job_location_text`(VARCHAR, 可空)
|
||||
含义:主地点文本(首个地点)。
|
||||
|
||||
- `job_location_tags_json`(JSON, 可空)
|
||||
含义:地点标签数组。无地点时为 `NULL`(不是空数组)。
|
||||
|
||||
- `employment_type_raw`(TEXT, 可空)
|
||||
含义:原始“合作方式”文本,便于回溯规则。
|
||||
示例:`🛵 合作方式:#全职 #远程 #吉隆坡`
|
||||
|
||||
- `position_name`(VARCHAR, 可空)
|
||||
含义:岗位主名称。
|
||||
示例:`社区运营`
|
||||
|
||||
- `position_tags_json`(JSON)
|
||||
含义:岗位标签数组。
|
||||
示例:`[\"社区运营\",\"运营\"]`
|
||||
|
||||
- `salary_raw`(TEXT, 可空)
|
||||
含义:薪资原始字符串。
|
||||
示例:`$1000 - $3000 / month`
|
||||
|
||||
- `salary_currency`(VARCHAR, 可空)
|
||||
含义:薪资币种(已识别)。
|
||||
示例:`USD`
|
||||
|
||||
- `salary_min`(BIGINT, 可空)
|
||||
含义:薪资下限数值。
|
||||
|
||||
- `salary_max`(BIGINT, 可空)
|
||||
含义:薪资上限数值。
|
||||
|
||||
- `salary_period`(VARCHAR, 可空)
|
||||
含义:薪资周期。
|
||||
枚举:`month | year | day | NULL`
|
||||
|
||||
- `responsibilities_json`(JSON)
|
||||
含义:岗位职责数组(按条目拆分)。
|
||||
|
||||
- `requirements_json`(JSON)
|
||||
含义:岗位要求数组(按条目拆分)。
|
||||
|
||||
- `apply_email`(VARCHAR, 可空)
|
||||
含义:投递邮箱。
|
||||
|
||||
- `apply_telegram`(VARCHAR, 可空)
|
||||
含义:投递 Telegram 用户名。
|
||||
示例:`@lulu_lucky1`
|
||||
|
||||
- `job_source_url`(TEXT, 可空)
|
||||
含义:岗位来源原文链接(如 DeJob 详情页)。
|
||||
|
||||
- `body_text`(LONGTEXT)
|
||||
含义:清洗后的主体文本(去除部分技术元段)。
|
||||
|
||||
- `raw_content`(LONGTEXT)
|
||||
含义:原始消息内容快照(用于审计/回刷)。
|
||||
|
||||
- `cleaned_at`(DATETIME)
|
||||
含义:最近清洗/更新该条结构化记录的时间。
|
||||
|
||||
### clean_state(清洗增量状态)
|
||||
|
||||
- `pipeline_name`(VARCHAR, PK)
|
||||
含义:清洗流程标识。
|
||||
示例:`structured_cleaner_v1`
|
||||
|
||||
- `last_message_row_id`(BIGINT)
|
||||
含义:已处理到的 `messages.id` 最大值。
|
||||
用途:下次清洗只处理更大的 `messages.id`。
|
||||
|
||||
- `updated_at`(DATETIME)
|
||||
含义:检查点更新时间。
|
||||
|
||||
## 3. 配置文件说明(config.json)
|
||||
```bash
|
||||
cp config.example.json config.json
|
||||
```
|
||||
|
||||
关键字段:
|
||||
|
||||
- `sources`: Telegram 来源列表
|
||||
- `time_window.enabled`: 是否启用时间窗口
|
||||
- `time_window.start` / `time_window.end`: 抓取窗口(脚本会每日自动刷新)
|
||||
- `daily_window_days`: 滚动窗口天数(当前默认 `2`)
|
||||
- `throttle`: 限频配置
|
||||
- `enabled`
|
||||
- `per_message_delay_sec`
|
||||
- `between_sources_delay_sec`
|
||||
- `mysql`: MySQL 连接配置
|
||||
- `host`, `port`, `user`, `password`, `database`, `charset`
|
||||
- `sources`: 要抓取的 Telegram 来源列表
|
||||
- `time_window`: 抓取时间窗口
|
||||
- `daily_window_days`: 每日滚动窗口天数(默认 `2`)
|
||||
- `backfill`: 回补配置
|
||||
- `throttle`: 限频配置,降低封号风险
|
||||
- `mysql`: 本地 MySQL 连接
|
||||
- `mysql_cloud`: 云端 MySQL 连接(用于同步)
|
||||
|
||||
## 4. 运行方式
|
||||
|
||||
### 4.1 手动运行
|
||||
### 4.1 手动执行
|
||||
|
||||
```bash
|
||||
uv run main.py
|
||||
uv run clean_to_structured.py
|
||||
uv run sync_to_cloud_mysql.py
|
||||
```
|
||||
|
||||
### 4.2 每日定时(推荐)
|
||||
如果在 cron/非交互环境,建议用 venv Python:
|
||||
|
||||
脚本:`run_daily_incremental.sh`
|
||||
```bash
|
||||
.venv/bin/python main.py
|
||||
.venv/bin/python clean_to_structured.py
|
||||
.venv/bin/python sync_to_cloud_mysql.py
|
||||
```
|
||||
|
||||
### 4.2 Excel 导入
|
||||
|
||||
默认读取 `sheets/` 下文件:
|
||||
|
||||
```bash
|
||||
uv run import_excel_jobs.py
|
||||
```
|
||||
|
||||
指定文件/工作表:
|
||||
|
||||
```bash
|
||||
uv run import_excel_jobs.py --file /path/to/jobs.xlsx --sheet Sheet1 --source @excel_import
|
||||
```
|
||||
|
||||
导入规则:
|
||||
|
||||
- 普通岗位:清洗后写入 `structured_jobs`
|
||||
- 实习岗位:写入 `internship_jobs_raw`,不进入结构化主表
|
||||
|
||||
### 4.3 每日定时(推荐)
|
||||
|
||||
调度脚本:
|
||||
|
||||
- `/home/liam/code/python/jobs_robots/run_daily_incremental.sh`
|
||||
|
||||
示例 crontab(每天 01:10):
|
||||
|
||||
@@ -243,49 +97,114 @@ uv run clean_to_structured.py
|
||||
10 1 * * * /home/liam/code/python/jobs_robots/run_daily_incremental.sh
|
||||
```
|
||||
|
||||
日志文件:
|
||||
脚本执行顺序:
|
||||
|
||||
- `logs/app.log`
|
||||
- `logs/clean_to_structured.log`
|
||||
- `logs/daily_job.log`
|
||||
1. 自动更新 `config.json` 的 `time_window.start/end`(按 `daily_window_days`)
|
||||
2. 运行 `main.py` 增量抓取
|
||||
3. 运行 `clean_to_structured.py` 增量清洗
|
||||
4. 若 `mysql_cloud` 已配置,运行 `sync_to_cloud_mysql.py` 同步云端
|
||||
|
||||
## 5. 增量策略
|
||||
## 5. 增量与回补策略
|
||||
|
||||
### Telegram 抓取增量
|
||||
### 5.1 抓取增量
|
||||
|
||||
- 依据 `sync_state.last_message_id`
|
||||
- 每个来源独立增量
|
||||
- 状态表:`sync_state`
|
||||
- 游标字段:`last_message_id`
|
||||
- 粒度:每个 source 独立
|
||||
|
||||
### 清洗增量
|
||||
### 5.2 清洗增量
|
||||
|
||||
- 依据 `clean_state.last_message_row_id`
|
||||
- 每次只处理 `messages.id > checkpoint`
|
||||
- 成功后更新 checkpoint
|
||||
- 状态表:`clean_state`
|
||||
- 游标字段:`last_message_row_id`(对应 `messages.id`)
|
||||
- 规则:仅处理 `messages.id > checkpoint`
|
||||
|
||||
## 6. 字段约定(结构化就业类型)
|
||||
### 5.3 回补(Backfill)
|
||||
|
||||
`structured_jobs` 使用拆分字段,不再依赖 `employment_type_json`:
|
||||
在 `config.json` 设置:
|
||||
|
||||
- `work_mode`: `remote | onsite | hybrid | unknown`
|
||||
- `job_nature`: `full_time | part_time | contract | intern | freelance | unknown`
|
||||
- `job_location_text`: 主地点文本
|
||||
- `job_location_tags_json`: 地点数组(无地点为 `NULL`)
|
||||
- `employment_type_raw`: 原始“合作方式”行
|
||||
- `backfill.enabled = true`
|
||||
- `backfill.start / backfill.end`
|
||||
- `backfill.sources`
|
||||
- `backfill.ignore_sync_state`(回补时是否忽略抓取游标)
|
||||
|
||||
## 7. 常见问题
|
||||
回补结束后建议关闭 `backfill.enabled`,恢复日常增量。
|
||||
|
||||
1. 为什么当天凌晨跑出来窗口看着不对?
|
||||
- 当前滚动窗口按 UTC 日期更新。如果需要按本地时区(如 Asia/Shanghai)可再改。
|
||||
## 6. 本地到云端同步
|
||||
|
||||
2. 为什么清洗没有新增?
|
||||
- 看 `clean_state` 检查点是否已经到最新。
|
||||
- 看 `messages` 是否有新数据。
|
||||
脚本:`sync_to_cloud_mysql.py`
|
||||
|
||||
3. 为什么 MySQL 报字段超长/类型错误?
|
||||
- 优先看对应脚本日志,字段已做多数保护;若仍报错,保留错误堆栈并反馈。
|
||||
同步规则:
|
||||
|
||||
## 8. 协作建议
|
||||
- `messages`: 按本地 `id` 增量,云端按 `(source, message_id)` upsert
|
||||
- `structured_jobs`: 按本地 `id` 增量 + `cleaned_at` 补偿更新
|
||||
- `sync_state` / `clean_state`: 小表全量 upsert
|
||||
- `internship_jobs_raw`: 存在则按 `id` 增量 upsert
|
||||
|
||||
- 改规则时优先只改对应来源 parser,避免影响全局。
|
||||
- 改字段前先确认 `structured_jobs` 兼容性与迁移策略。
|
||||
- 所有定时行为以 `run_daily_incremental.sh` 为统一入口,避免多处调度冲突。
|
||||
状态表(云端):
|
||||
|
||||
- `cloud_sync_state`
|
||||
|
||||
注意:
|
||||
|
||||
- 同步脚本会自动在云端补齐缺失目标表(从本地表结构复制 DDL)
|
||||
- `mysql_cloud` 未配置时,日常脚本会跳过云同步
|
||||
|
||||
## 7. 数据库表与字段含义
|
||||
|
||||
### 7.1 原始层
|
||||
|
||||
- `messages`
|
||||
- 原始消息正文、媒体补充文本、来源、消息时间
|
||||
- 唯一键:`(source, message_id)`
|
||||
- `sync_state`
|
||||
- 每个 source 的抓取游标
|
||||
|
||||
### 7.2 清洗层
|
||||
|
||||
- `structured_jobs`
|
||||
- 清洗后结构化岗位数据
|
||||
- 唯一键:`(source, message_id)`
|
||||
- 关键字段:
|
||||
- `source`, `source_channel`
|
||||
- `company_name`, `position_name`
|
||||
- `work_mode`(`remote|onsite|hybrid|unknown`)
|
||||
- `job_nature`(`full_time|part_time|contract|intern|freelance|unknown`)
|
||||
- `job_location_text`, `job_location_tags_json`(无地点为 `NULL`)
|
||||
- `apply_email`, `apply_telegram`, `job_source_url`
|
||||
- `salary_raw`, `salary_currency`, `salary_min`, `salary_max`, `salary_period`
|
||||
- `body_text`, `raw_content`, `cleaned_at`
|
||||
- `clean_state`
|
||||
- 清洗检查点
|
||||
- `internship_jobs_raw`
|
||||
- Excel 导入时保留的实习原始数据
|
||||
|
||||
## 8. 日志
|
||||
|
||||
- `logs/app.log`: 抓取日志
|
||||
- `logs/clean_to_structured.log`: 清洗日志
|
||||
- `logs/sync_to_cloud_mysql.log`: 云同步日志
|
||||
- `logs/daily_job.log`: 每日调度总日志
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
1. `uv: command not found`(cron)
|
||||
- 使用 `.venv/bin/python` 运行,已在 `run_daily_incremental.sh` 中处理。
|
||||
|
||||
2. `Table 'jobs.messages' doesn't exist`(云同步)
|
||||
- 云端目标库为空。新版同步脚本会自动建表后再同步。
|
||||
|
||||
3. `Public Key Retrieval is not allowed`(DBeaver 连 MySQL)
|
||||
- 连接参数添加 `allowPublicKeyRetrieval=true&useSSL=false`(排障用)。
|
||||
|
||||
4. `ERROR 1410 You are not allowed to create a user with GRANT`
|
||||
- 先 `CREATE USER`,再 `GRANT`,不要用旧式 `GRANT ... IDENTIFIED BY ...`。
|
||||
|
||||
5. 清洗无新增
|
||||
- 检查 `messages` 是否有新数据。
|
||||
- 检查 `clean_state.last_message_row_id` 是否已到最新。
|
||||
|
||||
## 10. 协作规范建议
|
||||
|
||||
- 新增来源规则时,优先增加 source 专用 parser,避免影响已有来源。
|
||||
- 结构字段变更前,先确认 `structured_jobs` 迁移策略和历史兼容。
|
||||
- 定时任务统一走 `run_daily_incremental.sh`,避免多个入口重复执行。
|
||||
|
||||
Reference in New Issue
Block a user