# jobs_robots 本项目用于采集 Telegram 招聘数据并进行结构化清洗,当前统一使用 MySQL。 ## 1. 当前流程 1. `main.py` - 从 `config.json` 读取数据源、时间窗口、限频、MySQL 配置。 - 爬取 Telegram 消息,写入 MySQL `messages`。 - 维护每个来源的增量游标到 `sync_state`。 2. `clean_to_structured.py` - 从 MySQL `messages` 增量读取新增消息(按 `messages.id` + `clean_state` 检查点)。 - 按来源规则清洗(`@DeJob_official` 有专用规则,其他走通用规则)。 - 仅保留招聘类数据,写入 MySQL `structured_jobs`。 3. `run_daily_incremental.sh` - 每日调度入口。 - 运行前自动更新 `config.json` 时间窗口(滚动窗口)。 - 依次执行 `main.py` 和 `clean_to_structured.py`。 ## 2. 数据库表 ### 2.1 原始层 - `messages` - 原始消息存储(`source + message_id` 唯一) - `sync_state` - Telegram 增量抓取游标 ### 2.2 清洗层 - `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) 关键字段: - `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` ## 4. 运行方式 ### 4.1 手动运行 ```bash uv run main.py uv run clean_to_structured.py ``` ### 4.2 每日定时(推荐) 脚本:`run_daily_incremental.sh` 示例 crontab(每天 01:10): ```cron 10 1 * * * /home/liam/code/python/jobs_robots/run_daily_incremental.sh ``` 日志文件: - `logs/app.log` - `logs/clean_to_structured.log` - `logs/daily_job.log` ## 5. 增量策略 ### Telegram 抓取增量 - 依据 `sync_state.last_message_id` - 每个来源独立增量 ### 清洗增量 - 依据 `clean_state.last_message_row_id` - 每次只处理 `messages.id > checkpoint` - 成功后更新 checkpoint ## 6. 字段约定(结构化就业类型) `structured_jobs` 使用拆分字段,不再依赖 `employment_type_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`: 原始“合作方式”行 ## 7. 常见问题 1. 为什么当天凌晨跑出来窗口看着不对? - 当前滚动窗口按 UTC 日期更新。如果需要按本地时区(如 Asia/Shanghai)可再改。 2. 为什么清洗没有新增? - 看 `clean_state` 检查点是否已经到最新。 - 看 `messages` 是否有新数据。 3. 为什么 MySQL 报字段超长/类型错误? - 优先看对应脚本日志,字段已做多数保护;若仍报错,保留错误堆栈并反馈。 ## 8. 协作建议 - 改规则时优先只改对应来源 parser,避免影响全局。 - 改字段前先确认 `structured_jobs` 兼容性与迁移策略。 - 所有定时行为以 `run_daily_incremental.sh` 为统一入口,避免多处调度冲突。