这个项目叫 get_jobs,定位是一个跑在自己电脑上的 AI 找工作助手:输入城市、岗位方向和技能清单,它会定时去抓取招聘平台上的公开岗位,清洗后交给大模型逐条分析,最后给你一份带匹配度分数的推荐清单。配置并搭建起来之后,每天只需花几分钟看结果,比手动刷十几个页面高效得多。这篇文章把 get_jobs 的环境配置、参数选择和完整搭建过程复盘一遍,找工作或者想跑一个真实可用的自动化 AI 项目的人,都能照着落地。
实际动手之前,我先花了半天理清楚这个项目到底需要做哪些事。下面这部分是整个项目的地基,理解了再配置,后面所有环节都会顺畅很多。
1. 项目定位与整体设计思路
1.1 求职场景里的真实痛点
先说需求来源。我发现大多数人求职时并不是没有岗位可看,而是被海量信息淹没。同一个岗位在好几个平台重复出现,JD 描述长短不一,技能要求藏在一大段话里,薪资范围忽高忽低。手动刷完一遍,眼睛累了,脑子里对“哪个岗位真正适合自己”依然没概念。现有的招聘站虽然有智能推荐,但推荐逻辑黑盒,个人可控程度低,数据又多集中在单一平台。
get_jobs 的逻辑是把“找岗位、筛 JD、判匹配、给建议”拆成一条自动流水线:采集端负责把多个源的公开岗位抓下来存进本地数据库,分析端用大模型逐条读 JD,再结合我的技能清单输出匹配度评分和理由。整个过程可配置、可复现,数据也完全属于自己。
1.2 技术选型与功能边界
选型上我没有上太重的东西。Python 3.10+ 提供生态支持,SQLite 存数据,脚本里用 requests 和 BeautifulSoup 处理采集,大模型部分走通用 HTTP 接口。为什么不直接用某些平台上现成的“AI 推荐”?因为这类现成分析往往受限于平台内部字段,我拿不到原始 JD 全文,也没法自定义自己的技能权重。自己搭的好处是每个环节都透明,JD 文本、评分规则、推荐理由都能看见,想调就能调。
功能边界也提前划清楚:get_jobs 不做简历自动投递,只做信息筛选和匹配建议。自动投递涉及账号操作和隐私风险,不适合脚本处理。项目产出是一份“值得你看的岗位列表”,而不是替你完成申请动作。
2. 环境配置:先把地基打牢
2.1 Python 版本与虚拟环境准备
我的建议是 Python 3.10 或更高版本,3.9 以下有些类型语法和库版本支持会麻烦。安装完成后,先给项目单独建虚拟环境,千万不要直接往系统 Python 里装依赖,不同项目之间很容易互相污染版本。
在项目根目录执行:
python3 -m venv venv source venv/bin/activateWindows 环境把最后一句换成venv\Scripts\activate。激活后命令行前缀会变成(venv),后续所有 pip 安装都装进这个独立环境,迁移和删除都干净。
这里有个小技巧:虚拟环境建好之后先升级 pip,再装依赖。有些依赖解析问题其实是 pip 版本太旧导致的:
pip install --upgrade pip2.2 核心依赖清单与安装
get_jobs 的依赖不多,requirements.txt 我按用途分成几类:
requests>=2.31.0 beautifulsoup4>=4.12.0 lxml>=4.9.3 PyYAML>=6.0 python-dotenv>=1.0.0 APScheduler>=3.10.4 loguru>=0.7.2 rich>=13.6.0requests 做采集请求,BeautifulSoup 配合 lxml 解析 HTML,PyYAML 读配置文件,python-dotenv 加载密钥,APScheduler 负责定时任务,loguru 打日志,rich 用来在终端里格式化输出结果。
为什么不把大模型 SDK 写进依赖?不同厂商 SDK 风格差异大,直接用 requests 对接通用接口反而更省心,换模型服务商时只需要改 base_url 和 API Key,不用重装依赖。安装时一条命令:
pip install -r requirements.txt装完可以用pip list检查关键版本,确认没有异常。国内网络环境下,某些依赖下载慢就把 pip 源切到常用镜像,这属于正常的工程配置手段。
2.3 项目目录与配置文件模板
我的目录结构是长这样的:
get_jobs/ ├── config.yaml ├── .env ├── requirements.txt ├── main.py ├── modules/ │ ├── __init__.py │ ├── collector.py │ ├── cleaner.py │ ├── scorer.py │ └── notifier.py └── data/ └── get_jobs.dbmain.py 是入口,modules 里每个文件各管一件事,data 目录放 SQLite 数据库文件。注意 .env 文件默认不会进 git,如果你用 git 管理代码,记得在 .gitignore 里加上.env和data/,避免密钥和本地数据被提交到仓库。
配置分成两个文件是刻意的:config.yaml 保存可分享的非敏感配置,比如检索关键词、技能清单、匹配分数阈值;.env 只放密钥类信息,比如 API Key、接口地址。这样即使把 config.yaml 发给别人看,也不会泄露凭据。
3. 核心配置项逐项拆解
3.1 检索条件配置的四个关键字段
打开 config.yaml,开头是检索条件,这部分直接决定采集回来的岗位是不是你想要的:
search: keywords: ["python 后端", "golang 后端"] locations: ["目标城市"] job_type: "全职" page_limit: 5keywords 支持多个词,采集时会拆成多个关键词分别检索,再统一去重。locations 按实际居住城市填,你不填真实城市名也可以,留成占位符是允许的,关键是理解数组结构可以配置多个地点。
page_limit 控制每个关键词抓取多少页。这个参数影响很大:数值太大容易带回来大量过期岗位,太小又可能漏掉当天新增。我默认给 5,实测覆盖了大多数平台的最近活跃岗位。抓回来的原始字段包含岗位标题、公司、薪资区间、工作地点、经验要求、全文 JD、发布时间和原链接。
3.2 API 密钥与模型接口接入方式
密钥统一放进 .env 文件:
LLM_API_KEY=你的大模型接口密钥 LLM_BASE_URL=你的大模型接口地址 LLM_MODEL=你的模型标识代码启动时用 python-dotenv 把这些读进环境变量,程序内部不硬编码任何密钥。这里有几个工程上要注意的点:API Key 不要写进 config.yaml,不要直接粘贴在命令行历史里,更不要提交到 git 仓库。如果怀疑密钥泄露,第一时间去控制台重置。
base_url 是接口的基础路径,后面拼接/chat/completions。为什么要单独配这个而不是写死?因为不同的模型服务商提供的地址格式不完全一致,抽成配置以后换服务只改这里,模块代码不用动。模型标识同理,不同时期想换更强或更便宜的模型,改一个值即可。
3.3 匹配参数、存储与调度配置
matching 部分控制 AI 评估的尺度:
matching: skills: ["Python", "FastAPI", "MySQL", "Redis", "Docker"] min_score: 60 top_n: 20skills 是自己的技能清单,这是匹配的灵魂。我建议按“精通、熟悉、了解”分档,但第一版先平铺就好,后面再细化。min_score 是推荐门槛,低于这个分的岗位不会进最终清单。top_n 限制每天最多保留多少条推荐结果。
大模型参数也在这里:
llm: temperature: 0.2 max_tokens: 1200temperature 我设得很低,0.2 左右。因为匹配评估是分析任务,不是创意写作,低温度能明显减少评分忽高忽低的情况。max_tokens 给到 1200,足够返回结构化 JSON 结果,也不会太费流量。
调度配置决定脚本多久跑一轮:
schedule: interval_hours: 6 storage: db_path: "data/get_jobs.db"我实测下来 6 小时一轮比较合理:既不会漏掉当天新岗位,也不会给平台造成压力。如果你只在早晚各看一次,也可以改成 12 小时。
4. 从零搭建:关键模块的实现细节
4.1 数据库表结构设计
数据库是本地 SQLite,不用额外安装服务,非常适合这种单人使用的工具。第一张表存岗位原始信息:
CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_name TEXT NOT NULL, job_title TEXT NOT NULL, company_name TEXT, salary_min INTEGER, salary_max INTEGER, location TEXT, experience_required TEXT, jd_text TEXT, url TEXT UNIQUE, published_at TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')) );url 字段加了 UNIQUE 约束,这是天然去重键。同一个岗位在不同渠道可能标题不同,但跳转链接通常不变,靠 url 排重简单有效。
第二张表存 AI 评分结果:
CREATE TABLE IF NOT EXISTS job_scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id INTEGER NOT NULL, match_score INTEGER NOT NULL, core_requirements TEXT, reasons TEXT, resume_suggestion TEXT, scored_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (job_id) REFERENCES jobs(id) );job_scores 和 jobs 是一对多关系,同一岗位如果技能清单调整后重新评分,会保留多条记录,方便对比不同版本的差异。
4.2 岗位采集与清洗模块
采集模块的核心循环不复杂:维护一个关键词队列,依次请求搜索页,解析列表页中的岗位卡片,拿到详情页链接后逐个抓详情。为了防止给目标站带来压力,每个请求之间需要加延迟,我会在 3 到 6 秒之间取随机值,顺便按请求头伪装成正常浏览器访问。
所有平台返回的 HTML 结构都不一样,我为不同数据源各写了一个解析函数,统一输出成相同结构:
def parse_job(html, source_name): # 不同数据源各自实现的解析逻辑,字段对齐到以下结构 return { "source_name": source_name, "job_title": title, "company_name": company, "salary_min": min_salary, "salary_max": max_salary, "location": city, "experience_required": experience, "jd_text": jd, "url": url, "published_at": publish_time, }清洗环节主要处理三类问题:薪资字段含“面议”时置空,JD 文本里多余空白和换行统一压缩,发布时间解析成标准格式。清洗后的数据先写入内存,确认没有明显异常再批量插入数据库,避免脏数据污染表。
4.3 AI 分析匹配模块的实现
这是 get_jobs 最核心的部分。采集到的新岗位会进入评分队列,调用大模型接口逐条评估。调用方式用通用 HTTP 接口,不绑定特定 SDK:
import requests import json def call_llm(system_prompt, user_content, config): headers = { "Authorization": f"Bearer {config['llm_api_key']}", "Content-Type": "application/json", } payload = { "model": config["llm_model"], "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ], "temperature": config["llm_temperature"], "max_tokens": config["llm_max_tokens"], "response_format": {"type": "json_object"}, } resp = requests.post( f"{config['llm_base_url']}/chat/completions", headers=headers, json=payload, timeout=60, ) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return json.loads(content)评分提示词我写了这样一段模板:
你是一名资深技术招聘顾问。候选人的核心技能是:{skills}。 请分析下面这个岗位是否适合该候选人。 岗位标题:{title} 岗位要求:{jd} 请以JSON格式返回: { "match_score": 0到100的整数, "core_requirements": "一句话概括岗位核心要求", "three_reasons": ["三个匹配理由"], "resume_suggestion": "两条针对该JD的简历修改建议" }这里特意要求返回 JSON 而不是普通文本,方便程序直接解析入库。用 response_format 声明 json_object,能减少格式漂移。评分完成后和 job id 一起写进 job_scores 表。
4.4 结果输出与定时调度
结果输出我用 rich 在终端渲染成表格:岗位标题、公司、匹配度、核心要求、薪资和链接一列排开。匹配度高的条目标成绿色,中等的黄色,偏低的灰色。这样每天打开终端扫一眼颜色就能定位重点。
最后是调度。我用了 APScheduler 而不是系统 crontab,因为我想让项目在任何机器上都能直接跑,不依赖外部服务。调度配置很简单:
from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() scheduler.add_job( run_pipeline, trigger="interval", hours=config["schedule"]["interval_hours"], misfire_grace_time=3600, ) scheduler.start()misfire_grace_time 这个参数很容易被忽略,但它很关键。它表示如果任务到点没跑成(比如电脑休眠),一小时内补跑都是可接受的;没有这个设置,错过的时间点会被直接丢弃。我一开始忘了加,结果有两天正值睡午觉,当轮抓取直接跳过一次,第二天数据明显少了一段。
5. 实战踩坑与问题排查实录
5.1 环境依赖类问题
我遇到过最典型的是 BeautifulSoup 解析速度慢,后来才发现是没装 lxml,库退回去用了纯 Python 解析器。装上 lxml 之后速度提升明显。这类问题在安装依赖后跑pip list检查一遍基本能发现。
另一个坑是 APScheduler 和系统 Python 版本不兼容。有的机器预装 Python 3.8,APScheduler 最新版在依赖解析时可能报错。解决办法是升级 Python 或锁定 APScheduler 版本,用虚拟环境就能把这类问题隔离掉。
5.2 数据采集阶段的问题
采集阶段最大问题来自平台页面结构调整。某个数据源改版后,我原来的 CSS 选择器全失效,列表页解析出来全是空值。排查方法很粗暴:把抓下来的 HTML 存成文件,对照新版页面慢慢调整解析逻辑。后来我养成了习惯,每个数据源单独写一个解析单元测试,页面结构一变,跑测试就能定位到哪个函数挂掉。
乱码问题也遇到过一次。部分页面用 gbk 编码,默认解析方式读出来是乱码。在 requests 响应后显式指定编码可解决:
response.encoding = "utf-8"如果你遇到乱码,先看返回头里的 charset 字段,再去用对应编码解码,盲目换编码只会更乱。
反爬限制容易在长时间运行时出现。我的做法是控制频率、随机延迟、轮换请求头,并且严格遵守 robots 语义,只采集公开数据。脚本如果看到返回异常状态码,应该主动中止该数据源的抓取,等下一轮再试,而不是无限重试加重压力。
5.3 大模型调用环节的问题
大模型接口返回超时是常见问题。有些 JD 特别长,加上提示词可能超出单次处理的输入长度。我在发送前会对 JD 做一个长度截断,保留前 2000 字符,核心要求基本都集中在前半部分。如果调用频率太高被限流,就降低调度频率,或者在 scorer 模块里加一个简单的失败重试,指数退避加随机抖动。
返回格式不稳定也踩过坑。即使我要求 JSON,偶尔还是有模型输出额外的解释文字。稳妥做法是解析前先做一次正则提取,把第一个{到最后一个}之间的内容截出来,再 json.loads。
费用控制方面,每轮评分会消耗一定 token。我实测一千条岗位全量评分很快就能吃掉不少额度,所以 config 里才加了 min_score 和 top_n 两道闸门:先用规则过滤明显不合适的岗位,再送进大模型,能省下好几倍的成本。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| pip 安装依赖失败 | 网络问题或版本冲突 | 用镜像源,或固定依赖版本 |
| 解析结果全为空 | 页面结构改版 | 抓 HTML 快照,更新解析函数 |
| 中文变成乱码 | 编码判断错误 | 检查响应头 charset,设置 response.encoding |
| 大模型返回超时 | JD 过长或接口限流 | 截断 JD,加重试和退避 |
| 模型返回不是 JSON | 输出格式漂移 | 正则提取 JSON 片段,或调低 temperature |
| 定时任务漏跑 | 错过调度时间 | 设置 misfire_grace_time |
| 数据库锁错误 | 多个进程同时写库 | SQLite 设置 WAL 模式,避免并发写 |
6. 实际使用体验与后续还能怎么扩展
6.1 跑起来之后,每天省下多少时间
我本地稳定跑了一个多月,每轮抓回三百到五百条岗位,经过规则预筛和大模型评分后,真正进到推荐列表的大约二十条,值得投的通常不到一半。原来我每天晚上要花两个小时刷招聘信息,现在改成早起喝热水的时候看一遍推荐表,匹配度靠前的岗位再点进原链接确认,整体时间压缩到十五分钟左右。
大模型对“硬技能匹配”的判断确实准。技能栈匹配度高且 JD 描述具体、技术要求明确的岗位,它给的分数和我的主观判断经常一致。但对“公司氛围”“晋升空间”这类软信息它几乎没判断力。所以我没有让它把这些维度也打进去,只让它做技能匹配和岗位核心要求提炼,其他判断保留给本人。
6.2 后续可以进行扩展的方向
这个项目继续往上加功能的空间很大。比如把采集到的薪资区间解析成结构化数字,按分位数统计目标城市、目标岗位的薪资区间分布;比如把数月积累的岗位数据做成趋势分析,看哪些技能出现的频率在上升;比如结合简历模板生成器,把 resume_suggestion 直接渲染成一页排版干净的文档;再比如接入 Webhook,把每日推荐推送到手机端。
我自己的下一步计划是把“沉默岗位”统计做出来:同一岗位连续刊登两周以上,说明长期招不到或 HR 响应慢,这类岗位在推荐表里自动降权。
最后分享一个使用习惯:不要让 get_jobs 完全替代你判断岗位好坏。它真正擅长的是把分散的信息压缩成情报,把你的技能栈和岗位要求做一次初筛。看到高匹配岗位后,还是要亲自打开 JD 仔细读一遍,再决定是否投递。AI 负责效率,人负责决策,这才是这类工具正确的用法。