1. 从零搭建一个"今日招聘"信息聚合工具:我的完整实战复盘
每天早上八点半,我端着咖啡坐到工位上,第一件事不是看邮件,而是打开七八个招聘渠道来回刷。这个习惯持续了大概半年,直到有一天我实在受不了了——同样的岗位在三个平台重复出现,有的已经截止了还挂在首页,有的薪资范围写得含糊其辞,真正合适的机会反而被淹没在信息流里。于是我花了两个周末,做了一个叫"今日招聘"的小工具,把分散在各处的招聘信息按我的需求聚合、去重、筛选,每天早上自动推送到我的笔记软件里。这篇文章就是整个项目的完整复盘,从需求拆解到技术选型,从代码实现到踩坑记录,全部摊开来讲。如果你也在找工作,或者你所在团队需要做类似的信息聚合工具,这篇内容应该能帮你省下不少试错时间。
1.1 这个工具到底解决什么问题
先说清楚"今日招聘"的定位。它不是招聘平台,不生产任何岗位信息,它做的事情只有一件:把公开渠道上分散的招聘信息,按照我设定的规则抓取回来,做结构化处理,然后以统一的格式呈现。核心解决三个痛点:信息分散导致的重复浏览、格式不统一导致的对比困难、时效性差导致的无效投递。
我试过市面上一些聚合类工具,要么需要付费,要么数据源有限,要么筛选维度太粗糙。自己动手的好处是,规则完全自定义,想加什么维度就加什么维度,想怎么排序就怎么排序。这个项目适合有一定编程基础的人参考,如果你完全不懂代码,也可以看看思路部分,理解信息聚合的基本逻辑,用现成的表格工具手动实现简化版。
1.2 整体架构与技术选型
整个工具分为四个模块:数据采集层、数据清洗层、数据存储层、展示推送层。数据采集用 Python 的 requests 和 BeautifulSoup 组合,没有上 Scrapy 框架,原因很简单——我的目标站点数量不多,结构也不复杂,上框架反而增加维护成本。数据清洗用 pandas 做去重和字段标准化,存储用 SQLite,轻量够用,不需要额外部署数据库服务。展示层最初想用 Web 页面,后来发现每天看邮件或笔记就够了,于是改成生成 Markdown 文件,通过本地脚本推送到我的笔记软件。
这个架构的核心考量是"够用就好"。很多人在做类似工具时容易过度设计,上来就搞分布式爬虫、消息队列、微服务,结果维护成本远超收益。我的建议是,先跑通最小闭环,再根据实际需求逐步迭代。
2. 数据采集:从目标站点到结构化数据
2.1 目标站点的选择与分析
我最终确定了五个数据源,选择标准有三个:信息更新频率高、岗位质量相对可靠、页面结构相对稳定。这里不具体点名是哪些网站,因为不同行业、不同地区的适用站点差异很大,你可以根据自己的求职方向去筛选。重点讲分析方法。
打开目标站点的搜索结果页,按 F12 打开开发者工具,切换到 Network 面板,刷新页面,找到返回岗位列表的请求。通常有两种情况:一种是服务端渲染,HTML 里直接包含岗位信息;另一种是客户端渲染,数据通过接口返回 JSON。前者用 BeautifulSoup 解析 HTML 即可,后者直接请求接口拿 JSON 更省事。
我遇到的一个坑是,有的站点虽然页面看起来是列表,但实际数据是通过多次异步请求拼出来的。这种情况下,要么找到真正的数据接口,要么用 Selenium 模拟浏览器行为。我优先找接口,因为接口返回的数据结构更干净,解析成本低。
2.2 请求头的伪装与频率控制
写爬虫绕不开请求头的问题。最基本的 User-Agent 必须设置,否则很多站点直接返回 403。我一般会准备三到五个不同浏览器的 User-Agent,随机轮换。除了 User-Agent,Referer 有时也需要设置,特别是当目标页面是从某个搜索页跳转过来的时候。
频率控制是另一个关键点。我的策略是每个请求之间随机间隔 2 到 5 秒,单站点每分钟请求不超过 10 次。这个频率下,抓取 100 个岗位大约需要 5 到 8 分钟,完全可以接受。不要贪快,一旦触发反爬机制,轻则封 IP 一段时间,重则账号被标记,得不偿失。
import requests import time import random HEADERS_LIST = [ { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", }, { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", "Accept-Language": "zh-CN,zh;q=0.9", }, ] def fetch_page(url, retry=3): for i in range(retry): try: headers = random.choice(HEADERS_LIST) resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code == 403: time.sleep(random.uniform(10, 20)) except requests.RequestException: time.sleep(random.uniform(5, 10)) return None上面这段代码是我实际在用的请求封装,核心逻辑是:随机选请求头、超时重试、遇到 403 时延长等待时间。实测下来,这套策略在大多数站点上都能稳定运行。
2.3 页面解析与字段提取
拿到 HTML 之后,用 BeautifulSoup 定位岗位列表。每个岗位通常包含以下字段:岗位名称、公司名称、工作地点、薪资范围、学历要求、经验要求、发布时间、岗位详情链接。不同站点的字段名称和位置不同,需要针对每个站点写单独的解析函数。
我的做法是定义一个统一的岗位数据结构,每个站点的解析函数负责把原始数据映射到这个结构上。这样后续的去重、筛选、展示逻辑只需要处理统一结构,不用关心数据来源。
from dataclasses import dataclass, field from typing import Optional @dataclass class JobPosting: title: str company: str location: str salary: Optional[str] = None education: Optional[str] = None experience: Optional[str] = None publish_date: Optional[str] = None source: str = "" url: str = "" job_id: str = ""字段提取时最容易出问题的是薪资范围。有的写成"15k-25k",有的写成"15-25K·13薪",有的写成"面议"。我写了一个正则表达式来统一提取数字部分,对于"面议"这类无法量化的,单独标记为"待确认"。学历和经验要求也类似处理,统一成有限的几个枚举值,方便后续筛选。
3. 数据清洗:去重、标准化与质量把控
3.1 去重策略的设计
去重是这个工具的核心价值之一。同一个岗位可能在多个平台发布,甚至同一平台的不同搜索条件下也会重复出现。我的去重策略分两层:第一层是精确去重,基于岗位链接或岗位 ID,完全相同则直接丢弃;第二层是模糊去重,基于公司名称加岗位名称的组合,如果两个岗位的公司和标题高度相似,且工作地点一致,则判定为重复。
模糊去重需要计算字符串相似度。我用的是 Python 的 difflib 库,计算两个字符串的相似比,阈值设为 0.85。这个阈值是试出来的,太低会误删不同岗位,太高会漏掉重复岗位。实际使用中,0.85 在我的数据集上表现比较均衡。
from difflib import SequenceMatcher def is_similar(a, b, threshold=0.85): return SequenceMatcher(None, a, b).ratio() >= threshold def deduplicate(jobs): unique = [] for job in jobs: key = f"{job.company}|{job.title}|{job.location}" duplicate = False for existing in unique: existing_key = f"{existing.company}|{existing.title}|{existing.location}" if is_similar(key, existing_key): duplicate = True break if not duplicate: unique.append(job) return unique注意:模糊去重的计算复杂度是 O(n²),当岗位数量超过 500 条时,速度会明显变慢。我的优化方案是先按公司名称分组,只在同一公司内部做模糊匹配,这样计算量大幅下降。
3.2 字段标准化与异常值处理
原始数据里有很多不规范的地方。比如工作地点,有的写"北京·朝阳区",有的写"北京市朝阳区",有的只写"北京"。我统一提取城市级别,区级信息单独存一个字段。薪资范围也是,统一转换成"最低-最高"的数字格式,单位统一为千元每月。
异常值处理主要针对以下几种情况:薪资范围上下限颠倒、发布时间格式不一致、公司名称为空。对于薪资颠倒的,直接交换上下限;对于时间格式,统一解析成标准日期格式;对于公司名称为空的,标记为"未知公司"并保留,因为有时候岗位信息本身就不完整,直接丢弃可能错过机会。
3.3 数据质量监控
我加了一个简单的质量监控逻辑:每次抓取完成后,统计各站点的抓取数量、去重后数量、字段完整率。如果某个站点的抓取数量突然下降超过 50%,或者字段完整率低于 80%,就发提醒给我。这个机制帮我发现过好几次目标站点改版的问题,及时调整了解析逻辑。
4. 筛选、排序与推送:让信息真正可用
4.1 筛选规则的配置化
筛选规则我设计成配置文件的形式,用 YAML 管理。这样调整规则不需要改代码,改完配置文件重新运行即可。规则包括:关键词包含与排除、薪资范围、工作地点、学历要求、经验要求、公司规模等。
filters: keywords_include: - "Python" - "后端" - "数据" keywords_exclude: - "外包" - "实习" salary_min: 15 salary_max: 40 locations: - "北京" - "上海" - "远程" education: - "本科" - "硕士" experience_max: 5关键词匹配我做了两级:一级是标题匹配,二级是描述匹配。标题匹配的权重更高,因为标题通常最能反映岗位核心内容。排除关键词主要用来过滤掉明显不合适的岗位,比如"外包"、"实习"这类。
4.2 排序算法的设计
排序直接影响你每天先看到哪些岗位。我的排序算法综合考虑四个因素:发布时间新鲜度、薪资匹配度、公司匹配度、关键词匹配度。每个因素赋予不同权重,最终算出一个综合得分。
发布时间新鲜度:今天发布的得 100 分,每往前一天减 10 分,最低 0 分。薪资匹配度:越接近我设定的理想薪资区间中值,得分越高。公司匹配度:我维护了一个目标公司列表,列表内的公司额外加 20 分。关键词匹配度:标题中包含关键词的加 15 分,描述中包含的加 5 分。
这个排序算法不是一成不变的,我会根据实际投递反馈调整权重。比如有一段时间发现薪资权重太高,导致一些薪资高但岗位内容不匹配的岗位排在前面,于是我把关键词匹配度的权重调高了。
4.3 推送格式与渠道
推送格式我最终选择了 Markdown,因为结构清晰,在笔记软件里渲染效果好。每条岗位信息包含:岗位名称、公司、地点、薪资、关键要求、发布时间、原文链接。按综合得分从高到低排列,前 20 条加粗显示。
推送渠道我用的是本地脚本加笔记软件的 API。每天定时运行,生成 Markdown 文件后自动同步到笔记里。这样我在手机、电脑上都能随时查看,不需要额外打开网页。
def generate_markdown(jobs, top_n=20): lines = ["# 今日招聘汇总", f"共 {len(jobs)} 条岗位", ""] for i, job in enumerate(jobs): prefix = "**" if i < top_n else "" suffix = "**" if i < top_n else "" lines.append(f"{prefix}{i+1}. {job.title} - {job.company}{suffix}") lines.append(f" - 地点:{job.location}") lines.append(f" - 薪资:{job.salary or '待确认'}") lines.append(f" - 要求:{job.education or '不限'} / {job.experience or '不限'}") lines.append(f" - 发布:{job.publish_date or '未知'}") lines.append(f" - [查看原文]({job.url})") lines.append("") return "\n".join(lines)5. 实操中踩过的坑与排查技巧
5.1 反爬机制与应对
最常见的反爬是 IP 限制。我的应对策略是控制频率加随机间隔,如果还是被封,就切换到备用网络环境。另一个常见的是验证码,遇到验证码说明频率已经偏高了,立即停止抓取,等几个小时再试。还有一种比较隐蔽的反爬是返回假数据,页面看起来正常,但岗位信息是空的或者乱码。这种情况需要加数据校验逻辑,发现异常数据比例过高时自动告警。
5.2 页面改版导致解析失败
目标站点改版是家常便饭。我的经验是,解析函数尽量用相对稳定的选择器,比如基于 data 属性或者特定的 class 命名规律,避免用绝对路径。同时加一个解析结果校验,如果某个站点连续三次抓取到的岗位数量为 0,就发提醒,大概率是改版了。
5.3 数据存储与备份
SQLite 虽然轻量,但也要注意备份。我的做法是每天抓取完成后,把数据库文件复制一份到备份目录,保留最近 30 天的备份。这样即使数据库损坏,也能快速恢复。另外,原始 HTML 我也会保留最近 7 天的,方便排查解析问题。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 请求返回 403 | 请求头被识别 | 检查 User-Agent 和 Referer | 更换请求头,降低频率 |
| 解析结果为空 | 页面结构变化 | 对比新旧 HTML 结构 | 更新解析选择器 |
| 数据重复率高 | 去重逻辑失效 | 检查去重键是否包含足够字段 | 调整去重策略 |
| 推送失败 | 笔记软件 API 变更 | 查看 API 返回错误码 | 更新 API 调用方式 |
| 抓取速度过慢 | 频率控制过严 | 统计单站点耗时 | 适当提高并发或缩短间隔 |
提示:排查问题时,建议先用单条数据跑通全流程,确认每个环节都正常后再批量运行。这样定位问题会快很多。
6. 后续可以扩展的方向
这个工具目前满足了我的基本需求,但还有一些可以优化的地方。比如加入岗位描述的关键词提取,自动生成岗位技能标签;比如接入邮件提醒,发现特别匹配的岗位时即时通知;比如做一个简单的 Web 界面,支持手动调整筛选条件并实时查看结果。
另外,如果你不想写代码,也可以用现成的表格工具加手动整理的方式实现简化版。核心思路是一样的:统一字段、去重、按规则筛选、按优先级排序。工具只是手段,关键是建立一套属于自己的信息处理流程。
我在实际使用中最大的体会是,工具的价值不在于技术多复杂,而在于它是否真正融入了你的日常工作流。一开始我追求大而全,后来发现每天真正看的岗位不超过 20 条,于是把精力集中在提升这 20 条的匹配度上,效果反而更好。